SpringMVC 的源码分析
1. SpringMVC的执行过程分析
首先,我们先来看一下springmvc 官方文档中提供的执行过程图。
通过此图,我们可以看到其实都是由前端控制器负责找到要执行的控制器方法。这个前端控制器就是SpringMVC 的核心控制器DispatcherServlet。
接下来,我们通过一个示例来看一下SpringMVC 请求的全流程。
/**
*
* <p>Title: HelloControler</p>
* <p>Description: 第一个SpringMVC 的控制器</p>
*/
@Controller
public class HelloControler {
/**
* 处理请求的控制器方法
* @return
*/
@RequestMapping("hello")
public String sayHello() {
System.out.println("控制器方法执行了");
return "success";
}
/**
* 处理请求的控制器方法
* @return
*/
@RequestMapping("hello2")
public String sayHello2() {
System.out.println("控制器方法执行了2");
return "success";
}
}
示例代码的整个执行过程如下图所示:
我们关注的是DispatcherServlet 是如何找到我们的控制器的,下图展示了代码跟踪,最终发现它是通过反射调用的。
DispatcherServlet本身也就是一个Servlet(有doGet和doPost和doService方法),调用其doService方法发现:
2. SpringMVC中三大组件详解
2.1 处理器映射器
它指的是:RequestMappingHandlerMapping,它的出现,可以让使用者更加轻松的去配置SpringMVC 的请求路径映射
。去掉了早期繁琐的xml 的配置。
它的配置有两种方式:都是在springmvc.xml 中加入配置。
第一种方式:
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
第二种方式:
<mvc:annotation-driven></mvc:annotation-driven>
在这两种方式中,第二种方式更加的完善,它可以帮我们在容器中添加很多的bean。
它起的作用是为我们建立起@RequestMapping 注解和控制器方法的对应关系。并且存在于
MappingRegistry 对象中的mappingLookup 的Map 中,该Map 是个LinkedHashMap。对应关系的建立时机是在应用加载的时候,也就是当服务器启动完成后,这些对应关系已经建立完成了。从而做到在我们访问的时候,只是从Map 中获取对应的类和方法的信息,并调用执行。
这种思想在我们连接池等技术中也经常用到!
2.2 处理器适配器
一般使用
RequestMappingHandlerAdapter
(@Controller注解的形式)
要清晰的认识SpringMVC 的处理器适配器,就先必须知道适配器以及它的作用。
我们知道适配器它是把不同的接口都转换成了USB 接口。
抽象到我们的SpringMVC 中,就是把不同的控制器,最终都可以看成是适配器类型,从而执行适配器中定义的方法。更深层次的是,我们可以把公共的功能都定义在适配器中,从而减少每种控制器中都有的重复性代码,做到分工合作。类似于接口的作用。
通过学习了SpringMVC 的执行过程,最终调用的是前端控制器DispatcherServlet 的doDispatch 方法,而该方法中的HandlerAdapter 的handle 方法实际调用了我们自己写的控制器方法。而我
们写的控制方法名称各不一样,它是通过handle 方法反射调用的。但是我们不知道的是,其实SpringMVC 中处理器适配器也有多个。
2.2.1 SimpleControllerHandlerAdapter
第一个:org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter
使用此适配器,适用的控制器写法:要求实现Controller 接口
/**
*
*/
public class HelloController2 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) throws Exception {
ModelAndView mv = new ModelAndView();
mv.setViewName("success");
return mv;
}
}
同时要求我们在springmvc.xml 中添加:
<bean id="simpleControllerHandlerAdapter " class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter">
</bean>
<bean name="/sayhello2" class="com.itheima.web.controller.HelloController2"></bean>
2.2.2 HttpRequestHandlerAdapter
第二个:org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter
使用此适配器的控制器写法:要求实现HttpRequestHandler
接口,与我们web工程是一样的,使用request来进行请求的转发。
/**
*
*/
public class HelloController3 implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
request.getRequestDispatcher("/WEB-INF/pages/success.jsp").forward(request,response);
}
}
同时要求我们在springmvc.xml 中添加:
<bean name="/sayhello3" class="com.itheima.web.controller.HelloController3"></bean>
<bean id=" httpRequestHandlerAdapter " class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"></bean>
2.2.3 使用@Controller注解
第三个:
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
这种方式也是我们实际开发中采用最多的。它的要求是我们用注解@Controller 配置控制器
使用的是RequestMappingHandlerAdapter
/**
* <p>Title: HelloControler</p>
* <p>Company: http://www.itheima.com/ </p>
*/
@Controller
public class HelloControler {
@RequestMapping("hello")
public String sayHello() {
System.out.println("控制器方法执行了");
return "success";
}
}
它的xml 中配置:
<bean id="requestMappingHandlerAdapter" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"></bean>
不过通常情况下我们都是直接配置:
<mvc:annotation-driven></mvc:annotation-driven>
2.3 视图解析器(了解即可)
我们只需要了解一下SpringMVC 中的视图即可。视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。为了实现视图模型和具体实现技术的解耦,Spring org.springframework.web.servlet 包中定义了一个高度抽象的View 接口。
作用:
渲染将处理结果通过页面展示给用户。
3. 不需要视图解析器的场景分析
在分析之前,我们先需要回顾下控制器方法的返回值,此处我们都是以注解@Controller 配置控制器为例,控制器的方法返回值其实支持三种方式:
第一种:String 类型。借助视图解析器,可以在指定位置为我们找到指定扩展名的视图。视图可以是JSP,HTML 或者其他的控制器方法上的RequestMapping 映射地址。前往指定视图的方式,默认是请求转发,可以通过redirect:前缀控制其使用重定向。
第二种:void,即没有返回值。因为我们在控制器方法的参数中可以直接使用原始SerlvetAPI 对象
HttpServletRequest 和HttpServletResponse 对象,所以无论是转发还是重定向都可以轻松实现,而无需使用返回值。
第三种:ModelAndView 类型。其实我们跟踪源码可以发现在DispatcherServlet 中的doDispatch 方法执行时,HandlerAdapter处理器适配器 的handle 方法的返回值就是ModelAndView,只有我们的控制器方法定义为void时,才不会返回此类型。当返回值是String 的时候也会创建ModelAndView 并返回。
通过上面三种控制器方法返回值,我们可以再深入的剖析一下我们请求之后接收响应的方式,其实无外乎就三种。
在我们的实际开发中,如果我们不需要页面跳转,即基于ajax 的异步请求
,用json 数据交互时,即可不配置任何视图解析器。前后端分离并且交互是通过json 数据的,利用@RequestBody
和@ResponseBody
实现数据到java对象的绑定(当然还要借助Jackson 开源框架)。
4. 请求参数封装
4.1 请求参数封装的实现原理
在使用SpringMVC 实现请求参数封装时,它支持基本类型,POJO 类型和集合类型。其封装原理其实就是使用我们原始的ServletAPI 中的方法,利用了request.getParameterValues方法并且配合反射实现的封装。
此处我们以最简单的String 和Integer 两个方法为例,带着大家把整个执行过程走一圈。
先来看控制器的方法:
/**
* <p>Title: HelloControler</p>
* <p>Company: http://www.itheima.com/ </p>
*/
@Controller
public class HelloControler {
@RequestMapping("hello")
public String sayHello(String name,Integer age) {
System.out.println("控制器方法执行了"+name+","+age);
return "success";
}
}
执行过程如下源码部分:
如图可知,通过RequstParamMethodArgumentResolver的resolveArgument方法来解析我们传过来的参数,封装到变量里面。
4.2 常用封装参数注解的使用:
4.2.1 RequestParam
首先我们要明确,我们的请求参数体现形式是什么样的。无论get/post/put/delete 请求方式,参数的体现形式都是key=value。
再来,通过上一小节我们知道,SpringMVC 是使用我们控制器方法的形参作为参数名称,再使用request 的getParameterValues
方法获取的参数。所以才会有请求参数的key 必须和方法形参变量名称保持一致的要求。
但是如果形参变量名称和请求参数的key 不一致呢?此时,参数将无法封装成功。
此时@RequestParam
注解就会起到作用,它会把该注解value 属性的值作为请求参数的key 来获取请求参数
的值,并传递给控制器方法。
@Controller
public class ParamController1 {
/**
* 处理请求的控制器方法
* @return
*/
@RequestMapping("hello")
public String sayHello(@RequestParam("username")String name,Integer age) {
System.out.println("控制器方法执行了"+name+","+age);
return "success";
}
}
4.2.2 RequestBody
我们通过源码分析得知,SpringMVC 在封装请求参数的时候,默认只会获取参数的值,而不
会把参数名称一同获取出来,这在我们使用表单提交的时候没有任何问题。因为我们的表单提交,请求参数是key=value 的。
但是当我们使用ajax 进行提交时,请求参数可能是json 格式的:{key:value}
,在此种情况
下,要想实现封装以我们前面的内容是无法实现的。此时需要我们使用@RequestBody
注解。
代码片段:
<script src="${pageContext.request.contextPath}/js/jquery.min.js"></script>
<script type="text/javascript">
$(function(){
$("#ajaxBtn").click(function(){
$.ajax({
type:"POST",
url:"${pageContext.request.contextPath}/hello2",
dataType:"text",
data:"{'name':'test','age':18}",
contentType:"application/json",
success:function(data){
alert(data);
}
});
});
})
</script>
<title>SpringMVC</title>
</head>
<body>
<button id="ajaxBtn">异步请求</button>
</body>
</html>
控制器代码片段:
@Controller
public class ParamController {
/**
* 处理请求的控制器方法
* @return
*/
@RequestMapping("hello2")
public String sayHello2(@RequestBody String body) {
System.out.println("控制器方法执行了2"+body);
return "success";
}
}
它的执行过程如下图:首先前面的执行和2.1.4 小节是一致的,在下图红框中进行参数解析时:
通过最后这个方法,我们可以看出,它是先获取的请求参数MIME 类型MediaType,然后再把整个内容获取出来,并传递给我们的控制器方法。
需要注意的是,此注解并不能为我们提供封装到pojo 的操作,它只能把请求体中全部内容获取出来,仅此而已,要想实现封装,需要借助jackson 开源框架。
4.2.3 PathVariable
它是SpringMVC 在3.0 之后新加入的一个注解,是SpringMVC 支持Restful 风格URL
的一个重要标志。
该注解的作用大家已经非常熟悉了,就是把藏在请求URL 中的参数,给我们控制器方法的形参赋值。而Restful风格的URL,在现如今的开发中使用越来越普遍了。那么它是如何实现封装的呢?请看下图:
首先还是执行到红框中解析参数这行代码,接下来执行的是AbstarctNameValueMethodArgumentResolver 这个类的方法:最后是执行PathVariableMethodArgumentResolver 类中的方法:
通过上面执行过程的全图,我们看出SpringMVC 在实现请求URL 使用占位符传参
并封装到控制器方法的形参中,是通过请求域
来实现的。最后把请求域转成一个Map,再根据形参的名称作为key,从map 中获取value,并给形参赋值。当然如果我们使用了PathVariable 注解的value 属性,则不会以形参名称为key,而是直接使用value属性的值作为key 了。
5. 拦截器的AOP 思想
AOP 思想是Spring 框架的两大核心之一,是解决方法调用依赖以及提高方便后期代码维护的重要思想。它是把我们代码中高度重复的部分抽取出来,并在适当的时机,通过代理机制来执行,从而做到不修改源码对已经写好的方法进行增强。而拦截器正式这种思想的具体实现。
拦截器代码:必须继承HandlerInterceptor
public class MyInterceptor1 implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("拦截器执行了");
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("执行了postHandle 方法");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse
response, Object handler, Exception ex) throws Exception {
System.out.println("执行了afterCompletion 方法");
}
}
它是在DispatcherServlet 中使用的,在DispatcherServlet的代码中:
去执行了HandlerExecutionChain 类中的:
而此时还没有执行我们的控制器方法,所以此时为前置增强。
在执行完调用控制方法,
之后,会执行HandlerExecutionChain 类中的:
此时是后置增强,但是此时还没有响应结果视图。
最后是HandlerExecutionChain 中的triggerAfterCompletion 方法执行:
而此方法执行时,结果视图的创建已经完成,只待展示,所以此时为最终增强。
当然我们自定义拦截器可以选择多种方式,通常我们实现HandlerInterceptor 接口,但是我们也可以选择继承HandlerInterceptorAdapter 类,而如果我们选择继承此类,则还会有HandlerExecutionChain 类中的:
6.自定义拦截器中三个方法说明及使用场景
6.1 preHandle
此方法的执行时机是在控制器方法执行之前,所以我们通常是使用此方法对请求部分进行增强。同时由于结果
视图还没有创建生成,所以此时我们可以指定响应的视图。
6.2 postHandle
此方法的执行时机是在控制器方法执行之后,结果视图创建生成之前。所以通常是使用此方法对响应部分进行
增强。因为结果视图没有生成,所以我们此时仍然可以控制响应结果。
6.3 afterCompletion
此方法的执行时机是在结果视图创建生成之后,展示到浏览器之前。所以此方法执行时,本次请求要准备的数
据具已生成完毕,且结果视图也已创建完成,所以我们可以利用此方法进行清理操作。同时,我们也无法控制响应 结果集内容。
7. 为什么不使用XML 配置SpringMVC
我们先来看基于XML 的SpringMVC 配置:
第一步:配置web.xml
第二步:编写控制器
第三步:编写springmvc.xml
第四步:配置控制器
第五步:配置处理器映射器,处理器适配器。
第六步:配置视图解析器。
其中,前3 步和第六步基于注解配置时也都有,而第四第五步注解配置只需:
<!-- 开启springmvc 对注解的支持-->
<mvc:annotation-driven></mvc:annotation-driven>
而XML 配置则需:
<!-- 实现Controller 接口-->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
<bean name="/sayhello2" class="com.itheima.web.controller.HelloController2"/>
<!-- 继承HttpRequestHandler 类-->
<bean name="/sayhello3" class="com.itheima.web.controller.HelloController3"/>
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"/>
而对比注解配置只需一个Controller 注解和一个RequestMapping 注解来比,显然注解来的更方便。
8. mvc:annotation-driven 的说明
它就相当于在xml中配置了:
<!-- Begin -->
<!-- HandlerMapping -->
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerM
apping"></bean>
<bean
class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"></bean>
<!-- HandlerAdapter -->
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerA
dapter"></bean>
<bean
class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"></bean>
<bean
class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"></bean>
<!-- HadnlerExceptionResolvers -->
<bean
class="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExcept
ionResolver"></bean>
<bean
class="org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolv
er"></bean>
<bean
class="org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver"
></bean>