在上文Spring MVC之RequestMappingHandlerMapping匹配中咱們講到,Spring在進行request匹配的時候,不只會匹配url,method,contentType等條件,還會對用戶提供的定製條件進行匹配,用戶提供的定製條件是使用RequestCondition進行封裝的。本文以本人工做的一個實際案例來說解若是使用RequestCondition進行request的匹配過程進行定製,而且會對這種匹配過程須要注意的問題進行講解。前端
本人從事的項目主要是售賣房源的,這裏每一個用戶都有本身的網址,這些網址是經過二級域名的方式進行配置的,好比A用戶的網址爲a.house.com,B用戶的網址爲b.house.com。另外,咱們也爲每一個用戶提供了多套模板進行房源的展現。這種設計的優勢在於用戶若是須要將其網址進行SEO,那麼能夠經過統一的方式進行處理,而且經過二級域名咱們就能夠知道當前網站所屬用戶是誰。但這裏存在的問題是,好比對於同一個頁面的不一樣模板,雖然主體部分是相同的,可是頁面細節上是有很大不一樣的,於是使用不一樣的接口對其進行處理是頗有必要的,可是這樣就須要前端每次在調用接口的時候判斷當前用戶是使用的哪一套模板,而後進行不一樣接口的調用。這樣的話,後續隨着模板頁面愈來愈多,代碼將變得極其難以維護。java
爲了解決上述問題,其實問題的根源在於將不一樣模板帶來的複雜性引入到了前端,若是前端在請求同一頁面時,不管當前用戶是什麼模板,均可以使用同一url進行請求,那麼這種複雜性將會被屏蔽掉。那麼這裏須要解決的問題是,在前端經過某一域名連接,好比a.house.com/user/detail請求服務器時,服務器如何經過請求的域名來獲取當前屬於哪套模板,而後將請求分發到能處理當前模板的接口中。web
這個問題其實就可使用定製RequestCondition的方式進行。首先在服務器編寫兩個接口,這兩個接口的簽名徹底一致,包括@RequestMapping
註解中的屬性,這兩個接口咱們會使用一個自定義的註解進行標註,註解參數值用來表示當前接口能夠處理哪幾套模板。在前端請求a.house.com/user/detail時,在自定義的RequestCondition中,首先會根據請求的域名獲取當前是哪一個用戶和使用的是哪套模板,而後再獲取當前RequestMappingInfo所表示的Handler(Controller中的某個處理請求的方法)所標註的自定義註解支持哪套模板,若是二者是匹配的,則說明當前Handler是能夠處理當前請求的,這樣就能夠達到請求轉發的目的。spring
這裏咱們首先展現目標接口的寫法,從下面的代碼能夠看出,兩個接口所使用的@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
註解的聲明,其比較簡單,只須要指定其支持的模板便可:tomcat
@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()方法返回值不爲空,則表示二者是匹配的,不然就是不匹配的。mvc
關於RequestCondition的注入,咱們須要重寫RequestMappingHandlerMapping
的getCustomMethodCondition()
方法,在RequestMappingHandlerMapping掃描BeanFactory中全部的能處理請求的bean(Controller對象)的時候,其會將每一個方法都聲明爲一個RequestMappingInfo對象,而且會調用RequestMappingHandlerMapping.getCustomMethodCondition()
方法,獲取當前RequestMappingInfo所註冊的條件,默認狀況下該方法返回值是null。以下是重寫的RequestMappingHandlerMapping:app
@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文件的配置:jsp
<?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的請求過程進行了定製。經過這種定製方式,咱們有效的將不一樣模板所帶來的請求方式的複雜性對前端進行了屏蔽,也將同一請求對不一樣模板的處理方式在後端進行了分離。
關於RequestCondition的自定義,須要說明的主要有三點:
MappingRegistry.assertUniqueMethodMapping()
方法的源碼。本文首先介紹了一個本人項目中使用多套模板時所存在的一個問題,而後介紹了使用RequestCondition處理該問題的解決思路,接着以代碼的形式將解決方案進行了展現,最後介紹了使用RequestCondition時須要注意的問題。總的來講,本文以一個實際案例對如何定製RequestMappingHandlerMapping的匹配過程進行了講解。