本文只是入门
为什么用springMVC?springMVC有什么有缺点?springMVC和Struts有什么区别?等等这些问题可以参考网路上资源,本文的重点是快速带入,让大家了解熟悉springMVC。springMVC毕竟是工具,工具的特点就是熟能生巧,通过快速掌握,多加练习、解决问题及归纳总结肯定可以掌握并且成为自己的东西。
简单描述
springMVC主要是通过前端控制器controller中的注解来完成请求处理的。前端无论是以何种方式请求,都会通过controller进行轻度处理、转发以及调度后端的处理器进行处理,最后返回正确的视图及响应。以此来看,springMVC说白了既可以返回合适的页面,也可以响应RESTful请求。
工作流程
以springMVC源码为导向
以springMVC工作流为导向
springMVC在项目中的位置(以maven为例)
-------------------. ------------. ------------. ,,,,,-------------------.------------. ------------. --------------------------------------------.--
HELLO WORLD
第一步,创建web工程
国际惯例,先搭建一个spring项目,通过实战的方式,渐进的学习,杜绝眼高手低的学习方式。
如果是maven需要archetype为maven-archetype-webapp。
第二步,导入依赖库
附上pom.xml,非maven项目可用导入相应的jar包
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>xhsTest</groupId> <artifactId>spring-mvc-test</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>spring-mvc-test Maven Webapp</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <webroot.path>src/main/webapp</webroot.path> <spring-version>4.1.0.RELEASE</spring-version> <log4j-version>2.2</log4j-version> </properties> <build> <finalName>springmvctest</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.3</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.3</version> <configuration> <webXml>src/main/webapp/WEB-INF/web.xml</webXml> </configuration> </plugin> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>8.1.8.v20121106</version> <configuration> <reload>manual</reload> <webAppConfig> <contextPath>/</contextPath> </webAppConfig> <connectors> <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector"> <port>8080</port> </connector> </connectors> </configuration> </plugin> </plugins> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources> </build> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring-version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring-version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring-version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring-version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring-version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>${spring-version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring-version}</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.7.4</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.1.3.Final</version> </dependency> </dependencies></project>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
第三步,配置web.xml加载spring到容器中
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" ><web-app> <display-name>springMVC-test</display-name> <servlet> <servlet-name>dispatcher</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>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list></web-app>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
第四步,配置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:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd"> <context:component-scan base-package="com.iboray.smt"></context:component-scan> <bean id="resolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/"></property> <property name="suffix" value=".jsp"></property> </bean></beans>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
第五步,编写第一个controller(UserController)
package com.iboray.smt.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;/** * Controller 标识该类为Controller * RequestMapping value为请求根路径 */@Controller@RequestMapping(value = "/user")public class UserController { /** * RequestMapping中value定义该方法的请求地址为userInfo,method定义请求方式为GET方式 * 最终访问地址为: 根路径 / 请求地址.也就是/user/userInfo * * @return 返回地址为视图解析器的前缀+返回值+后缀.也就是/userInfo.jsp */ @RequestMapping(value = "userInfo",method = RequestMethod.GET) public String getUserInfo(){ return "userInfo"; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
第六步,编写请求的jsp
附上两个jsp,一个是测试发送请求,一个是controller响应的视图jsp
index.jp
<html><body><h2>Hello World!</h2><hr><a href="/user/userInfo" >get UserInfo</a></body></html>
userInfo.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head> <title>user info</title></head><body><h2>This is user info page</h2></body></html>
进阶
通过以上6步,最简单的springMVC就搭建好了。接下来,我们就可以通过练习springmvc中各技术揭开她的神秘面纱。
之后所有练习都通过index.jsp和UserController.Java完成。
练习1:RequestMapping映射请求参数、请求方法或请求头
RequestMapping为控制器指定可以处理哪些请求的URL,在控制器的类定义和方法定义处都可以修饰。
- 类定义处:提供初步的请求映射信息,相当于web应用的根目录
- 方法定义出:提供进一步的细分映射信息,相对于类定义处的URL,如果类定义处未标注@RequestMapping,那么方法定义处就相对于web应用的根目录。
DispatchServlet截获请求后,就通过控制器上@RequestMapping提供的映射信息确定对应的处理方法
RequestMapping除了可以使用请求URL外,还可以通过请求参数,请求方法,请求头映射请求
requestMapping的value,method,params和heads分别表示,请求URL,请求方法,请求参数和请求头的映射条件,他们之间是与的关系,联合使用多个条件,可以使请求映射更加精准。
params和heads支持简单的表达式
1. param1:表示请求必须包含名为param1的请求参数
2. !param1:表示请求不能包含名为param1的请求参数
3. param1 != value1:表示请求必须包含名为param1的请求参数,且值不等于value1
4. {“param1 == value1”,”param2”}:请求必须包含名为param1和param2的参数,且param1的值必须等于value1
练习
index.jsp
<a href="/user/userInfo?name=xxx&age=20" >RequestMapping params</a>
UserController.java
@RequestMapping(value = "userInfo",method = RequestMethod.GET,params = {"name=xxx","age"}) public String getUserInfo(){ return "userInfo"; }
练习2:PathVariable映射URL绑定占位符
带URL占位符是Spring3.0新增功能,该功能在springmvc向REST目标挺进发展过程中具有里程碑的意义。
通过@PathVariable可以将URL中占位符参数绑定到控制器处理的方法的入参中,也就是说URL中的{paramName}占位符可以通过@PathVariable(paramName)绑定到操作方法的入参中。
index.jsp
<a href="/user/delUser/3" >PathVariable_delUser</a>
UserController.java
@RequestMapping(value = "/delUser/{delId}",method = RequestMethod.GET) public String delUser(@PathVariable(value = "delId") Integer id){ System.out.println("delId : "+id); return SUCCESS; }
练习3:REST请求
关于更多REST请参考如下两篇文章
理解本真REST架构
深入浅出REST
SpringMVC通过配置HiddenHttpMethodFilter来支持PUT/DELETE请求
1 web.xml增加如下配置
<filter> <filter-name>HiddenHttpMethodFilter</filter-name> <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class> </filter> <filter-mapping> <filter-name>HiddenHttpMethodFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
2 发送POST请求,并携带name=_method的隐藏域转为DELETE或PUT等请求
index.jsp
<form id="putUserForm" method="post" action="/user/putUser/5"> <input type="hidden" name="_method" value="PUT"> <input type="submit" value="submit"> </form>
UserController.java
@RequestMapping(value = "/putUser/{putId}",method = RequestMethod.PUT) public String putUser(@PathVariable(value = "putId") Integer id){ System.out.println("putId : "+id); return SUCCESS; }
练习4:请求处理方法签名@RequestParam
SpringMVC通过分析处理方法的签名,将HTTP请求的信息绑定到处理方法的入参中,并根据方法的返回类型做出相应的后续处理。SpirngMVC对控制器处理方法的限制很宽松,必要时可以对方法及方法入参标注注解(@PathVariable@RequestParam@RequestHeader)
使用RequestParam来映射参数
1. value 请求参数名
2. required 参数是否必须,默认为True
3. defaultValue 参数默认值
index.jsp
<a href="/user/queryUser?userId=9&name=Tim" >RequestParam_queryUser</a>
UserController.java
@RequestMapping(value = "/queryUser",method = RequestMethod.GET) public String queryUser(@RequestParam(value = "userId") Integer id,@RequestParam(value = "name",required = false,defaultValue = "") String name){ System.out.println("queryUserId : "+id + " name : "+ name); return SUCCESS; }
请求报头包含若干属性值,服务器可据此获取客户端信息,通过@RequestHeader即可将请求报头的属性值绑定到处理方法的入参中。
index.jsp
<a href="/user/userPwd" >RequestHeader_userPwd</a>
UserController.java
@RequestMapping(value = "/userPwd",method = RequestMethod.GET) public String userPwd(@RequestHeader("Accept-Encoding") String encoding){ System.out.println("encoding : "+encoding); return SUCCESS; }
练习6:使用@CookieValue绑定请求中的Cookie值
index.jsp
<a href="/user/testCookie" >CookieValue_testCookie</a>
UserController.java
@RequestMapping(value = "/testCookie") public String testCookie(@CookieValue(value = "JSESSIONID") String jsession){ System.out.println("jsession : "+jsession); return SUCCESS; }
练习7:使用POJO对象自动绑定参数
SpringMVC会按照请求参数名和对象属性名进行自动绑定,自动为该对象填充属性值,支持级联属性。如dept.deptName,dept.address.tel等
请求地址
http://127.0.0.1:8080/user/addUser?id=1&name=Tim&dept.deptName=dev
User.java
public class User { private int id; private String name; private Detp dept; getter setter toString...}
Dept.java
public class Detp { private int id; private String deptName; getter setter toString...}
后台输出:
user : User{id=1, name='Tim', dept=Detp{id=0, deptName='dev'}}
练习8:使用servletAPI作为入参
UserController.java
@RequestMapping(value = "/testServletAPI") public String testServletAPI(HttpServletRequest request){ System.out.println("request : "+request.getRequestURI()); return SUCCESS; }
练习9:处理模型数据ModelAndView
SpringMVC提供以下几种途径输出模型数据
ModelAndView:处理方法返回值类型为ModelAndView时,方法体即可通过该对象添加模型数据。
Map 或 Model:入参为Model、ModelMap或Map时,处理方法返回时,Map中的数据会自动添加到模型中。
@SessionAttributes:将模型中的某个数据暂时存到HttpSession中,以便多个请求之间可以共享这个属性。
@ModelAttribute:方法入参标书该注解后,入参的对象就会放到数据模型中。
ModelAndView
控制器返回值如果为ModelAndView时,则其既包含视图信息,也包含数据模型信息。
添加模型数据:
ModelAndView addObject(String attributeName,Object attributeValue);
ModelAndView addAllObject(Map《String,?》, modelMap)
设置视图
void setView(View view);
void setViewName(String viewName);
Controller.java
@RequestMapping(value = "/testModelAndView") public ModelAndView testModelAndView(){ ModelAndView mv = new ModelAndView(SUCCESS); mv.addObject("msg","testModelAndView"); return mv; }
那究竟是怎样一个运行流程呢?
跟踪源码主线为:
由关键对象DispacherServlet最外层doDispatch进入
第一层
org.springframework.web.servlet.DispatcherServlet#doDispatch
this.processDispatchResult(processedRequest, response, mappedHandler, err, dispatchException);
第二层
org.springframework.web.servlet.DispatcherServlet#processDispatchResult
this.render(mv, request, response);
第三层
org.springframework.web.servlet.DispatcherServlet#render
view.render(mv.getModelInternal(), request, response)
第四层
org.springframework.web.servlet.view.AbstractView#render
this.renderMergedOutputModel(mergedModel, this.getRequestToExpose(request), response);
第五层
这一步比较关键,需要找到我们配置的视图解析器类型InternalResourceView(默认也是这个)
org.springframework.web.servlet.view.InternalResourceView#renderMergedOutputModel
this.exposeModelAsRequestAttributes(model, request);
第六层
org.springframework.web.servlet.view.AbstractView#exposeModelAsRequestAttributes
while(var3.hasNext()) { Entry entry = (Entry)var3.next(); String modelName = (String)entry.getKey(); Object modelValue = entry.getValue(); if(modelValue != null) { request.setAttribute(modelName, modelValue); if(this.logger.isDebugEnabled()) { this.logger.debug("Added model object \'" + modelName + "\' of type [" + modelValue.getClass().getName() + "] to request in view with name \'" + this.getBeanName() + "\'"); } } ...
说明1:通过modelAndView的addObject设置的K/V值,最终是通过setAttribute一个个set到request请求域中。这下就明白了吧!再看modelAndView对象,包括两个重要属性是private Object view;private ModelMap model;,那其中的addObject方法
public ModelAndView addObject(String attributeName, Object attributeValue) { this.getModelMap().addAttribute(attributeName, attributeValue); return this; }
就是获取modelMap,执行addAttribute方法,再进入这个方法
public ModelMap addAttribute(String attributeName, Object attributeValue) { Assert.notNull(attributeName, "Model attribute name must not be null"); this.put(attributeName, attributeValue); return this; }
因为modelMap继承了public class ModelMap extends LinkedHashMap,所以put方法就是我们常用的Map对象的put方法。说白了,ModelAndView中的model就是一个Map对象,搞明白这个很重要。
练习10:处理模型数据Map
SpringMVC内部使用了一个org.springframework.ui.model接口存储模型数据
SpringMVC在调用方法前会创建一个隐含的模型对象存储模型数据。
如果方法的入参为Map or Model类型,SpringMVC会将隐含的模型的引用传递给这些入参,开发者可以通过这个入参对象访问到模型中的所有数据,也可以向模型中添加新的属性数据。
Controller.java
@RequestMapping(value = "/testMap") public String testMap(Map<String,Object> map){ map.put("msg", Arrays.asList("a","b","c")); return SUCCESS; }
数据结果页面接参msg : ${msg}
结果为:msg : [a, b, c]
练习11:处理模型数据@SessionAtrribute
若想再多个请求之间共享某个模型属性数据,则可以在控制器上标注@SessionAttribute注解,SpringMVC将模型中对应的属性暂存到HttpSession中。
@SessionAttribute除了可以通过属性名指定放到会话中的属性外(value属性),还可以根据对象类型指定哪些类型可以放到会话中(types属性)。
Controller.java
@SessionAttributes(value = {"user"},types = {String.class} )@Controller@RequestMapping(value = "/user")public class UserController {@RequestMapping(value = "/testSessionAttribute") public String testSessionAttribute(Map<String,Object> map){ map.put("user", new User(1,"Tim")); map.put("sessStr","John"); return SUCCESS; }}
练习12:@ModelAttribute修饰
我个人使用ModelAttribute的地方很少。一般的使用是修饰方法和修饰入参。修饰方法会在调用目标方法前首先调用ModelAttribute修饰的方法,修饰入参意思是获取ModelAttribute中指定的对象名称。
修饰方法controller.java
@ModelAttribute public void userModelAttributeModel(@RequestParam(value = "id",required = false) Integer id ,Map<String,Object> map){ System.out.println("userModelAttributeModel coming..."); if(id != null){ User u = new User(1,"Jackson","98765"); System.out.println("userModelAttributeModel : " + u); map.put("user",u); } }
修饰入参
public ModelAndView sendMail(HttpServletRequest request, @ModelAttribute("user") User user)
究竟什么地方调用的ModelAttribute修饰的方法,运行流程是什么?
源码分析
第一步,探究@ModelAttribute修饰的方法入参Map
在修饰的方法map.put…也就是设置值的时候打断点,进入方法后,先查看Map入参的类型为BindingAwareModelMap,那BindingAwareModelMap 继承 ExtendedModelMap,而 ExtendedModelMap 继承 ModelMap 实现了 Model接口。而ModelMap 又继承 LinkedHashMap。这个入参Map便是我们常用的ModelAndView中的Model。
第一层(顶层执行[ 类 # 方法 ])我觉得有必要把整个方法附上
org.springframework.web.bind.annotation.support.HandlerMethodInvoker#invokeHandlerMethod
public final Object invokeHandlerMethod(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception { Method handlerMethodToInvoke = BridgeMethodResolver.findBridgedMethod(handlerMethod); try { boolean debug = logger.isDebugEnabled(); for (String attrName : this.methodResolver.getActualSessionAttributeNames()) { Object attrValue = this.sessionAttributeStore.retrieveAttribute(webRequest, attrName); if (attrValue != null) { implicitModel.addAttribute(attrName, attrValue); } } for (Method attributeMethod : this.methodResolver.getModelAttributeMethods()) { Method attributeMethodToInvoke = BridgeMethodResolver.findBridgedMethod(attributeMethod); Object[] args = resolveHandlerArguments(attributeMethodToInvoke, handler, webRequest, implicitModel); if (debug) { logger.debug("Invoking model attribute method: " + attributeMethodToInvoke); } String attrName = AnnotationUtils.findAnnotation(attributeMethod, ModelAttribute.class).value(); if (!"".equals(attrName) && implicitModel.containsAttribute(attrName)) { continue; } ReflectionUtils.makeAccessible(attributeMethodToInvoke); Object attrValue = attributeMethodToInvoke.invoke(handler, args); if ("".equals(attrName)) { Class<?> resolvedType = GenericTypeResolver.resolveReturnType(attributeMethodToInvoke, handler.getClass()); attrName = Conventions.getVariableNameForReturnType(attributeMethodToInvoke, resolvedType, attrValue); } if (!implicitModel.containsAttribute(attrName)) { implicitModel.addAttribute(attrName, attrValue); } } Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel); if (debug) { logger.debug("Invoking request handler method: " + handlerMethodToInvoke); } ReflectionUtils.makeAccessible(handlerMethodToInvoke); return handlerMethodToInvoke.invoke(handler, args); } catch (IllegalStateException ex) { throw new HandlerMethodInvocationException(handlerMethodToInvoke, ex); } catch (InvocationTargetException ex) { ReflectionUtils.rethrowException(ex.getTargetException()); return null; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
由此可见不管请求这个controller的什么方法,都会先获取@SessionAttribute中的KV值,并进入@ModelAttribute修饰的方法,执行invoke后进入ModelAttribute方法,执行后给implicitModel赋值,
最终目标方法的入参的具体赋值的关键代码为:
org.springframework.web.bind.annotation.support.HandlerMethodInvoker#resolveHandlerArguments
private Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception { Class<?>[] paramTypes = handlerMethod.getParameterTypes(); Object[] args = new Object[paramTypes.length]; for (int i = 0; i < args.length; i++) { MethodParameter methodParam = new MethodParameter(handlerMethod, i); methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer); GenericTypeResolver.resolveParameterType(methodParam, handler.getClass()); String paramName = null; String headerName = null; boolean requestBodyFound = false; String cookieName = null; String pathVarName = null; String attrName = null; boolean required = false; String defaultValue = null; boolean validate = false; Object[] validationHints = null; int annotationsFound = 0; Annotation[] paramAnns = methodParam.getParameterAnnotations(); for (Annotation paramAnn : paramAnns) { if (RequestParam.class.isInstance(paramAnn)) { RequestParam requestParam = (RequestParam) paramAnn; paramName = requestParam.value(); required = requestParam.required(); defaultValue = parseDefaultValueAttribute(requestParam.defaultValue()); annotationsFound++; } else if (RequestHeader.class.isInstance(paramAnn)) { RequestHeader requestHeader = (RequestHeader) paramAnn; headerName = requestHeader.value(); required = requestHeader.required(); defaultValue = parseDefaultValueAttribute(requestHeader.defaultValue()); annotationsFound++; } else if (RequestBody.class.isInstance(paramAnn)) { requestBodyFound = true; annotationsFound++; } else if (CookieValue.class.isInstance(paramAnn)) { CookieValue cookieValue = (CookieValue) paramAnn; cookieName = cookieValue.value(); required = cookieValue.required(); defaultValue = parseDefaultValueAttribute(cookieValue.defaultValue()); annotationsFound++; } else if (PathVariable.class.isInstance(paramAnn)) { PathVariable pathVar = (PathVariable) paramAnn; pathVarName = pathVar.value(); annotationsFound++; } else if (ModelAttribute.class.isInstance(paramAnn)) { ModelAttribute attr = (ModelAttribute) paramAnn; attrName = attr.value(); annotationsFound++; } else if (Value.class.isInstance(paramAnn)) { defaultValue = ((Value) paramAnn).value(); } else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) { validate = true; Object value = AnnotationUtils.getValue(paramAnn); validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value}); } } if (annotationsFound > 1) { throw new IllegalStateException("Handler parameter annotations are exclusive choices - " + "do not specify more than one such annotation on the same parameter: " + handlerMethod); } if (annotationsFound == 0) { Object argValue = resolveCommonArgument(methodParam, webRequest); if (argValue != WebArgumentResolver.UNRESOLVED) { args[i] = argValue; } else if (defaultValue != null) { args[i] = resolveDefaultValue(defaultValue); } else { Class<?> paramType = methodParam.getParameterType(); if (Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType)) { if (!paramType.isAssignableFrom(implicitModel.getClass())) { throw new IllegalStateException("Argument [" + paramType.getSimpleName() + "] is of type " + "Model or Map but is not assignable from the actual model. You may need to switch " + "newer MVC infrastructure classes to use this argument."); } args[i] = implicitModel; } else if (SessionStatus.class.isAssignableFrom(paramType)) { args[i] = this.sessionStatus; } else if (HttpEntity.class.isAssignableFrom(paramType)) { args[i] = resolveHttpEntityRequest(methodParam, webRequest); } else if (Errors.class.isAssignableFrom(paramType)) { throw new IllegalStateException("Errors/BindingResult argument declared " + "without preceding model attribute. Check your handler method signature!"); } else if (BeanUtils.isSimpleProperty(paramType)) { paramName = ""; } else { attrName = ""; } } } if (paramName != null) { args[i] = resolveRequestParam(paramName, required, defaultValue, methodParam, webRequest, handler); } else if (headerName != null) { args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler); } else if (requestBodyFound) { args[i] = resolveRequestBody(methodParam, webRequest, handler); } else if (cookieName != null) { args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler); } else if (pathVarName != null) { args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler); } else if (attrName != null) { WebDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); if (binder.getTarget() != null) { doBind(binder, webRequest, validate, validationHints, !assignBindingResult); } args[i] = binder.getTarget(); if (assignBindingResult) { args[i + 1] = binder.getBindingResult(); i++; } /***这里将request域中的请求参数覆盖到@SessionAttribute和@ModelAttribute赋值的implicitModel中。*这样得到的结果就是@SessionAttribute和ModelAttribute先将所有字段初始化,然后和request中的请求值构成并集,*这样得出的结果就是request中没有的字段保留,如果key一样的,就覆盖新值。*也就得到我们想要的结果。最终在目标方法中得到的就是已被改写的参数。**/ implicitModel.putAll(binder.getBindingResult().getModel()); } } return args; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
整个的运行流程
- 发起request请求—->
- 各种filter—>
- org.springframework.web.servlet.DispatcherServlet#doDispatch—–>
- org.springframework.web.servlet.HandlerAdapter#handle(具体执行适配器为:org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter#handle)—–>
- org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter#invokeHandlerMethod——>
- invokeHandlerMethodd的for (String attrName : this.methodResolver.getActualSessionAttributeNames())(先看看SessionAttribute修饰的Handler有没有值,有就放入org.springframework.ui.ExtendedModelMap implicitModel 中)——>
- invokeHandlerMethodd的for (Method attributeMethod : this.methodResolver.getModelAttributeMethods()) (再看@ModelAttribute修饰的所有方法,如果有就循环每个ModelAttribute修饰的方法,并将每个方法的返回值放入org.springframework.ui.ExtendedModelMap implicitModel )——>
- org.springframework.web.bind.annotation.support.HandlerMethodInvoker#resolveHandlerArguments(将@SessionAttribute和@ModelAttribute初始化的对象和自己的请求参数并集(相关代码:Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel); )作为自己请求的目标方法的入参,注意,如果没有使用ModelAttribute注解,则目标方法的入参key为POJO类名的首字母小写。)—–>(反射执行自己请求的目标方法。handlerMethodToInvoke.invoke(handler, args);)—->
- 最后方法得到已被处理过的入参。执行业务操作。
SpringMVC确定POJO入参的过程
这里需要注意的是:
如果implicitModel不存在key对应的对象(也就是说ModelAttribute中也没有为key赋值),则会检查当前Handler是否标注了SessionAttribute注解 且value属性值包含了这个key,则会从HttpSession中寻找这个key对应的value值,如果有则绑定到目标方法的入参中,如果返回null,将抛出异常:org.springframework.web.HttpSessionRequiredException: Session attribute ‘user’ required - not found in session
相关源代码
private WebDataBinder resolveModelAttribute(String attrName, MethodParameter methodParam, ExtendedModelMap implicitModel, NativeWebRequest webRequest, Object handler) throws Exception { String name = attrName; if ("".equals(name)) { name = Conventions.getVariableNameForParameter(methodParam); } Class<?> paramType = methodParam.getParameterType(); Object bindObject; if (implicitModel.containsKey(name)) { bindObject = implicitModel.get(name); } else if (this.methodResolver.isSessionAttribute(name, paramType)) { bindObject = this.sessionAttributeStore.retrieveAttribute(webRequest, name); if (bindObject == null) { raiseSessionRequiredException("Session attribute '" + name + "' required - not found in session"); } } else { bindObject = BeanUtils.instantiateClass(paramType); } WebDataBinder binder = createBinder(webRequest, bindObject, name); initBinder(handler, name, binder, webRequest); return binder; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
练习13 视图和视图解析器
请求处理方法完成后,最终会返回一个ModelAndView对象,对于返回String,View或ModelMap等类型的处理方法,SpringMVC也会在内部将它装配成一个ModelAndView对象,它包含了逻辑名和模型对象的视图。
SpringMVC借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是JSP,也可能是Excel、jfreechart等各种形式的视图。
对于最终采取何种视图对象对模型数据进行渲染,处理器并不关心,处理器的工作重点聚焦在生产模型数据的工作上,从而实现了MVC的充分解耦。
视图
视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。为了实现视图模型和具体实现技术的解耦,spring在org.springframework.web.servlet包中定义了一个高度抽象的接口View
视图对象由视图解析器负责实例化,由于视图是无状态的,所以他们不会有线程安全问题。
视图解析器
SpringMVC为逻辑视图名的解析提供了不同的策略,可以在spring WEB 上下文中配置一种或多种解析策略,并指定他们的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。
视图解析器的作用比较单一:将逻辑视图解析为一个具体的视图对象。
所有的视图解析器都必须实现org.springframework.web.servlet.ViewResolver接口
InternalResourceViewResolver
JSP是最常见是视图技术,可以使用InternalResourceViewResolver作为视图解析器:
<bean id="resolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/"></property> <property name="suffix" value=".jsp"></property> </bean>
若项目中用了JSTL,则SpringMVC会自动把InternalResourceViewResolver转换为JstlView
若使用了JSTL的fmt标签,则需要在SpringMVC的配置文件中配置国际化资源文件
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="i18n"></property> </bean>
若希望直接响应SpringMVC渲染的页面,可以使用
<mvc:view-controller path="user/testMap" view-name="success" />
源码分析
通过InternalResourceViewResolver了解视图解析流程,
InternalResourceViewResolver是我们常用来渲染JSP的解析器,它继承了UrlBasedViewResolver 实现了ViewResolver的resolveViewName方法,获取最终实现视图渲染的解析器,从而最终得到我们想要的视图。
上次我们了解处理器运行流程的时候跟踪的源码是handle方法
这次我们是需要跟踪处理完成后返回视图的运行流程
这里意思是不管执行结果是什么,都返回ModelAndView
顶层入口
org.springframework.web.servlet.DispatcherServlet#doDispatch
的
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
这里返回的ModelAndView如下:
//这里就是处理结果后续操作,其中包括视图渲染
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
//进入方法后
org.springframework.web.servlet.DispatcherServlet#processDispatchResult
if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } } if (mv != null && !mv.wasCleared()) { render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
进入render
org.springframework.web.servlet.DispatcherServlet#render
View view; if (mv.isReference()) { view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'"); } }
进入resolveViewName
org.springframework.web.servlet.DispatcherServlet#resolveViewName
for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } }
获得view的正确视图后执行渲染方法
org.springframework.web.servlet.DispatcherServlet#render
`view.render(mv.getModelInternal(), request, response);`
进入
org.springframework.web.servlet.view.AbstractView#render
,一定要明白,这个方法是通过org.springframework.web.servlet.DispatcherServlet#render调用的,具体实现就是InternalResourceViewResolver的子类JstlView
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (logger.isTraceEnabled()) { logger.trace("Rendering view with name '" + this.beanName + "' with model " + model + " and static attributes " + this.staticAttributes); } Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); prepareResponse(request, response); renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); }
我们用的是InternalResourceView,因为DispatcherServlet中获得的view是是InternalResourceViewResolver的子类JstlView,且执行了render()方法,进而可以执行父类的renderMergedOutputModel方法。
org.springframework.web.servlet.view.InternalResourceView#renderMergedOutputModel
protected void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { ... ... RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath); ... ... if (useInclude(request, response)) { response.setContentType(getContentType()); ... ... rd.include(request, response); } else { ... ... rd.forward(request, response); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
从以上源码可以大概了解到我们配置的InternalResouceViewResolver的视图渲染过程,也可以举一反三明白其他视图解析器的渲染过程。
常用的视图实现类
大类 | 视图类型 | 说明 | URL资源视图InternalResouceView将JSP或其他资源封装成一个视图,是InternalResouceViewResolver默认的视图实现类URL资源视图JstlView如果JSP中使用了JSTL国际化标签的功能,则需要使用该视图实现类文档视图AbstractExcelViewExcel文档视图的抽象类,该视图基于POI构造Excel文档文档视图AbstractPdfViewExcel文档视图的抽象类,该视图基于iText构造PDF文档报表视图ConfigurableJasperReportsView使用JasperReports报表技术的视图报表视图JasperReportsCsvView同上报表视图JasperReportsHtmlView同上报表视图JasperReportsPdfView同上报表视图JasperReportsXlsView同上报表视图JasperReportsMultiFormatView同上JSON视图MappingJackson2JsonView将模型数据通过Jackson开业框架的ObjectMapper以JSON方式输出常用的视图解析器实现类
大类 | 视图类型 | 说明 | 解析为Bean的名字BeanNameViewResolver将逻辑视图名解析为一个Bean,Bean的id等于逻辑视图名解析为URL文件InternalResourceViewResolver将视图名解析为一个URL文件,一般使用该解析器将视图名映射为一个保存在WEB-INF下的程序文件,如.jsp解析为URL文件JasperReportsViewResolverJasperReports是一个基于Java的开源报表工具,该解析器将视图名解析为报表文件对应的URL模板文件视图FreeMarkerViewResolver解析为基于FreeMarker模板技术的模板文件模板文件视图VelocityViewResolver解析为基于Velocity模板技术的模板文件模板文件视图VelocityLayoutViewResolver同上练习14 springMVC+JSTL 实现国际化
步骤1:需要在项目中导入jstl依赖库,并在需要国际化的页面引入jstl的fmt标签
步骤2:配置资源文件(在src/main/resource下)
资源文件基本都是KV结构所以这三个文件内容的key都必须一样,value就换为对应的值就可以了
i18n.properties
i18n.name=namei18n.pwd=password
i18n_en_US.properties
i18n.name=namei18n.pwd=password
i18n_zh_CN.properties
i18n.name=用户名i18n.pwd=密码
步骤3:springMVC配置文件
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="i18n"></property> <property name="defaultEncoding" value="UTF-8"></property> </bean>
步骤4:需要国际化的页面
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %><%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %><html><head> <title>user info</title></head><body><p> <fmt:message key="i18n.name"></fmt:message></p></body></html>
这样,随着本地语言环境的变化,name会自动切换为对应的语言
练习15 mvc:view-controller
有适合我们需要直接访问某个视图,不需要经过controller,那么可以增加springMVC的配置
<mvc:annotation-driven></mvc:annotation-driven> <mvc:view-controller path="/abc" view-name="userInfo"></mvc:view-controller>
这样访问http://ip:port/abc就可以跳转到userInfo.jsp了,注意mvc:annotation-driven这个配置,这是为了防止配置view-controller后无法正常访问controller而增加的。至于什么原因,之后补充。
练习16 自定义视图之BeanNameViewResolver
我们有的时候需要通过自定义的视图解决业务的需要,如公司内部的模板等等…
这里我们通过BeanNameViewResolver这个视图解析器来看看自定义视图的实现方式。
首先我们需要一个View接口的实现类MyView1.java
/** * 因为BeanNameViewResolver是直接根据视图名称来获取的, * 所以需要用@Component注解加入到spring容器中 */@Componentpublic class MyView1 implements View { @Override public String getContentType() { return "text/html"; } @Override public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { PrintWriter pw = response.getWriter(); pw.print("this is myView page , Time = "+System.currentTimeMillis()); pw.close(); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
然后我们需要为springMVC增配
<bean id="beanNameViewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver"> <property name="order" value="10"></property> </bean>
顺便看下源码,看到BeanNameViewResolver是通过context.getBean返回咱们定义的视图,所以这就是为什么要把咱们的View实现类放到容器中的原因。也就是为什么我们在controller中返回视图名称就可以实现自定义视图了。
@Overridepublic View resolveViewName(String viewName, Locale locale) throws BeansException { .... return context.getBean(viewName, View.class); }
最后再controller中写测试方法
@RequestMapping("/testView") public String testView(){ System.out.println("testView"); return "myView1"; }
测试结果:
页面会打印出
this is myView page , Time = 1452041832760
其他的视图解析器可以查看练习13下的《常用的视图解析器实现类》
练习17 重定向
一般情况下,控制器方法返回字符串类型的值会被当做逻辑视图名处理。
如果返回的字符串中含有forward:或redirect:前缀时,SpringMVC会对他们进行特殊处理,将forward:和redirect:当成指示符处理,其后的字符串当成URL来处理。
redirect:success.jsp 会完成一个到success.jsp的重定向
forward:success.jsp 会完成一个到success.jsp的转发
源码分析
为了看怎么处理返回结果(即:返回哪个视图VIEW。也就是View接口的哪个实现类),定位到doDispatch这个方法,看过上面源码分析的朋友,相信已经很熟悉了
顶层方法doDispatch
org.springframework.web.servlet.DispatcherServlet#doDispatch
进入processDispatchResult
org.springframework.web.servlet.DispatcherServlet#processDispatchResult
进入render
org.springframework.web.servlet.DispatcherServlet#render
进入resolveViewName
org.springframework.web.servlet.DispatcherServlet#resolveViewName
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception { for (ViewResolver viewResolver : this.viewResolvers) { /**这里就得到我们所配置的View,并且注意List<ViewResolver> viewResolvers这个List是排序的, *会按照配置的视图解析器的order属性进行存取 *练习16中,我们定义了两个视图解析器,一个是InternalResourceViewResolver *还有一个是BeanNameViewResolver, *这里会首先用BeanNameViewResolver的resolveViewName方法尝试返回View对象, *但是我们是通过return "redirect:/index.jsp"返回的, *所以BeanNameViewResolver通过他的context.getBean(...)方法无法根据viewName获取到对应的View *所以进入下一次循环。得到的是InternalResourceViewResolver, *而InternalResourceViewResolver用的是父类UrlBasedViewResolver的父类AbstractCachingViewResolver的 *resolveViewName方法; **/ View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } return null; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
进入
org.springframework.web.servlet.view.AbstractCachingViewResolver#resolveViewName
@Override public View resolveViewName(String viewName, Locale locale) throws Exception { if (!isCache()) { return createView(viewName, locale); } else { Object cacheKey = getCacheKey(viewName, locale); View view = this.viewAccessCache.get(cacheKey); if (view == null) { synchronized (this.viewCreationCache) { view = this.viewCreationCache.get(cacheKey); if (view == null) { view = createView(viewName, locale); if (view == null && this.cacheUnresolved) { view = UNRESOLVED_VIEW; } if (view != null) { this.viewAccessCache.put(cacheKey, view); this.viewCreationCache.put(cacheKey, view); if (logger.isTraceEnabled()) { logger.trace("Cached view [" + cacheKey + "]"); } } } } } return (view != UNRESOLVED_VIEW ? view : null); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
这里需要明白抽象类之间的相互调用,以免混淆的具体实现方法。
最后看org.springframework.web.servlet.view.UrlBasedViewResolver#createView
@Override protected View createView(String viewName, Locale locale) throws Exception { if (!canHandle(viewName, locale)) { return null; } /** * 这里就一目了然了。redirect返回的是RedirectView * forward返回的是InternalResourceView **/ if (viewName.startsWith(REDIRECT_URL_PREFIX)) { String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); return applyLifecycleMethods(viewName, view); } if (viewName.startsWith(FORWARD_URL_PREFIX)) { String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); return new InternalResourceView(forwardUrl); } return super.createView(viewName, locale); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
练习18 springMVC表单标签
通过使用springMVC的表单标签可以实现将模型数据中的属性与HTML表单元素相绑定,以实现表单数据更便捷编辑与表单值回显
一、Form标签
一般情况下通过GET请求获取表单页面,而通过POST请求提交表单页面,因此获取表单页面和提交表单页面的URL是相同的。只要满足最佳条件的契约,<form:form>
标签就无需通过action属性指定表单起脚的URL。
可以通过modelAttribute属性指定绑定模型属性,若没有指定该属性,则默认从request域中读取command的表单bean。如该属性值也不存在,则报如下错误:
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'command' available as request attribute
二、表单标签
springMVC提供了多个表单组件标签,如<form:input />、<form:select />
等,用以绑定表单字段的属性值,它们的共有属性如下:
path:表单字段,对应HTML的name属性,支持级联属性
htmlEscape:是否对表单值的特殊字符进行转换,默认为true
cssClass:表单组件的css样式名
cssErrorClass:表单组件的数据存在错误时,采取的css样式
<form:input /><form:password /><form:hidden /><form:textarea />
:对应HTML表单的text,password,hidden,textarea标签
<form:radiobutton />
:单选框标签,当表单bean的属性值和value相同时,单选框被选中。
<form:radiobuttons />
:单选框组标签,用于构造多个单选框
- items:可以是一个list,string[] 或map
- itemValue:是定radio的value值,可以是集合bean中的一个属性值
- itemLabel:指定radio的label名称
- delimiter:多个单选框可以指定分隔符
<form:checkbox />
:复选框组件,用于构造单个复选框
<form:checkboxs />
:用于构造多个复选框,使用方式同<form:radiobuttons />
标签
<form:select />
:用于构造下拉组件,使用方式同<form:radiobuttons />
标签
<form:option />
:下拉框选项组件标签,使用方式同<form:radiobuttons />
标签
<form:errors />
:显示表单组件或数据校验所对应的错误
- <form:errors path="*"/>
:显示表单所有的错误
- <form:errors path="user*"/>
:显示以user为前缀的属性对应的错误
- <form:errors path="username"/>
:显示特定表单对象属性的错误
三、表单标签练习
通过一个小例子练习上面几个标签
STEP1 :准备数据
初始化数据类,因为没有DAO,所以我放到static变量中。不管通过说明办法,初始化数据就OK。我是通过spring的初始化配置方法进行初始化的。
<bean id="dataInit" class="com.iboray.smt.commons.DataInit" scope="singleton" lazy-init="false" init-method="init"></bean>
public class DataInit { private static List<User> users = null; private static List<Detp> detps = null; private void init(){ System.out.println("数据初始化... ..."); users = new ArrayList<>(); users.add(new User(1,"zs","112233",new Detp(1,"dept1"))); users.add(new User(2,"ls","22233",new Detp(1,"dept1"))); users.add(new User(3,"wz","41223",new Detp(2,"dept2"))); users.add(new User(4,"zl","535454",new Detp(3,"dept3"))); users.add(new User(5,"mq","565575",new Detp(3,"dept3"))); detps = new ArrayList<>(); detps.add(new Detp(1,"dept1")); detps.add(new Detp(2,"dept2")); detps.add(new Detp(3,"dept3")); detps.add(new Detp(4,"dept4")); } public static List<User> getUsers() { return users; } public static List<Detp> getDetps() { return detps; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
STEP2 :增加controller对应的方法
@Controller@RequestMapping(value = "/user")public class UserController { @RequestMapping(value = "/saveUser",method = RequestMethod.POST) public String saveUser(User user){ DataInit.getUsers().add(user); List l = DataInit.getUsers(); return "redirect:/user/userInput"; } /** *这里是偷懒。需要说明一下,为了少写一个页面 *所以添加和列表都放在同一个页面中。 * **/ @RequestMapping(value = "/userInput",method=RequestMethod.GET) public String userInput(Map<String,Object> map){ map.put("user",new User()); map.put("depts", DataInit.getDetps()); map.put("users",DataInit.getUsers()); return "springForm"; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %><%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><html><head> <title>Title</title></head><body><form:form action="/user/saveUser" method="post" modelAttribute="user"> <form:input path="id" ></form:input> <form:input path="name" ></form:input> <form:input path="pwd" ></form:input> <form:select path="dept.id" items="${depts }" itemLabel="deptName" itemValue="id" ></form:select> <input type="submit" value="submit"></form:form><hr><table border="1"><c:forEach items="${users}" var="user"> <tr> <td width="20">${user.id}</td> <td width="150">${user.name}</td> <td width="150">${user.pwd}</td> <td width="20">${user.dept.id}</td> <td width="40">${user.dept.deptName}</td> </tr></c:forEach> </table></body></html>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
STEP4 :请求地址
请求:http://ip:port/user/userInput
返回:submit后结果
1到5为咱们初始化数据
练习19 处理静态资源
REST风格的资源URL不希望带有.html或.do等后缀,若将dispatcherServlet的请求映射为/,则springMVC将捕获web应用的所有请求,包括静态资源的请求,springMVC会将他当成一个普通的请求来处理,因找不到对应处理器而报错
我们可以在springMVC配置中增加<mvc:default-servlet-handler />
的方式解决静态资源的问题。
<mvc:default-servlet-handler />
将在springMVC上下文中定义一个defaultservlethttprequesthandler,他会对进入dispatcherServlet的请求进行筛查,如发现没有经过映射的请求,就讲请求交由web应用服务器默认的servlet处理,如果不是静态资源且有映射的才交由dispatcherservlet进行处理
一般web应用服务器默认的servlet的名称都是default,所以不用显示配置<mvc:default-servlet-handler />
的default-servlet-name=”“属性,若不是默认的名称,则需要配置。
特别提醒,如果配置了<mvc:default-servlet-handler />
,需要配置<mvc:annotation-driven >
,原因后面再单说。
练习20 自定义类型转换器
有时候要写自定义类型转换器,那就得先了解一下数据绑定流程。
数据绑定流程
- springMVC主框架将servletRequest对象及目标方法的入参实例传递给webDataBinderFactory实例(可再源码分析查看内部结构),以创建DataBinder实例对象。
- DataBinder调用装配在springMVC上下文中的conversionService组件进行数据类型转换,数据格式化工作,将servlet请求信息填充到入参对象中。
- 调用validator组件对已绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果BindingData对象。
- springMVC抽取BindingResult中的入参对象和校验错误对象,将它们赋给处理方法的响应入参。
数据绑定
springMVC通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中,数据绑定的核心部件是DataBinder,运行机制如下:
数据转换
springMVC上下文中内建了很多转换器,可完成大多数java类型的转换工作。
自定义类型转换器
- ConversionService是Spring类型转换体系的核心接口,可以利用ConversionServiceFactoryBean在Spring IOC容器中定义一个ConversionService。Spring将自动识别出IOC容器中的ConversionService,并在Bean属性配置及SpringMVC处理方法入参绑定等场合使用它进行数据转换。
- 可通过ConversionServiceFactoryBean的converters属性注册自定义的类型转换器。
spring支持的类型转换器
spring定义了三种类型的转换器接口,实现任意一个接口都可以作为自定义类型转换器注册到ConversionServiceFactoryBean中
1. Converter<S,T>
将S类型对象转换为T类型对象
2. ConverterFactory
将相同系列多个“同质”Converter封装在一起,如果希望将一种类型对象转换为另一种类型及子类的对象(例如将String转换为Number及Number的子类[Interger,Double,Long]等)可使用该转换器工厂类
3. GenericConverter
会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换
写一个自定义类型转换器UserConverterService
STEP 1:
@Componentpublic class UserConverterService implements Converter<String,User> { @Override public User convert(String source) { if(source != null && !"".equals(source.trim())){ String[] s = source.split(";"); if (s.length == 3){ User u = new User(Integer.parseInt(s[0]),s[1],s[2]); System.out.println("source:"+source); System.out.println("result:"+u); return u; } } return null; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
STEP 2 :SpringMVC配制文件
<mvc:annotation-driven conversion-service="conversionService" ></mvc:annotation-driven> <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <set> <ref bean="userConverterService"></ref> </set> </property> </bean>
STEP 3: Controller方法
/**看入参@RequestParam("userStr") User user,其实是我们请求的字符串,但我们是用User接参的。*通过http://host/user/saveUserByConverter?userStr=13;qweqwe;xxxooooo请求地址,*找到我们对应的controller,在此期间,经过了上面说的数据绑定流程,*判断出我们需要的转换器是String 转 User对象的。*那正好是我们写的自定义转换器,所以就可以正常转换了。*/ @RequestMapping(value = "/saveUserByConverter",method = RequestMethod.GET) public String saveUserByConverter(@RequestParam("userStr") User user){ DataInit.getUsers().add(user); List l = DataInit.getUsers(); return "redirect:/user/userInput"; }
源码分析
在自定义的转换器中打断点,找到如下方法
org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument
if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); arg = binder.convertIfNecessary(arg, paramType, parameter); }
WebDataBinder对象:
测试
- 请求:http://host/user/saveUserByConverter?userStr=13;qweqwe;xxxooooo
- 在自定义的converter中打断点,查看变量
进一步查看
……
可以看出已经有我们自己的converter了 - console输出:
source:13;qweqwe;xxxoooooresult:User{id=13, name='qweqwe', pwd='xxxooooo', dept=null}
练习21 mvc:annotation-driven
<mvc:annotation-driven>
会自动注册RequestMappingHandlerMapping
、RequestMappingHandlerAdapter
和ExceptionHandlerExceptionResolver
三个Bean。
还提供以下支持。
1. 支持使用ConversionService
实例对表单数据进行类型转换
2. 支持使用@NumberFormatAnnotation、@DateTimeFormat
注解完成数据类型格式化。
3. 支持使用@Valid
注解对JavaBean实例进行JSR 303验证
4. 支持使用@RequestBody和@ResponseBody
注解
对比配置annotation-driven前后的差异
都在org.springframework.web.servlet.DispatcherServlet#doDispatch
的mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
打断点重点查看handlerAdapters
1. 既没有配置<mvc:default-servlet-handler />
也没有配置<mvc:annotation-driven>
请求正常,但是查看源码AnnotationMethodHandlerAdapter已经有删除线了Spring已经不建议使用了,具体原因这里不阐述了。
2. 配置<mvc:default-servlet-handler />
没有配置<mvc:annotation-driven>
请求出错 HTTP Status 404 - /user/saveUser
3. 既配置<mvc:default-servlet-handler />
也配置<mvc:annotation-driven>
请求正常
这样通过对比可以看出<mvc:annotation-driven>
的作用,所以在使用了springMVC框架的项目中,建议都增加该配置。
练习22 initBinder
由InitBinder标识的方法可以对webDataBinder对象进行初始化。webDataBinder是DataBinder的子类,用于完成由表单字段到javaBean属性的绑定。
InitBinder方法不能有返回值,必须声明void。
InitBinder方法的参数通常是webDataBinder。
数据绑定流程参考练习20自定义类型转换器中的数据绑定流程,
练习代码
controller
@InitBinder public void testInitBinder(WebDataBinder binder){ binder.setDisallowedFields("name"); }
练习23 数据格式化
如果我们配置了annotation-driver并且使用了格式化注解 例如:NumberFormat或DateTimeFormat,那么springMVC在处理数据绑定的过程中用的转换器就是FormattingConversionServiceFactoryBean。
FormattingConversionServiceFactoryBean内部已注册了:
1. NumberFormatAnnotationFormaterFactory:支持对数字类型的属性用@NumberFormat注解
2. JodaDateTimeFormatAnnotationFormatterFactory:支持对日期类型属性使用@DateTimeFormat注解
装配了FormattingConversionServiceFactoryBean后就可以在springMVC入参绑定及模型数据输出时使用注解驱动了。mvc:annotation-driven默认创建的ConversionService实例即为FormattingConversionServiceFactoryBean。
日期格式化
@DateTimeFormat
注解可对java.util.Date、java.util.Calendar、java.long.Long时间类型进行标注:
1. pattern属性:类型为字符串,指定解析和格式化字段数据的模式,如:yyyy-MM-dd
2. iOS属性:类型为DateTimeFormat.ISO,指定解析和格式化字段数据的ISO模式,包括四种,ISO.NONE(不使用,默认),ISO.DATE,ISO.TIME,ISO.DATE_TIME
3. style属性,字符串类型,通过样式指定日期时间的格式,由两位字符组成,第一位表示日期的格式,第二为表示时间的格式,S:短日期/时间 格式,M:中日期/时间 格式,L:长日期/时间 格式,F:完整日期/时间 格式,- :忽略日期或时间格式。
数值格式化
@NumberFormat
可对数字类型的属性进行标注,它拥有两个互斥的属性:
1. style:类型为NumberFormat.Style。用于指定样式类型,包括三种:Style.NUMBER(正常数字类型),Style.CURRENCY(货币类型),Style.PERCENT(百分比)
2. pattern:类型为String,自定义样式,如pattern=”#,###,###.#”。
源码分析
断点进入
org.springframework.web.method.annotation.ModelAttributeMethodProcessor#resolveArgument
查看WebDataBinder binder
这里会有三种情况出现,大家需要注意一下
接参的bean:User.java
public class User { private int id; private String name; private String pwd; @DateTimeFormat(pattern = "yyyy-MM-dd") private Date birth; @NumberFormat(pattern = "#,###,###.#") private BigDecimal pay; getter setter ... ... }
第一种,配置了annotation-driven且自定义类型转换器是通过ConversionServiceFactoryBean加入的。
这种情况下即便是加入了@DateTimeFormart @NumberFormat
都无法正确格式化,会出现404错误。
springMVC配置如下
<mvc:annotation-driven conversion-service="conversionService" ></mvc:annotation-driven> <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <set> <ref bean="userConverterService"></ref> </set> </property> </bean>
binder相关部分截图
第二种,只配置annotation-driven
结果是可以正确格式化参数,但无法使用自定义类型转换器。
springMVC配置如下
<mvc:annotation-driven ></mvc:annotation-driven>
binder相关截图,可以看出有两个格式化Parser,但是converters有119个,里面我看了。没有我们自己的String –> User的转换器,这个转换器功能见数据绑定流程参考练习20自定义类型转换器中的数据绑定流程
springMVC配置如下
<mvc:annotation-driven conversion-service="conversionService" ></mvc:annotation-driven> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <set> <ref bean="userConverterService"></ref> </set> </property> </bean>
binder的相关截图如下,可以看出有两个格式化Parser,并且converters有120个,里面我看了,包含我们自己的类型转换器。
结论
通过测验,我们知道,如果我们需要格式化且有自己的类型转换器,那我们需要注意在增加自定义类型转换器时,需要通过FormattingConversionServiceFactoryBean来加入。这样才能正常转换,并且正常格式化。
练习24 数据校验
原理
Spring4.0拥有自己独立的数据校验框架,同时支持JSR303标准的校验框架。
Spring在进行数据绑定时,可同时调用数据校验框架完成数据校验工作,在springMVC中可直接通过注解驱动的方式进行数据校验。
Spring的LocalValidatorFactoryBean既实现了spring的validator接口,也实现了JSR303的validator接口,只要在spring容器中定义一个LocalValidatorFactoryBean即可将其注入到需要校验的bean中。
Spring本身并没有提供JSR303的实现,所以必须将JSR303的实现者的jar包放到类路径下。
<mvc:annotation-driven>
会默认装配好一个LocalValidatorFactoryBean,通过在处理方法的入参上标注@Valid注解,即可让springMVC在完成数据绑定后执行数据校验工作。
在已经标注了JSR303注解的表单/命令对象前标注一个@Valid,springMVC框架在将请求参数绑定到该入参对象后,就会调用验证框架根据注解声明的校验规则实施校验。
springMVC是通过对处理方法签名的规约来保存校验结果的:前一个表单/命令对象(springForm表单中Form标签的modelAttribute属性就是命令对象)的校验结果保存到随后的入参中,这个保存校验结果的入参必须是BindingResult或Errors类型。这两个类都位于org.springframework.validation包中。
需要校验的Bean对象和其绑定结果对象或错误对象是成对出现的,它们之间不允许声明其他入参。
Errors接口提供了获取错误信息的方法,如getErrorCount(),result.getFieldErrors()
等。
BindingResult扩展了Errors接口
在页面上显示错误
springMVC除了会将表单/命令对象的校验结果保存到对象的BindingResult或Errors对象中,还会将所有的校验结果保存到“隐含模型”中。
即使处理方法的签名中没有对表单/命令对象的结果入参,校验结果也会保存。
隐含对象中的所有数据最终会通过HttpServletRequest的属性列表暴露给JSP视图对象,因此在JSP中可以获取错误信息
在JSP页面上可通过<form:errors path="name"></form:errors> 显示对应属性的错误
国际化
每个属性在数据绑定或数据校验发生错误时,都会生成一个FieldError对象。
当一个属性校验失败后,校验框架会为该属性生成4个教习代码,这些代码以校验注解类名为前缀,结合modelAttribute,属性名及属性类型名生成多个对应的消息代码,例如User类中的pwd属性标注了一个Pattern注解,当属性不满足Pattern的规则时,就会产生以下4个消息代码:
1. Pattern.user.pwd
2. Pattern.pwd
3. Pattern.java.lang.String
4. Pattern
当使用springMVC的Form标签显示错误消息时,springMVC会查看上下文是否装配了对应的国际消息。如果没有,则显示默认的错误消息,若有,就显示对应的国际化消息。
若数据类型转换或数据格式化发生错误时,或该有的参数不存在时,或调用处理方法发生错误时,都会在隐含模型中创建错误消息,其错误代码前缀说明如下。
1. required 必要的参数不存在时,如@RequestParam(“param1”) 标注了一个入参,但该参数不存在
2. typeMismatch 在数据绑定时发生类型不匹配。
3. methodInvocation springMVC在调用处理方法时发生了错误
测试
STEP1 其次要加入依赖库
maven就很容易了。
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.1.3.Final</version> </dependency>
STEP2 添加校验规则
public class User { @NotEmpty private String name; @NotEmpty @Length(max = 5) private String pwd; @Past @DateTimeFormat(pattern = "yyyy-MM-dd") private Date birth; getter setter....... }
STEP3 按之前的练习14配置资源文件。我这里偷懒就只在中文资源文件i18n_zh_CN.properties里面配置了。
i18n.name=用户名i18n.pwd=密码NotEmpty.user.name=name必填NotEmpty.user.pwd=pwd必填Length.user.pwd=长度为1~5之间Past.user.birth=非法的出生日期typeMismatch.user.birth=出生日期格式错误
STEP4 随后是springMVC配置文件的配置,前提是你已经配置了</mvc:annotation-driven>
,这个是必须的。具体可参考练习21
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="i18n"></property> <property name="defaultEncoding" value="UTF-8"></property> </bean>
STEP5 然后再改造练习18的controller,当然最好你可以自己重新写一个。
@RequestMapping(value = "/saveUser",method = RequestMethod.POST) public String saveUser(@Valid User user, BindingResult result //User和其绑定结果的对象,必须成对出现,之间不能有其他入参声明 ,Map<String,Object> map){ if(result.getErrorCount() > 0){ System.out.println("出错了,错误条数为:"+result.getErrorCount()); for (FieldError e : result.getFieldErrors()){ System.out.println("ERROR: "+e.getField() + " : "+e.getDefaultMessage()); } /** * 这里需要注意的是,错误会绑定到验证的入参对象中,而表单如果需要显示错误, * 就需要在<form:form action="xx" method="xx" modelAttribute="user"> * <form:errors path="*" ></form:errors> 显示所有错误 * <form:errors path="name"></form:errors> 显示对应属性的错误 */ return "springForm"; } DataInit.getUsers().add(user); map.put("depts", DataInit.getDetps()); map.put("users",DataInit.getUsers()); return "redirect:/user/userInput"; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
STEP6 最后是jsp视图片段
<form:form action="/user/saveUser" method="post" modelAttribute="user"> <form:errors path="*" ></form:errors> <br> id:<form:input path="id" ></form:input><br> name:<form:input path="name" ></form:input> <form:errors path="name"></form:errors><br> pwd:<form:input path="pwd" ></form:input> <form:errors path="pwd"></form:errors><br> birth:<form:input path="birth"></form:input> <form:errors path="birth"></form:errors><br> pay:<form:input path="pay"></form:input><br> deptId:<form:select path="dept.id" items="${depts }" itemLabel="deptName" itemValue="id" ></form:select><br> <input type="submit" value="submit"></form:form>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
练习25 返回Json。分析HttpMessageConverter
在实际开发中,很多情况下需要给终端返回Json格式的响应,springMVC给我们提供了非常便捷的返回方式。
返回Json
STEP1 加入Jackson的依赖库
maven 配置文件
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.7.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.7.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.7.0</version> </dependency>
STEP2 然后controller
@ResponseBody @RequestMapping("/getUserJson") public Object getUserJson(){ List l = DataInit.getUsers(); return l; }
以上两步就可以返回Json了,这是为何呢,其实都是一个叫HttpMessageConverter的对象再起作用。
HttpMessageConverter
一、概述
HttpMessageConverter是spring3.0新添加的一个接口,负责将请求信息转换为一个对象(类型为T),将对象(类型为T)输出为响应信息。
HttpMessageConverter<T>
接口定义的方法:
1. Boolean canRead(Class<?> clazz,MediaType mediaType)
:指定转换器可以读取的对象类型,即转换器是否可以将请求信息转换为clazz类型的对象,同时支持MIME类型(text/html,application/json等)
2. Boolean canWrite(Class<?> clazz,MediaType mediaType)
:只转换器是否可以将类型为clazz的对象写入到响应流中,响应流中支持的媒体类型在MediaType中定义。
3. List<MediaType> getSupportMediaType()
:该转换器支持的媒体类型。
4. T read(Class<? extends T> clazz,HttpInputMessage inputMessage)
:将请求信息流转为T类型的对象。
5. void write(T t,MediaType contentType,HttpOutputMessage outputMessage)
:将T类型的对象写入到响应流中,同时指定相应的媒体类型为contentType。
使用HttpMessageConverter<T>
将请求信息转换并绑定到处理方法的入参中,或将响应结果转换为对应类型的响应信息,spring提供了两种途径。
1. 使用@RequestBody/@ResponseBody
对处理方法进行标注
2. 使用HttpEntity<T>/ResponseEntity<T>
作为处理方法的入参或返回值。
当控制器使用到以上两种方法时,Spring首先根据请求头或响应头的Accept属性选择匹配的HttpMessageConverter,进而根据参数类型或泛型类型的过滤得到匹配的HttpMessageConverter,若找不到可用的HttpMessageConverter将报错。
注意:@RequestBody/@ResponseBody不需要成对出现
二、HttpMessageConverter工作原理
三、HttpMessageConverter的实现类
序号 | 实现类 | 功能说明 | 1StringHttpMessageConverter将请求信息转为字符串2FormHttpMessageConverter将表单数据读取到MultiValueMap中3XmlAwareFormHttpMessageConverter扩展与FormHttpMessageConverter,如果部分表单属性是XML数据,可用该转换器进行读取4ResourceHttpMessageConverter读写org.springframework.core.io.Resource对象5BufferedImageHttpMessageConverter读写BufferedImage对象6ByteArrayHttpMessageConverter读写二进制数据7SourceHttpMessageConverter读写java.xml.transform.Source类型的对象8MarshallingHttpMessageConverter通过Spring的org.springframework,xml.Marshaller和Unmarshaller读写XML消息9Jaxb2RootElementHttpMessageConverter通过JAXB2读写XML消息,将请求消息转换为标注的XmlRootElement和XmlType连接的类中10MappingJacksonHttpMessageConverter利用Jackson开源包的ObjectMapper读写JSON数据11RssChannelHttpMessageConverter读写RSS种子消息12AtomFeedHttpMessageConverter和RssChannelHttpMessageConverter能够读写RSS种子消息四、加入Jackson的Jar包,转换器的前后对比
DispatchServlet默认装配RequestMappingHandlerAdapter,而RequestMappingHandlerAdapter默认装配如下HttpMessageConverter:
加入Jackson相关依赖包之后RequestMappingHandlerAdapter装配的HttpMessageConverter增加了转换Json的转换器
为了更好的理解HttpMessageConverter,可以看看这张图(来自网络)
小练习
Eg:1 通过@RequestBody/@ResponseBody
实现将上传文本文件(并不是真正上传),转换为String 并打印
controller
@ResponseBody @RequestMapping("/testHttpMessageConverter") public String testHttpMessageConverter(@RequestBody String file ){ System.out.println(file); return "testHttpMessageConverter date : "+ new Date(); }
jsp
<form action="/user/testHttpMessageConverter" method="post" enctype="multipart/form-data"> <input type="file" name="file"> <input type="submit" name="submit"> </form>
结果
文件内容
打印结果
Eg:2 通过ResponseEntity<T>
实现下载文件
controller
@RequestMapping("/testResponseEntity") public ResponseEntity<byte[]> testResponseEntity(){ String str = "xttttttsssss >>>>> testResponseEntity"; byte[] body = str.getBytes(); HttpHeaders hh = new HttpHeaders(); hh.add("Content-Disposition","attachment;filename=xxx.txt"); HttpStatus status = HttpStatus.OK; ResponseEntity<byte[]> responseEntity =new ResponseEntity<byte[]>(body,hh,status); return responseEntity; }
jsp
<a href="/user/testResponseEntity" >testResponseEntity</a>
下载的结果是
文件为:xxx.txt.html
内容为:
练习26 国际化
概述
默认情况下,springMVC会根据Accept-Language参数判断客户端的本地化类型。
当接受请求时,springMVC会在上下文中查找一个本地化解析器(LocalResolver)找到后,使用它获取请求所对应的本地化类型信息。
springMVC还允许装配一个动态更改类型本地化类型的拦截器,这样通过指定一个参数就可用控制单个请求的本地化类型。(因为本地化类型是放到session作用域中)
本地化解析器和本地化拦截器
1. AcceptHeaderLocaleResolver:根据HTTP请求头的Accept-Language来确定本地化类型,如果没有显示定义本地化解析器,springMVC默认使用该解析器。
2. CookieLocaleResolver:根据指定的Cookie值确定本地化类型。
3. SessionLocaleResolver:根据Session中特定的本地化参数确定本地化类型。
4. LocaleChangeInterceptor:从请求中获取本次请求对应的本地化类型。
加入SessionLocaleResolver前后对比
加入前
加入后
为了更好的理解LocaleChangeInterceptor和SessionLocaleResolver,可以参考如下图(来自网络)
小练习
通过默认的本地化解析器AcceptHeaderLocaleResolver这次就不测试了,咱们直接用SessionLocaleResolver来动态改变本地化类型。
STEP1 准备资源文件。其实这些资源文件咱们前面用过的。
在resource目录下,建三个国际化资源文件
i18n.properties和i18n_en_US.properties内容一样
i18n.name=namei18n.pwd=password
i18n_zh_CN.properties内容如下
i18n.name=用户名i18n.pwd=密码
STEP2 springMVC增配
这里需要注意的是
1. SessionLocaleResolver的bean的ID一定要是localeResolver,否则会报Cannot change HTTP accept header - use a different locale resolution strategy
错误
2. 资源文件要配置_en_US或_zh_CN否则找不到对应本地化类型的资源文件。
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="i18n"></property> <property name="defaultEncoding" value="UTF-8"></property> </bean> <bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"></bean> <mvc:interceptors> <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"></bean> </mvc:interceptors>
STEP3 controller添加目标方法
@Resource private ResourceBundleMessageSource messageSource; @RequestMapping("/i18n") public String testI18n(Locale locale){ String str = messageSource.getMessage("i18n.name",null,locale); System.out.println(str); return "index"; }
STEP4 jsp
[ <a href="/user/i18n?locale=zh_CN">i18n_Change_zh_CN</a> ]<br> [ <a href="/user/i18n?locale=en_US">i18n_Change_en_US</a> ] <br> <p> <fmt:message key="i18n.name"></fmt:message></p> <p> <fmt:message key="i18n.pwd"></fmt:message></p>
结果:
点击i18n_Change_zh_CN链接,name和pwd变为用户名和密码,
点击i18n_Change_en_US链接,name和pwd变为name和password,它们直接可以互相切换,并且后台console窗口可以输出对应的值。
源码分析
STEP1 进入顶层方法
org.springframework.web.servlet.DispatcherServlet#doDispatch
if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; }
STEP2 进入
org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { if (getInterceptors() != null) { for (int i = 0; i < getInterceptors().length; i++) { HandlerInterceptor interceptor = getInterceptors()[i]; if (!interceptor.preHandle(request, response, this.handler)) { triggerAfterCompletion(request, response, null); return false; } this.interceptorIndex = i; } } return true; }
STEP3 进入
org.springframework.web.servlet.i18n.LocaleChangeInterceptor#preHandle
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException {/**可以看到这里获取我们请求的参数。paramName,这个是已经写死的。* public static final String DEFAULT_PARAM_NAME = "locale";* private String paramName = DEFAULT_PARAM_NAME;**/ String newLocale = request.getParameter(this.paramName); if (newLocale != null) { LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request); if (localeResolver == null) { throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?"); } localeResolver.setLocale(request, response, StringUtils.parseLocaleString(newLocale)); } return true; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
STEP4 进入
org.springframework.web.servlet.i18n.SessionLocaleResolver#setLocaleContext
@Override public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) { Locale locale = null; TimeZone timeZone = null; if (localeContext != null) { locale = localeContext.getLocale(); if (localeContext instanceof TimeZoneAwareLocaleContext) { timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone(); } } WebUtils.setSessionAttribute(request, LOCALE_SESSION_ATTRIBUTE_NAME, locale); WebUtils.setSessionAttribute(request, TIME_ZONE_SESSION_ATTRIBUTE_NAME, timeZone); }
STEP5 进入
org.springframework.web.util.WebUtils#setSessionAttribute
public static void setSessionAttribute(HttpServletRequest request, String name, Object value) { Assert.notNull(request, "Request must not be null"); if (value != null) { request.getSession().setAttribute(name, value); } else { HttpSession session = request.getSession(false); if (session != null) { session.removeAttribute(name); } } }
STEP6 将STEP1 的mv放到org.springframework.web.servlet.DispatcherServlet#processDispatchResult方法中
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
STEP7 进入processDispatchResult方法
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception { ... ... if (mv != null && !mv.wasCleared()) { render(mv, request, response);方法 if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } ... .. }
STEP8 进入render方法
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { Locale locale = this.localeResolver.resolveLocale(request); response.setLocale(locale); ... ...}
练习27 文件上传
springMVC为文件上传提供了直接的支持,这种支持是通过即插即用的MultipartResolver实现的,spring用Jakarta Commons FileUpload技术实现了一个MultipartResolver实现类CommonsMultipartResolver。
springMVC上下文中默认没有装配MultipartResolver,因此默认情况下不能处理文件上传工作,如需使用,则需要配置MultipartResolver。
配置MultipartResolver时需要注意两个点
1. defaultEncoding 必须和用户JSP的pageEncoding属性一致,以便正确的解析表单内容。
2. 为了让CommonsMultipartResolver正常工作,需要将Jakarta Commons FileUpload及Commons io的包加到项目中。
小练习
STEP1 加入jar包,增加maven配置,就把fileupload和io引入进来
<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version> </dependency>
STEP2 springMVC增配
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="1024000"></property> <property name="defaultEncoding" value="UTF-8"></property> <property name="resolveLazily" value="true"></property> </bean>
STEP3 controller增加目标方法
@RequestMapping("/testFileUpload") public String testFileUpload(@RequestParam("file") MultipartFile file) throws IOException { System.out.println("getOriginalFilename :"+file.getOriginalFilename()) System.out.println("getSize :"+file.getSize()) System.out.println("getInputStream :"+file.getInputStream()) return "index" }
STEP3 jsp增加form表单
<form action="/user/testFileUpload" method="post" enctype="multipart/form-data"> <input type="file" name="file"> <input type="submit" name="submit"> </form>
结果,后台正常输出:
getOriginalFilename :屏幕快照 2016-01-13 上午11.32.04.pnggetSize :48541getInputStream :java.io.FileInputStream@1f460230
练习28 自定义拦截器
springMVC可以使用拦截器对请求进行拦截,用户可以自定义拦截器来实现特定的需求,自定义的拦截器必须实现HandlerInterceptor接口。接口中需要实现三个方法
1. preHandler() 这个方法再调用目标方法之前被执行,在该方法中可以对用户请求request 进行处理,如果需要在执行这个方法之后还需要调用其他拦截器,或者目标方法,则必须返回true。否则返回false。
2. postHandler() 这个方法是在目标方法执行完后,且在试图渲染之前执行(也就是DispatchServlet向客户端返回响应前调用),在该方法中对request进行处理。
3. afterCompletion() 这个方法在DispatchServlet完全处理完请求后被调用,可以在该方法中进行一些资源清理的工作。
练习
STEP1 实现HandlerInterceptor接口的实现类MyFirstInterceptor和MySecondInterceptor
MySecondInterceptor和MyFirstInterceptor一样,自己写。
public class MyFirstInterceptor implements HandlerInterceptor{ /** * 执行时间:在调用目标方法之前调用 * 返回true,执行后续方法,返回false,则都不回执行. * 作用:可以用作 权限,日志,事务等. * * 特殊情况说明: * 在多个拦截器出现的时候,如果最后的拦截器返回false,则任然会执行上一个拦截器会的afterCompletion方法 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("MyFirstInterceptor preHandle" ); return true; } /** * 执行时间:调用目标方法之后,渲染视图事前 * 作用:可以对请求域中的属性或视图进行修改 */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("MyFirstInterceptor postHandle" ); } /** * 实现时间:渲染视图之后调用 * 作用:释放资源 */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("MyFirstInterceptor afterCompletion" ); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
STEP2 springMVC增配
<mvc:interceptors> <bean class="com.iboray.smt.controller.MyFirstInterceptor"></bean> <mvc:interceptor> <mvc:mapping path="/user/**"/> <bean class="com.iboray.smt.controller.MySecondInterceptor"></bean> </mvc:interceptor> <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"></bean> </mvc:interceptors>
执行/user下的所有目标方法,看后台打印结果,我执行的是<a href="/user/i18n?locale=zh_CN">i18n_Change_zh_CN</a>
这个方法,例子 再练习26 国际化中。
后台打印
MyFirstInterceptor preHandleMySecondInterceptor preHandle用户名MySecondInterceptor postHandleMyFirstInterceptor postHandleMySecondInterceptor afterCompletionMyFirstInterceptor afterCompletion
为什么会是这个顺序呢?
因为spring默认会按配置文件是顺序加载自定义的拦截器
如图:
为了进一步了解拦截器执行顺序,参考下图(来自网络)
情况1 first和second拦截器的preHandle方法都返回true,也就是正常实行
情况2 first拦截器的preHandle方法返回true,second拦截器的preHandle方法返回false,执行流程为实线。
练习29 异常处理
springMVC通过HandlerExceptionResolver处理程序异常,包括Handler映射,数据绑定以及目标方法执行时发生的异常。
springMVC提供了HandlerExceptionResolver的实现类
DispatchServlet默认装配的HandlerExceptionResolver如果使用<mvc:annotation-driven>
,则实现类为
1. ExceptionHandlerExceptionResolver(如果不使用<mvc:annotation-driven>
,默认加载的是AnnotationMethodHandlerExceptionResolver)
2. ResponseStatusExceptionResolver
3. DefaultHandlerExceptionResolver
ExceptionHandlerExceptionResolver
主要处理Handler中@ExceptionHandler注解定义的方法
@ExceptionHandler有处理优先级的问题,如发生的是NullPointerException,但声明的是RuntimeException和Exception,此时会根据异常的最近继承关系找到继承深度最浅的那一个@ExceptionHandler注解方法,也就是标记了RuntimeException的方法。
ExceptionHandlerMethodResolver内部若找不到@ExceptionHandler注解的话,会找@ControllerAdvice中的@ExceptionHandler注解方法。并且也会有优先级的问题。
小练习
STEP1 controller加入目标方法和@ExceptionHandler方法
/** * @ExceptionHandler 标注的方法,可以在入参中加入 Exception类型的参数,该参数即对应发生的异常对象. * 若希望将异常对象传到页面上,则需要返回ModelAndView,而不能是Map */ @ExceptionHandler({NullPointerException.class}) public ModelAndView testExceptionHandlerExceptionResolver(Exception ex){ ModelAndView mv = new ModelAndView("error"); mv.addObject("ex",ex); return mv; } @RequestMapping("/testMatch") public String testMatch(@RequestParam("a") int a ){ System.out.println( 10 / a); return "index"; }
STEP2 ControllerAdvice标记的类
需要注意的是,这个类的@ExceptionHandler方法中放入的异常比上面controller中的异常要“小”,也就是继承级别很浅。那如果发生ArithmeticException.class异常,对应处理方法是MyExceptionHandle的testExceptionHandlerExceptionResolver方法。
@ControllerAdvicepublic class MyExceptionHandle { @ExceptionHandler({ArithmeticException.class}) public ModelAndView testExceptionHandlerExceptionResolver(Exception ex){ System.out.println("MyExceptionHandle"+ex); ModelAndView mv = new ModelAndView("error"); mv.addObject("ex",ex); return mv; }}
STEP3 error界面承接exception对象
<body> ${ex}</body>
STEP4 测试
<a href="/user/testMatch?a=0" >testMatch</a>
结果打印的是
MyExceptionHandle java.lang.ArithmeticException: / by zero
ResponseStatusExceptionResolver
在异常及异常父类中找到@ResponseStatus注解,然后使用这个注解的属性进行处理
定义一个@ResponseStatus注解修饰的异常类
若再处理器方法中抛出@ResponseStatus修饰的类类型,若ExceptionHandlerExceptionResolver不解析异常,由于触发的异常的类带有@ResponseStatus注解,因此会被ResponseStatusExceptionResolver解析到,最后响应HttpStatus.XXXX代码给客户端,关于其他响应代码参考org.springframework.http.HttpStatus枚举类
小练习
STEP1 添加@ResponseStatus标注的类
@ResponseStatus(value = HttpStatus.NOT_ACCEPTABLE,reason = "非法授权")public class MyResponseStatusException extends RuntimeException {}
STEP2 添加controller目标方法
/** *注意:如果将@ResponseStatus放到这里,那么执行完这个方法后会直接返回错误状态码和原因。 *这样就不用新建那个自己的异常处理类(MyResponseStatusException)了。可以根据自身的业务来做调整。 **/ @RequestMapping("/testResponseStatusException") public String testResponseStatusException(@RequestParam("a") int a){ if (a == 10){ throw new MyResponseStatusException(); } System.out.println("testResponseStatusException .. . . . ."); return "index"; }
STEP3 测试
<a href="/user/testResponseStatusException?a=1" >testResponseStatusException</a>
结果
源码分析
进入doResolveException方法
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver#doResolveException
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class); if (responseStatus != null) { try { return resolveResponseStatus(responseStatus, request, response, handler, ex); } catch (Exception resolveEx) { logger.warn("Handling of @ResponseStatus resulted in Exception", resolveEx); } } return null; }
进入resolveResponseStatus
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver#resolveResponseStatus
protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { int statusCode = responseStatus.value().value(); String reason = responseStatus.reason(); if (this.messageSource != null) { reason = this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()); } if (!StringUtils.hasLength(reason)) { response.sendError(statusCode); } else { response.sendError(statusCode, reason); } return new ModelAndView(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
DefaultHandlerExceptionResolver
这个异常解析器是对一些特殊的异常进行处理的。如
HttpRequestMethodNotSupportedException
MissingServletRequestParameterException
ServletRequestBindingException
ConversionNotSupportedException
….等。是不是似曾相识呢?对,这些就是springMVC自己的一些异常处理方法。
小练习
STEP1 controller
@RequestMapping(value = "/testDefaultHandlerExceptionResolver",method = RequestMethod.POST) public String testDefaultHandlerExceptionResolver(){ System.out.println("testDefaultHandlerExceptionResolver .. . . . ."); return "index"; }
STEP2 请求
<a href="/user/testDefaultHandlerExceptionResolver" >testDefaultHandlerExceptionResolver</a>
结果:
源码分析
进入
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver#doResolveException
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { if (ex instanceof NoSuchRequestHandlingMethodException) { return handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex, request, response, handler); } else if (ex instanceof HttpRequestMethodNotSupportedException) { return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, request, response, handler); } ... }
SimpleMappingExceptionResolver
SimpleMappingExceptionResolver可以对所有异常进行统一处理,它将异常名映射为视图名,也就是说。捕获异常后,输出到指定的视图。
小练习
STEP1 增加controller
@RequestMapping("/testSimpleMappingExceptionResolver") public String testSimpleMappingExceptionResolver(@RequestParam("a") String a){ Integer.parseInt(a); System.out.println("testSimpleMappingExceptionResolver .. . . . ."); return "index"; }
STEP2 测试 未配置异常解析器
<a href="/user/testSimpleMappingExceptionResolver?a=a" >testSimpleMappingExceptionResolver</a>
结果
STEP3测试 配置异常解析器
增加springMVC配置
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionAttribute" value="ex"></property> <property name="exceptionMappings"> <props> <prop key="java.lang.NumberFormatException">error</prop> </props> </property> </bean>
再进行STEP2 测试,结果:
最后想说的
学在苦中求,艺在勤中练。