介紹
3月份的時候由於疫情被裁員,換了一家公司,結果來了這個公司一看代碼就暈了,我熟悉的@RequestMapping註解哪去了?不用@RequestMapping註解怎麼做映射啊?然後看了一波文檔,原來映射規則是這樣做的,有如下一個Controller類
@View
public class UserApi {
public String index() {
return "index";
}
public String test() {
return "test";
}
}
用@View註解來表明這是一個Controller類
當訪問http://userApi/index.json的時候調用的是UserApi類的index方法
當訪問http://userApi/test.json的時候調用的是UserApi類的test方法
即映射規則是類名+方法名+.json,姿勢確實夠騷。後續我寫個demo給大家演示一下是怎麼做到的。先來分析一下Spring MVC原生的映射規則是怎麼做到的,搞懂了Spring MVC原生的映射規則,再騷的映射規則照樣能看懂。
先來回憶一下Spring MVC的執行過程
HandlerMapping是根據請求的url找到對應的handler(你暫且可以認爲你寫的controller類)
HandlerAdapter則是根據找到的handler執行對應的方法,然後返回ModelAndView
Spring MVC將Handler的查找和執行分開了,你覺得哪個不好用,就把它替換一下
Handler的三種實現方式
前面之所以說Handler,是因爲在Spring MVC中,Handler常見的實現方式有三種,雖然一般我們只用@RequestMapping註解
實現Controller接口
@Component("/index")
public class IndexController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().write("IndexController");
return null;
}
}
訪問http://localhost:8080/index,頁面輸出IndexController,這裏需要說明的有2點
- 當Handler放回的ModelAndView爲null時,後續ViewResolver查找View,View進行渲染的過程會被省略
- @Component註解的value值必須以/開頭,後續會說原因
實現HttpRequestHandler接口
@Component("/address")
public class AddressController implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("AddressController");
}
}
訪問http://localhost:8080/address,頁面輸出AddressController
使用@RequestMapping註解
這種方式大家應該很熟悉了,就不再介紹了
HandlerMapping
HandlerMapping接口定義如下
public interface HandlerMapping {
// 根據請求獲取HandlerExecutionChain
@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
HandlerExecutionChain的定義也很簡單,是對Handler和這個Handler執行前後要執行的攔截器的封裝
public class HandlerExecutionChain {
private final Object handler;
@Nullable
private List<HandlerInterceptor> interceptorList;
}
DispatcherServlet有一個成員變量
private List<HandlerMapping> handlerMappings;
當通過請求找Handler時,會依次調用handlerMappings的handler方法,找到第一個不爲null的handler則返回,繼續後面的流程,如果遍歷完handlerMappings,handler還爲null,則報404的錯誤。
Spring MVC有三種映射策略
映射策略 | 實現類 |
---|---|
簡單url映射 | SimpleUrlHandlerMapping |
BeanName映射 | BeanNameUrlHandlerMapping |
@RequestMapping映射 | RequestMappingHandlerMapping |
各自的實現我就不放源碼了,說一下主要思路,思路理解了,追着看源碼就很容易理解了
BeanNameUrlHandlerMapping
上面演示Handler的三種寫法的時候已經演示了BeanNameUrlHandlerMapping的作用了@Componet註解的值和請求的url相同,這種映射關係還挺簡單的哈,當然支持統配符哈
註冊
在Spring啓動過程中,會拿到所有以/開頭的BeanName,並註冊到AbstractUrlHandlerMapping類的成員變量handlerMap 中,註冊的時候key重複會報異常
// AbstractUrlHandlerMapping
Map<String, Object> handlerMap = new LinkedHashMap<>();
其中key爲@Component的value值,value爲@Component修飾的類
查找
查找的時候分爲如下幾步,因爲要考慮統配符的存在,所以不可能是簡單的get
- 先直接從handlerMap中,如果不爲空則直接返回
- 遍歷handlerMap,調用AntPathMatcher的匹配方法,看請求的路徑和註冊的路徑是否有匹配的。如果有多個匹配,則對匹配的路徑進行排序。選出最優的,返回對應的Handler
- 如果還是沒有找到,則返回null
這個查找的邏輯我舉個例子
@Test
public void test1() {
AntPathMatcher pathMatcher = new AntPathMatcher();
Assert.assertTrue(pathMatcher.match("index/user", "index/user"));
Assert.assertTrue(pathMatcher.match("index/**", "index/product/a"));
Assert.assertTrue(pathMatcher.match("index/**/a", "index/product/a"));
}
如果有如下3個handler
@Component("/index/user")
public class AController implements Controller
@Component("/index/**")
public class BController implements Controller
@Component("/index/**/a")
public class CController implements Controller
則初始化完後handlerMap爲
"/index/user" -> AController
"/index/**" -> BController
"/index/**/a" -> CController
當訪問/index/user,能直接從map中取出AController然後返回
當訪問index/product/a,直接從map中拿不到,就開始遍歷key做路徑匹配,結果發現有2個路徑index/**和/index/**a匹配
因爲有2個路徑符合,所以排序,排序後得到的最優路徑爲index/**/a,取出CController,然後執行
SimpleUrlHandlerMapping
這個其實和BeanNameUrlHandlerMapping差不多,只是需要Handler對應的路徑,而不是把BeanName作爲路徑
還是以上面的IndexController和AddressController爲例,不用@Component指定BeanName,則默認爲類名,首字母小寫
@Component
public class IndexController implements Controller
@Component
public class AddressController implements HttpRequestHandler
增加如下配置類
@Configuration
public class HandlerMappingConfig {
@Autowired
private AddressController addressController;
@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
Map<String, Object> urlMap = new HashMap<>();
urlMap.put("/indexV2", "indexController");
urlMap.put("/addressV2", addressController);
mapping.setUrlMap(urlMap);
return mapping;
}
}
訪問http://localhost:8080/indexV2,輸出IndexController
訪問http://localhost:8080/addressV2,輸出AddressController
註冊
註冊的邏輯和BeanNameUrlHandlerMapping相似,也是將映射關係存在AbstractUrlHandlerMapping類的成員變量handlerMap 中
// AbstractUrlHandlerMapping
Map<String, Object> handlerMap = new LinkedHashMap<>();
key爲SimpleUrlHandlerMapping的urlMap中指定的路徑,value爲urlMap中指定的Handler,當然如果urlMap中指定的Handler爲一個String,則會從容器中找到相應的實現類註冊進去
查找
查找的邏輯和BeanNameUrlHandlerMapping的邏輯一樣,因爲2個類的映射關係都存在
AbstractUrlHandlerMapping中,並且各自沒有重新查找的邏輯
RequestMappingHandlerMapping
@RequestMapping的對應的RequestMappingHandlerMapping和RequestMappingHandlerAdapter應該是Spring MVC中最複雜的部分了。因爲RequestMappingHandlerMapping和RequestMappingHandlerAdapter各成體系,包含了大量組件來協同工作,單開一篇來分享把。這篇就只分享映射關係的註冊,查找過程
註冊
之前的映射關係,是直接存在Map中,而RequestMappingHandlerMapping的映射關係是存在AbstractHandlerMethodMapping的內部類MappingRegistry的成員變量中的
class MappingRegistry {
// 從RequestMappingInfoHandlerMapping類上可以看到T爲RequestMappingInfo
// RequestMappingInfo -> HandlerMethod
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
// 不包含通配符的url -> List<RequestMappingInfo>
// 這裏爲什麼是一個List呢?因爲一個url有可能對應多個方法
// 即這些方法的@RequestMapping註解path屬性一樣,但是其他屬性不一樣
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
}
我只放2個分析用到的屬性,其餘的屬性就不分析了
- spring容器在啓動的時候,會拿到所有的bean,判斷這個bean上是否有Controller或者RequestMapping註解,如果有則執行後面的步驟
- 解析類上的@RequestMapping註解,將其信息封裝爲RequestMappingInfo
- 將RequestMappingInfo及其對應的HandlerMethod註冊到mappingLookup中
- 如果@RequestMapping指定的url沒有通配符,則將url -> RequestMappingInfo註冊到urlLookup中
舉個例子,假如有如下一個Controller
@RestController
@RequestMapping("manage")
public class ProductController {
@RequestMapping("product/*")
public String index() {
return "product";
}
@RequestMapping(value = "user", method = {RequestMethod.GET})
public String userByGet() {
return "userByGet";
}
@RequestMapping(value = "user", method = {RequestMethod.POST})
public String userByPost() {
return "userByPost";
}
}
初始化完成後2個Map的值爲
查找
查找的過程還是和上面提到的2個Map有關
urlLookup
key=不包含通配符的url
value=List<RequestMappingInfo>
mappingLookup
key=RequestMappingInfo
value=HandlerMethod
- 先根據url從urlLookup中查找對應的RequestMappingInfo,如果找到的List不爲空,則判斷其他匹配條件是否符合
- 如果其他條件也有符合的(params,headers等),則不再遍歷所有的RequestMappingInfo,否則遍歷所有的RequestMappingInfo,因爲考慮到有通配符形式的url所以必須遍歷所有的RequestMappingInfo才能找出來符合條件的
- 如果最終找到的RequestMappingInfo有多個,則按照特定的規則找出一個最匹配的,再從mappingLookup返回其對應的HandlerMethod
可能有小夥伴有很多疑惑,爲什麼一個不含通配符的url會有多個handler。
因爲用@RequestMapping標記後,請求時不只要路徑匹配就可以,還有很多其他條件。
上面不就演示了一個因爲方法不同,導致了一個url會有多個handler嗎?
如果找到多個符合條件的Handler,返回最優Handler的過程也比較麻煩,不再像之前的SimpleUrlHandlerMapping只考慮路徑就可以了,還要考慮其他的條件,比較複雜,就不再分析了
總之註冊和查找的過程主要和這2個map打交道,總體來說也不復雜
HandlerAdapter
接口定義如下
public interface HandlerAdapter {
// 該適配器是否能支持指定處理器
boolean supports(Object handler);
// 執行處理邏輯,返回 ModelAndView
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
// 請求的目標資源最近一次的修改時間
long getLastModified(HttpServletRequest request, Object handler);
}
DispatcherServlet有一個成員變量
private List<HandlerAdapter> handlerAdapters;
當通過HandlerMapping找到Handler後,會依次調用handlerAdapters的supports方法,找到第一個返回true的HandlerAdapter,然後調用HandlerAdapter的handle方法,完成執行。
常用的HandlerAdapter如下
類名 | 作用 |
---|---|
HttpRequestHandlerAdapter | 執行實現了HttpRequestHandler接口的Handler |
SimpleControllerHandlerAdapter | 執行實現了Controller接口的Handler |
RequestMappingHandlerAdapter | 執行Handler類型是HandlerMethod及其子類的Handler,RequestMappingHandlerMapping返回的Handler是HandlerMethod類型 |
HttpRequestHandlerAdapter
public class HttpRequestHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof HttpRequestHandler);
}
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
((HttpRequestHandler) handler).handleRequest(request, response);
return null;
}
}
強轉爲HttpRequestHandler然後調用handleRequest方法,最後返回null,當ModelAndView爲null的時候,ViewResolver查找View,並且View進行渲染的過程會被省略
SimpleControllerHandlerAdapter
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof Controller);
}
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return ((Controller) handler).handleRequest(request, response);
}
}
直接強轉然後調用handleRequest方法
RequestMappingHandlerAdapter
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
// 參數解析器,對各種參數註解如@PathVariable,@RequestParam的的支持
@Nullable
private List<HandlerMethodArgumentResolver> customArgumentResolvers;
// 返回值處理器,如當方法或類上有@ResponseBody註解時,用消息轉換器轉爲json
@Nullable
private List<HandlerMethodReturnValueHandler> customReturnValueHandlers;
// 消息轉換器,對@RequestBody @ResponseBody註解的支持
private List<HttpMessageConverter<?>> messageConverters;
}
RequestMappingHandlerAdapter自成體系,包含了大量組件對請求進行處理,如參數解析器,返回值處理器等。
RequestMappingHandlerAdapter的handler函數在父類AbstractHandlerMethodAdapter中,定義如下
public final boolean supports(Object handler) {
return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}
其中RequestMappingHandlerAdapter重寫了supportsInternal方法,永遠返回true,即RequestMappingHandlerAdapter支持Handler類型是HandlerMethod的Handler
而RequestMappingHandlerMapping返回的Handler類型就是HandlerMethod,因此可以知道@RequestMapping對應的HandlerMapping是RequestMappingHandlerMapping,對應的HandlerAdapter是RequestMappingHandlerAdapter
HandlerMethod的定義也很簡單,封裝了要執行方法所對應的類,方法,參數。這樣直接就能通過反射來執行。
public class HandlerMethod {
// 封裝的handler的類
private final Object bean;
// 封裝的handler的方法
private final Method bridgedMethod;
// 封裝方法的參數
private final MethodParameter[] parameters;
}
method.invoke(obj, args);
所以你看真正執行的過程也不復雜,但在執行前後有個很多組件參與,如參數解析器,返回值處理器等,就導致這個類有點複雜,再開文章分享把
總結
Spring MVC爲什麼要搞這麼多HandlerMapping和HandlerAdapter呢?
主要還是爲了適應不同的場景,靜態資源的請求用SimpleUrlHandlerMapping是不是特別方便,邏輯清晰還容易調試。而RequestMappingHandlerMapping則比較適合寫業務,因爲能適應複雜多變的場景
最開始提到的映射規則如何實現?
其實很簡單,寫一個HandlerMapping的實現類,返回HandlerMethod,這樣只改變了查找過程,後續執行的過程沒有改變,因此各種參數註解@RequestParam,@RequestBody,@PathVariable等都可以使用