SpringMVC源码分析
一、SpringMVC请求处理
执行流程:
(1) 客户端浏览器发送请求
(2) tomcat接受请求,解析请求
(3) tomcat将请求交给dispatcherServlet
(4) dispatcher调用handlerMapping,通过url查找对应的controller对象
(5) dispatcher将controller对象交给HandlerAdapter,返回适配之后的controller(handler)
(6) 根据url找到controller中对应的处理方法
(7) 执行适配后的controller的处理方法,返回ModelAndView
(8) dispatcherServlet将modelAndView交给视图解析器渲染,返回渲染后的视图
(9) dispatcher将选择后的视图交给tomcat
(10) tomcat将视图响应给客户端浏览器
(11) 客户端浏览器解析响应,展示视图
二、SpringMVC的工作机制
在容器初始化时会建立所有url和controller的对应关系,保存到Map<url,controller>中。tomcat启动时会通知spring初始化容器(加载bean的定义信息和初始化所有单例bean),然后springmvc会遍历容器中的bean,获取每一个controller中的所有方法访问的url,然后将url和Controller保存到一个Map中;这样就可以根据request快速定位到Controller,因为最终处理request的是Controller中的方法,Map中只保留了url和Controller中的对应关系,所以要根据request的url进一步确认Controller中的method,这一步工作的原理就是拼接Controller的url(Controller上@RequestMapping的值)和方法的url(method上@RequestMapping的值),与request的url进行匹配,找到匹配的那个方法;确定处理请求的method后,接下来的任务就是参数绑定,把request中参数绑定到方法的形式参数上,这一步是整个请求处理过程中最复杂的一个步骤。SpringMVC提供了两种request参数与方法形参的绑定方法:
① 通过注解进行绑定 @RequestParam
② 通过参数名称进行绑定.
使用注解进行绑定,我们只要在方法参数前面声明@RequestParam(“a”),就可以将request中参数a的值绑定到方法的该参数上。使用参数名称进行绑定的前提是必须要获取方法中参数的名称,Java反射只提供了获取方法的参数的类型,并没有提供获取参数名称的方法。SpringMVC解决这个问题的方法是用asm框架读取字节码文件,来获取方法的参数名称。asm框架是一个字节码操作框架,关于asm更多介绍可以参考它的官网。个人建议,使用注解来完成参数绑定,这样就可以省去asm框架的读取字节码的操作。
三、源码的分析
第一步、建立Map<url, controller>的关系。
第一部分的入口类为ApplicationObjectSupport和setApplicationContext方法。setApplicationContext方法中核心的部分就是初始化容器initsetApplicationContext(context),子类AbstractDetectingUrlHandlerMapping实现了该方法。子类的初始化容器方法:
1 | public void initApplicationContext() throws ApplicationContextException { |
determineUrlsForHandler(String beanName)方法的作用是获取每一个controller中的url,不同的子类有不同的实现,这是采用了模板设计模式。DefaultAnnotationHandlerMapping是AbstractDetectingUrlHandlerMapping的子类,处理注解形式的url映射,所以我们这里以DefaultAnnotationHandlerMapping来进行分析。我们看DefaultAnnotationHandlerMapping是如何查beanName上所有映射的url。
1 | /** |
到这里HandlerMapping组件就已经建立所有url和controller的对应关系。
第二步、根据访问url找到对应controller中处理请求的方法
第二个步骤是由请求触发的,所以入口是DispatcherServlet, DispatcherServlet的核心方法为doService(), diservice()中的核心逻辑由diDispatch()实现,都Dispath()的源代码
1 | /** 中央控制器,控制请求的转发 **/ |
第二步getHandler(processedRequest)方法实际上就是从HandlerMapping中找到url和controller的对应关系。这也就是第一个步骤建立Map<url, controller>
的意义,我们知道,最终处理request的是controller中的方法。
第三步、反射调用处理请求的方法,返回结果视图
第2步就是从第一个步骤中的Map<urls,beanName>中取得Controller,然后经过拦截器的预处理方法,到最核心的部分–第5步调用controller的方法处理请求。第2步中我们可以知道处理request的Controller,第5步就是要根据url确定Controller中处理请求的方法,然后通过反射获取该方法上的注解和参数,解析方法和参数上的注解,最后反射调用方法获取ModelAndView结果视图。第5步调用的就是AnnotationMethodHandlerAdapter的handle().handle()中的核心逻辑由invokeHandlerMethod(request, response, handler)实现。
1 | /** 获取处理请求的方法,执行并返回结果视图 **/ |
这一部分的核心就在2和4了。先看第2步,通过request找controller的处理方法。实际上就是拼接controller的url和方法的url,与request的url进行匹配,找到匹配的方法。
1 | /** 根据url获取处理请求的方法 **/ |
通过上面的代码,已经可以找到处理request的Controller中的方法了,现在看如何解析该方法上的参数,并调用该方法。也就是执行方法这一步。执行方法这一步最重要的就是获取方法的参数,然后我们就可以反射调用方法了。
1 | public final Object invokeHandlerMethod(Method handlerMethod, Object handler, |
resolveHandlerArguments方法实现代码比较长,它最终要实现的目的就是:完成request中的参数和方法参数上数据的绑定。
SpringMVC中提供两种request参数到方法中参数的绑定方式:
① 通过注解进行绑定 @RequestParam
② 通过参数名称进行绑定
使用注解进行绑定,我们只要在方法参数前面声明@RequestParam(“a”),就可以将request中参数a的值绑定到方法的该参数上。使用参数名称进行绑定的前提是必须要获取方法中参数的名称,Java反射只提供了获取方法的参数的类型,并没有提供获取参数名称的方法。SpringMVC解决这个问题的方法是用asm框架读取字节码文件,来获取方法的参数名称。asm框架是一个字节码操作框架,关于asm更多介绍可以参考它的官网。个人建议,使用注解来完成参数绑定,这样就可以省去asm框架的读取字节码的操作。
1 | private Object[] resolveHandlerArguments(Method handlerMethod, Object handler, |
到这里,方法的参数值列表也获取到了,就可以直接进行方法的调用了。整个请求过程中最复杂的一步就是在这里了。
四、SpringMVC的优化
Controller如果能保持单例,尽量使用单例,这样可以减少创建对象和回收对象的开销。也就是说,如果Controller的类变量和实例变量可以以方法形参声明的尽量以方法的形参声明,不要以类变量和实例变量声明,这样可以避免线程安全问题。
处理request的方法中的形参务必加上@RequestParam注解,这样可以避免SpringMVC使用asm框架读取class文件获取方法参数名的过程。即便SpringMVC对读取出的方法参数名进行了缓存,如果不要读取class文件当然是更加好。
- 阅读源码的过程中,发现SpringMVC并没有对处理url的方法进行缓存,也就是说每次都要根据请求url去匹配Controller中的方法url,如果把url和method的关系缓存起来,会不会带来性能上的提升呢?有点恶心的是,负责解析url和method对应关系的ServletHandlerMethodResolver是一个private的内部类,不能直接继承该类增强代码,必须要该代码后重新编译。当然,如果缓存起来,必须要考虑缓存的线程安全问题。
执行流程:
(1) 客户端浏览器发送请求
(2) tomcat接受请求,解析请求
(3) tomcat将请求交给dispatcherServlet,这里面有各种初始化策略
(4) dispatcher调用handlerMapping, 通过url查找对应的controller对象,HandlerMapping的核心实现类是RequsestMappingHandlerMapping,它的作用是找出Spring容器中被@Controller注释修饰的Bean以及被RequestMapping修饰的类和方法,找到对应的HandlerMapping并得到HandlerExecutionChain内部包括了拦截器
(5) dispatcher将controller对象交给HandlerAdapter(处理映射器),拦截器调用预处理方法,使用执行链中的Handler遍历处理映射器集合,找到支持此Handler的处理映射器,返回适配之后的controller(handler),处理映射器的核心实现类是RequsestMappingHandlerAdapter,他有两个功能第一个是解析方法的参数,使用HandlerMethodArgumentResolverComposite(处理程序方法参数解析器组合)处理HandlerMethodArgumentResolver集合,第二个功能是处理方法的返回值HandlerMethodReturnValueHandler,解析方法的参数的实现类有RequestParamMethodArgumentResolver来处理@RequestParam注解修饰的参数,处理返回值的实现类有ModelAndViewMethodReturnValueHandler处理返回值类型为ModelAndView的方法,他们俩有一个共同的实现类RequestResponseBodyMethodProcessor处理@RequestBody注解修饰的参数
/((6) 根据url找到controller中对应的处理方法)/
(7) 执行适配后的controller的处理方法,返回ModelAndView,拦截器调用postHandle方法,如果在之前发生异常了那么就执行HandlerExeceptionResolver策略解决
(8) dispatcherServlet将modelAndView交给视图解析器渲染,返回渲染后的视图,根据ModelAndView使用ViewResolver进行解析得到View
(9) dispatcher将选择后的视图交给tomcat
(10) tomcat将视图响应给客户端浏览器
(11) 客户端浏览器解析响应,展示视图