在上文Spring MVC之RequestMappingHandlerMapping匹配中我们讲到,Spring在进行request匹配的时候,不仅会匹配url,method,contentType等条件,还会对用户提供的定制条件进行匹配,用户提供的定制条件是使用RequestCondition进行封装的。本文以本人工作的一个实际案例来讲解如果使用RequestCondition进行request的匹配过程进行定制,并且会对这种匹配过程需要注意的问题进行讲解。
1. 背景描述
本人从事的项目主要是售卖房源的,这里每个用户都有自己的网址,这些网址是通过二级域名的方式进行配置的,比如A用户的网址为a.house.com,B用户的网址为b.house.com。另外,我们也为每个用户提供了多套模板进行房源的展示。这种设计的优点在于用户如果需要将其网址进行SEO,那么可以通过统一的方式进行处理,并且通过二级域名我们就可以知道当前网站所属用户是谁。但这里存在的问题是,比如对于同一个页面的不同模板,虽然主体部分是相同的,但是页面细节上是有很大不同的,因而使用不同的接口对其进行处理是很有必要的,但是这样就需要前端每次在调用接口的时候判断当前用户是使用的哪一套模板,然后进行不同接口的调用。这样的话,后续随着模板页面越来越多,代码将变得极其难以维护。
为了解决上述问题,其实问题的根源在于将不同模板带来的复杂性引入到了前端,如果前端在请求同一页面时,无论当前用户是什么模板,都可以使用同一url进行请求,那么这种复杂性将会被屏蔽掉。那么这里需要解决的问题是,在前端通过某一域名链接,比如a.house.com/user/detail请求服务器时,服务器如何通过请求的域名来获取当前属于哪套模板,然后将请求分发到能处理当前模板的接口中。
这个问题其实就可以使用定制RequestCondition的方式进行。首先在服务器编写两个接口,这两个接口的签名完全一致,包括@RequestMapping
注解中的属性,这两个接口我们会使用一个自定义的注解进行标注,注解参数值用来表示当前接口可以处理哪几套模板。在前端请求a.house.com/user/detail时,在自定义的RequestCondition中,首先会根据请求的域名获取当前是哪个用户和使用的是哪套模板,然后再获取当前RequestMappingInfo所表示的Handler(Controller中的某个处理请求的方法)所标注的自定义注解支持哪套模板,如果两者是匹配的,则说明当前Handler是可以处理当前请求的,这样就可以达到请求转发的目的。
2. 实现代码
这里我们首先展示目标接口的写法,从下面的代码可以看出,两个接口所使用的@RequestMapping
中的参数是一模一样的,只是两个接口所使用的@Template
注解的参数值不同,这样就达到了将不同模板的接口进行分离的目的,从而屏蔽了不同模板所造成的接口处理方式不同的复杂性,并且也提供了一个统一的请求方式,即/user/detail给前端,增强了前端代码的可维护性。如下是接口的具体声明:
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Template(1)
@RequestMapping(value = "/detail", method = RequestMethod.GET)
public ModelAndView detailForTemplateOne(@RequestParam("id") long id) {
System.out.println("handled by detailForTemplateOne");
ModelAndView view = new ModelAndView("user");
User user = userService.detail(id);
view.addObject("user", user);
return view;
}
@Template(2)
@RequestMapping(value = "/detail", method = {RequestMethod.GET, RequestMethod.POST})
public ModelAndView detailForTemplateTwo(@RequestParam("id") long id) {
System.out.println("handled by detailForTemplateTwo");
ModelAndView view = new ModelAndView("user");
User user = userService.detail(id);
view.addObject("user", user);
return view;
}
}
关于@Template
注解的声明,其比较简单,只需要指定其支持的模板即可:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Template {
int[] value();
}
前面我们讲到了,如果需要对RequestMappingHandlerMapping的匹配过程进行定制,则需要为每个注册的RequestMappingInfo注册一个RequestCondition对象。如下是该对象的声明方式:
// 由于每个RequestMappingInfo中都会持有一个RequestCondition对象,并且这些对象都是有状态的,
// 因而这里必须为其使用prototype进行标注,表示每次从BeanFactory中获取该对象时都会创建一个新的对象
@Component
@Scope("prototype")
public class TemplateRequestCondition implements RequestCondition<TemplateRequestCondition> {
private int[] templates;
public TemplateRequestCondition(int[] templates) {
this.templates = templates;
}
// 这里combine()方法主要是供给复合类型的RequestMapping使用的,这种类型的Mapping可以持有
// 两个Mapping信息,因而需要对两个Mapping进行合并,这个合并的过程其实就是对每个RequestMappingInfo
// 中的各个条件进行合并,这里就是对RequestCondition条件进行合并
public TemplateRequestCondition combine(TemplateRequestCondition other) {
int[] allTemplates = mergeTemplates(other.templates);
return new TemplateRequestCondition(allTemplates);
}
// 判断当前请求对应用户选择的模板与当前接口所能处理的模板是否一致,
// 如果一致则返回当前RequestCondition,这里RequestMappingHandlerMapping在匹配请求时,
// 如果当前条件的匹配结果不为空,则说明当前条件是能够匹配上的,如果返回值为空,则说明其不能匹配
public TemplateRequestCondition getMatchingCondition(HttpServletRequest request) {
String serverName = request.getServerName();
int template = getTemplateByServerName(serverName);
for (int i = 0; i < templates.length; i++) {
if (template == templates[i]) {
return this;
}
}
return null;
}
// 对两个RequestCondition对象进行比较,这里主要是如果存在两个注册的一样的Mapping,那么就会对
// 这两个Mapping进行排序,以判断哪个Mapping更适合处理当前request请求
public int compareTo(TemplateRequestCondition other, HttpServletRequest request) {
return null != templates && null == other.templates ? 1
: null == templates && null != other.templates ? -1 : 0;
}
// 项目中实际会用到的,根据当前请求的域名获取其对应用户所选择的模板
private int getTemplateByServerName(String serverName) {
if (serverName.equalsIgnoreCase("peer1")) {
return 1;
} else if (serverName.equalsIgnoreCase("peer2")) {
return 2;
}
return 0;
}
// 将两个template数据进行合并
private int[] mergeTemplates(int[] otherTemplates) {
if (null == otherTemplates) {
return templates;
}
int[] results = new int[templates.length + otherTemplates.length];
for (int i = 0; i < templates.length; i++) {
results[i] = templates[i];
}
for (int i = templates.length; i < results.length; i++) {
results[i] = otherTemplates[i - templates.length];
}
return results;
}
}
上述就是对RequestMappingHandlerMapping的匹配过程进行定制的核心代码,这里主要需要关注的是getMatchingCondition()方法,该方法首先会获取当前请求的域名,然后与当前RequestMappingInfo所支持的templates进行比较,如果是其支持的,则返回当前ReqeustCondition对象,否则返回空。这需要说明的是,在进行RequestCondition与request匹配的时候,如果其getMatchingCondition()方法返回值不为空,则表示两者是匹配的,否则就是不匹配的。
关于RequestCondition的注入,我们需要重写RequestMappingHandlerMapping
的getCustomMethodCondition()
方法,在RequestMappingHandlerMapping扫描BeanFactory中所有的能处理请求的bean(Controller对象)的时候,其会将每个方法都声明为一个RequestMappingInfo对象,并且会调用RequestMappingHandlerMapping.getCustomMethodCondition()
方法,获取当前RequestMappingInfo所注册的条件,默认情况下该方法返回值是null。如下是重写的RequestMappingHandlerMapping:
@Component
public class TemplateHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
method.setAccessible(true);
Template template = method.getAnnotation(Template.class);
int[] templates = null == template ? new int[0] : template.value();
return obtainApplicationContext().getBean(RequestCondition.class, templates);
}
}
这里重写RequestMappingHandlerMapping其实就是自定义了一个HandlerMapping对象。Spring在初始化时,其会判断当前BeanFactory中是否存在HandlerMapping对象,如果有,则使用用户定义的,如果不存在,才会创建一个RequestMappingHandlerMapping用于处理请求。下面是Spring的xml文件的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="mvc"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/view/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
将上述代码使用Spring运行于tomcat容器中,然后访问分别http://a.house.com/user/detail?id=1和http://b.house.com/user/detail?id=1,可以看到控制台打印了如下日志:
handled by detailForTemplateOne
handled by detailForTemplateTwo
这说明我们成功的对RequestMappingHandlerMapping的请求过程进行了定制。通过这种定制方式,我们有效的将不同模板所带来的请求方式的复杂性对前端进行了屏蔽,也将同一请求对不同模板的处理方式在后端进行了分离。
3. 注意点
关于RequestCondition的自定义,需要说明的主要有三点:
- 在DispatcherServlet初始化时,Spring判断当前BeanFactory中是否存在自定义的HandlerMapping对象时,有两种方式:①判断当前BeanFacotry中是否有名称为handlerMapping的bean;②直接获取当前BeanFactory中实现了HandlerMapping接口的所有bean,这也是默认使用的一种方式。如果按照这两种方式中指定的一种无法获取的HandlerMapping对象,然后才会创建一个默认的RequestMappingHandlerMapping对象来处理请求;
- 如果使用了自定义的RequestMapping处理请求,那么在Spring的配置文件中尽量不要使用<mvc:annotation-driven/>标签,因为Spring在解析该标签时会往BeanFactory中注册一个RequestMappingHandlerMapping的对象,这样就会对我们自定义的HandlerMapping进行干扰,因为需要注意request具体是由我们定义的HandlerMapping处理的还是Spring提供的RequestMappingHandlerMapping处理的。如果确实需要使用该标签,这里可以为我们自定义的HandlerMapping实现一个PriorityOrdered接口,这样可以保证其会在RequestMappingHandlerMapping之前先判断是否能够处理当前请求;
- 上述UserController中定义了两个几乎完全的接口,对于Spring而言,其是不允许容器中有两个一模一样的接口的。但这是针对于没有定制RequestCondition的情况而言的,Spring在进行两个接口判断(其实就是封装接口的RequestMappingInfo对象的判断)的时候,会将RequestCondition对象的判断也纳入其中,如果对其进行了定制,并且两个RequestMappingInfo所持有的RequestCondition对象是不一样的,那么即使其他条件相同,也会认为两个RequestMappingInfo是不一样的,具体的读者可以阅读
MappingRegistry.assertUniqueMethodMapping()
方法的源码。
4. 小结
本文首先介绍了一个本人项目中使用多套模板时所存在的一个问题,然后介绍了使用RequestCondition处理该问题的解决思路,接着以代码的形式将解决方案进行了展示,最后介绍了使用RequestCondition时需要注意的问题。总的来说,本文以一个实际案例对如何定制RequestMappingHandlerMapping的匹配过程进行了讲解。