如何基于存储库查找将自定义/动态请求映射到给定的控制器?
用例是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提供的默认值?
答案 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
}