SpringMVC
1. SpringMVC概述
-
关于SpringMVC
- Spring 为展现层提供的基于 MVC 设计理念的优秀的 Web 框架,是目前最主流的 MVC 框架之一
- Spring3.0 后全面超越 Struts2,成为最优秀的 MVC 框架
- 一种轻量级的、基于MVC的Web层应用框架, 偏前端而不是基于业务逻辑层, 是Spring框架的一个后续产品
- Spring MVC 通过一套 MVC 注解,让 POJO 成为处理请求的控制器,而无须实现任何接口
- 支持 REST 风格的 URL 请求
- 采用了松散耦合可插拔组件结构,比其他 MVC 框架更具扩展性和灵活性
-
SpringMVC的功能
- 天生与Spring框架集成,如:(IOC,AOP)
- 支持Restful风格
- 进行更简洁的Web层开发
- 支持灵活的URL到页面控制器的映射
- 因为模型数据不存放在特定的API里,而是放在一个Model里(Map数据结构实现,因此很容易被其他框架使用)
- 非常灵活的数据验证、格式化和数据绑定机制、能使用任何对象进行数据绑定,不必实现特定框架的API
- 更加简单、强大的异常处理
- 对静态资源的支持
- 支持灵活的本地化、主题等解析
-
常用主要组件
- DispatcherServlet:前端控制器
- Controller:处理器/页面控制器,做的是MVC中的C的事情,但控制逻辑转移到前端控制器了,用于对请求进行处理
- HandlerMapping:请求映射到处理器,找谁来处理,如果映射成功返回一个HandlerExecutiongChain对象(包含一个Handler处理器(页面控制器)对象、多个HandlerInterceptor拦截器对象)
- ViewResolver: 视图解析器,找谁来处理返回的页面。把逻辑视图解析为具体的View,进行这种策略模式,很容易更换其他视图技术
- 如InternalResourceViewResolver将逻辑视图名映射为JSP视图
- LocalResolver:本地化、国际化
- MultipartResolver:文件上传解析器
- HandlerExceptionResolver:异常处理器
-
SpringMVC的工作流程
2. HelloWorld
<!--1. 导入依赖-->
<dependencies>
<!--spring的配置包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.8.RELEASE</version>
</dependency>
<!--spring的日志依赖包-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<!--spring的单元测试-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.8.RELEASE</version>
</dependency>
<!--AOP-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
<!--SpringMVC-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.8.RELEASE</version>
</dependency>
</dependencies>
<!--2. web.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--配置前端控制器DispatcherServlet-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--指定springmvc的配置文件位置-->
<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>
<!--/*和/都会拦截所有请求-->
<!--/*会拦截jsp请求,一旦拦截jsp请求,页面将无法正常显示-->
<!--/不会拦截jsp请求,页面可以正常显示-->
<!--处理*.jsp是Tomcat做的事-->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
<!--3. springmvc.xml-->
<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.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--包扫描-->
<context:component-scan base-package="com.siki"/>
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
//4. 控制器
@Controller
public class HelloWorld {
@RequestMapping("/hello")
public String hello(){
System.out.println("请求收到...");
return "success";
}
}
<!--5. index.jsp-->
<body>
<a href="hello">helloworld</a>
</body>
<!--success.jsp-->
<body>
<font color="#98fb98">成功</font>
</body>
- 如果项目启动报错,可能是缺少lib依赖
- HelloWorld运行细节
- 运行流程
- 客户端点击链接会发送http://localhost:8080/SpringMVC_HelloWorld/hello请求
2. 来到Tomcat服务器
3. SpringMVC的前端控制器会收到所有请求,看请求地址和@RequestMapping标注的哪个请求匹配,来找到到底使用哪个类的哪个请求
4. 前端控制器找到了目标处理器和目标方法,直接利用反射执行目标方法
5. 方法执行完以后会有一个返回值,SpringMVC认为这个返回值就是要去的页面地址
6. 拿到返回值以后,用视图解析器进行拼串,得到完整的页面地址
7. 拿到页面地址,前端控制器帮我们转发到相应的页面
- 客户端点击链接会发送http://localhost:8080/SpringMVC_HelloWorld/hello请求
- @RequestMapping
- 告诉SpringMVC,这个方法用来处理什么请求
- 这个"/"是可以省略的,也是默认从当前项目下开始
- 习惯性加上比较好
- 如果不指定配置文件的位置,也会默认去/WEB-INF/xxx-servlet.xml路径下找配置文件,如果resources文件夹下没有配置文件,可以在WEB-INF下创建一个名叫"前端控制器名-servlet.xml"的配置文件
- url-pattern
- /配置的是"/"
- Tomcat服务器的web.xml中有一个DefaultServlet,它的url-pattern设置的是"/"
- DefaultServlet是用来处理Tomcat中的静态资源的,除jsp、servlet外,其余的页面都是静态资源
- jsp是由Tomcat服务器的JspServlet处理的,所以可以访问
- 运行流程
3. @RequestMapping
-
SpringMVC使用@RequestMapping注解为控制器指定可以处理哪些 URL 请求
-
在控制器的类定义及方法定义处都可标注 @RequestMapping
- 标记在类上:提供初步的请求映射信息。相对于 WEB 应用的根目录
- 标记在方法上:提供进一步的细分映射信息。相对于标记在类上的 URL
-
若类上未标注 @RequestMapping,则方法处标记的 URL 相对于 WEB 应用的根目录
-
作用:DispatcherServlet 截获请求后,就通过控制器上 @RequestMapping 提供的映射信息确定请求所对应的处理方法
//@RequestMapping源码 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface RequestMapping { String name() default ""; @AliasFor("path") String[] value() default {}; @AliasFor("value") String[] path() default {}; RequestMethod[] method() default {}; String[] params() default {}; String[] headers() default {}; String[] consumes() default {}; String[] produces() default {}; }
-
RequestMapping支持Ant 路径风格
-
?:匹配文件名中的一个字符,0个或多个都不行,模糊和精确的情况下,精确优先
-
*:匹配文件名中的任意字符,也可以匹配一层路径
-
**:匹配多层路径
/user/*/createUser 匹配 /user/aaa/createUser、/user/bbb/createUser 等 URL /user/**/createUser 匹配 /user/createUser、/user/aaa/bbb/createUser 等 URL /user/createUser?? 匹配 /user/createUseraa、/user/createUserbb 等 URL
-
-
RequestMapping映射请求占位符PathVariable注解
- @PathVariable 映射 URL 绑定的占位符
- 带占位符的 URL 是 Spring3.0 新增的功能,该功能在 SpringMVC 向 REST目标挺进发展过程中具有里程碑的意义
- 通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中
- URL 中的 {xxx} 占位符可以通过 @PathVariable(“xxx”) 绑定到操作方法的入参中
@RequestMapping("/haha") //为该类中的所有方法提供一个基准路径
@Controller
public class RequestMappingTest {
@RequestMapping("/hello01")
public String hello01(){
return "success";
}
@RequestMapping("/hello02")
public String hello02(){
return "success";
}
/*
1. @RequestMapping的其他属性
1. method: 限定请求方式,默认是任意类型
HTTP协议中的所有请求方式
POST GET HEAD PATCH PUT DELETE OPTIONS等
2. params: 规定请求参数,可以写一些简单的表达式
1. param1: 表示请求必须包含名为param1的请求参数
params = {"username"}
2. !param2: 表示请求不能包含名为param2的请求参数
params = {"!username"}
3. param1 != value1: 表示请求必须包含名为param1的参数,但是其值不能为value1
params = {"username!=root"}
3. headers: 规定请求头,也和params一样,可以写一些简单的表达式
4. consumes: 只接受内容类型是哪一种的请求,规定请求头中的Content-Type
*/
//表示只接收post请求,其他的方式就会报错
@RequestMapping(value = "/hello03",method = RequestMethod.POST)
public String hello03(){
return "success";
}
@RequestMapping(value = "/hello04",params = {"username"})
public String hello04(){
return "success";
}
//表示只有谷歌能访问,其他浏览器不行
@RequestMapping(value = "/hello05",headers = {"User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36"})
public String hello05(){
return "success";
}
//路径上的占位符{username}只占一层路径
@RequestMapping("/hello06/{username}")
public String hello06(@PathVariable("username") String username){
System.out.println("username = " + username);
return "success";
}
}
4. Rest风格的增删改查
-
Rest风格:即 Representational State Transfer 资源表现层状态转化。是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用
- 资源(Resources):网络上的一个实体,或者说是网络上的一个具体信息,它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的存在,可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的 URI 。获取这个资源,访问它的URI就可以,因此 URI 即为每一个资源的独一无二的识别符
- 表现层(Representation):把资源具体呈现出来的形式,叫做它的表现层(Representation)。比如,文本可以用 txt 格式表现,也可以用 HTML 格式、XML 格式、JSON 格式表现,甚至可以采用二进制格式
- 状态转化(State Transfer):每发出一个请求,就代表了客户端和服务器的一次交互过程。HTTP协议,是一个无状态协议,即所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转化”(State Transfer)而这种转化是建立在表现层之上的,所以就是 “表现层状态转化”
- 具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE,它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源
-
HiddenHttpMethodFilter
-
浏览器 form 表单只支持 GET 与 POST 请求,而DELETE、PUT 等 method 并不支持,
-
Spring3.0 添加了一个过滤器,可以将这些请求转换为标准的 http 方法,使得支持 GET、POST、PUT 与 DELETE 请求
-
<!--web.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--配置前端控制器-->
<servlet>
<servlet-name>springmvc</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>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--配置过滤器,支持Rest风格的PUT和DELETE-->
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<!--除了DispatcherServlet使用"/",其他的拦截使用"/*"-->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
<!--springmvc.xml-->
<!--包扫描-->
<context:component-scan base-package="com.siki"/>
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
@Controller
public class BookController {
//增加图书
@RequestMapping(value = "/book",method = RequestMethod.POST)
public String addBook(){
System.out.println("添加了新的图书");
return "success";
}
//修改图书
@RequestMapping(value = "/book/{id}",method = RequestMethod.PUT)
public String updateBook(@PathVariable("id") Integer id){
System.out.println("更新了 " + id + " 号图书");
return "success";
}
//查询图书
@RequestMapping(value = "/book/{id}",method = RequestMethod.GET)
public String getBook(@PathVariable("id") Integer id){
System.out.println("查询到了 " + id + " 号图书");
return "success";
}
//删除图书
@RequestMapping(value = "/book/{id}",method = RequestMethod.DELETE)
public String deleteBook(@PathVariable("id") Integer id){
System.out.println("删除了 " + id + " 号图书");
return "success";
}
}
<!--success.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true"%>
<html>
<head>
<title>Title</title>
</head>
<body>
<font color="green">成功</font>
</body>
</html>
<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<!--
发起图书的增删改查请求,使用Rest风格的URL地址
1. 服务器支持POST、GET请求,但是不支持PUT、DELETE请求
2. 如何从页面发起PUT、DELETE请求
1. SpringMVC有一个Filter,它可以把普通的请求转换为规定形式的请求,我们可以在web.xml中配置这个Filter
2. 按照以下要求
1. 创建一个post类型的表单
2. 表单项中携带一个_method的参数,这个_method的值设置为PUT或者DELETE
3. 注意: 高版本的Tomcat(8.0及以上版本)对PUT、DELETE请求方式不支持
解决办法: <%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
在success.jsp中设置isErrorPage="true",默认为false
-->
<a href="book/1">查询图书</a><br>
<form action="book" method="post">
<input type="submit" value="添加图书"/>
</form><br>
<form action="book/1" method="post">
<input name="_method" value="delete"/>
<input type="submit" value="删除图书"/>
</form><br>
<form action="book/1" method="post">
<input name="_method" value="put"/>
<input type="submit" value="更新图书"/>
</form>
</body>
</html>
//HiddenHttpMethodFilter源码分析
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List<String> ALLOWED_METHODS;
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = "_method";
public HiddenHttpMethodFilter() {
}
public void setMethodParam(String methodParam) {
Assert.hasText(methodParam, "'methodParam' must not be empty");
this.methodParam = methodParam;
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest requestToUse = request;
//表单是一个Post
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
//获取表单上_method带来的值
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
//而且是PUT、DELETE、PATCH中的一种,就会转换为对应的请求方式
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter((ServletRequest)requestToUse, response);
}
static {
ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
}
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}
public String getMethod() {
return this.method;
}
}
}
5. 请求数据传入
- 请求处理方法参数
- Spring MVC 通过分析处理方法的签名,HTTP请求信息绑定到处理方法的相应人参中
- Spring MVC 对控制器处理方法签名的限制是很宽松的,几乎可以按喜欢的任何方式对方法进行签名
- 必要时可以对方法及方法入参标注相应的注解( @PathVariable 、@RequestParam、@RequestHeader 等)
- Spring MVC 框架会将 HTTP 请求的信息绑定到相应的方法入参中,并根据方法的返回值类型做出相应的后续处理
<!--web.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--
注意: 使用SpringMVC时,前端控制器写完就写字符编码过滤器
1. 设置GET请求编码方式
在Tomcat的conf文件夹中的server.xml中8080端口处添加URLEncoding="UTF-8"
2. 设置字符编码过滤器
-->
<!--配置前端控制器-->
<servlet>
<servlet-name>springmvc</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>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--配置字符编码过滤器 一定要注意: 字符编码过滤器一定要配置在其他过滤器之前,否则无法生效-->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!--指定解决POST请求乱码-->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!--解决请求乱码和响应乱码-->
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--配置请求方式过滤器-->
<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>
</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:context="http://www.springframework.org/schema/context"
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.xsd">
<!--包扫描-->
<context:component-scan base-package="com.siki"/>
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
<!--index.jsp-->
<html>
<head>
<title>$Title$</title>
</head>
<body>
<a href="hello">hello</a><br>
<a href="hello01?username=xinxin">默认方式请求参数</a><br>
<a href="hello02?user=root">@RequestParameter方式请求参数</a>
<form action="book" method="post">
书名: <input type="text" name="bookName"/><br>
作者: <input type="text" name="authorName"/><br>
价格: <input type="text" name="price"/><br>
库存: <input type="text" name="stock"/><br>
销量: <input type="text" name="sales"/><br>
省: <input type="text" name="address.province"/><br>
市: <input type="text" name="address.city"/><br>
街道: <input type="text" name="address.street"/><br>
<input type="submit" value="添加图书"/>
</form>
<a href="hello03">原生API</a>
</body>
</html>
<!--success.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<font color="green">成功</font><br>
请求: ${requestScope.requestMsg}<br>
会话: ${sessionScope.sessionMsg}<br>
</body>
</html>
@Controller
public class RequestController {
@RequestMapping("/hello")
public String hello(){
return "success";
}
/*
SpringMVC如何获取请求带来的各种信息
1. @RequestParam: 获取请求参数
1. 默认方式获取请求参数
直接给方法形参是写一个和请求参数名相同的变量,这个变量用来接收请求参数的值
2. @RequestParam("user") String username 相当于username = request.getParameter("user")
1. 参数默认是必须带的
2. 属性
1. value: 指定要获取参数的key
2. required: 如果为true,表示参数必须要带
3. defaultValue: 如果没带参数,我们可以指定默认值
2. @RequestHeader: 获取请求头中某个key的值,如果请求头中没有这个值,就会报错
@RequestHeader("User-Agent") String userAgent
相当于userAgent = request.getParameter("User-Agent")
3. @CookieValue: 获取某个Cookie的值
@CookieValue("JSESESSIONID") String id
相当于id = request.getCookies()[i].getName.equals("JSESESSIONID")
4. 传入POJO,SpringMVC会自动封装
1. 我们的请求参数是一个POJO,SpringMVC会自动的为这个POJO进行赋值
2. 将POJO中的每一个属性,从request参数中尝试获取出来,并进行封装
3. 还可以进行级联封装
5. SpringMVC也支持传入原生API,但也不是所有的API都支持,只支持以下几种
1. HttpServletRequest
2. HttpServletResponse
3. HttpSession
4. java.security.Principal
5. Local: 国际化有关的区域信息对象
6. InputStream、OutputStream、Reader、Writer
request.getInputStream();
response.getOutputStream();
request.getReader();
response.getWriter();
6. 当我们传入POJO的时候,可能会出现乱码
1. 请求乱码
GET请求: 在Tomcat的conf文件夹中的server.xml中8080端口处添加URLEncoding="UTF-8"
POST请求: 在第一次获取请求参数之前设置
request.setCharacterEncoding("utf-8");
SpringMVC为我们准备了一个CharacterEncodingFilter过滤器,解决乱码问题,在web.xml中配置
2. 响应乱码
response.setContentType("text/html;charset=utf-8");
*/
//默认方式获取请求参数
@RequestMapping("/hello01")
public String hello01(String username){
System.out.println("username = " + username);
return "success";
}
@RequestMapping("/hello02")
public String hello02(@RequestParam(value = "user",required = false) String username,
@RequestHeader("User-Agent") String userAgent,
@CookieValue(value = "JSESESSIONID",required = false) String id){
System.out.println("username = " + username);
System.out.println("User-Agent = " + userAgent);
System.out.println("JSESESSIONID = " + id);
return "success";
}
@RequestMapping("/book")
public String addBook(Book book){
System.out.println(book);
return "success";
}
@RequestMapping("/hello03")
public String hello03(HttpSession session, HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
request.setAttribute("requestMsg","我是HttpServletRequest");
session.setAttribute("sessionMsg","我是HttpSession");
return "success";
}
}
//CharacterEncodingFilter源码
public class CharacterEncodingFilter extends OncePerRequestFilter {
@Nullable
private String encoding;
private boolean forceRequestEncoding;
private boolean forceResponseEncoding;
public CharacterEncodingFilter() {
//默认不解决请求乱码和响应乱码
this.forceRequestEncoding = false;
this.forceResponseEncoding = false;
}
public CharacterEncodingFilter(String encoding) {
this(encoding, false);
}
public CharacterEncodingFilter(String encoding, boolean forceEncoding) {
this(encoding, forceEncoding, forceEncoding);
}
public CharacterEncodingFilter(String encoding, boolean forceRequestEncoding, boolean forceResponseEncoding) {
this.forceRequestEncoding = false;
this.forceResponseEncoding = false;
Assert.hasLength(encoding, "Encoding must not be empty");
this.encoding = encoding;
this.forceRequestEncoding = forceRequestEncoding;
this.forceResponseEncoding = forceResponseEncoding;
}
public void setEncoding(@Nullable String encoding) {
this.encoding = encoding;
}
@Nullable
public String getEncoding() {
return this.encoding;
}
public void setForceEncoding(boolean forceEncoding) {
this.forceRequestEncoding = forceEncoding;
this.forceResponseEncoding = forceEncoding;
}
public void setForceRequestEncoding(boolean forceRequestEncoding) {
this.forceRequestEncoding = forceRequestEncoding;
}
public boolean isForceRequestEncoding() {
return this.forceRequestEncoding;
}
public void setForceResponseEncoding(boolean forceResponseEncoding) {
this.forceResponseEncoding = forceResponseEncoding;
}
public boolean isForceResponseEncoding() {
return this.forceResponseEncoding;
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String encoding = this.getEncoding();
if (encoding != null) {
if (this.isForceRequestEncoding() || request.getCharacterEncoding() == null) {
//这里设置字符编码方式
request.setCharacterEncoding(encoding);
}
if (this.isForceResponseEncoding()) {
response.setCharacterEncoding(encoding);
}
}
filterChain.doFilter(request, response);
}
}
6. 响应数据传出
- SpringMVC输出模型数据概述
- ModelAndView: 处理方法返回值类型为 ModelAndView 时, 方法体即可通过该对象添加模型数据
- Map 及 Model: 入参为 org.springframework.ui.Model、org.springframework.ui.ModelMap 或 java.uti.Map 时,处理方法返回时,Map 中的数据会自动添加到模型中
- @SessionAttributes: 将模型中的某个属性暂存到 HttpSession 中,以便多个请求之间可以共享这个属性
- @ModelAttribute: 方法入参标注该注解后, 入参的对象就会放到数据模型中
<!--web.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>springmvc</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>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
<!--springmvc.xml-->
<!--包扫描-->
<context:component-scan base-package="com.siki"/>
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<a href="hello01">hello01</a><br>
<a href="hello02">hello02</a><br>
<a href="hello03">hello03</a><br>
<a href="hello04">hello04</a><br>
<a href="hello05">hello05</a><br>
</body>
</html>
<!--success.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<font color="green">成功</font><br>
<!--
SpringMVC如何将数据带到页面中来
1.可以在方法处传入Map、Model或者ModelMap,给这些参数里面保存的所有数据都会放在request域中,可以在页面获取到
1. 这三种方式最终都是BindingAwareModelMap进行的处理
2. 相当于给BindingAwareModelMap中保存的东西都会被放到request域中
3. Map: JDK中的接口
4. Model: Spring中的接口
5. ModelMap: 继承自LinkedHashMap的一个类
6. BindingAwareModelMap: 继承自ExtendedModelMap,ExtendedModelMap继承自ModelMap,并实现了Model接口
2. 方法的返回值还可以是ModelAndView类型
1. 既包含视图信息(页面地址),也包含模型数据(带给页面的数据)
2. 而且数据也是放在request域中的
3. SpringMVC提供了一种可以临时给session域中保存数据的方式
1. 使用@SessionAttributes注解(只能标在类上)
2. @SessionAttributes(value = {"haha","msg"},types = {String.class})
表示给BindingAwareModelMap的request域中保存数据的同时,给session域中也放一份
value={"haha","msg"} 指定保存数据时,也要给session中存放数据,数据的key为"msg"和"haha"
types={String.class} 指定保存的数据类型为String
3. 了解即可,因为不可控,所以基本上不用,如果想往session域中存放数据,推荐使用原生API
4. @ModelAttribute注解
1. 在方法上使用@ModelAttribute注解,Spring MVC在调用目标处理方法前,
会先逐个调用在方法级上标注了@ModelAttribute的方法
2. 在方法的形参前使用@ModelAttribute注解,可以从隐含对象中获取隐含的模型数据中获取对象,
再将请求参数绑定到对象中,再传给形参
3. 由@ModelAttribute标记的方法, 会在每个目标方法执行之前被SpringMVC调用
-->
pageContext: ${pageScope.msg}<br>
request: ${requestScope.msg}<br>
session: ${sessionScope.msg}<br>
application: ${applicationScope.msg}<br>
</body>
</html>
@SessionAttributes(value = {"haha","msg"},types = {String.class})
@Controller
public class ModelAndViewController {
@RequestMapping("/hello01")
public String hello01(Map<String,Object> map){
map.put("msg","你好,昕昕");
System.out.println("map的类型: " + map.getClass());
return "success";
}
@RequestMapping("/hello02") //Model是一个接口
public String hello02(Model model){
model.addAttribute("msg","我喜欢昕昕");
System.out.println("model的类型: " + model.getClass());
return "success";
}
@RequestMapping("/hello03")
public String hello03(ModelMap modelMap){
modelMap.addAttribute("msg","1314");
System.out.println("modelMap的类型: " + modelMap.getClass());
return "success";
}
@RequestMapping("/hello04")
public ModelAndView hello04(){
//之前的返回值就叫视图名,视图解析器会帮我们进行自动拼串,得到一个真实的地址
//ModelAndView mv = new ModelAndView();
//mv.setViewName("success");
ModelAndView mv = new ModelAndView("success");
mv.addObject("msg","520");
return mv;
}
}
//Map、Model、ModelMap的关系
public interface Map {
}
public interface Model {
}
public class ModelMap extends LinkedHashMap<String, Object> {
}
public class BindingAwareModelMap extends ExtendedModelMap {
}
public class ExtendedModelMap extends ModelMap implements Model {
}
7. DispatcherServet运行流程源码分析
- 前端控制器(DispatcherServet)的运行流程
- DispatcherServlet收到请求后调用doDispatch()方法
- 执行getHandler()方法,根据当前所有控制器类找到能处理这个请求的目标控制器类
- 执行getHandlerAdapter()方法,根据当前处理器获取到能执行这个处理器方法的适配器
- 使用刚才获取到的适配器(RequestMappingHandlerAdapter)执行目标方法
- 目标方法执行后会返回一个ModelAndView对象
- 根据ModelAndView的信息转发到具体的页面,并可以在请求域中取出ModelAndView中的模型数据
//1. DispatcherServlet经过两层继承,最终继承自HttpServlet
public class DispatcherServlet extends FrameworkServlet {
}
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
}
public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
}
//2. FrameworkServlet类实现了HttpServlet的doGet()和doPost()方法
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
//3. 请求一进来,必然经过doGet()或者doPost()方法
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//4. 不管是get还是post方式,都会调用这个方法处理请求
processRequest(request, response);
}
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected final void doPut(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected final void doDelete(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
processRequest(request, response);
if (response.containsHeader("Allow")) {
// Proper OPTIONS response coming from a handler - we're done.
return;
}
}
// Use response wrapper in order to always add PATCH to the allowed methods
super.doOptions(request, new HttpServletResponseWrapper(response) {
@Override
public void setHeader(String name, String value) {
if ("Allow".equals(name)) {
value = (StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();
}
super.setHeader(name, value);
}
});
}
}
//5. 处理请求
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
try {
//6. 抽象方法,留给子类实现的
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
if (logger.isDebugEnabled()) {
if (failureCause != null) {
this.logger.debug("Could not complete request", failureCause);
}
else {
if (asyncManager.isConcurrentHandlingStarted()) {
logger.debug("Leaving response open for concurrent processing");
}
else {
this.logger.debug("Successfully completed request");
}
}
}
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
//7. DispatcherServlet实现了父类的doService()方法
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (logger.isDebugEnabled()) {
String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
" processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]");
}
// Keep a snapshot of the request attributes in case of an include,
// to be able to restore the original attributes after the include.
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
try {
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}
//8. 最终的处理是这个方法,需要掌握
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
//1. 检查该请求是否是文件上传请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
//2. 根据当前的请求地址,去所有的Handler(Controller)中去找哪个Handler能够处理当前请求,找不到就直接404抛异常
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
//3. 拿到能执行这个类的所有方法的适配器(反射工具)RequestMappingHandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
//4. 获取请求方式
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
//5. 这里调用了目标方法,真正的执行目标方法
//适配器来执行目标方法,并且将目标方法执行完成后的返回值作为视图名,设置保存到ModelAndView中,目标方法无论怎么写,最终适配器执行完成后都会将执行后的信息封装层ModelAndView
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//判断是否是异步处理请求,如果是,直接返回
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
//6. 如果没有视图名,SpringMVC会帮我们自动设置一个默认视图名
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
//7. 根据方法最终执行完成后封装的ModelAndView,转发到目标页面,并且ModelAndView中的数据可以直接从请求域中获取到
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
-
getHandler()方法分析
- 返回的是一个目标方法的执行链
-
HandlerMapping:处理器映射
-
handlerMap:IOC容器启动创建Controller对象的时候会扫描每个处理器都能够处理哪些请求,将这些信息保存到HandlerMapping的HandlerMap属性中,下一次请求过来的时候,就会从handlerMap中查找对应的请求映射信息
-
@Nullable protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { //它里面保存了每个处理器都能处理哪些方法的映射信息 for (HandlerMapping hm : this.handlerMappings) { if (logger.isTraceEnabled()) { logger.trace( "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); } HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } } return null; }
-
getHandlerAdapter()方法分析
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { if (this.handlerAdapters != null) { for (HandlerAdapter ha : this.handlerAdapters) { 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"); }
-
ha.handle(processedRequest, response, mappedHandler.getHandler())方法分析
@Override @Nullable public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return handleInternal(request, response, (HandlerMethod) handler); } @Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; checkRequest(request); // Execute invokeHandlerMethod in synchronized block if required. if (this.synchronizeOnSession) { HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { mav = invokeHandlerMethod(request, response, handlerMethod); } } else { // No HttpSession available -> no mutex necessary mav = invokeHandlerMethod(request, response, handlerMethod); } } else { // No synchronization on session demanded at all... mav = invokeHandlerMethod(request, response, handlerMethod); } if (!response.containsHeader(HEADER_CACHE_CONTROL)) { if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers); } else { prepareResponse(response); } } return mav; }
-
DispatcherServlet中的九大组件
- SpringMVC在工作的时候,关键位置都是由这些组件来完成的
- 共同点:九大组件全部都是接口,接口就是默认规范,提供了非常强大的扩展性
/** Well-known name for the MultipartResolver object in the bean factory for this namespace. */ //文件上传解析器 public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver"; /** Well-known name for the LocaleResolver object in the bean factory for this namespace. */ //区域信息解析器(跟国际化有关) public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver"; /** Well-known name for the ThemeResolver object in the bean factory for this namespace. */ //主题解析器 public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver"; /** * Well-known name for the HandlerMapping object in the bean factory for this namespace. * Only used when "detectAllHandlerMappings" is turned off. * @see #setDetectAllHandlerMappings */ //Handler映射信息 public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping"; /** * Well-known name for the HandlerAdapter object in the bean factory for this namespace. * Only used when "detectAllHandlerAdapters" is turned off. * @see #setDetectAllHandlerAdapters */ //Handler适配器 public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter"; /** * Well-known name for the HandlerExceptionResolver object in the bean factory for this namespace. * Only used when "detectAllHandlerExceptionResolvers" is turned off. * @see #setDetectAllHandlerExceptionResolvers */ //异常解析器: SpringMVC强大的异常解析功能 public static final String HANDLER_EXCEPTION_RESOLVER_BEAN_NAME = "handlerExceptionResolver"; /** * Well-known name for the RequestToViewNameTranslator object in the bean factory for this namespace. */ //请求的视图名的转换器 public static final String REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME = "viewNameTranslator"; /** * Well-known name for the ViewResolver object in the bean factory for this namespace. * Only used when "detectAllViewResolvers" is turned off. * @see #setDetectAllViewResolvers */ //视图解析器 public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver"; /** * Well-known name for the FlashMapManager object in the bean factory for this namespace. */ //SpringMVC中允许重定向携带数据的功能 public static final String FLASH_MAP_MANAGER_BEAN_NAME = "flashMapManager";
-
九大组件初始化
- 先去容器中找这个组件,如果没有找到就会使用默认的配置
- 有些组件是使用类型查找的,有些组件是使用id查找的
//这个方法是Spring初始化IOC容器的时候留给子类实现的 @Override protected void onRefresh(ApplicationContext context) { initStrategies(context); } //初始化九大组件 protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); } //HandlerMapping初始化 private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null; //detectAllHandlerMappings属性默认为true if (this.detectAllHandlerMappings) { //从容器中去找这个HandlerMapping类型的组件 Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList<>(matchingBeans.values()); // We keep HandlerMappings in sorted order. AnnotationAwareOrderComparator.sort(this.handlerMappings); } } else { try { HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); this.handlerMappings = Collections.singletonList(hm); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default HandlerMapping later. } } // Ensure we have at least one HandlerMapping, by registering // a default HandlerMapping if no other mappings are found. //如果找不到,就会使用默认的配置 if (this.handlerMappings == null) { this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); if (logger.isDebugEnabled()) { logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default"); } } }
-
执行目标方法的细节(最难理解的执行流程)
- 通过反射定位到某个目标方法,还要准确获取到方法的每一个参数,才是最难的
@Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; checkRequest(request); // Execute invokeHandlerMethod in synchronized block if required. //synchronizeOnSession默认为false if (this.synchronizeOnSession) { HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { //真正的执行目标方法 mav = invokeHandlerMethod(request, response, handlerMethod); } } else { // No HttpSession available -> no mutex necessary mav = invokeHandlerMethod(request, response, handlerMethod); } } else { // No synchronization on session demanded at all... mav = invokeHandlerMethod(request, response, handlerMethod); } if (!response.containsHeader(HEADER_CACHE_CONTROL)) { if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers); } else { prepareResponse(response); } } return mav; } @Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } invocableMethod.setDataBinderFactory(binderFactory); invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); modelFactory.initModel(webRequest, mavContainer, invocableMethod); mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); asyncWebRequest.setTimeout(this.asyncRequestTimeout); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.setTaskExecutor(this.taskExecutor); asyncManager.setAsyncWebRequest(asyncWebRequest); asyncManager.registerCallableInterceptors(this.callableInterceptors); asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); if (asyncManager.hasConcurrentResult()) { Object result = asyncManager.getConcurrentResult(); mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; asyncManager.clearConcurrentResult(); if (logger.isDebugEnabled()) { logger.debug("Found concurrent result value [" + result + "]"); } invocableMethod = invocableMethod.wrapConcurrentResult(result); } invocableMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } }
8. 视图解析
-
SpringMVC如何解析视图
-
不论控制器返回一个String,ModelAndView,View都会转换为ModelAndView对象,由视图解析器解析视图,然后,进行页面的跳转
-
-
视图和视图解析器
- 请求处理方法执行完成后,最终返回一个 ModelAndView 对象。对于那些返回 String,View 或 ModeMap 等类型的处理方法,Spring MVC 也会在内部将它们装配成一个 ModelAndView 对象,它包含了逻辑名和模型对象的视图
- l Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是 JSP ,也可能是 Excel、JFreeChart等各种表现形式的视图
- 对于最终究竟采取何种视图对象对模型数据进行渲染,处理器并不关心,处理器工作重点聚焦在生产模型数据的工作上,从而实现 MVC 的充分解耦
-
重定向和请求转发
@Controller public class MyController { @RequestMapping("/hello01") public String hello01(){ return "success"; } //forward请求转发 /hello.jsp表示转发到当前项目下的hello.jsp @RequestMapping("/hello02") public String hello02(){ return "forward:/hello.jsp"; //return "forward:/hello01"; } //redirect重定向,SpringMVC会为我们自动的拼接上项目名 @RequestMapping() public String hello03(){ return "redirect:/hello.jsp"; } }
-
视图
- 视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户
- 为了实现视图模型和具体实现技术的解耦,Spring 在 org.springframework.web.servlet 包中定义了一个高度抽象的 View接口
- 视图对象由视图解析器负责实例化。由于视图是无状态的,所以他们不会有线程安全的问题
-
常用的视图实现类
-
视图解析器
- SpringMVC 为逻辑视图名的解析提供了不同的策略,可以在 Spring WEB 上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类
- 视图解析器的作用比较单一:将逻辑视图解析为一个具体的视图对象
- 所有的视图解析器都必须实现 ViewResolver 接口
-
常用的视图解析器实现类
- 程序员可以选择一种视图解析器或混用多种视图解析器
- 每个视图解析器都实现了 Ordered 接口并开放出一个 order 属性,可以通过 order 属性指定解析器的优先顺序,order越小优先级越高
- SpringMVC 会按视图解析器顺序的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则将抛出 ServletException 异常
- JSP 是最常见的视图技术,可以使用 InternalResourceViewResolve作为视图解析器
-
视图解析器流程分析
-
方法执行完成后的返回值会作为页面地址进行参考,转发或者重定向到目标页面
-
视图解析器也可能会进行页面地址的拼串
-
任何方法的返回值,最终都会被包装成ModelAndView对象
-
视图渲染流程:将域中的数据在页面进行展示,页面就是用来渲染模型数据的
-
View和ViewResolver
- View和ViewResolver的作用是根据视图名(方法的返回值)得到view对象
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; 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); } } // Did the handler return a view to render? if (mv != null && !mv.wasCleared()) { //渲染页面 render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else { if (logger.isDebugEnabled()) { logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() + "': assuming HandlerAdapter completed request handling"); } } if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Concurrent handling started during a forward return; } if (mappedHandler != null) { mappedHandler.triggerAfterCompletion(request, response, null); } } protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { // Determine locale for request and apply it to the response. //国际化 Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale()); response.setLocale(locale); View view; //定义一个view对象 String viewName = mv.getViewName(); //获取视图名称 if (viewName != null) { // We need to resolve the view name. view = resolveViewName(viewName, mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'"); } } else { // No need to lookup: the ModelAndView object contains the actual View object. view = mv.getView(); if (view == null) { throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'"); } } // Delegate to the View object for rendering. if (logger.isDebugEnabled()) { logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'"); } try { if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'", ex); } throw ex; } }
-
如何根据方法的返回值(视图名)得到View对象
- 视图解析器得到View对象的流程:所有配置好的视图解析器都来尝试根据视图名(返回值)得到View对象,如果能得到就返回,得不到就换下一个解析器
@Nullable protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,Locale locale, HttpServletRequest request) throws Exception { if (this.viewResolvers != null) { //遍历所有的viewResolver for (ViewResolver viewResolver : this.viewResolvers) { //获取view对象 View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } } return null; } @Override @Nullable 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) { // Ask the subclass to create the View object. //根据方法的返回值创建出视图对象 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); } } @Override protected View createView(String viewName, Locale locale) throws Exception { // If this resolver is not supposed to handle the given view, // return null to pass on to the next resolver in the chain. if (!canHandle(viewName, locale)) { return null; } // Check for special "redirect:" prefix. //是否是重定向 if (viewName.startsWith(REDIRECT_URL_PREFIX)) { String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); //创建重定向视图对象 RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); String[] hosts = getRedirectHosts(); if (hosts != null) { view.setHosts(hosts); } return applyLifecycleMethods(REDIRECT_URL_PREFIX, view); } // Check for special "forward:" prefix. //是否是请求转发 if (viewName.startsWith(FORWARD_URL_PREFIX)) { String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); return new InternalResourceView(forwardUrl); } // Else fall back to superclass implementation: calling loadView. //如果没有前缀,就使用父类默认创建一个视图对象 return super.createView(viewName, locale); }
-
默认的组件赋值
- 综上所述:视图解析器只是为了得到视图对象,视图对象才能真正的转发或者重定向到目标页面(将模型数据全部放在请求域中),视图对象才能够真正的渲染视图
-
-
JstlView
- 若项目中使用了JSTL,则SpringMVC 会自动把视图由InternalResourceView转为JstlView
- 若使用 JSTL 的 fmt 标签则需要在 SpringMVC 的配置文件中配置国际化资源文件
- 若希望直接响应通过 SpringMVC 渲染的页面,可以使用 mvc:view-controller 标签实现
-
自定义视图
- 自定义视图(需要加入SpringMVC,那么,一定需要实现框架的接口)
<!--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" 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.xsd"> <!--包扫描--> <context:component-scan base-package="com.siki"/> <!--配置视图解析器--> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/pages/"/> <property name="suffix" value=".jsp"/> </bean> <!--自定义视图解析器,数字越小优先级越高--> <bean class="com.siki.view.MyViewResolver"> <property name="order" value="1"/> </bean> </beans>
/* 1. 让我们自定义的视图解析器工作 2. 得到我们自定义的视图对象 3. 自定义视图对象自定义渲染逻辑 自动逸视图和视图解析器的实现步骤 1. 编写自定义视图和视图解析器 2. 视图解析器必须放在IOC容器中 */ //自定义视图 public class MyView implements View { @Override public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { System.out.println("之前保存的数据: " + model); response.setContentType("text/html"); //设置编码类型 response.getWriter().write("哈哈~~~~~"); response.getWriter().write(model.get("msg").toString()); } } //自定义视图解析器 public class MyViewResolver implements ViewResolver, Ordered { private Integer order; //根据视图名返回视图对象 @Override public View resolveViewName(String viewName, Locale locale) throws Exception { if(viewName.startsWith("view:")){ return new MyView(); }else{ //如果不能处理,返回null即可 return null; } } @Override public int getOrder() { return 0; } //改变视图解析顺序的方法 public void setOrder(Integer order){ this.order = order; } } @Controller public class MyViewResolverController { @RequestMapping("/hello") public String myViewResolver(Model model){ List<String> list = new ArrayList<>(); list.add("昕昕"); list.add("1314"); list.add("520"); model.addAttribute("msg",list); return "view:/success"; } }
-
mvc:view-controller标签
- 若希望直接响应通过 SpringMVC 渲染的页面,可以使用 mvc:view-controller 标签实现
- 直接配置响应的页面:无需经过控制器来执行结果
- <mvc:view-controller path="/success" view-name=“success”/>
- 注意:配置mvc:view-controller会导致其他请求路径失效
- 解决办法:配置mvc:annotation-driven标签
9.RESTFUL-CRUD
<!--web.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--配置前端控制器-->
<servlet>
<servlet-name>springmvc</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>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--配置字符编码过滤器 一定要注意: 字符编码过滤器一定要配置在其他过滤器之前,否则无法生效-->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!--指定解决POST请求乱码-->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!--解决请求乱码和响应乱码-->
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--配置请求方式过滤器-->
<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>
</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: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.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--包扫描-->
<context:component-scan base-package="com.siki"/>
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!--默认前端控制器是拦截所有资源的(除了jsp),当我们导入js文件的时候,就被拦截了,js文件的请求应该是Tomcat来处理的-->
<!--告诉SpringMVC,自己映射的请求就自己处理,不能处理的请求就交给Tomcat的默认处理器(DefaultServlet)处理-->
<mvc:default-servlet-handler/>
<!--上面的注解用来处理静态资源,下面的注解用来处理动态请求,这两个就是标配,少一个可能就会报错-->
<mvc:annotation-driven/>
</beans>
<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<!--访问项目就直接展示员工页面-->
<jsp:forward page="/emps"></jsp:forward>
</body>
</html>
<!--list.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--导入jstl--%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<%
request.setAttribute("path",request.getContextPath());
%>
<title>员工列表页面</title>
<!--引入js-->
<script type="text/javascript" src="${path}/js/jquery-1.9.1.min.js"></script>
</head>
<body>
<h1>员工列表</h1>
<table border="1" cellpadding="5" cellspacing="0">
<tr>
<th>ID</th>
<th>lastName</th>
<th>email</th>
<th>gender</th>
<th>departmentName</th>
<th>edit</th>
<th>delete</th>
</tr>
<c:forEach items="${emps}" var="emp">
<tr>
<td>${emp.id}</td>
<td>${emp.lastName}</td>
<td>${emp.email}</td>
<td>${emp.gender == 0 ? "女" : "男"}</td>
<td>${emp.department.departmentName}</td>
<td>
<a href="${path}/emp/${emp.id}">edit</a>
</td>
<td>
<!--删除的简单方式,只不过删除变成了按钮,不是超链接-->
<%--<form action="${path}/emp/${emp.id}" method="post">
<input type="hidden" name="_method" value="delete"/>
<input type="submit" value="delete"/>
</form>--%>
<a href="${path}/emp/${emp.id}" class="delBtn">delete</a>
</td>
</tr>
</c:forEach>
</table>
<a href="${path}/toAddPage">添加员工</a>
<form id="deleteForm" action="" method="post">
<input type="hidden" name="_method" value="delete"/>
</form>
<!--我们可以使用js,给删除链接绑定一个事件-->
<script type="text/javascript">
$(function(){
$(".delBtn").click(function () {
//我们通过修改表单的action,让其跟删除链接的页面跳转地址一样,就可以实现通过点击删除链接
//让表单替我们提交post请求
$("#deleteForm").attr("action",this.href);
//提交表单
$("#deleteForm").submit();
//阻止页面跳转
return false;
})
})
</script>
</body>
</html>
<!--add.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!--导入表单标签-->
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%--<form action="/emp" method="post">
lastName: <input type="text" name="lastName"/><br>
email: <input type="text" name="email"/><br>
gender: 男:<input type="radio" name="gender" value="1"/>
女:<input type="radio" name="gender" value="0"/><br>
dept: <select name="department.id">
<c:forEach items="${depts}" var="dept">
<!--标签体中的是在页面的提示选项信息,value才是真正提交的信息-->
<option value="${dept.id}">${dept.departmentName}</option>
</c:forEach>
</select>
<input type="submit" value="提交"/>
</form>--%>
<!--
SpringMVC使用表单标签可以实现将模型数据中的属性和html表单元素互相绑定,以实现表单数据更便捷的编辑和表单值的回显
注意: 1. SpringMVC认为,表单数据中的每一项最终都是要回显的
2. path指定的属性是从隐含模型(请求域)中取出的某个对象中的属性
3. path指定的每一个属性,请求域中必须有一个对象,拥有这个属性,这个对象就是请求域中的command
解决办法1:
model.addAttribute("command",
new Employee(null, "xinxin", "1314520@163.com", 0, departmentDao.getDepartment(101)));
解决办法2:
form:form action="" modelAttribute="employee"
model.addAttribute("employee",new Employee()); 属性刚好默认全为空
在表单标签中添加modelAttribute=""
以前我们表单标签会从请求域中获取一个command对象,把这个对象中的每一个属性对应的显示出来
现在,我们可以告诉SpringMVC,不要去取command的值,我们自己放了一个modelAttribute指定的值
取对象用的key,就用modelAttribute指定的值
-->
<%
//请求尽量都使用绝对路径
request.setAttribute("ctp",request.getContextPath());
%>
<form:form action="${ctp}/emp" modelAttribute="employee" method="post">
<!--
path: 就是原来html-input中的name属性
1. 当做原生的name
2. 自动回显隐含模型中某个对象对应的某个属性的值
-->
lastName: <form:input path="lastName"/><br>
email: <form:input path="email"/><br>
gender: 男: <form:radiobutton path="gender" value="1"/>
女: <form:radiobutton path="gender" value="0"/><br>
<!--items: 指定要遍历的集合,自动遍历-->
<!--itemLabel: 指定遍历出的这个对象的哪个属性是作为option标签体的值-->
<!--itemValue: 指定遍历出的这个对象的哪个属性是作为要提交的value值-->
dept: <form:select path="department.id" items="${depts}"
itemLabel="departmentName" itemValue="id"><br>
</form:select><br>
<input type="submit" value="提交"/><br>
</form:form>
</body>
</html>
<!--update.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<title>员工修改页面</title>
</head>
<body>
<%
request.setAttribute("path",request.getContextPath());
%>
<!--这个表单的所有内容显示绑定的是请求域中的updateEmp的值-->
<form:form action="${path}/emp/${updateEmp.id}" modelAttribute="updateEmp" method="post">
<input type="hidden" name="_method" value="put"/>
<input type="hidden" name="id" value="${updateEmp.id}"/>
email: <form:input path="email"/><br>
gender: 男: <form:radiobutton path="gender" value="1"/>
女: <form:radiobutton path="gender" value="0"/><br>
dept: <form:select path="department.id" items="${depts}"
itemLabel="departmentName" itemValue="id">
</form:select><br>
<input type="submit" value="修改"/><br>
</form:form>
</body>
</html>
//dao层
@Repository
public class EmployeeDao {
private static Map<Integer, Employee> employees = null;
@Autowired
private DepartmentDao departmentDao;
static{
employees = new HashMap<Integer, Employee>();
employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1, new Department(101, "D-AA")));
employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1, new Department(102, "D-BB")));
employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0, new Department(103, "D-CC")));
employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0, new Department(104, "D-DD")));
employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1, new Department(105, "D-EE")));
}
//添加员工时,初始化员工的id
private static Integer initId = 1006;
//员工保存/更新
public void save(Employee employee){
if(employee.getId() == null){
employee.setId(initId++);
}
employee.setDepartment(departmentDao.getDepartment(employee.getDepartment().getId()));
employees.put(employee.getId(), employee);
}
public Collection<Employee> getAll(){
return employees.values();
}
public Employee get(Integer id){
return employees.get(id);
}
public void delete(Integer id){
employees.remove(id);
}
}
@Repository
public class DepartmentDao {
private static Map<Integer, Department> departments = null;
static{
departments = new HashMap<Integer, Department>();
departments.put(101, new Department(101, "D-AA"));
departments.put(102, new Department(102, "D-BB"));
departments.put(103, new Department(103, "D-CC"));
departments.put(104, new Department(104, "D-DD"));
departments.put(105, new Department(105, "D-EE"));
}
public Collection<Department> getDepartments(){
return departments.values();
}
public Department getDepartment(Integer id){
return departments.get(id);
}
}
//controller层
@Controller
public class RestControllerTest {
@Autowired
private EmployeeDao employeeDao;
@Autowired
private DepartmentDao departmentDao;
//查询所有员工
@RequestMapping("/emps")
public String getEmps(Model model){
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps",employees);
return "list";
}
//员工添加,添加之前需要查询出所有的部门信息,再页面进行展示
@RequestMapping("/toAddPage")
public String toAddPage(Model model){
//先查询出所有部门
Collection<Department> departments = departmentDao.getDepartments();
//再放到请求域中
model.addAttribute("depts",departments);
// model.addAttribute("command",
// new Employee(null, "xinxin", "1314520@163.com", 0, departmentDao.getDepartment(101)));
model.addAttribute("employee",new Employee()); //属性刚好默认全为空
return "add";
}
@RequestMapping(value = "/emp",method = RequestMethod.POST)
public String addEmp(Employee employee){
employeeDao.save(employee);
//直接重定向到查询所有员工的请求
return "redirect:/emps";
}
//来到修改页面
@RequestMapping(value = "/emp/{id}",method = RequestMethod.GET)
public String getEmp(@PathVariable("id") Integer id,Model model){
Employee employee = employeeDao.get(id);
model.addAttribute("updateEmp",employee);
//注意: 还要将部门信息放入请求域中
model.addAttribute("depts",departmentDao.getDepartments());
return "update";
}
//修改员工之前,先将员工的完整信息获取到
@ModelAttribute
public void myModelAttribute(@RequestParam(value = "id",required = false) Integer id,Model model){
//因为所有的处理器方法执行之前都会先执行这个方法,而/emps、/emp这样的请求是没有携带id参数的
//所以需要设置required = false,否则会报错
if(id != null){ //这里必须要判断,否则添加员工时,没有携带id属性,这个时候就会创建一个空的employee
Employee employee = employeeDao.get(id);
model.addAttribute("employee",employee);
}
}
//修改员工
@RequestMapping(value = "/emp/{id}",method = RequestMethod.PUT)
public String updateEmp(Employee employee){
//此时的员工是缺少lastName的,因为页面跳转过来的时候并没有携带lastName
//所以我们可以使用@ModelAttribute提前获取到lastName的值,再放入请求域中,供修改方法使用
System.out.println(employee);
employeeDao.save(employee); //修改完后保存
return "redirect:/emps";
}
//删除员工
@RequestMapping(value = "/emp/{id}",method = RequestMethod.DELETE)
public String deleteEmp(@PathVariable("id") Integer id){
employeeDao.delete(id);
return "redirect:/emps";
}
}
-
使用表单标签可能会出现的错误
10. 数据转换、数据绑定、数据校验
-
数据绑定流程原理
- Spring MVC 主框架将 ServletRequest 对象及目标方法的入参实例传递给 WebDataBinderFactory 实例,以创建 DataBinder 实例对象
- DataBinder 调用装配在 Spring MVC 上下文中的 ConversionService 组件进行数据类型转换、数据格式化工作。将 Servlet 中的请求信息填充到入参对象中
- 调用 Validator 组件对已经绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果 BindingData 对象
- Spring MVC 抽取 BindingResult 中的入参对象和校验错误对象,将它们赋给处理方法的响应入参
- 综上所述:Spring MVC 通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中。数据绑定的核心部件是DataBinder
-
自定义类型转换器
-
ConversionService 是 Spring 类型转换体系的核心接口
-
可以利用 ConversionServiceFactoryBean 在 Spring 的 IOC 容器中定义一个
ConversionService,Spring 将自动识别出 IOC 容器中的 ConversionService,并在 Bean 属性配置及 SpringMVC 处理方法入参绑定等场合使用它进行数据的转换
-
可通过 ConversionServiceFactoryBean 的converters属性注册自定义的类型转换器
<!--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.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!--包扫描--> <context:component-scan base-package="com.siki"/> <!--配置视图解析器--> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/pages/"/> <property name="suffix" value=".jsp"/> </bean> <!--自定义ConversionService--> <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <!--在converters中添加我们自己定义的converter,让它生效--> <property name="converters"> <set> <bean class="com.siki.component.MyStringToEmployee"></bean> </set> </property> </bean> <mvc:default-servlet-handler/> <!--使用自己配置的conversionService类型转换组件--> <mvc:annotation-driven conversion-service="conversionService" /> </beans>
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>$Title$</title> </head> <body> <% request.setAttribute("path",request.getContextPath()); %> <form action="${path}/quickAdd"> <input name="empInfo" value="1-xinxin-123456"/> <input type="submit" value="提交"/> </form> </body> </html>
/* 1. 实现Converter接口,自定义类型转换器 2. Converter只是ConversionService中的组件 1. 将自定义的Converter放入ConversionService中 2. 将WebDataBinder中的ConversionService设置为我们自己定义的ConversionService 3. 在配置文件中配置ConversionService */ //自定义类型转换器 public class MyStringToEmployee implements Converter<String, Employee> { //自定义转换规则 @Override public Employee convert(String s) { System.out.println("s = " + s); Employee employee = new Employee(); if(s.contains("-")){ String[] split = s.split("-"); employee.setId(Integer.parseInt(split[0])); employee.setName(split[1]); employee.setPassword(split[2]); } return employee; } } @Controller public class EmpController { //我们需要自定义类型转换器,将String转换为Employee类型 @RequestMapping("/quickAdd") public String test01(@RequestParam("empInfo") Employee employee){ System.out.println(employee); return "success"; } }
-
-
Spring 支持的转换器类型:Spring 定义了 3 种类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到 ConversionServiceFactoryBean 中
- Converter<S,T>:将 S 类型对象转为 T 类型对象
- ConverterFactory:将相同系列多个 “同质” Converter 封装在一起。如果希望将一种类型的对象转换为另一种类型及其子类的对象(例如将 String 转换为 Number 及 Number 子类(Integer、Long、Double 等)对象)可使用该转换器工厂类
- GenericConverter:会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换
-
mvc:annotation-driven配置在什么时候必须配置
-
直接配置响应的页面:无需经过控制器来执行结果 ;但会导致其他请求路径失效,需要配置mvc:annotation-driven标签
-
RESTful-CRUD操作,删除时,通过jQuery执行delete请求时,找不到静态资源,需要配置mvc:annotation-driven标签
-
mvc:default-servlet-handler 将在 SpringMVC 上下文中定义一个
DefaultServletHttpRequestHandler,它会对进入 DispatcherServlet 的请求进行筛查,如果发现是没有经过映射的请求,就将该请求交由 WEB 应用服务器默认的 Servlet 处理,如果不是静态资源的请求,才由 DispatcherServlet 继续处理
-
-
配置类型转换器服务时,需要指定转换器服务引用
- <mvc:annotation-driven conversion-service=“conversionService”/> 会将自定义的ConversionService 注册到 Spring MVC 的上下文中
-
后面完成JSR 303数据验证,也需要配置
-
-
<mvc:annotation-driven /> 作用
- <mvc:annotation-driven /> 会自动注册:RequestMappingHandlerMapping 、RequestMappingHandlerAdapter与ExceptionHandlerExceptionResolver 三个bean
- 支持使用 ConversionService 实例对表单参数进行类型转换
- 支持使用 @NumberFormat、@DateTimeFormat 注解完成数据类型的格式化
- 支持使用 @Valid 注解对 JavaBean 实例进行 JSR 303 验证
- 支持使用 @RequestBody 和 @ResponseBody 注解
-
日期格式化
- @DateTimeFormat 注解可对 java.util.Date、java.util.Calendar、java.long.Long 时间类型进行标注
- pattern 属性:类型为字符串。指定解析/格式化字段数据的模式,如:”yyyy-MM-dd hh:mm:ss”
- iso 属性:类型为 DateTimeFormat.ISO。指定解析/格式化字段数据的ISO模式,包括四种:ISO.NONE(不使用) – 默认、ISO.DATE(yyyy-MM-dd) 、ISO.TIME(hh:mm:ss.SSSZ)、 ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.SSSZ)
- n style 属性:字符串类型。通过样式指定日期时间的格式,由两位字符组成,第一位表示日期的格式,第二位表示时间的格式:S:短日期/时间格式、M:中日期/时间格式、L:长日期/时间格式、F:完整日期/时间格式、-:忽略日期或时间格式
<!-- 日期格式化 1. 方式一: 去掉conversion-service <mvc:annotation-driven /> 2. 方式二: 将conversion-service配置改为FormattingConversionServiceFactoryBean <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <set> <bean class="com.siki.component.MyStringToEmployee"></bean> </set> </property> </bean> <mvc:annotation-driven conversion-service="conversionService" /> 因为FormattingConversionService里面也有converters,还有formatters,既可以实现类型转换器,还可以实现日期格式化 --> <mvc:annotation-driven />
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <% request.setAttribute("path",request.getContextPath()); %> <form action="${path}/addBirth"> id: <input type="text" name="id"/><br> name: <input type="text" name="name"/><br> password: <input type="password" name="password"/><br> birth: <input type="text" name="birth"/><br> <input type="submit" value=""提交/><br> </form> </body> </html>
public class Employee { private Integer id; private String name; private String password; //规定页面提交的日期格式 @DateTimeFormat(pattern = "yyyy-MM-dd") private Date birth; } //日期格式化 @RequestMapping("/addBirth") public String addBirth(Employee employee,Model model){ System.out.println(employee); model.addAttribute(employee); return "success"; }
- @DateTimeFormat 注解可对 java.util.Date、java.util.Calendar、java.long.Long 时间类型进行标注
-
数值格式化概述
- @NumberFormat 可对类似数字类型的属性进行标注,它拥有两个互斥的属性
- style:类型为 NumberFormat.Style。用于指定样式类型,包括三种:Style.NUMBER(正常数字类型)、 Style.CURRENCY(货币类型)、 Style.PERCENT(百分数类型)
- pattern:类型为 String,自定义样式,如pattern="#,###"
-
数据校验
-
如何校验
- 使用JSR 303验证标准
- 加入hibernate validator验证框架
- 在SpringMVC配置文件中增加mvc:annotation-driven/
- 需要在bean的属性上增加对应验证的注解
- 在目标方法bean类型的前面增加@Valid注解
-
JSR 303
-
是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 中
-
JSR 303 (Java Specification Requests)意思是Java 规范提案)通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证
-
-
Hibernate Validator 扩展注解
-
Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解
-
-
Spring MVC 数据校验
- Spring 4.0 拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架
- Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在 Spring MVC 中,可直接通过注解驱动的方式进行数据校验
- Spring 的 LocalValidatorFactroyBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在 Spring 容器中定义了一个 LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean 中
- Spring 本身并没有提供 JSR303 的实现,所以必须将 JSR303 的实现者的 jar 包放到类路径下
- mvc:annotation-driven/ 会默认装配好一个 LocalValidatorFactoryBean,通过在处理方法的入参上标注 @Valid 注解即可让 Spring MVC 在完成数据绑定后执行数据校验的工作
- 在已经标注了 JSR303 注解的表单/命令对象前标注一个 @Valid,Spring MVC 框架在将请求参数绑定到该入参对象后,就会调用校验框架根据注解声明的校验规则实施校验
<!--Hibernate Validator--> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.13.Final</version> </dependency> <dependency> <groupId>org.jboss.logging</groupId> <artifactId>jboss-logging</artifactId> <version>3.3.2.Final</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.1.0.Final</version> </dependency> <dependency> <groupId>com.fasterxml</groupId> <artifactId>classmate</artifactId> <version>1.4.0</version> </dependency>
/* 1. 导包 2. 只需要给JavaBean的属性添加上校验注解 3. 在SpringMVC封装对象的时候,告诉SpringMVC这个JavaBean需要校验 4. 如何知道校验结果 给需要校验的JavaBean后面紧跟一个BindingResult,这个BindingResult就是封装前一个bean的 校验结果,根据不同的校验结果决定后面该怎么做 */ public class Employee { @NonNull private Integer id; //也可以直接在message属性中写,但是这个方式不能使用国际化 @NotNull(message = "name不能为null") private String name; @Length(min = 6,max = 18) private String password; @DateTimeFormat(pattern = "yyyy-MM-dd") //@Future 必须是一个将来的时间 @Past //生日必须是一个过去的时间 private Date birth; } @RequestMapping("/addBirth") public String addBirth(@Valid Employee employee, BindingResult bindingResult){ //进行校验 System.out.println(employee); //获取是否有校验错误 boolean b = bindingResult.hasErrors(); System.out.println(b); if(b){ List<FieldError> errors = bindingResult.getFieldErrors(); for(FieldError error : errors){ System.out.println(error.getField() + error.getDefaultMessage()); } return "error"; }else{ return "success"; } }
-
错误消息的显示及国际化
-
在页面上显示错误
- l Spring MVC 除了会将表单/命令对象的校验结果保存到对应的 BindingResult 或 Errors 对象中外,还会将所有校验结果保存到 “隐含模型”
- 即使处理方法的签名中没有对应于表单/命令对象的结果入参,校验结果也会保存在 “隐含对象” 中
- 隐含模型中的所有数据最终将通过 HttpServletRequest 的属性列表暴露给 JSP 视图对象,因此在 JSP 中可以获取错误信息
- l 在 JSP 页面上可通过 <form:errors path=“userName”> 显示错误消息
-
自定义国际化错误消息的显示
- 每个属性在数据绑定和数据校验发生错误时,都会生成一个对应的 FieldError 对象
- 当一个属性校验失败后,校验框架会为该属性生成 4 个消息代码,这些代码以校验注解类名为前缀,结合 modleAttribute、属性名及属性类型名生成多个对应的消息代码:例如 User 类中的 password 属性标注了一个 @Pattern 注解,当该属性值不满足 @Pattern 所定义的规则时, 就会产生以下 4 个错误代码:
- Pattern.user.password 校验规则.隐含模型中这个对象的key.对象的属性名
- Pattern.password 校验规则.属性名
- Pattern.java.lang.String 校验规则.属性类型
- Pattern 校验规则
- 当使用 Spring MVC 标签显示错误消息时, Spring MVC 会查看 WEB 上下文是否装配了对应的国际化消息,如果没有,则显示默认的错误消息,否则使用国际化消息
- 若数据类型转换或数据格式转换时发生错误,或该有的参数不存在,或调用处理方法时发生错误,都会在隐含模型中创建错误消息。其错误代码前缀说明如下:
- required:必要的参数不存在。如 @RequiredParam(“param1”) 标注了一个入参,但是该参数不存在
- typeMismatch:在数据绑定时,发生数据类型不匹配的问题
- methodInvocation:Spring MVC 在调用处理方法时发生了错误
#errors_zh_CN.properties NotNull.id=id不能为空 Length.password=密码必须为6到18位 #errors_en_US.properties NotNull.id=id is not null Length.password=password must between 6 and 18
<!--管理国际化资源文件--> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="errors"/> </bean>
/* 1. 先编写国际化配置文件 2. 让SpringMVC管理国际化资源配置文件 */
-
-
11. SpringMVC支持ajax
<!--json依赖,支持ajax-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.1</version>
</dependency>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
<script type="text/javascript" src="js/jquery-1.9.1.min.js"></script>
</head>
<body>
<%
request.setAttribute("path",request.getContextPath());
%>
<a href="${path}/getAllajax">ajax获取所有员工</a>
<div></div>
<script type="text/javascript">
$("a:first").click(function () {
//1. 发送ajax获取所有员工
$.ajax({
url:"${path}/getAllajax",
type:"get",
success:function () {
$.each(data,function () {
var empInfo = this.lastName + "--->" + this.birth;
$("div").append(empInfo + "<br/>");
});
}
})
return false;
});
</script>
</body>
</html>
@Controller
public class AjaxTestController {
@Autowired
private EmployeeDao employeeDao;
@ResponseBody //想返回的数据放进响应体中,如果是对象,将会自动转为json格式
@RequestMapping("/getAllajax")
public Collection<Employee> ajaxAll(){
Collection<Employee> all = employeeDao.getAll();
return all;
}
}
12. 文件上传和下载
- Spring MVC 为文件上传提供了直接的支持,这种支持是通过即插即用的 MultipartResolver 实现的
- Spring 用 Jakarta Commons FileUpload 技术实现了一个 MultipartResolver 实现类:CommonsMultipartResovler
- Spring MVC 上下文中默认没有装配 MultipartResovler,因此默认情况下不能处理文件的上传工作,如果想使用 Spring 的文件上传功能,需现在上下文中配置 MultipartResolver
- MultipartResolver的defaultEncoding属性: 必须和用户 JSP 的 pageEncoding 属性一致,以便正确解析表单的内容,为了让 CommonsMultipartResovler 正确工作,必须先将 Jakarta Commons FileUpload 及 Jakarta Commons io 的类包添加到类路径下
<!--文件上传和下载-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<?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.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--包扫描-->
<context:component-scan base-package="com.siki"/>
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
<mvc:default-servlet-handler/>
<mvc:annotation-driven />
<!--配置文件上传解析器-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--设置最大的文件上传大小为20MB-->
<property name="maxUploadSize" value="#{1024 * 1024 * 20}"/>
<!--设置默认编码-->
<property name="defaultEncoding" value="utf-8"/>
</bean>
</beans>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<!--
文件上传:
1. 文件上传表单准备: enctype="multipart/form-data"
2. 导入jar包
3. 在配置文件中配置文件上传解析器MultipartResolver的实现类CommonsMultipartResolver
4. 文件上传请求处理
在处理器方法上写一个@RequestParam("headerImg") MultipartFile file,封装当前文件的信息,可以直接保存
-->
<%
request.setAttribute("path",request.getContextPath());
%>
${msg}
<!--单文件上传-->
<form action="${path}/upload" method="post" enctype="multipart/form-data">
用户头像: <input type="file" name="headerImg"/><br>
用户名: <input type="text" name="username"/><br>
<input type="submit" value="提交"/>
</form>
<!--多文件上传-->
<form action="${path}/uploads" method="post" enctype="multipart/form-data">
用户头像: <input type="file" name="headerImg"/><br>
用户头像: <input type="file" name="headerImg"/><br>
用户头像: <input type="file" name="headerImg"/><br>
用户头像: <input type="file" name="headerImg"/><br>
用户名: <input type="text" name="username"/><br>
<input type="submit" value="提交"/>
</form>
</body>
</html>
@Controller
public class FileController {
//单文件上传
@RequestMapping("/upload")
public String upload(@RequestParam(value = "username",required = false) String username,
@RequestParam("headerImg") MultipartFile file, Model model){
System.out.println("========上传的文件的信息========");
System.out.println("文件项的名字: " + file.getName());
System.out.println("文件的名字: " + file.getOriginalFilename());
System.out.println("文件的大小: " + file.getSize());
//文件保存
try {
file.transferTo(new File("E:\\IDEA\\" + file.getOriginalFilename()));
model.addAttribute("msg","文件上传成功...");
} catch (IOException e) {
model.addAttribute("msg","文件上传失败..." + e.getMessage());
}
return "forward:/index.jsp";
}
//多文件上传
@RequestMapping("/uploads")
public String uploads(@RequestParam(value = "username",required = false) String username,
@RequestParam("headerImg") MultipartFile[] multipartFiles, Model model){
System.out.println("========上传的文件的信息========");
for(MultipartFile file : multipartFiles){
if(!file.isEmpty()){
//进行文件保存
try {
file.transferTo(new File("E:\\IDEA\\" + file.getOriginalFilename()));
model.addAttribute("msg","文件上传成功...");
} catch (IOException e) {
model.addAttribute("msg","文件上传失败..." + e.getMessage());
}
}
}
return "forward:/index.jsp";
}
}
13. 拦截器
-
自定义拦截器概述
- SpringMVC提供了拦截器机制,允许我们在运行目标方法之前进行一些拦截工作,或者在目标方法运行之后进行一些其他的处理
- Spring MVC可以使用拦截器对请求进行拦截处理,用户可以自定义拦截器来实现特定的功能,自定义的拦截器必须实现HandlerInterceptor接口
- preHandle():这个方法在业务处理器处理请求之前被调用,在该方法中对用户请求 request 进行处理。如果程序员决定该拦截器对请求进行拦截处理后还要调用其他的拦截器,或者是业务处理器去进行处理,则返回true;如果程序员决定不需要再调用其他的组件去处理请求,则返回false
- postHandle():这个方法在业务处理器处理完请求后,但是DispatcherServlet 向客户端返回响应前被调用,在该方法中对用户请求request进行处理
- afterCompletion():这个方法在 DispatcherServlet 完全处理完请求后被调用,可以在该方法中进行一些资源清理的操作
-
单拦截器的正常运行流程
拦截器的preHandle----->目标方法----->拦截器的postHandle----->页面----->拦截器的afterCompletion
-
其他流程
- 只要preHandle方法返回false,表示不放行,后面的流程就不会有了
- 只要放行了,不管中间有没有报错,afterCompletion方法都会执行
-
多拦截器运行流程
-
正常流程:跟Filter过滤器一样,谁先配置,谁就先执行
/* MyFirstInterceptor--->preHandle... MySecondInterceptor--->preHandle... testInterceptor... MySecondInterceptor--->postHandle... MyFirstInterceptor--->postHandle... success.jsp... MySecondInterceptor--->afterCompletion... MyFirstInterceptor--->afterCompletion... */
-
异常流程
- 任何一个不放行,都不会来到目标方法及页面
- 如果MySecondInterceptor不放行,但是它前面已经放行了的那些拦截器还是会执行afterCompletion方法
-
-
总结
- 拦截器的preHandle方法是按照顺序执行的
- 拦截器的postHandle方法是逆序执行的
- 拦截器的afterCompletion也是按照逆序执行的
- 不管当前拦截器有没有放行,前面已经放行了的拦截器的afterCompletion方法总会执行
<!--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.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--包扫描-->
<context:component-scan base-package="com.siki"/>
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
<mvc:default-servlet-handler/>
<mvc:annotation-driven />
<!--自定义拦截器-->
<mvc:interceptors>
<!--配置某个拦截器,默认拦截所有请求-->
<bean class="com.siki.interceptor.MyFirstInterceptor"></bean>
<!--配置某个拦截器更详细的信息-->
<mvc:interceptor>
<!--path: 表示拦截哪个请求-->
<mvc:mapping path="/testInterceptor"/>
<bean class="com.siki.interceptor.MySecondInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
</beans>
<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<%
request.setAttribute("path",request.getContextPath());
%>
<a href="${path}/testInterceptor">测试拦截器</a>
</body>
</html>
<!--success.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%
System.out.println("success.jsp...");
%>
<font color="#98fb98">成功</font>
</body>
</html>
/*
自定义拦截器
1. 实现HandlerInterceptor接口
2. 在SpringMVC配置文件中注册这个拦截器
配置这个拦截器用来拦截哪些目标方法
*/
public class MyFirstInterceptor implements HandlerInterceptor {
//目标方法运行之前
@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...");
}
}
public class MySecondInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MySecondInterceptor--->preHandle...");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MySecondInterceptor--->postHandle...");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MySecondInterceptor--->afterCompletion...");
}
}
//测试
@Controller
public class InterceptorTestController {
@RequestMapping("/testInterceptor")
public String testInterceptor(){
System.out.println("testInterceptor...");
return "success";
}
}
14. 国际化
-
页面中获取国际化资源信息
-
在页面上能够根据浏 览器语言设置的情况对文本, 时间, 数值进行本地化处理
-
可以在 bean 中获取国际化资源文件 Locale 对应的消息
-
可以通过超链接切换 Locale, 而不再依赖于浏览器的语言设置情况
-
实现
- 使用 JSTL 的 fmt 标签
- 在 bean 中注入 ResourceBundleMessageSource 的实例, 使用其对应的getMessage 方法即可
- 配置 LocalResolver 和 LocaleChangeInterceptor
-
通过超链接切换Locale
-
默认情况下,SpringMVC 根据 Accept-Language 参数判断客户端的本地化类型
-
当接受到请求时,SpringMVC 会在上下文中查找一个本地化解析器(LocalResolver),找到后使用它获取请求所对应的本地化类型信息
-
SpringMVC 还允许装配一个动态更改本地化类型的拦截器,这样通过指定一个请求参数就可以控制单个请求的本地化类型
-
SessionLocaleResolver & LocaleChangeInterceptor 工作原理
-
本地化解析器和本地化拦截器
- AcceptHeaderLocaleResolver:根据 HTTP 请求头的 Accept-Language 参数确定本地化类型,如果没有显式定义本地化解析器, SpringMVC 默认使用该解析器
- CookieLocaleResolver:根据指定的 Cookie 值确定本地化类型
- SessionLocaleResolver:根据 Session 中特定的属性确定本地化类型
- LocaleChangeInterceptor:从请求参数中获取本次请求对应的本地化类型
-
-
#login_zh_CN.properties
welcomeInfo=欢迎来到siki学院
username=用户名
password=密码
loginBtn=登录
#login_en_US.properties
welcomeInfo=welcome to siki.com
username=username
password=password
loginBtn=login
<!--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.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--包扫描-->
<context:component-scan base-package="com.siki"/>
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
<mvc:default-servlet-handler/>
<mvc:annotation-driven />
<!--配置国际化解析器-->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="login"/>
<property name="defaultEncoding" value="utf-8"/>
</bean>
<!--配置自定义区域信息解析器-->
<!--<bean id="localeResolver" class="resolver.MyLocaleResolver" />-->
<!--通过SessionLocaleResolver + LocaleChangeInterceptor获取区域信息-->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"></bean>
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
</mvc:interceptors>
</beans>
<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<a href="toLoginPage">去登录页面</a>
</body>
</html>
<!--login.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!--导入fmt标签-->
<%@taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<font color="#98fb98">
<fmt:message key="welcomeInfo"/>
</font>
<form action="" method="post">
<fmt:message key="username"/>: <input type="text"/><br>
<fmt:message key="password"/>: <input type="password"/><br>
<input type="submit" value="<fmt:message key="loginBtn"/>"/><br>
</form>
<a href="toLoginPage?locale=zh_CN">中文</a> | <a href="toLoginPage?locale=en_US">English</a>
</body>
</html>
//自定义区域信息解析器
public class MyLocaleResolver implements LocaleResolver {
//解析后返回locale
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale l = null;
String locale = request.getParameter("locale");
//如果带了参数,就用参数指定的区域信息
//区域信息是从session中获取的,使用的是SessionLocaleResolver
// if(locale != null && !"".equals(locale)){
// String[] s = locale.split("_");
// l = new Locale(s[0],s[1]);
// }else{
// //没带的话就直接用请求头中的
// l = request.getLocale();
// }
//我们也可以创建一个对象,存储区域信息,然后将这个对象放入session域中,再取出来,也可以实现同样的效果
//只需要配置SessionLocaleResolver + LocaleChangeInterceptor,帮我们指定区域信息
//这种方式挺麻烦,了解即可
return l;
}
//修改Locale
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
//测试
@Controller
public class I18nTestController {
@RequestMapping("/toLoginPage")
public String testI18n(Locale locale){ //locale: 国际化信息
System.out.println(locale);
return "login";
}
}
15. 异常处理
-
异常处理概述
-
Spring MVC 通过 HandlerExceptionResolver 处理程序的异常,包括 Handler 映射、数据绑定以及目标方法执行时发生的异常
-
SpringMVC 提供的 HandlerExceptionResolver 的实现类
-
HandlerExceptionResolver
-
DispatcherServlet 默认装配的 HandlerExceptionResolver
-
没有使用 mvc:annotation-driven/ 配置
-
使用了 mvc:annotation-driven/ 配置
-
-
ExceptionHandlerExceptionResolver
- 主要处理 Handler 中用 @ExceptionHandler 注解定义的方法
- @ExceptionHandler 注解定义的方法的优先级问题:例如发生的是NullPointerException,但是声明的异常有 RuntimeException 和 Exception,此候会根据异常的最近继承关系找到继承深度最浅的那个 @ExceptionHandler 注解方法,即标记了 RuntimeException 的方法
- ExceptionHandlerMethodResolver 内部若找不到@ExceptionHandler 注解的话,会找@ControllerAdvice 中的@ExceptionHandler 注解方法
-
ResponseStatusExceptionResolver
- 在异常及异常父类中找到 @ResponseStatus 注解,然后使用这个注解的属性进行处理
- 定义一个 @ResponseStatus 注解修饰的异常类
- 若在处理器方法中抛出了上述异常:若ExceptionHandlerExceptionResolver 不解析上述异常。由于触发的异常 UnauthorizedException 带有@ResponseStatus 注解。因此会被ResponseStatusExceptionResolver 解析到。最后响应HttpStatus.UNAUTHORIZED 代码给客户端。HttpStatus.UNAUTHORIZED 代表响应码401,无权限
- @ResponseStatus专门用来给自定义异常类上标注的
-
DefaultHandlerExceptionResolver
-
对一些特殊的异常进行处理
NoSuchRequestHandlingMethodException、
HttpRequestMethodNotSupportedException、
HttpMediaTypeNotSupportedException、
HttpMediaTypeNotAcceptableException等
-
-
SimpleMappingExceptionResolver
如果希望对所有异常进行统一处理,可以使用 SimpleMappingExceptionResolver,它将异常类名映射为视图名,即发生异常时使用对应的视图报告异常
-
<!--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.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--包扫描-->
<context:component-scan base-package="com.siki"/>
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
<mvc:default-servlet-handler/>
<mvc:annotation-driven />
<!--配置SimpleMappingExceptionResolver-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!--public void setExceptionMappings(Properties mappings)-->
<property name="exceptionMappings">
<props>
<!--key: 异常的全类名 value: 目标页面-->
<prop key="java.lang.NullPointerException">myError</prop>
</props>
</property>
<!--指定错误信息取出时的key-->
<property name="exceptionAttribute" value="exception"/>
</bean>
</beans>
<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<%
request.setAttribute("path",request.getContextPath());
%>
<a href="${path}/test01?i=10">测试异常1</a><br>
<a href="${path}/test02?i=10">测试异常2</a><br>
<a href="${path}/test03">测试异常2</a><br>
<a href="${path}/test04">测试异常4</a><br>
</body>
</html>
<!--myError.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>出错啦~~~</h1>
<h2>错误信息: ${exception}</h2>
</body>
</html>
@Controller
public class ExceptionTestController {
@RequestMapping("/test01")
public String test01(Integer i){
System.out.println(10 / i);
return "success";
}
/*
告诉SpringMVC,这个方法专门用来处理ArithmeticException异常
1. 方法参数中写Exception对象,获取异常信息
2. 参数位置只能写Exception对象,不能写Model或者其他的对象
3. 如果想将异常信息带到页面上显示,可以将返回值设置为ModelAndView,携带异常信息
4. 如果有多个@ExceptionHandler都能处理某个异常,精确优先
5. 全局异常处理和本类异常处理同时存在,本类优先
*/
@ExceptionHandler(value = {ArithmeticException.class})
public ModelAndView handleException(Exception e){ //参数e可以获取异常信息
System.out.println("本类的handleException..." + e);
ModelAndView view = new ModelAndView("myError");
view.addObject("exception",e);
return view;
}
@RequestMapping("/test02")
public String test02(Integer i){
if(i != 10){
System.out.println("i不是合法的...");
throw new MyResponseStatusException();
}
System.out.println("i是合法的...");
return "success";
}
//如果某个异常是由spring自己引起的,而且没有ExceptionHandlerExceptionResolver和ResponseStatusExceptionResolver
//进行相应的处理,则会由spring默认的DefaultHandlerExceptionResolver进行处理
@RequestMapping(value = "/test03",method = RequestMethod.POST)
public String test03(){
return "success";
}
//SimpleMappingExceptionResolver处理空指针异常
@RequestMapping("/test04")
public String test04(){
String s = null;
System.out.println(s.length());
return "success";
}
}
//我们可以编写一个类,集中处理所有的异常,要加入到IOC容器中
@ControllerAdvice //这是一个专门用来处理异常的类
public class MyException {
@ExceptionHandler(value = {ArithmeticException.class})
public ModelAndView handleException(Exception e){ //参数e可以获取异常信息
System.out.println("全局的handleException..." + e);
ModelAndView view = new ModelAndView("myError");
view.addObject("exception",e);
return view;
}
}
//自定义异常类
@ResponseStatus(reason = "i值不合法",value = HttpStatus.NOT_ACCEPTABLE)
public class MyResponseStatusException extends RuntimeException{
}
16. SpringMVC运行流程
-
流程图
-
流程描述
-
用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获
-
DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)
判断请求URI对应的映射
- 不存在
- 再判断是否配置了mvc:default-servlet-handler
- 如果没配置,则控制台报映射查找不到,客户端展示404错误
- 如果有配置,则执行目标资源(一般为静态资源,如:JSP,HTML)
- 不存在
-
如果存在,则根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回;
DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter
-
拿到HandlerAdapter之后,就会执行目标方法
- ModelAttribute注解标注的方法会提前运行
- 执行目标方法的时候,确定目标方法用的参数
- 有注解
- 没注解
- 看是否有Model、Map或者其他的
- 如果是自定义类型
- 从隐含模型中看有没有,如果有就从隐含模型中直接拿
- 如果没有,就要看是否有SessionAttributes注解标注的属性,如果是从session中拿,拿不到就会抛异常
- 如果都不是,就会利用反射创建对象
-
如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler()方法【正向】
-
提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)方法,处理请求。在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作
- HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
- 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等
- 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
- 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
-
HandlerAdapter执行完成后,向DispatcherServlet 返回一个ModelAndView对象
-
此时将开始执行拦截器的postHandle()方法【逆向】
-
根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet,根据Model和View,来渲染视图
-
在返回给客户端时需要执行拦截器的AfterCompletion方法【逆向】
-
将渲染结果返回给客户端
-