強大的DispatcherServlet
還記得在web.xml中配置的DispatcherServlet嗎?其實那個就是SpringMVC框架的入口,這也是struts2和springmvc不同點之一沉颂,struts2是通過filter的锈颗,而springmvc是通過servlet的齐苛】缂危看下servlet的結構圖
從上面這張圖很明顯可以看出DispatcherServlet和Servlet以及Spring的關系尊蚁。而我們今天的重點就從DispatchServlet說起亡笑。
在分析之前我用SpringBoot搭建了一個很簡單的后臺項目,用于分析横朋。代碼如下
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
@AllArgsConstructor
public class User {
private Integer id;
private String name;
private Integer age;
private String address;
public User() {
}
}
/**
* @author generalthink
*/
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public User getUser(HttpServletRequest request,@PathVariable Integer id) {
//創(chuàng)建一個user,不走數據庫只是為了分析springmvc源碼
User user = User.builder()
.id(id)
.age(ThreadLocalRandom.current().nextInt(30))
.name("zzz" + id)
.address("成都市").build();
return user;
}
@RequestMapping(value = "/condition",method = RequestMethod.GET)
public User getByNameOrAge(@RequestParam String name,@RequestParam Integer age) {
User user = User.builder().name(name).age(age).address("成都市").id(2).build();
return user;
}
@PostMapping
public Integer saveUser(@RequestBody User user) {
Integer id = user.getName().hashCode() - user.getAge().hashCode();
return id > 0 ? id : -id;
}
}
這里為了方便調試把關注點更多集中在SpringMVC源碼中,所以這里的數據都是偽造的仑乌。而且這里的關注點也集中到使用注解的Controller(org.springframework.stereotype.Controlle
r),而不是Controller接口(org.springframework.web.servlet.mvc.Controller
),這兩者的區(qū)別主要在意一個只用標注注解琴锭,一個需要實現接口晰甚,但是它們都能完成處理請求的基本功能。我們都知道訪問servlet的時候默認是訪問service方法的决帖,所以我們將斷點打在HttpServlet的service方法中厕九,此時查看整個調用棧如下
從這里我們也知道了請求時如何從servlet到了DispatcherServlet的衣吠,我們先來看一下DispatcherServlet的doDiapatch的方法邏輯资溃,這里把核心邏輯列出來了遏匆,把其他的一些非核心邏輯移除了
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
//注意這里放回的是HandlerExecutionChain對象
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
ModelAndView mv = null;
Exception dispatchException = null;
//檢查是否存在文件上傳
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 根據當前request獲取handler,handler中包含了請求url,以及最終定位到的controller以及controller中的方法
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 通過handler獲取對應的適配器,主要完成參數解析
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 調用Controller中的方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
可以看到核心邏輯其實非常簡單家破,首先檢查是不是multipart request
酪耳,如果是則對當前的request進行一定的封裝(提取文件等)板甘,然后獲取對應的handler(保存了請求url對應的controller以及method以及一系列的Interceptor),然后在通過handler獲取到對應的handlerAdapter
(參數組裝)同仆,通過它來進行最終方法的調用
解析multipart
那么是如何解析當前請求是文件上傳請求呢倚舀?這里直接進入到checkMultipart方法看看是如何解析的:
//我精簡了下代碼细睡,只提取了核心邏輯
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
return this.multipartResolver.resolveMultipart(request);
}
return request;
}
從這里可以看出通過multipartResolver
判斷當前請求是否是文件上傳請求,如果是則返回MultipartHttpServletRequest
(繼承自HttpServletRequest).不是則返回原本request對象谷羞。
那么問題來了multipartResolver
是什么時候初始化的呢?
我們在idea中可以直接將斷點定位到multipartResolver屬性上溜徙,進行請求訪問這個時候會發(fā)現斷點直接進入到了initMultipartResolver方法中湃缎,接著跟蹤整個調用棧犀填,可以發(fā)現調用關系如下:
圖上表明了是在初始化servlet的時候對multipartResolver進行了初始化的。
private void initMultipartResolver(ApplicationContext context) {
//從Spring中獲取id為multipartResolver的類
this.multipartResolver = context.getBean("multipartResolver", MultipartResolver.class);
}
MultipartResolver接口有CommonsMultipartResolve
以及StandardServletMultipartResolver
2種實現嗓违,CommonsMultipartResolver接口是依賴于commons-upload組件實現的九巡,而 StandardServletMultipartResolver是依賴于Servlet的part(servlet3才存在)實現的.兩者判斷是否是文件上傳請求的方法isMultipart均是通過判定請求方法是否為post以及content-type頭是否包含multipart/來進行判定的。
DispatchServlet初始化了哪些內容
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context); //初始化multipartResolver
initLocaleResolver(context);//初始化localeResolver
initThemeResolver(context);//初始化themResolver
initHandlerMappings(context);//初始化handerMappings
initHandlerAdapters(context);//初始化handlerAdapters
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);//初始化試圖解析器
initFlashMapManager(context);
}
這些初始化的內容都會在后面被逐一使用靠瞎,這里先有一個印象比庄。
根據請求獲取mapperHandler
還是進入到getHander方法中看看到底做了什么?
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping hm : this.handlerMappings) {
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
根據HandlerMapping來查看對應的handler,那么進入到initHandlerMappings方法中查看如何初始化handlerMappings
其中獲取默認的handlerMappings是去spring-webmvc的org.springframework.web.servlet
中的DispatcherServlet.properties中查找乏盐,文件內容是這樣的
因為
detechAllhanderMappings
默認為true佳窑,所以會獲取到所有HanderMapping的實現類,來看看它的類圖結構是怎樣的這幾個HandlerMapping的作用如下:
SimpleUrlHandlerMapping : 允許明確指定URL模式和Handler的映射關系父能,內部維護了一個urlMap來確定url和handler的關系
BeanNameUrlHandlerMapping: 指定URL和bean名稱的映射關系神凑,不常用,我們的關注點也主要集中在
RequestMappingHandlerMapping
中
這里也基本明確了HandlerMapping的作用:幫助DispatcherServlet進行Web請求的URL到具體類的匹配,之所以稱為HandlerMapping是因為在SpringMVC中并不局限于
必須使用注解的Controller我們也可以繼承Controller接口何吝,也同樣可以使用第三方接口溉委,比如Struts2中的Action
接著看下getHandler的實現:
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping hm : this.handlerMappings) {
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
返回的handler是HandlerExecutionChain,這其中包含了真實的handler以及攔擊器,可以在執(zhí)行前,執(zhí)行后爱榕,執(zhí)行完成這三個階段處理業(yè)務邏輯瓣喊。
RequestMappingHandlerMapping
的getHandler的調用邏輯如下:
會遍歷所有Controller的url查看是否有符合條件的match(head,url,produce,consume,method都要滿足要求),采用antMatcher的方式來進行url匹配黔酥,如果匹配上了則返回對應的handler藻三,否則返回null,如果映射發(fā)現有重復的映射(url映射相同,請求方法相同跪者,參數相同棵帽,請求頭相同,consume相同渣玲,produce相同逗概,自定義參數相同),則會拋出異常忘衍。
而SimpleUrlHandlerMapping的調用邏輯如下:
其中維護了url到handler的映射逾苫,先通過url到urlMap中找對應的handler,如果沒有找到則嘗試pattenMatch枚钓,成功則返回對應的handler,未匹配則返回null铅搓。
會發(fā)現處理HandlerMapping這里運用了模板方法,在抽象類中定義好了業(yè)務邏輯秘噪,具體實現只需要實現自己的業(yè)務邏輯即可。同時也符合開閉原則勉耀,完全是面向接口編程指煎,不得不讓人嘆服這里的涉及邏輯蹋偏。
分析到這里的時候我們會發(fā)現我們之前定義的Controller明顯是符合RequestMappingHandlerMapping
的策略的,所以返回的HandlerExecutionChain已經包含了需要訪問的方法的全路徑了至壤。
關于HandlerAdapter
HandlerMapping會通過HandlerExecutionChain
返回一個Object類型的Handler對象威始,用于Web請求處理,這里的Handler并沒有限制具體是什么類型像街,一般來說任何類型的Handler都可以在
SpringMVC中使用黎棠,只要它是用于處理Web請求的處理對象就行。
不過對于DispatcherServlet來說就存在問題了镰绎,它無法判斷到底使用的是什么類型的Handler脓斩,也無法知道是調用Handler的哪個方法來處理請求,為了以同意的方式來調用各種類型的Handler,
DispatcherServlet將不同Handler的調用職責轉交給了一個成為HandlerAdapte
r的角色畴栖。
先看一下HandlerAdpter接口的定義
public interface HandlerAdapter {
boolean supports(Object handler);
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
long getLastModified(HttpServletRequest request, Object handler);
}
主要關注supports和handle方法随静。先看下DispatcherServlet
中handlerAdapters
的初始化過程,和handlerMappings
的初始化過程是類似的
接著在看一下HandlerAdapter的類關系
同樣的吗讶,仍然通過合適的策略尋找對應的Adapter燎猛,我們主要關注的是RequestMappingHandlerAdapter(其他的用得很少),所以這里就主要講解它照皆。查看它support的實現代碼:
上面關于handler的說明中說了其實Object handler實際上是HandlerMethod重绷,所以這里對應的HandlerAdapter就是
RequestMappingHandlerAdapter
。
找到對應的適配器之后膜毁,這個時候就可以調用真正的邏輯了昭卓。在這之前使用者可以通過攔截器做一些事兒,比如記錄日志爽茴,打印執(zhí)行時間等葬凳,所以如果想要在執(zhí)行的方法之前添加一條語句,我們只需要配置自己的攔擊器即可室奏。
接下來我們重點分析handle方法火焰,看看它到底會做什么?胧沫,先看一下handle方法的執(zhí)行流程昌简,同樣的adapter同樣使用了模板方法,先在父類里面定義流程绒怨,子類只需要實現邏輯即可纯赎,所以這里首先會調用AbstracthandlerMethodAdapter的invokeHadlerMethod方法,其中對HandlerMethod進行了封裝南蹂。
我們進入到第一步犬金,看看invokeForRequest方法中主要做了什么
發(fā)現這個方法的調用邏輯實際上很簡單,就是解析參數,然后調用方法晚顷。我們來看一下如何進行參數解析的呢峰伙?
可以看到幾乎所有的核心邏輯都集中到了
argumentResovlers
中去,那么支持的arguementResolver有哪些该默?又是在哪里初始化的呢瞳氓?
首先需要定位到這個屬性是從哪里過來的,RequestMappingHandlerAdapter
實現了InitializingBean
栓袖,所以在初始化的時候會執(zhí)行afterPropertiesSet
方法匣摘,在這其中對arguementResolvers
以及returnValueHandlers
進行了初始化。
不同的resovler支持的參數解析不一樣裹刮,比如說有支持HttpServletRequest注入的音榜,有支持HttpServletREsponse注入的還有支持body體注入的等等。
經過參數解析之后就得到了反射需要的數據了,class,method以及參數必指,最后通過java的反射api調用即可囊咏。
至此,springmvc的整個調用流程基本就清晰了塔橡。
但是到了這里問題仍然沒有結束梅割,因為我們還不知道參數具體是如何解析的。比如get方式提交的數據葛家?post方式提交的數據户辞?如何轉換成對象的?這寫問題都還存在癞谒,那我們繼續(xù)研究底燎。
這里我使用postman工具來發(fā)起請求,首先訪問 Get http://localhost:8080/user/condition?name=zhangsan&age=25,定位到resolveArgument
方法
接著又執(zhí)行revolver.resolveArgument
方法弹砚,同樣的這里還是使用的模板方法双仍,在抽象類AbstractNamedValueMethodArgumentResolver
中定義流程,各個子類只需要實現自己的邏輯即可桌吃。RequestParamMethodArgumentResolver
的參數就是通過request.getParameter來獲取到的朱沃。獲取到了參數之后就執(zhí)行反射調用,這個時候就執(zhí)行了我們寫的UserController的對應方法茅诱,獲取到了User對象逗物,接下來就是處理返回值了,通過returnValueHandlers進行處理
handler會根據返回的類型對數據進行處理瑟俭,比如說這里就通過response向請求方輸出數據翎卓,輸出數據也是通過messageConverter來實現的
最后獲取ModalAndView對象,但是這里由于沒有modalAndView所以返回的null.最后在DispatcherServlet的processDispatchResult方法的調用邏輯如下
么對于這樣的請求又時如何解析的呢摆寄?
@PostMapping
public Integer saveUser(@RequestBody User user) {
Integer id = user.getName().hashCode() - user.getAge().hashCode();
return id > 0 ? id : -id;
}
同樣我們聚焦在解析參數的時候失暴,在上一個get請求的示例中我說了會先訪問AbstractNamedValueMethodArgumentResolver
坯门,但是在處理@RequestBody
的參數中它使用的是RequestResponseBodyMethodProcessor
,它復寫了resolveArgument
方法。所以不會去執(zhí)行父類的邏輯逗扒。
這里最后會定位到jakson的objectMapper中田盈, 在spring boot中,默認使用Jackson來實現java對象到json格式的序列化與反序列化缴阎。當然是可以配置messageConvert的,只需要實現Spring的HttpMessageConverter
即可简软。
源碼分析到這里就結束了蛮拔,當然其中還存在一些沒有講的地方,比如View的渲染呀痹升,一般視圖是多種多樣的建炫,又html,xml,jsp等等,所以springmvc也提供了接口供用戶選擇自己需要的模板疼蛾,只需要實現ViewResolver接口即可肛跌。還有關于Theme,MessageResource,Exception的處理等等察郁,如果鋪開來講篇幅實在是太長了衍慎,我更相信掌握了核心流程看其他的處理就會很簡單了,所以這里也就不對其他枝節(jié)內容做分析了皮钠。