如何将自定义/动态请求映射到控制器(Spring)?

时间:2020-02-23 01:44:22

标签: java spring spring-boot spring-mvc

如何基于存储库查找将自定义/动态请求映射到给定的控制器?

用例是Web平台中类似于CMS的功能,其中存储在数据库中的某些URL模式(“页面”)应由单独的控制器PageController.java处理。这些模式在编译时不一定是已知的,还可以在部署应用程序时进行添加和修改(因此,不能由注释驱动)。

我确实尝试将控制器映射到“ **”(见下文),但是由于两个原因而无法正常工作:首先,所有其他请求都解析为相同的控制器方法(我希望它会使用“ **” “作为后备广告,请先尝试其他方法),并且最终还会将对此我的静态/资产文件的所有请求解析到此控制器(导致不必要的404响应)。

@Controller
public class PageController {

    @Inject
    private PageService pageService;

    @RequestMapping(value = "**", method = RequestMethod.GET)
    public String getPage(Model model, HttpServletRequest request, @CurrentUser User user) {
        String path = request.getRequestURI();
        Page page = this.pageService.getByPath(path, user);
        if (page == null) {
            throw new NotFoundException();
        }
        model.addAttribute("page", page);
        return "web/page";
    }
}

到目前为止,对上述方法的临时变通/修改是将预定义的URL前缀映射到该控制器(例如/page/**/info/**/news/**等等),但这是一个不雅的解决方案,为我现在要消除的系统增加了任意限制。

我目前正在使用Spring Boot 2.0。除了在常规**类中使用@Controller注释将天真映射到@RequestMapping之外,我还尝试过以下方式配置SimpleUrlHandlerMapping

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Inject
    private PageDao pageDao;

    @Bean
    public PageController pageController() {
        return new PageController();
    }

    @Bean
    public SimpleUrlHandlerMapping pageUrlHandlerMapping() {
        SimpleUrlHandlerMapping pageUrlHandlerMapping = new SimpleUrlHandlerMapping();
        PageController pageController = this.pageController();
        Map<String, Object> urlMap = this.pageDao.findAll().stream()
                .map(Page::getNormalizedSlug)
                .collect(Collectors.toMap(Function.identity(),
                        slug -> pageController, (existing, duplicate) -> existing));
        pageUrlHandlerMapping.setUrlMap(urlMap);
        pageUrlHandlerMapping.setOrder(Ordered.HIGHEST_PRECEDENCE); // <- Cannot be LOWEST_PRECEDENCE for some reason...
        return pageUrlHandlerMapping;
    }
}

public class PageController implements Controller {

    @Inject
    private PageService pageService;
    @Inject
    private DmaWebControllerAdvice controllerAdvice;

    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        User user = null;
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (principal instanceof User) {
            user = (User) principal;
        }
        String path = request.getRequestURI();
        Page page = this.pageService.getByPath(path, user);
        if (page == null) {
            throw new NotFoundException();
        }
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("web/page");
        modelAndView.addObject("page", page);
        controllerAdvice.globalModelAttributes(modelAndView.getModel(), null);
        return modelAndView;
    }
}

这种方法在技术上确实有效,但是每当其中一个页面发生更改时,都必须以某种方式将页面列表重新加载到SimpleUrlHandlerMapping中(我不太确定该怎么做)。这也可能会覆盖一些我理想情况下希望保留的默认Spring Boot配置。与使用@Controller@RequesMapping解析控制器相比,它还有一些缺点,因为我目前正在将某些数据注入以这种方式解析的所有视图中(主要是网站整体设计中使用的模型数据,例如菜单) ,快速链接等)。在上面的尝试中,我不得不通过分别调用controllerAdvice-globalModelAttributes()来设置它们。

我正在寻找一种解决方案,其中在运行时查询我的存储库以查找潜在的页面匹配,如果有效,则将由适当的页面控制器处理该请求。自定义HandlerMapping -implementation是执行此操作的方法吗?如果没有,我该如何解决?而且,如果为页面创建单独的HandlerMapping,如何在我的配置中添加/注册它而不覆盖Spring提供的默认值?

2 个答案:

答案 0 :(得分:1)

为什么不只是实现一个包罗万象的控制器,该控制器将模式作为参数进行解析,执行数据库查找,然后对特定的控制器(信息,页面,新闻等)使用forward ?就像CMS一样,这种查找逻辑属于您的代码(例如服务层)。

答案 1 :(得分:1)

实现所需的最简单(但不是最好)的方法是创建自定义HandlerMapping实现:

public class PageMapper implements HandlerMapping, Ordered {

    private HandlerMethod handlerMethod;        

    public CustomMapper(Object controller, Method method) {
        this.handlerMethod = new HandlerMethod(controller, method);
    }

    @Override
    public HandlerExecutionChain getHandler(HttpServletRequest httpServletRequest) throws Exception {
        return new HandlerExecutionChain(handlerMethod);
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE; //you have to add the handler to the end
    }     
}

现在从@Controller中删除PageController注释,因为您不再需要自动检测它。在注册控制器并映射到配置之后:

@Configuration
public class AppWebConfig implements WebMvcConfigurer {

    @Bean
    public PageController pageController() {
        return new PageController();
    }

    @Bean
    public HandlerMapping pageMapping(PageController pageController) {
        Method method = BeanUtils.resolveSignature("getPage", PageController.class);
        return new PageMapping(pageController, method);
    }
}

现在,其他HandlerMapping实例无法识别的每个请求都将发送到您的映射,因此也发送到您的控制器。但是这种方法有明显的缺点。由于映射是映射链中的最后一个,因此永远不会出现404错误。因此,您永远不会知道您的资源有问题(例如,其中的某些资源是否丢失)。

我希望让应用程序通过前缀来区分路径(就像您已经做过的一样),其中前缀是应用程序要对页面进行的操作。例如,如果您需要显示或编辑页面:

@Controller
public class PageController {

    private final static String SHOW = "/show";
    private final static String EDIT = "/edit";

    @Inject
    private PageService pageService;

    GetMapping(value = SHOW + "/**")
    public String getPage(Model model, HttpServletRequest request, @CurrentUser User user) {
        String path = request.getRequestURI().substring(SHOW.length());
        Page page = this.pageService.getByPath(path, user);
        ...        
        model.addAttribute("page", page);
        return "web/page";
    }

    //the same for EDIT operation
}
相关问题