关于spring-mvc:SpringMVC源码分析

弱小的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) {

//创立一个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.Controller),而不是Controller接口(org.springframework.web.servlet.mvc.Controller),这两者的区别次要在意一个只用标注注解,一个须要实现接口,然而它们都能实现解决申请的基本功能。咱们都晓得拜访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属性上,进行申请拜访这个时候会发现断点间接进入到了initMultipartResolver办法中,接着跟踪整个调用栈,能够发现调用关系如下:

图上表明了是在初始化servlet的时候对multipartResolver进行了初始化的。

private void initMultipartResolver(ApplicationContext context) {

//从Spring中获取id为multipartResolver的类
    this.multipartResolver = context.getBean("multipartResolver", MultipartResolver.class);
}

MultipartResolver接口有CommonsMultipartResolve以及StandardServletMultipartResolver2种实现,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以及拦击器,能够在执行前,执行后,执行实现这三个阶段解决业务逻辑。
RequestMappingHandlerMapping的getHandler的调用逻辑如下:

会遍历所有Controller的url查看是否有符合条件的match(head,url,produce,consume,method都要满足要求),采纳antMatcher的形式来进行url匹配,如果匹配上了则返回对应的handler,否则返回null,如果映射发现有反复的映射(url映射雷同,申请办法雷同,参数雷同,申请头雷同,consume雷同,produce雷同,自定义参数雷同),则会抛出异样。

而SimpleUrlHandlerMapping的调用逻辑如下:

其中保护了url到handler的映射,先通过url到urlMap中找对应的handler,如果没有找到则尝试pattenMatch,胜利则返回对应的handler,未匹配则返回null。

会发现解决HandlerMapping这里使用了模板办法,在抽象类中定义好了业务逻辑,具体实现只须要实现本人的业务逻辑即可。同时也合乎开闭准则,齐全是面向接口编程,不得不让人叹服这里的波及逻辑。

剖析到这里的时候咱们会发现咱们之前定义的Controller显著是合乎RequestMappingHandlerMapping的策略的,所以返回的HandlerExecutionChain曾经蕴含了须要拜访的办法的全门路了。

对于HandlerAdapter

HandlerMapping会通过HandlerExecutionChain返回一个Object类型的Handler对象,用于Web申请解决,这里的Handler并没有限度具体是什么类型,一般来说任何类型的Handler都能够在
SpringMVC中应用,只有它是用于解决Web申请的解决对象就行。

不过对于DispatcherServlet来说就存在问题了,它无奈判断到底应用的是什么类型的Handler,也无奈晓得是调用Handler的哪个办法来解决申请,为了以批准的形式来调用各种类型的Handler,
DispatcherServlet将不同Handler的调用职责转交给了一个成为HandlerAdapter的角色。

先看一下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办法。先看下DispatcherServlethandlerAdapters的初始化过程,和handlerMappings的初始化过程是相似的

接着在看一下HandlerAdapter的类关系

同样的,依然通过适合的策略寻找对应的Adapter,咱们次要关注的是RequestMappingHandlerAdapter(其余的用得很少),所以这里就次要解说它。查看它support的实现代码:

下面对于handler的阐明中说了其实Object handler实际上是HandlerMethod,所以这里对应的HandlerAdapter就是RequestMappingHandlerAdapter

找到对应的适配器之后,这个时候就能够调用真正的逻辑了。在这之前使用者能够通过拦截器做一些事儿,比方记录日志,打印执行工夫等,所以如果想要在执行的办法之前增加一条语句,咱们只须要配置本人的拦击器即可。

接下来咱们重点剖析handle办法,看看它到底会做什么?,先看一下handle办法的执行流程,同样的adapter同样应用了模板办法,先在父类外面定义流程,子类只须要实现逻辑即可,所以这里首先会调用AbstracthandlerMethodAdapter的invokeHadlerMethod办法,其中对HandlerMethod进行了封装。


咱们进入到第一步,看看invokeForRequest办法中次要做了什么

发现这个办法的调用逻辑实际上很简略,就是解析参数,而后调用办法。咱们来看一下如何进行参数解析的呢?

能够看到简直所有的外围逻辑都集中到了argumentResovlers中去,那么反对的arguementResolver有哪些?又是在哪里初始化的呢?

首先须要定位到这个属性是从哪里过去的,RequestMappingHandlerAdapter实现了InitializingBean,所以在初始化的时候会执行afterPropertiesSet办法,在这其中对arguementResolvers以及returnValueHandlers进行了初始化。
不同的resovler反对的参数解析不一样,比如说有反对HttpServletRequest注入的,有反对HttpServletREsponse注入的还有反对body体注入的等等。


通过参数解析之后就失去了反射须要的数据了,class,method以及参数,最初通过java的反射api调用即可。

至此,springmvc的整个调用流程根本就清晰了。
然而到了这里问题依然没有完结,因为咱们还不晓得参数具体是如何解析的。比方get形式提交的数据?post形式提交的数据?如何转换成对象的?这写问题都还存在,那咱们持续钻研。
这里我应用postman工具来发动申请,首先拜访 Get http://localhost:8080/user/condition?name=zhangsan&age=25,定位到resolveArgument办法

接着又执行revolver.resolveArgument办法,同样的这里还是应用的模板办法,在抽象类AbstractNamedValueMethodArgumentResolver中定义流程,各个子类只须要实现本人的逻辑即可。RequestParamMethodArgumentResolver的参数就是通过request.getParameter来获取到的。获取到了参数之后就执行反射调用,这个时候就执行了咱们写的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办法。所以不会去执行父类的逻辑。

这里最初会定位到jakson的objectMapper中, 在spring boot中,默认应用Jackson来实现java对象到json格局的序列化与反序列化。当然是能够配置messageConvert的,只须要实现Spring的HttpMessageConverter即可。

源码剖析到这里就完结了,当然其中还存在一些没有讲的中央,比方View的渲染呀,个别视图是多种多样的,又html,xml,jsp等等,所以springmvc也提供了接口供用户抉择本人须要的模板,只须要实现ViewResolver接口即可。还有对于Theme,MessageResource,Exception的解决等等,如果铺开来讲篇幅切实是太长了,我更置信把握了外围流程看其余的解决就会很简略了,所以这里也就不对其余枝节内容做剖析了。

一图胜千言

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理