SpringMVC
- 1.1 前端控制器
- 1.2 @RequestMapping
- 1.3 @PathVariable
- 1.4 请求处理
- 1.3 数据输出
- 1.4 视图解析
- 1.5 数据绑定
- 1.7 SpringMVC支持AJAX
- 1.7 文件上传与下载
- 1.8 拦截器
- 1.9 国际化
- 2.0 异常处理
- 2.1 Spring整合SpringMVC (父子容器)
- 2.2 九大组件
- DispatchServlet中的核心方法 doDispatch()
介绍SpringMVC之前,我们需要了解以下内容:
Web容器(WEB服务器)主要有:Apache、IIS、Tomcat、Jetty、JBoss、webLogic等。有了web容器,客户端只能向服务端访问静态网页。
Servlet容器:web容器,用户/客户端只能向服务器请求静态网页。如果用户想要根据自己的输入来读取网页,这种方式就不能满足用户的要求。所以web容器中又增添了Servlet容器。(Servlet容器的基本思想是在服务器端使用Java来动态生成网页。因此,Servlet容器是Web服务器和servlet进行交互的必不可少的组件。)
web容器就是管理servlet(通过servlet容器)、监听器(Listener)和过滤器(Filter)的。但它们不在Spring和SpringMVC的掌控范围里。因此,我们无法将这些类直接使用Spring注解的方式注入(无效的),因为web容器无法识别。
web容器中有servlet容器,spring项目部署后存在spring容器和springmvc容器。其中spring控制service层和dao层的bean对象。springmvc容器控制controller层bean对象。servlet容器控制servlet对象。项目启动是,首先 servlet初始化,初始化过程中通过web.xml中spring的配置加载spring配置,初始化spring容器和springmvc容器。待容器加载完成。servlet初始化完成,则完成启动。HTTP请求到达web容器后,会到达Servlet容器,容器通过分发器分发到具体的spring的Controller层。执行业务操作后返回结果。
1.1 前端控制器
web.xml配置三大组件:Servlet、Listener(监听器)、Filter(过滤器),三大组件必须配置在web.xml中。前端控制器属于Servlet,所以配置在web.xml中。
- 前端控制器可以初始化SpringMVC容器(Servlet WebApplicationContext)与Spring容器
- SpringMVC容器中配置Controller、ViewResource、HandlerMapping和其它web相关bean。
- Spring容器中配置Service、dao、datasources等
- SpringMVC容器是Spring的子容器,子容器可以调用父容器中组件,但父容器不可调用子容器中组件。通俗说,Controller可以调用Service、dao,但Service、dao不能调用Controller
容器启动的步骤
Initializing Spring FrameworkServlet ‘DispatcherServlet’ Refreshing
Refreshing WebApplicationContext for namespace ‘DispatcherServlet-servlet’
Loading XML bean definitions from class path resource [springmvc.xml]
FrameworkServlet ‘DispatcherServlet’: initialization completed in 6593 ms
加载所有bean
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<display-name>SpringMVC</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value><!--自定义的springmvc容器-->
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
如果没有自己配置SpringMVC的配置文件,会默认加载:/WEB-INF/前端控制器名-servlet.xml。比如:Could not open ServletContext resource [/WEB-INF/DispatcherServlet-servlet.xml]
1.2 @RequestMapping
url路径的要求
@Target({ElementType.METHOD, ElementType.TYPE}) //此注解可以标在方法上、类上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String[] value() default {};//指定要映射的 URL
RequestMethod[] method() default {};//指定请求方法。比如【GET】, 【POST】, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE请求
String[] params() default {};//规定携带的请求参数。
String[] headers() default {};//规定请求头
String[] consumes() default {};//规定请求头中的Conyent-Type
String[] produces() default {};//给响应头中加上Content-Type
}
属性介绍
value
url请求映射。
普通的url
ANT风格的URL
- /url/*/user :匹配的URL路径有 /url/aaa/user /url/bbb/user 等。
- /url/**/user :匹配的URL路径有 /url/aaa/bbb/user /url/user 等。
- /url/user?? :匹配的URL路径有 /url/useraa /url/userbb 等。
什么是ANT风格?
?
:匹配文件名中的一个字符*
:匹配文件名中的任意字符**
:匹配多层路径
REST风格的URL(重点)
什么是REST?
资源(Resources):网络上的一个实体。它可以是一段文本、一张图片、一首歌曲等。每种资源对应一个特定的URL(统一资源定位符)。要想获得这个资源,只需访问它的URL即可,因此URL即为每一个资源的独一无二的识别符。
表现层(Representation):把资源具体呈现出来的形式。比如文本用txt格式表现,或者HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式。
转态转化(State Transfer):每发出一次请求,就代表客户端和服务器的一次交互过程。HTTP协议,是一个无状态协议,即所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转化”。而这种转化是建立在表现层上的,所以就是“表现层状态转化”。具体说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源,PUT用来更新资源,DELETE用来删除资源。
在这里插入代码片
method:规定请求方法
限定请求方式。默认接收所有请求,一旦指定请求,那么只接受该请求。
@Controller
public class HelloWorld {
@RequestMapping(value="hello",method=RequestMethod.POST)
public String hello(){
System.out.println("hello");
return "success";
}
}
当我使用GET发送请求时,会报错:Request method ‘GET’ not supported
params:规定请求参数
params和headers支持简单的表达式:
- param1:表示请求必须包含名为param1的请求参数。
- !param1:表示请求不能包含名为param1的请求参数
- param1 != value1:表示请求必须包含名为param1的请求参数,且值不能为value1
- {“param1 = value1”,“param2”}:表示请求必须包含param1与param2的请求参数,且param1的值必须为value1
@Controller
public class HelloWorld {
@RequestMapping(value="hello",params="name")
public String hello(){
System.out.println("hello");
return "success";
}
}
访问:http://localhost:8080/SpringMVC/hello?name
上述params规定必须有name的请求参数。如果不写name=xxx,则默认name是空串。
@Controller
public class HelloWorld {
@RequestMapping(value="hello",params="name!=123")
public String hello(){
System.out.println("hello");
return "success";
}
}
eg:访问http://localhost:8080/SpringMVC/hello ?name 或 http://localhost:8080/SpringMVC/hello ?
上述params规定必须有name的请求参数,且不能等于123。
?name:说明带了name请求参数,且是空串(空串不等于123)。
?:说明带了name请求参数,且值为null (null不等于123)
headers:规定请求头
HTTP请求头(不完整,具体以后查阅)
:authority: blog.csdn.net
:method: GET
:path: /u012373281/article/details/91350377
:scheme: https
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 //告诉服务器,客户端支持的数据类型
accept-encoding: gzip, deflate, br //告诉服务器,客户机支持的数据压缩格式。
accept-language: zh-CN,zh;q=0.9 //告诉服务器,客户机的语言环境。
cache-control: max-age=0
cookie: uuid_tt_dd=10_6042550560-1582025718682-680062; dc_session_id=10_1582025718682.654004; //客户机通过这个头告诉服务器,可以向服务器带数据。
sec-fetch-dest: document
sec-fetch-mode: navigate
sec-fetch-site: cross-site
sec-fetch-user: ?1
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36 //客户机通过这个头告诉服务器,客户机的软件环境(这里是Chrome)。
eg:通过请求头中的user-agent,让Chrome浏览器可以访问,不让360访问。
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36 //这是360的user-agent
下面代码,规定请求头中的User-Agent为360的,所有360可以访问 /hello,而Chrome不可以访问。
@Controller
public class HelloWorld {
@RequestMapping(value="/hello",headers={"User-Agent=Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"})
public String hello(){
System.out.println("hello");
return "success";
}
}
1.3 @PathVariable
Spring 3.0 新增功能,带占位符的URL。
通过@PathVariable可以将URL中占位符参数绑定到控制器处理方法的入参中:URL中的{xxx}占位符可以通过@PathVariable(“xxx”)绑定到操作方法的入参中。
@Controller
public class HelloWorld {
@RequestMapping("/hello/{id}")
public String hello(@PathVariable("id") String id){
System.out.println(id);
return "success";
}
}
- 请求URL上的占位符必须被替换。eg:http://localhost:8080/SpringMVC/hello /ss12。
- 上面请求URL路径为两层,所以多一层少一层都是404。eg:http://localhost:8080/SpringMVC/hello/
1.4 请求处理
处理url上?
后携带的参数,无论使用哪一种,都是由Controller层的方法的形参接收参数。
@RequestParam
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
String value() default "";//指定要获取参数的key
boolean required() default true;//参数是否是必须的。默认是必须的
String defaultValue() default ValueConstants.DEFAULT_NONE;//指定默认值,没指定之前默认为null
}
我们以前通过request.getParameter(“xxxx”);获取请求参数。而在SpringMVC中,只需要使用方法的入参接收请求参数(前提是请求参数名与方法的入参名相同)
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(String name){
System.out.println(name);
return "success";
}
}
- http://localhost:8080/SpringMVC/hello?name=ab12 :输出 ab12
- http://localhost:8080/SpringMVC/hello?name :输出 空串
- http://localhost:8080/SpringMVC/hello? :输出 null(不使用注解情况下会有)
使用注解@RequestParam
@RequestParam中的value值必须与请求参数名保持相同。此时方法的入参名随意,不必与请求参数名保持相同。
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(@RequestParam("name") String n){//n = request.getParameter("name");
System.out.println(n);
return "success";
}
}
- http://localhost:8080/SpringMVC/hello?name=26s :输出:26s
- http://localhost:8080/SpringMVC/hello?name :输出:空串
- http://localhost:8080/SpringMVC/hello? :不满足,404
针对上面404:hello(@RequestParam(value=“name”,required=false) String n) 访问:http://localhost:8080/SpringMVC/hello? 输出 null;如果加了defaultValue=“123”,则不输出null而输出123
@RequestHeader
与@RequestParam中方法的含义相同。
@Target(ElementType.PARAMETER)//参数上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestHeader {
String value() default "";//HTTP请求头的key
boolean required() default true;//是否是必须的
String defaultValue() default ValueConstants.DEFAULT_NONE;//默认值
}
我们以前通过request.getHeader(“User-Agent”);获取浏览器信息。
在SpringMVC中使用@RequestHeader获取请求头信息。
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(@RequestHeader("User-Agent") String userAgent ){//userAgent = request.getHeader("User-Agent");
System.out.println(userAgent);
return "success";
}
}
@CookieValue
获取请求中带来的某个Cookie的值。
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CookieValue {
String value() default "";//指定要获取的Cookie名
boolean required() default true;//指定Cookie是否是必须的。默认是必须的,访问如果没有获取Cookie,500
String defaultValue() default ValueConstants.DEFAULT_NONE;//在required=false的前提下,默认值为null,我们可以指定自己的默认值
}
以前获取某个cookie的值,需要获取所有的cookie,然后遍历取出自己想要的cookie值,比如获取JSESSIONID的值:
Cookie[] cookies = request.getCookies();
for(Cookie c : cookies ){
if(c.getName().equals("JSESSIONID")){
String cvalue = c.getValue();
}
}
SpringMVC提供 @CookieValue注解来获取指定的Cookie值,如下示例获取JSESSIONID的值:
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(@CookieValue("JSESSIONID") String jid){
System.out.println(jid);
return "success";
}
}
传入POJO
SpringMVC会按请求参数名和POJO属性名进行自动匹配,自动为该对象填充属性值。支持级联属性。
<!--
错误写法:action="/hello" :localhost:8080/hello
正确写法:action="hello":localhost:8080/SpringMVC/hello
-->
<form action="hello" method="post">
<input type="text" name="name"/>
<input type="text" name="age"/>
<input type="text" name="address.addr"/> <!--级联封装-->
<input type="submit" value="提交"/>
</form>
public class User {
private String name;
private Integer age;
private Address address;
//getter()\setter()方法 eg:setName(String name)
//toString()方法
}
public class Address {
private String addr;
//getter()\setter()方法
//toString()方法
}
SpringMVC如何将表单中数据封装成POJO?(重点),解释如下:
- 上面表单中
<input type="text" name="name"/>
,那么SpringMVC就会调用user.setName()方法注入值。 - 如果表单中是
<input type="text" name="name01"/>
,那么SpringMVC会调用user.setName01()方法注入值,但POJO中没有这个方法,所有不能注入。如果setName(String name)改为setName01(String name)就可以注入值了,但是这不是标准的setter方法,所有不这么写。
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(User user){
System.out.println(user);
return "success";
}
}
传入原生API
- HttpServletRequest
- HttpServletResponse
- HttpSession
- java.security.Principal
- Locale
- InputStream
- OutputStream
- Reader
- Writer
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(HttpServletRequest request,HttpSession session){
request.setAttribute("request", "我是Request域中的数据");
session.setAttribute("session", "我是session域中的数据");
System.out.println();
return "success";
}
}
<body>
${requestScope.request }
${sessionScope.session }
</body>
解决乱码问题
提交的数据可能乱码,分两种情况:
① 请求乱码
- GET请求:改tomcat中的server.xml,在8080端口处加上URIEncoding=“UTF-8” :
<Connector URIEncoding="UTF-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
在SpringMVC中,Controller没有获取请求参数的选项,所以需要自己写一个Filter(这里SpringMVC给我们提供了一个Filter:org.springframework.web.filter.CharacterEncodingFilter
),既然是Filter,就需要在web.xml中配置。
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param> <!--encoding:指定解决POST乱码-->
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<!--forceEncoding:解决响应乱码 response.setCharacterEncoding(this.encoding);-->
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
web.xml中的Filter是有顺序的,从上至下依次执行。CharacterEncodingFilter一般在其它Filter之前。
- POST请求:在第一次请求参数之前设置:
request.setCharacterEncoding("UTF-8");
② 响应乱码:
response.setContentType("text/html;charset=utf-8");
1.3 数据输出
1.3.1 Map、Model、ModelAndMap
1.3.2 ModelAndView
1.3.3 @SessionAttributes(不推荐使用)
不推荐使用,给session中放数据,还是推荐使用原生API
1.3.4 @ModelAttribute(现在不使用了)
1.4 视图解析
1.4.1 forward
- 页面转发,可以转发到页面,也可以转发到 handler 。不会用我们配置的视图解析器拼串。
- 一定要加上 / ,表示当前项目下。
- 转发不会改变路径:http://localhost:8080/SpringMVC /hello --> http://localhost:8080/SpringMVC /hello ,但已经到biaodan.jsp页面。
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(){
System.out.println("ss");
return "forward:/biaodan.jsp";
}
}
请求hello02,转发到hello,再转发到biaodan.jsp。
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(){
System.out.println("ss");
return "forward:/biaodan.jsp";
}
@RequestMapping("/hello02")
public String hello02(){
System.out.println("ss");
return "forward:/hello";
}
}
1.4.2 redirect
- 原生的servlet重定向需要加上项目名才能成功,而SpringMVC会自动为我们拼上项目名。
response.sendRedirect("/SpringMVC/hello.jsp")
- 重定向不会用我们自己配置的视图解析器进行拼串。
- 重定向会改变路径。http://localhost:8080/SpringMVC /hello --> http://localhost:8080/SpringMVC /biaodan.jsp
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(){
System.out.println("ss");
return "redirect:/biaodan.jsp";
}
}
请求hello02,重定向到hello,再重定向到biaodan.jsp。
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(){
System.out.println("ss");
return "redirect:/biaodan.jsp";
}
@RequestMapping("/hello02")
public String hello02(){
System.out.println("ss");
return "redirect:/hello";
}
}
1.5 数据绑定
<mvc:annotation-driven />
- 会自动注册RequestMappingHandlerMapping、RequestMappingHandlerAdapter与ExceptionHandlerExceptionResolver三个Bean。
- 支持使用ConversionService实例对表单参数进行类型转换
- 支持使用 @NumberFormat annotation、@Date TimeFormat注解完成数据类型的格式化
- 支持使用 @Valid注解对JavaBean实例进行JSR 303验证
- 支持使用 @RequestBody 和 @ResponseBody注解
只要请求不好使就召唤mvc:annotation-driven:
一、<mvc:defalut-servlet-handler>
<mvc:annotation-driven />
现象:
- 1、两个注解都没配:动态资源(@RequestMapping映射的资源可以访问,静态资源(.html,.js,.img)不能访问)。
- 2、只加上
<mvc:defalut-servlet-handler>
注解,静态资源可以访问,但动态资源不可以访问。 - 3、两个注解同时使用,动态资源、静态资源都可以被访问。
数据绑定的过程中,会出现数据类型转换、数据格式化、数据校验等问题。
1.5.1 类型转换
1.5.2 数据格式化
1.5.3 数据校验
数据校验是数据绑定过程中的一个步骤,所以,我们还是需要数据绑定中的两个注解。这样数据校验才生效。
只做前端校验是不安全的(我们可以绕过前端验证,比如禁用JS等)。所以,我们需要在重要数据上使用后端校验:
- 第一种方式:可以将每一个数据取出来进行校验,如果失败直接来到添加页面,提示重新填写 (×)
- 第二种方式:SpringMVC中,使用JSR303做数据校验。—>我们下面使用JSR303的实现Hibernate Validator(第三方校验框架)
JSR303 是Java为Bean数据合法性校验提供的标准框架,它已经包含在JavaEE 6.0中。
JSR303 通过在Bean属性上标注类似于@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。
注解 | 功能说明 |
---|---|
@Null | 被注释的元素必须为null |
@NotNull | 被注释的元素必须不为null |
@AssertTrue | 被注释的元素必须为true |
@AssertFalse | 被注释的元素必须为false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max,min) | 被注释的元素的大小必须在指定的范围内 |
@Digits(integer,fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个过去的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
Hibernate validator 是JSR 303的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解:
注解 | 功能说明 |
---|---|
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串必须是非空 |
@Range | 被注释的元素必须在合适的范围内 |
如何快速的进行后端校验(以Hibernate Validator为例)?
- 导入校验框架的JAR包。但不导入带 el 的jar包,因为Toncat7.0以上有,所以避免冲突,我们选择不导入。
- classmate-0.8.0.jar
- hibernate-validator-5.0.0.CR2.jar
- hibernate-validator-annotation-processor-5.0.0.CR2.jar
- jboss-logging-3.1.1.GA.jar
- validation-api-1.1.0.CR1.jar
- 给JavaBean的属性上加上校验注解
public class User {
@NotEmpty
private String name;
@Max(100)
private Integer age;
private Address address;
//getter()\setter()方法
}
- 告诉SpringMVC哪个JavaBean需要校验(@Valid)
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(@Valid User user){
System.out.println(user);
return "redirect:/biaodan.jsp";
}
}
- 获取校验结果
- BindingResult一定是紧跟在被验证的JavaBean之后,中间不能有其他参数。
- BindingResult中有引发的异常信息等。封装了校检结果。
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(@Valid User user,BindingResult bindingResult){
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
System.out.println(fieldError);
}
if(bindingResult.hasErrors()){
return "redirect:/index.jsp";
}else{
return "redirect:/biaodan.jsp";
}
}
}
- 页面中获取校验错误信息
我们可以将错误信息放在请求域中,比如Model、Map、ModelAndMap。然后在页面通过请求域获取。
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(@Valid User user,BindingResult bindingResult,Model model){
Map<String,Object> errorMap = new HashMap<>();
if(bindingResult.hasErrors()){
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
System.out.println(fieldError.getField());
System.out.println(fieldError.getDefaultMessage());
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
}
model.addAttribute("errorInfo", errorMap);
return "forward:/index.jsp"; //使用redirect:/index.jsp不行,解释如下
}else{
return "redirect:/biaodan.jsp";
}
}
}
<body>
<form action="hello" method="post">
<input type="text" name="name"/> --> ${errorInfo.name }<br/>
<input type="text" name="age"/> --> ${errorInfo.age }<br/>
<input type="text" name="address.addr"/><br/>
<input type="submit" value="提交"/>
</form>
</body>
遇到的小问题:重定向获取不到请求域中的数据。
原因:重定向向服务器发送两次请求。第一次向服务器发送/hello
请求我们将errorMap
放入请求域中。但当重定向的时候,我们向服务器又发送一次请求,但不是/hello
请求,所以请求域中没有了errorMap
。
如果使用转发就可以获取请求域中的值,因为转发只向服务器发送一次请求。
1.7 SpringMVC支持AJAX
1.7 文件上传与下载
1.8 拦截器
Spring MVC的处理器拦截器类似于Servlet开发中的过滤器Filter。区别如下:
- 过滤器是servlet规范,任何java web工程都可以使用。而拦截器是SpringMVC框架自己的,只有使用了SpringMVC框架才能用。
- 过滤器在url-pattern中配置了/* 之后,会拦截所有要访问的资源。而拦截器只会拦截要访问的控制器方法,不会拦截jsp、html、css、image、js等
- 处理器也是AOP思想的具体应用。
public interface HandlerInterceptor {
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception; //目标方法之前执行; return false不放行,return true放行
void postHandle(
HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception; // 目标方法之后运行
void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception;// 在页面之后执行(此页面可能是目标页面,也可能是错误页面)
}
具体使用示例
- 实现接口
public class MyInterceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// TODO Auto-generated method stub
System.out.println("preHandle...");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
System.out.println("postHandle....");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// TODO Auto-generated method stub
System.out.println("afterCompletion....");
}
}
- 在springmvc.xml配置自己的拦截器
<mvc:interceptors>
<!--不在<mvc:interceptor>里,默认拦截所有Controller请求-->
<bean class="com.shang.interceptor.MyInterceptor1"></bean>
<!--在<mvc:interceptor>里,指定通过指定的拦截器拦截指定的请求-->
<!--
<mvc:interceptor>
<mvc:mapping path="/hello"/> <==> mvc:mapping一定配在bean前面
<bean class="com.shang.interceptor.MyInterceptor1"></bean>
</mvc:interceptor>
-->
</mvc:interceptors>
- 页面
<body>
<%
System.out.println("正常页面");
%>
</body>
- 测试
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(){
System.out.println("hello");
return "biaodan";
}
}
- 执行结果:
preHandle...
hello
postHandle....
正常页面
afterCompletion....
- 总结
- 上面的结果就是执行顺序,一旦preHandle不放行,那后面就都不会执行。
- 如果preHandle放行,但handler方法(目标方法)出现错误,那么后面只有afterCompletion会被执行。
<mvc:interceptors>
<bean class="com.shang.interceptor.MyInterceptor1"></bean>
<bean class="com.shang.interceptor.MyInterceptor2"></bean>
</mvc:interceptors>
多个拦截器,当preHandle不放行时的情况:
- 当拦截器01的preHandle不放行时
preHandle01… - 当拦截器02的preHandle不放行时
preHandle01…
preHandle02…
afterCompletion01…
1.9 国际化
2.0 异常处理
2.1 Spring整合SpringMVC (父子容器)
我们先看一个最简单的整合,但是这个整合是最蠢的,只是让你过度一下。
- web.xml(没有变化)
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<display-name>SpringMVC</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<!-- 父子容器整合的时候才加上。
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
-->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
- springmvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<!--可以合并配置文件,缺点:只有一个IOC容器-->
<import resource="classpath:spring.xml"/>
<context:component-scan base-package="com.shang.controller"/>
<mvc:default-servlet-handler/>
<mvc:annotation-driven></mvc:annotation-driven>
<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
</beans>
好了,上面就已经整合好了。但是,这是优缺点了,这是将两个容器整合为一个大容器,一旦大容器不行了,其余都会挂掉。所以,这是很low的,我们下面介绍父子容器。(分容器整合)
- web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<display-name>SpringMVC</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<!--整合了Spring容器 <context-param> <listener>-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
好了,上面就已经整合ok了。细节方面如下:
SpringMVC与Spirng整合的目的:分工明确
- SpringMVC的配置文件只配置和网站转发逻辑(Controller层)以及网站功能有关的(视图解析器,文件上传解析器,支持ajax等等)
- Spring的配置文件配置和业务有关的(Service层、dao层,事务控制、数据源、xxx)
- 注意点:Spring是父容器,SpringMVC是子容器,子容器可以调用父容器中的组件,但父容器不可以调用子容器的组件。
=============================== 源码分析===================================
2.2 九大组件
2.2.1 HandlerMapping
它的作用是根据request找到相应的处理器Handler和Interceptors,HandlerMapping接口里面只有一个方法。
public interface HandlerMapping {
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
从继承图可以看出HandlerMapping家族的成员可以分为两支,一支继承AbstractUrlHandlerMapping,另一支继承AbstractHandlerMethodMapping,而这两个支都继承自抽象类AbstractHandlerMapping。
所以首先分析AbstractHandlerMapping,然后分别分析AbstractUrlHandlerMapping和AbstractHandlerMethodMapping两个系列
2.2.1.1 AbstractHandlerMapping
AbstractHandlerMapping采用了模板方法模式设计了HandlerMapping实现的整体结构,子类只需要通过模板方法提供一些初始值或者具体的算法即可。将AbstractHandlerMapping分析透对整个HandlerMapping实现方式的理解至关重要。在Spring MVC中有很多组件都是采用的这种模式——首先使用一个抽象实现采用模板模式进行整体设计,然后在子类通过实现模板方法具体完成业务,所以在分析Spring MVC源码的过程中尤其要重视对组件接口直接实现的抽象实现类的分析。
模板方法模式的定义如下:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。
HandlerMapping的作用是根据request查找Handler和Interceptors。获取Handler的过程通过模板方法getHandlerInternal交给了子类。AbstractHandlerMapping中保存了所用配置的Interceptor,在获取到Handler后会自己根据从request提取的lookupPath将相应的Interceptors装配上去,当然子类也可以通过getHandlerInternal方法设置自己的Interceptor,getHandlerInternal的返回值为Object类型。
AbstractHandlerMapping的创建其实就是初始化这三个Interceptor.
- Interceptors:用于配置Spring MVC的拦截器,有两种设置方式:①注册Handler-Mapping时通过属性设置;②通过子类的extendInterceptors钩子方法进行设置。Interceptors并不会直接使用,而是通过initInterceptors方法按类型分配到mapped-Interceptors和adaptedInterceptors中进行使用,Interceptors只用于配置。
- mappedInterceptors:此类Interceptor在使用时需要与请求的url进行匹配,只有匹配成功后才会添加到getHandler的返回值HandlerExecutionChain里。它有两种获取途径:从interceptors获取或者注册到spring的容器中通过detectMappedInterceptors方法获取。
- adaptedInterceptors:这种类型的Interceptor不需要进行匹配,在getHandler中会全部添加到返回值HandlerExecutionChain里面。它只能从interceptors中获取。
protected void initApplicationContext() throws BeansException {
extendInterceptors(this.interceptors);//是模板方法,用于给子类提供一个添加(或者修改)Interceptors的入口,不过在现有Spring MVC的实现中并没有使用。
detectMappedInterceptors(this.mappedInterceptors);//将Spring MVC容器及父容器中的所有MappedInterceptor类型的Bean添加到mappedInterceptors属性
initInterceptors();//将interceptors属性里所包含的对象按类型添加到mappedInterceptors或者adaptedInterceptors
}
HandlerMapping是通过getHandler方法来获取处理器Handler和拦截器Interceptor的,下面看一下在AbstractHandlerMapping中的实现方法。
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = getHandlerInternal(request);//模板方法,由具体的子类实现。获取handler
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = getApplicationContext().getBean(handlerName);
}
return getHandlerExecutionChain(handler, request);
}
可以看到getHandler方法的实现共分两部分,getHandlerExecutionChain之前是找Handl-er,getHandlerExecutionChain方法用于添加拦截器。
找Handler的过程是这样的:
- 通过getHandlerInternal(request)方法获取,这是个模板方法,留给子类具体实现(这也是其子类主要做的事情)。
- 如果没有获取到则使用默认的Handler,默认的Handler保存在AbstractHandler-Mapping的一个Object类型的属性defaultHandler中,可以在配置HandlerMapping时进行配置,也可以在子类中进行设置。
- 如果找到的Handler是String类型,则以它为名到Spring MVC的容器里查找相应的Bean。
下面再来看一下getHandlerExecutionChain方法。
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain =(handler instanceof HandlerExecutionChain)
? (HandlerExecutionChain) handler : new HandlerExecutionChain(handler);
chain.addInterceptors(getAdaptedInterceptors());
String lookupPath = urlPathHelper.getLookupPathForRequest(request);
for (MappedInterceptor mappedInterceptor : mappedInterceptors) {
if (mappedInterceptor.matches(lookupPath, pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
}
return chain;
}
这个方法也非常简单,首先使用handler创建出HandlerExecutionChain类型的变量,然后将adaptedInterceptors和符合要求的mappedInterceptors添加进去,最后将其返回。
2.2.1.2 AbstractUrlHandlerMapping系列
AbstractUrlHandlerMapping
AbstractUrlHandlerMapping系列都继承自AbstractUrlHandlerMapping,从名字就可以看出它是通过url来进行匹配的。此系列大致原理是将url与对应的Handler保存在一个Map中,在getHandlerInternal方法中使用url从Map中获取Handler,AbstractUrlHandlerMapping中实现了具体用url从Map中获取Handler的过程,而Map的初始化则交给了具体的子孙类去完成。这里的Map就是定义在AbstractUrlHandlerMapping中的handlerMap,另外还单独定义了处理“/”请求的处理器rootHandler,定义如下:
private Object rootHandler;
private final Map<String, Object> handlerMap = new LinkedHashMap<String, Object>();
下面看一下具体是怎么获取Handler的,以及这个Map是怎么创建的。
前面讲过获取Handler的入口是getHandlerInternal方法,它在AbstractUrlHandlerMapping中代码如下:
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
Object handler = lookupHandler(lookupPath, request);
if (handler == null) {
// We need to care for the default handler directly, since we need to
// expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
Object rawHandler = null;
if ("/".equals(lookupPath)) {
rawHandler = getRootHandler();
}
if (rawHandler == null) {
rawHandler = getDefaultHandler();
}
if (rawHandler != null) {
// Bean name or resolved handler?
if (rawHandler instanceof String) {
String handlerName = (String) rawHandler;
rawHandler = getApplicationContext().getBean(handlerName);
}
validateHandler(rawHandler, request);
handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
}
}
if (handler != null && logger.isDebugEnabled()) {
logger.debug("Mapping [" + lookupPath + "] to " + handler);
}
else if (handler == null && logger.isTraceEnabled()) {
logger.trace("No handler mapping found for [" + lookupPath + "]");
}
return handler;
}
buildPathExposingHandler方法用于给查找到的Handler注册两个拦截器PathExposingHandlerInterceptor和UriTemplateVariablesHandlerInterceptor,这是两个内部拦截器,主要作用是将与当前url实际匹配的Pattern、匹配条件(后面介绍)和url模板参数等设置到request的属性里,这样在后面的处理过程中就可以直接从request属性中获取,而不需要再重新查找一遍了,代码如下:
protected Object buildPathExposingHandler(Object rawHandler, String bestMatchingPattern,
String pathWithinMapping, Map<String, String> uriTemplateVariables) {
HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler);
chain.addInterceptor(new PathExposingHandlerInterceptor(bestMatchingPattern, pathWithinMapping));
if (!CollectionUtils.isEmpty(uriTemplateVariables)) {
chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables));
}
return chain;
}
在buildPathExposingHandler方法中给Handler注册两个内部拦截器PathExposingHandler-Interceptor和UriTemplateVariablesHandlerInterceptor,这两个拦截器分别在preHandle中调用了exposePathWithinMapping和exposeUriTemplateVariables方法将相应内容设置到了request的属性。下面介绍Map的初始化,它通过registerHandler方法进行,这个方法承担AbstractUrlHandler-Mapping的创建工作,不过和之前的创建不同的是这里的registerHandler方法并不是自己调用,也不是父类调用,而是子类调用。这样不同的子类就可以通过注册不同的Handler将组件创建出来。这种思路也值得我们学习和借鉴。AbstractUrlHandlerMapping中有两个registerHandler方法,第一个方法可以注册多个url到一个处理器,处理器用的是String类型的beanName,这种用法在前面已经多次见到过,它可以使用beanName到spring的容器里找到真实的bean做处理器。在这个方法里只是遍历了所有的url,然后调用第二个registerHandler方法具体将Handler注册到Map上。第二个registerHandler方法的具体注册过程也非常简单,首先看Map里原来有没有传入的url,如果没有就put进去,如果有就看一下原来保存的和现在要注册的Handler是不是同一个,如果不是同一个就有问题了,总不能相同的url有两个不同的Handler吧(这个系列只根据url查找Handler)!这时就得抛异常。往Map里放的时候还需要看一下url是不是处理“/”或者“/*”,如果是就不往Map里放了,而是分别设置到rootHandler和defaultHandler。具体代码如下:
protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
Assert.notNull(urlPaths, "URL path array must not be null");
for (String urlPath : urlPaths) {
registerHandler(urlPath, beanName);
}
}
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
Assert.notNull(urlPath, "URL path must not be null");
Assert.notNull(handler, "Handler object must not be null");
Object resolvedHandler = handler;
// Eagerly resolve handler if referencing singleton via name.
if (!this.lazyInitHandlers && handler instanceof String) {
String handlerName = (String) handler;
if (getApplicationContext().isSingleton(handlerName)) {
resolvedHandler = getApplicationContext().getBean(handlerName);
}
}
Object mappedHandler = this.handlerMap.get(urlPath);
if (mappedHandler != null) {
if (mappedHandler != resolvedHandler) {
throw new IllegalStateException(
"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
}
}
else {
if (urlPath.equals("/")) {
if (logger.isInfoEnabled()) {
logger.info("Root mapping to " + getHandlerDescription(handler));
}
setRootHandler(resolvedHandler);
}
else if (urlPath.equals("/*")) {
if (logger.isInfoEnabled()) {
logger.info("Default mapping to " + getHandlerDescription(handler));
}
setDefaultHandler(resolvedHandler);
}
else {
this.handlerMap.put(urlPath, resolvedHandler);
if (logger.isInfoEnabled()) {
logger.info("Mapped URL path [" + urlPath + "] onto " + getHandlerDescription(handler));
}
}
}
}
我们现在就把整个AbstractUrlHandlerMapping系列的原理分析完了,AbstractUrl-Handler-Mapping里面定义了整体架构,子类只需要将Map初始化就可以了。下面来看子类具体是怎么做的。有两个类直接继承AbstractUrlHandlerMapping,分别是SimpleUrlHandlerMapping和AbstractDetectingUrlHandlerMapping。
SimpleUrlHandlerMapping
SimpleUrlHandlerMapping定义了一个Map变量(自己定义一个Map主要有两个作用,第一是方便配置,第二是可以在注册前做一些预处理,如确保所有url都以“/”开头),将所有的url和Handler的对应关系放在里面,最后注册到父类的Map中;而AbstractDetectingUrlHandlerMapping则是将容器中的所有bean都拿出来,按一定规则注册到父类的Map中。下面分别来看一下。
SimpleUrlHandlerMapping在创建时通过重写父类的initApplicationContext方法调用了registerHandlers方法完成Handler的注册,registerHandlers内部又调用了AbstractUrlHandlerMapping的registerHandler方法将我们配置的urlMap注册到AbstractUrlHandler-Mapping的Map中(如果要使用SimpleUrlHandlerMapping就需要在注册时给它配置urlMap),代码如下:
//将所有的 url和 Handler 的对应关系放在里面
private final Map<String, Object> urlMap = new HashMap<String, Object>();
protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
if (urlMap.isEmpty()) {
logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping");
}
else {
for (Map.Entry<String, Object> entry : urlMap.entrySet()) {
String url = entry.getKey();
Object handler = entry.getValue();
// 确保所有url都以“/”开头
if (!url.startsWith("/")) {
url = "/" + url;
}
// Remove whitespace from handler bean name.
if (handler instanceof String) {
handler = ((String) handler).trim();
}
registerHandler(url, handler);
}
}
}
SimpleUrlHandlerMapping类非常简单,就是直接将配置的内容注册到了AbstractUrlHandlerMapping中。
AbstractDetectingUrlHandlerMapping
AbstractDetectingUrlHandlerMapping也是通过重写initApplicationContext来注册Handler的,里面调用了detectHandlers方法,在detectHandlers中根据配置的detectHand-lersInAn-cestorContexts参数从Spring MVC容器或者Spring MVC及其父容器中找到所有bean的beanName,然后用determineUrlsForHandler方法对每个beanName解析出对应的urls,如果解析结果不为空则将解析出的urls和beanName(作为Handler)注册到父类的Map,注册方法依然是调用AbstractUrlHandlerMapping的registerHandler方法。使用beanName解析urls的determineUrlsForHandler方法是模板方法,交给具体子类实现。AbstractDetectingUrlHandlerMapping类非常简单,代码如下:
protected void detectHandlers() throws BeansException {
if (logger.isDebugEnabled()) {
logger.debug("Looking for URL mappings in application context: " + getApplicationContext());
}
String[] beanNames = (this.detectHandlersInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
getApplicationContext().getBeanNamesForType(Object.class));
// Take any bean name that we can determine URLs for.
for (String beanName : beanNames) {
String[] urls = determineUrlsForHandler(beanName);
if (!ObjectUtils.isEmpty(urls)) {
// URL paths found: Let's consider it a handler.
registerHandler(urls, beanName);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Rejected bean name '" + beanName + "': no URL paths identified");
}
}
}
}
BeanNameUrlHandlerMapping是检查beanName和alias是不是以“/”开头,如果是则将其作为url。
public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping {
@Override
protected String[] determineUrlsForHandler(String beanName) {
List<String> urls = new ArrayList<String>();
if (beanName.startsWith("/")) {
urls.add(beanName);
}
String[] aliases = getApplicationContext().getAliases(beanName);
for (String alias : aliases) {
if (alias.startsWith("/")) {
urls.add(alias);
}
}
return StringUtils.toStringArray(urls);
}
}
AbstractControllerUrlHandlerMapping是将实现了Controller接口或者注释了@Controller的bean作为Handler,并且可以通过设置excludedClasses和excludedPackages将不包含的bean或者不包含的包下的所有bean排除在外,这里的determineUrlsForHandler方法主要负责将符合条件的Handler找出来,而具体用什么url则使用模板方法buildUrlsForHandler交给子类去做,代码如下(省略了打印日志代码):
@Override
protected String[] determineUrlsForHandler(String beanName) {
Class<?> beanClass = getApplicationContext().getType(beanName);
if (isEligibleForMapping(beanName, beanClass)) {
return buildUrlsForHandler(beanName, beanClass);
}
else {
return null;
}
}
protected boolean isEligibleForMapping(String beanName, Class<?> beanClass) {
String beanClassName = beanClass.getName();
for (String packageName : this.excludedPackages) {
if (beanClassName.startsWith(packageName)) {
if (logger.isDebugEnabled()) {
logger.debug("Excluding controller bean '" + beanName + "' from class name mapping " +
"because its bean class is defined in an excluded package: " + beanClass.getName());
}
return false;
}
}
return isControllerType(beanClass);
}
protected boolean isControllerType(Class<?> beanClass) {
return this.predicate.isControllerType(beanClass);//是不是实现了Controller接口或者标注了@Controller注解
}
它有两个子类ControllerClassNameHandlerMapping和ControllerBeanNameHandlerMapping,从名称就可以看出来一个使用className作为url,另一个使用spring容器中的beanName作为url,具体代码不再列出。AbstractUrlHandlerMapping系列就分析完了,层次非常清晰,我们来回顾一下:首先AbstractUrlHandlerMapping中设计了整体的结构,并完成了查找Handler的具体逻辑,其中需要用到一个保存url和Handler对应关系的Map,这个Map的内容是留给子类初始化的,这里提供了注册(也就是初始化Map)的工具方法registerHandler。初始化Map时分了两种实现方式,一种是通过手工在配置文件里注册,另一种是在spring的容器里面找,第二种方式需要将容器里的bean按照特定的需求筛选出来,并解析出一个url,所以又根据这两个需求增加了两层子类。
2.2.1.3 AbstractHandlerMethodMapping系列
从结构图可以看出,AbstractHandlerMethodMapping系列只要三个类。虽然看起来这里的结构要比UrlHandlerMapping简单很多,但是并没有因为类结构简单而降低复杂度。
AbstractHandlerMethodMapping系列是将Method作为Handler来使用的,这也是我们现在用得最多的一种Handler,比如经常使用的 @RequestMapping所注释的方法就是这种Handler,它专门有一个类型——HandlerMethod,也就是Method类型的Handler。
- 要想弄明白AbstractHandlerMethodMapping系列,最关键的是要先弄明白AbstractHan-dlerMethodMapping里三个Map的含义,它们定义如下(注:nameMap可能已经被删除了):
private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();
private final MultiValueMap<String, T> urlMap = new LinkedMultiValueMap<String, T>();
泛型T在spring官方的解释是“The mapping for a HandlerMethod containing theconditions needed to match the handler method to incoming request”也就是用来代表匹配Handler的条件专门使用的一种类,这里的条件就不只是url了,还可以有很多其他条件,如request的类型(Get、Post等)、请求的参数、Header等都可以作为匹配HandlerMethod的条件。默认使用的是RequestMappingInfo,从RequestMappingInfoHandlerMapping的定义就可以看出。
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {...}
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {...}
RequestMappingInfo实现了RequestCondition(翻译:请求条件)接口,此接口专门用于保存从request提取出的用于匹配Handler的条件
抽象实现AbstractRequestCondition中重写了equals、hashCode和toString三个方法,有8个子类,除了CompositeRequestCondition外每一个子类表示一种匹配条件。比如,PatternsRequestCondition表示使用url做匹配,RequestMethodsRequestCondition表示使用RequestMethod做匹配等。CompositeRequestCondition本身并不实际做匹配,而是可以将多个别的RequestCondition封装到自己的一个变量里,在用的时候遍历封装RequestCondition的那个变量里所有的RequestCondition进行匹配,也就是大家所熟悉的责任链模式,这种模式在Spring MVC中非常常见,类名一般是CompositeXXX或者XXXComposite的形式,它们主要的作用就是为了方便调用。
Composite翻译:组合
RequestCondition的另一个实现就是这里要用的RequestMappingInfo,它里面其实是用七个变量保存了七个RequestCondition,在匹配时使用那七个变量进行匹配,这也就是可以在@RequestMapping中给处理器指定多种匹配方式的原因。具体的代码也不难理解,这里就不分析了,如果感兴趣可以自己找到源码看一下。下面接着介绍三个Map。
- handlerMethods:保存着匹配条件(也就是RequestCondition)和HandlerMethod的对应关系。
- urlMap:保存着url与匹配条件(也就是RequestCondition)的对应关系,当然这里的url是Pattern式的,可以使用通配符。另外,这里使用的Map并不是普通的Map,而是MultiValueMap,这是一种一个key对应多个值的Map,其实它的value是一个List类型的值,看MultiValueMap的定义就能明白,MultiValueMap定义如下:
public interface MultiValueMap<K, V> extends Map<K, List<V>> {...}
由于RequestCondition可以同时使用多种不同的匹配方式而不只是url一种,所以反过来说同一个url就可能有多个RequestCondition与之对应。这里的RequestCondition其实就是在@RequestMapping中注释的内容。
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
//在设置了所有提供的bean属性之后由BeanFactory调用
public void afterPropertiesSet() {
initHandlerMethods();
}
}
protected void initHandlerMethods() {
if (logger.isDebugEnabled()) {
logger.debug("Looking for request mappings in application context: " + getApplicationContext());
}
// 1、拿到容器里所有的bean
String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
getApplicationContext().getBeanNamesForType(Object.class));
// 2、根据指定type筛选出Handler
for (String beanName : beanNames) {
if (isHandler(getApplicationContext().getType(beanName))){
//3、detectHandlerMethods负责将Handler保存到Map里
detectHandlerMethods(beanName);
}
}
//handlerMethodsInitialized可以对Handler进行一些初始化,是一个模板方法,但子类并没有实现。
handlerMethodsInitialized(getHandlerMethods());
}
是不是感觉似曾相识?是的,在AbstractDetectingUrlHandlerMapping里面也有类似的代码,首先拿到容器里所有的bean,然后根据一定的规则筛选出Handler,最后保存到Map里。这里的筛选使用的方法是isHandler,这是一个模板方法设计模式,具体实现在RequestMappingHandlerMapping里面,筛选的逻辑是检查类前是否有@Controller或者@RequestMapping注释,代码如下:
//AbstractHandlerMethodMapping.java中的模板方法
protected abstract boolean isHandler(Class<?> beanType);
//RequestMappingHandlerMapping.java
protected boolean isHandler(Class<?> beanType) {
return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) ||
(AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null));
}
在initHandlerMethods方法中,detectHandlerMethods负责将Handler保存到Map里。
protected void detectHandlerMethods(final Object handler) {
//获取handler类型
Class<?> handlerType = (handler instanceof String) ?
getApplicationContext().getType((String) handler) : handler.getClass();
//如果是cglib代理返回父类型,否则直接返回传入的类型
final Class<?> userType = ClassUtils.getUserClass(handlerType);
//查询当前bean所有符合handler要求的Method
Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
@Override
public boolean matches(Method method) {
return getMappingForMethod(method, userType) != null;
}
});
for (Method method : methods) {
//保存method与RequestCondition的对应关系
T mapping = getMappingForMethod(method, userType);
//注册所有符合要求的method,也就是保存到三个map中
registerHandlerMethod(handler, method, mapping);
}
}
上面的MethodFilter()方法是匿名内部类,其实是调用getMappingForMethod()方法。
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = null;
RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
if (methodAnnotation != null) {
RequestCondition<?> methodCondition = getCustomMethodCondition(method);
info = createRequestMappingInfo(methodAnnotation, methodCondition);
RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class);
if (typeAnnotation != null) {
RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info);
}
}
return info;
}
说完了怎么找HandlerMethod,再来看一下怎么将找到的HandlerMethod注册到Map里。registerHandlerMethod的代码如下:
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
HandlerMethod newHandlerMethod = createHandlerMethod(handler, method);
HandlerMethod oldHandlerMethod = handlerMethods.get(mapping);
//检查是否已经在HandlerMethods中存在,如果已经存在而且和现在传入的不同则抛异常。
if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) {
throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean()
+ "' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '"
+ oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped.");
}
//添加到handlerMethods
this.handlerMethods.put(mapping, newHandlerMethod);
if (logger.isInfoEnabled()) {
logger.info("Mapped \"" + mapping + "\" onto " + newHandlerMethod);
}
//添加到urlMap中
Set<String> patterns = getMappingPathPatterns(mapping);
for (String pattern : patterns) {
if (!getPathMatcher().isPattern(pattern)) {
this.urlMap.add(pattern, mapping);
}
}
}
可以看到这个方法也非常简单,首先检查一下handlerMethods这个Map里是不是已经有这个匹配条件了,如果有而且所对应的值和现在传入的HandlerMethod不是同一个则抛出异常,否则依次添加到三个Map里。
通过前面的分析可知这里的主要功能是通过getHandlerInternal方法获取处理器,三件事:
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
//第一件是根据request获取lookupPath,可以简单地理解为url
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
//第二件是使用lookupHandlerMethod方法通过lookup-Path和request找handlerMethod
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
//第三件是如果可以找到handlerMethod则调用它的createWithResolvedBean方法创建新的HandlerMethod并返回.
return (handlerMethod != null) ? handlerMethod.createWithResolvedBean() : null;
}
createWithResolvedBean的作用是判断handlerMethod里的handler是不是String类型,如果是则改为将其作为beanName从容器中所取到的bean,不过HandlerMethod里的属性都是final类型的,不可以修改,所以在createWithResolvedBean方法中又用原来的属性和修改后的handler新建了一个HandlerMethod。下面来看一下具体查找HandlerMethod的方法lookupHandlerMethod。
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
//Match是内部类,保存匹配条件和Handler
List<Match> matches = new ArrayList<Match>();
//首先根据lookupPath获取匹配条件
List<T> directPathMatches = this.urlMap.get(lookupPath);
if (directPathMatches != null) {
//将找到的匹配条件添加到matches
addMatchingMappings(directPathMatches, matches, request);
}
//如果不能直接使用lookupPath得到匹配条件,则将所有匹配条件加入到matches
if (matches.isEmpty()) {
// No choice but to go through all mappings
addMatchingMappings(this.handlerMethods.keySet(), matches, request);
}
//将包含匹配条件和Handler的matches排序,并取出第一个作为bestMatche,如果前面两个排序相同则抛出异常。
if (!matches.isEmpty()) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
Collections.sort(matches, comparator);
if (logger.isTraceEnabled()) {
logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches);
}
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
throw new IllegalStateException(
"Ambiguous handler methods mapped for HTTP path '" + request.getRequestURL() + "': {" +
m1 + ", " + m2 + "}");
}
}
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(handlerMethods.keySet(), lookupPath, request);
}
}
详细过程已经做了注释,比较容易理解,整个过程中是使用Match作为载体的,Match是个内部类,封装了匹配条件和HandlerMethod两个属性。handleMatch方法是在返回前做一些处理,默认实现是将lookupPath设置到request的属性,子类RequestMappingInfo-HandlerMapping中进行了重写,将更多的参数设置到了request的属性,主要是为了以后使用时方便,跟Mapping没有关系。
2.2 总结
本章详细分析了Spring MVC中的HandlerMapping的各种具体实现方式。HandlerMapping的整体结构在AbstractHandlerMapping中设计,简单来说其功能就是根据request找到Handler和Interceptors,组合成HandlerExecutionChain类型并返回。找Handler的过程通过模板方法getHandlerInternal留给子类实现,查找Interceptors则是AbstractHandlerMapping自己完成的,查找的具体方法是通过几个与Interceptor相关的Map完成的,在初始化过程中对那些Map进行了初始化。AbstractHandlerMapping的子类分了两个系列:AbstractUrlHandlerMapping系列和Abstract-HandlerMethodMapping系列,前者是通过url匹配的,后者匹配的内容比较多,而且是直接匹配到Method,这也是现在使用最多的一种方式。
2.2.2 HandlerAdapter
可以理解为使用处理器干活的人。之所以要使用HandlerAdapter是因为Spring MVC中并没有对处理器做任何限制,处理器可以以任意合理的方式来表现,可以是一个类,也可以是一个方法,还可以是别的合理的方式,从handle方法可以看出它是Object的类型。这种模式就给开发者提供了极大的自由。
public interface HandlerAdapter {
//判断是否可以使用某个Handler
boolean supports(Object handler);
//使用具体的handler去处理请求
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
//getLastModified是获取资源的Last-Modified,Last-Modified是资源最后一次修改的时间
long getLastModified(HttpServletRequest request, Object handler);
}
选择使用哪个HandlerAdapter的过程在getHandlerAdapter方法中,它的逻辑是遍历所有的Adapter,然后检查哪个可以处理当前的Handler,找到第一个可以处理Handler的Adapter后就停止查找并将其返回。
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
for (HandlerAdapter ha : this.handlerAdapters) {
//logger啥的都是日志,和源码无关
//if (logger.isTraceEnabled()) {
// logger.trace("Testing handler adapter [" + ha + "]");
//}
if (ha.supports(handler)) {
return ha;
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
HandlerAdapter需要注册到Spring MVC的容器里,注册方法和HandlerMapping一样,只要配置一个Bean就可以了。Handler是从HandlerMapping里返回的。
2.2.3 HandlerExceptionResolver
别的组件都是在正常情况下用来干活的,不过干活的过程中难免会出现问题,出问题后怎么办呢?这就需要有一个专门的角色对异常情况进行处理,在Spring MVC中就是HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。render只负责将ModelAndView渲染成页面,具体ModelAndView是怎么来的render并不关心。这也是Spring MVC设计优秀的一个表现——分工明确互不干涉。
public interface HandlerExceptionResolver {
//获取handler执行抛出的异常,然后返回(ModelAndView)特殊的错误页面。
//具体实现可以维护一个异常为key、View为value的Map
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
2.2.4 ViewResolver
ViewResolver用来将String类型的视图名和Locale解析为View类型的视图。
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
从接口方法的定义可以看出解析视图所需的参数是视图名和Locale,不过一般情况下我们只需要根据视图名找到对应的视图,然后渲染就行,并不需要对不同的区域使用不同的视图进行显示。
View是用来渲染页面的,通俗点说就是要将程序返回的参数填入模板里,生成html(也可能是其他类型)文件。这里有两个关键的问题:
- 使用哪个模板?
- 用什么技术(或者规则)填入参数?
这其实就是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交给不同的视图自己完成。我们最常使用的UrlBasedViewResolver系列的解析器都是针对单一视图类型进行解析的,只需要找到使用的模板就可以了,比如,InternalResourceViewResolver只针对jsp类型的视图,FreeMarkerViewResolver只针对FreeMarker,VelocityViewResolver只针对Velocity。而ResourceBundleViewResolver、XmlViewResolver、BeanNameViewResolver等解析器可以同时解析多种类型的视图。
2.2.5 RequestToViewNameTranslator
ViewResolver是根据ViewName查找View,但有的Handler处理完后并没有设置View也没有设置viewName,这时就需要从request获取viewName了,而如何从request获取viewName就是RequestToViewNameTranslator要做的事情。RequestToViewNameTranslator接口定义如下:
public interface RequestToViewNameTranslator {
// 从request获取viewName
String getViewName(HttpServletRequest request) throws Exception;
}
2.2.6 LocaleResolver
解析视图需要两个参数:一个是视图名,另一个是Locale。视图名是处理器返回的(或者使用RequestToViewNameTranslator解析的默认视图名),Locale是从哪里来的呢?这就是LocaleResolver要做的事情。
public interface LocaleResolver {
//从request解析出Locale
Locale resolveLocale(HttpServletRequest request);
//将特定的Locale设置给某个request
void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);
}
在之前介绍doService方法时说过,容器会将localeResolver设置到request的attribute中,这样就让我们在需要使用Locale的时候可以直接从request拿到localeResolver,然后解析出Locale。
2.2.7 ThemeResolver
ThemeResolver从名字就可以看出是解析主题用的。什么是主题?如果不明白,你可以打开你的智能手机上的主题,顺便换一换。 不同的主题其实就是换了一套图片、显示效果以及样式等。Spring MVC中一套主题对应一个properties文件,里面存放着跟当前主题相关的所有资源,如图片、css样式表等。
- 比如我们可以根据不同的节日,切换不同的主题风格。而不需要重新写一个网站。
public interface ThemeResolver {
//通过request解析出当前主题名
String resolveThemeName(HttpServletRequest request);
void setThemeName(HttpServletRequest request, HttpServletResponse response, String themeName);
}
2.2.8 MultipartResolver
MultipartResolver用于处理上传请求,处理方法是将普通的request包装成MultipartHttpServletRequest,MultipartHttpServletRequest直接调用getFile方法获取到File,如果上传多个文件,还可以调用getFileMap得到FileName→File结构的Map,这样就使得上传请求的处理变得非常简单。
public interface MultipartResolver {
// 判断是不是上传请求
boolean isMultipart(HttpServletRequest request);
// 将request包装成MultipartHttpServletRequest
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
// 处理完后清理上传过程中产生的临时资源
void cleanupMultipart(MultipartHttpServletRequest request);
}
2.2.9 FlashMapManager
FlashMap主要用在redirect中传递参数。而FlashMap-Manager是用来管理FlashMap的。
public interface FlashMapManager {
// retrieveAndUpdate方法用于恢复参数,并将恢复过的和超时的参数从保存介质中删除
FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response);
//saveOutputFlashMap用于将参数保存起来。
void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response);
}
默认实现是org.springframework.web.servlet.support.SessionFlashMapManager,它是将参数保存到session中。
整个redirect的参数通过FlashMap传递的过程分三步:
- 在处理器中将需要传递的参数设置到outputFlashMap中,设置方法在分析DispatcherServlet的时候已经介绍了,可以直接使用request.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE)拿到outputFlashMap,然后将参数put进去,也可以将需要传递的参数设置到处理器的RedirectAttributes类型的参数中,当处理器处理完请求时,如果是redirect类型的返回值RequestMappingHandlerAdapter会将其设置到outputFlashMap中。
- 在RedirectView的renderMergedOutputModel方法中调用FlashMapManager的saveOutput-FlashMap方法,将outputFlashMap中的参数设置到Session中。
- 请求redirect后DispatcherServlet的doServic会调用FlashMapManager的retrieveAnd-Update方法从Session中获取inputFlashMap并设置到Request的属性中备用,同时从Session中删除。
DispatchServlet中的核心方法 doDispatch()
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
//sync:异步的
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);//判断是不是上传文件请求;如果不是,返回 request
multipartRequestParsed = (processedRequest != request);//如果不是上传请求,false
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);//根据request查找指定的handler
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());//获取能执行这个handler的适配器
// Process last-modified header, if supported by the handler.
String method = request.getMethod();//获取请求方法:GET、POST等
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());//获取资源的最后一次修改时间
if (logger.isDebugEnabled()) {
String requestUri = urlPathHelper.getRequestUri(request);
logger.debug("Last-Modified value for [" + requestUri + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {//拦截器的PreHandle()
return;
}
try {
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());//适配器执行handle方法,返回ModelAndView
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
}
applyDefaultViewName(request, mv);//没有视图名,设置一个默认的视图名
mappedHandler.applyPostHandle(processedRequest, response, mv);//拦截器的PostHandle()
}
catch (Exception ex) {
dispatchException = ex;
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);//返回执行结果。handler?正常页面?错误页面?
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Error err) {
triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
return;
}
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}