Spring Mvc之定製RequestMappingHandlerMapping

       在上文Spring MVC之RequestMappingHandlerMapping匹配中咱們講到,Spring在進行request匹配的時候,不只會匹配url,method,contentType等條件,還會對用戶提供的定製條件進行匹配,用戶提供的定製條件是使用RequestCondition進行封裝的。本文以本人工做的一個實際案例來說解若是使用RequestCondition進行request的匹配過程進行定製,而且會對這種匹配過程須要注意的問題進行講解。前端

1. 背景描述

       本人從事的項目主要是售賣房源的,這裏每一個用戶都有本身的網址,這些網址是經過二級域名的方式進行配置的,好比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

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註解的聲明,其比較簡單,只須要指定其支持的模板便可: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的注入,咱們須要重寫RequestMappingHandlerMappinggetCustomMethodCondition()方法,在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的請求過程進行了定製。經過這種定製方式,咱們有效的將不一樣模板所帶來的請求方式的複雜性對前端進行了屏蔽,也將同一請求對不一樣模板的處理方式在後端進行了分離。

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的匹配過程進行了講解。

相關文章
相關標籤/搜索