SpringMVC
这是我学习尚硅谷2022版ssm框架做的笔记
欢迎访问我的个人博客
1 SpringMVC简介
1.1 MVC
是一种架构思想,不是框架或者设计模式
1.2 SpringMVC
是Spring为表述层开发提供的一整套完备的解决方案,封装的就是servlet。
三层架构,是指的表述层、业务逻辑层、数据访问层,表述层表示前台页面和后台servlet
1.3 SpringMVC的特点
- Spring 家族原生产品,与 IOC 容器等基础设施无缝对接
- 基于原生的Servlet,通过了功能强大的前端控制器DispatcherServlet,对请求和响应进行统一处理
- 表述层各细分领域需要解决的问题全方位覆盖,提供全面解决方案
- 代码清新简洁,大幅度提升开发效率
- 内部组件化程度高,可插拔式组件即插即用,想要什么功能配置相应组件即可
- 性能卓著,尤其适合现代大型、超大型互联网项目要求
2 入门案例?
2.1 创建Maven工程
- 添加web模块
这里是以另外一个模块为例
- 打包方式设置为war
- 引入依赖
<dependencies>
<!-- SpringMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.1</version>
</dependency>
<!--thymeleaf需要slf4j日志接口,这里引入他的一个实现logback-->
<!-- 日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- ServletAPI -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- Spring5和Thymeleaf整合包 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>
</dependencies>
注:由于 Maven 的传递性,我们不必将所有需要的包全部配置依赖,而是配置最顶端的依赖,其他靠传递性导入。
2.2 在web.xml中配置DispatcherServlet
2.2.1 配置匹配请求路径
匹配请求时:写/
,匹配浏览器向服务器发送的所有请求,但是不包括以.jsp结尾的请求
而/*
,可以匹配.jsp的请求,但是DispatcherServlet不能处理这种请求,需要Tomcat中专门的的处理.jsp请求的servlet
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<!--这里写/,匹配浏览器向服务器发送的所有请求,但是不包括以.jsp结尾的请求
而/*,可以匹配.jsp的请求
而DispatcherServlet不能处理这种请求,Tomcat中有专门的的处理.jsp的请求-->
<url-pattern>/</url-pattern>
</servlet-mapping>
2.2.2 配置初始化时机
由于DispatcherServlet初始化时间很久,所以非常有必要提前初始化
<load-on-startup>1</load-on-startup>
2.2.3 配置文件的路径
这里使用类路径
类路径:是指java和resources下的路径
<init-param>
<param-name>contextConfigLocation</param-name>
<!--类路径:是指java和resources下的路径-->
<param-value>classpath:springmvc.xml</param-value>
</init-param>
总文件:
<?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的前端控制器DispatcherServlet
(以后不会吧配置文件放在这个位置下,都是放在resource下)
SpringMVC的配置文件默认的位置和名称:
位置:WEB-INF下
名称: <servlet-name>-servlet.xml,当前配置下的配置文件为SpringMVC-servlet.xml
-->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<!--类路径:是指java和resources下的路径-->
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!--将servlet初始化的时间提前到服务启动的时候,
DispatcherServlet初始化的时间很久,所以非常有必要提前初始化-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<!--这里写/,匹配浏览器向服务器发送的所有请求,但是不包括以.jsp结尾的请求
而/*,可以匹配.jsp的请求
而DispatcherServlet不能处理这种请求,Tomcat中有专门的的处理.jsp的请求-->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
2.3 创建请求控制器
由于前端控制器对浏览器发送的请求进行了统一的处理,但是具体的请求有不同的处理过程,因此需要创建处理具体请求的类,即请求控制器
SpringMVC的控制器由一个POJO担任,因此需要通过@Controller注解将其标识为一个控制层组件,交给Spring的IOC容器管理。
控制器方法匹配请求
斜线就代表着绝对路径,在服务器中和在浏览器中解析出来的不一样
在服务器中斜线被解析之后就是localhost:8080+上下文路径:http://localhost:8080/SpringMVC/
在浏览器中,不会有上下文路径,就是http://localhost:8080/
@Controller
public class HelloController {
// 在服务器中斜线代表着绝对路径
// 在服务器中斜线被解析之后就是localhost:8080+上下文路径:http://localhost:8080/SpringMVC/
@RequestMapping("/")
public String portal(){
//将逻辑视图返回,进行视图转发,加上thymeleaf中配置的前缀和后缀
return "index";
}
@RequestMapping("/hello")
public String hello(){
return "success";
}
}
2.4 创建SpringMVC的配置文件
<?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 https://www.springframework.org/schema/context/spring-context.xsd">
<!--开启扫描组件-->
<context:component-scan base-package="com.zylai.controller"/>
<!-- 配置Thymeleaf视图解析器 -->
<bean id="viewResolver"
class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
<property name="order" value="1"/>
<property name="characterEncoding" value="UTF-8"/>
<property name="templateEngine">
<bean class="org.thymeleaf.spring5.SpringTemplateEngine">
<property name="templateResolver">
<bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
<!--
物理视图路径:/WEB-INF/templates/index.html
逻辑视图:index
去掉前缀和后缀
物理视图:前缀+逻辑视图+后缀
-->
<!-- 视图前缀 -->
<property name="prefix" value="/WEB-INF/templates/"/>
<!-- 视图后缀 -->
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML5"/>
<property name="characterEncoding" value="UTF-8" />
</bean>
</property>
</bean>
</property>
</bean>
</beans>
2.5 请求访问页面和跳转页面
2.5.1 创建的位置
在/WEB-INF/templates/
创建index.html
注意:浏览器无法直接访问WEB-INF下的资源,只能通过服务器访问
2.5.2 访问页面
- 在前端控制器中具体的方法返回逻辑视图的名称,比如返回
index
- thymeleaf视图解析器会解析逻辑视图,加上前缀
/WEB-INF/templates/
和后缀.html
- 最后根据总的完整路径
/WEB-INF/templates/index.html
进行转发 - 转发到index.html页面,thymeleaf先回渲染这个页面中的thymeleaf表达式
2.5.3 页面中的路径解析
浏览器会解析为绝对路径,localhost:8080/hello
<!--使用thymeleaf解析,拼接上上下文路径,最后路径为localhost:8080/SpringMVC/hello-->
<a th:href="@{/hello}">测试SpringMVC</a>
<!--这里斜线是绝对路径,是浏览器解析的,会被解析为 localhost:8080/hello-->
<a href="/hello">测试绝对路径</a>
总结
流程:
- 浏览器发送请求
- 若请求地址符合前端控制器DispatcherServlet中url-pattern,该请求就会被DispatcherServlet处理
- 前端控制器会读取SpringMVC的核心配置文件,通过扫描组件找到控制器controller
- 将请求地址和控制器中方法上的@RequestMapping注解的value属性值进行匹配,匹配成功就会执行对应的方法
- 方法会返回一个字符串类型的视图名称,比如返回
index
,是逻辑地址。thymeleaf视图解析器会解析逻辑视图,加上前缀/WEB-INF/templates/
和后缀.html
- 根据总的完整路径
/WEB-INF/templates/index.html
进行转发,转发到index.html页面,thymeleaf先回渲染这个页面中的thymeleaf表达式
3 RequestMapping注解?
3.1 标识的位置
@RequestMapping标识一个类:设置映射请求的请求路径的初始信息
@RequestMapping标识一个方法:设置映射请求请求路径的具体信息
即,类上的路径加上方法上的路径
3.2 value属性⭐️
作用:通过请求的请求路径匹配请求
value属性是数组类型,即当前浏览器所发送请求的路径匹配value属性中的任何一个值,则当前请求就会被这个控制器方法进行处理
@RequestMapping(value = {"/hello","/abc"})
public String hello(){
return "success";
}
<a th:href="@{/abc}">测试@RequestMapping注解的value属性</a>
3.3 method属性⭐️
作用:通过请求的请求方式匹配请求
method属性是RequestMethod类型的数组,即若当前浏览器所发送请求的请求方式匹配method属性数组中的任何一种请求方式,则当前请求就会被注解所标识的方法进行处理
若浏览器所发送的请求的路径和value属性匹配,但是请求方式不匹配,则页面报错:HTTP状态 405 - 方法不允许
@RequestMapping(
value = {"/hello","/abc"},
method = {RequestMethod.POST,RequestMethod.GET}
)
public String hello(){
return "success";
}
如果不指定method属性,那就可以匹配任意类型的请求
3.4 params属性(了解)
* 作用:通过请求的请求参数匹配请求,即浏览器发送的请求的请求参数必须满足params属性的设置
* 可以使用四种表达式:
* "param":表示当前所匹配请求的请求参数中必须携带param参数,不管他的值是啥
* "!param":表示当前所匹配请求的请求参数中一定不能携带param参数
* "param=value":表示当前所匹配请求的请求参数中必须携带param参数且值必须为value
* "param!=value":表示当前所匹配请求的请求参数中可以不携带param参数,若携带,值一定不等于value
*
3.5 headers属性(了解)
* 作用:通过请求的请求头匹配请求,即浏览器发送的请求的请求头必须满足headers属性的设置
* "header":要求请求映射所匹配的请求必须携带header请求头信息
* "!header":要求请求映射所匹配的请求必须不能携带header请求头信息
* "header=value":要求请求映射所匹配的请求必须携带header请求头信息且header=value
* "header!=value":要求请求映射所匹配的请求必须携带header请求头信息且header!=value
* 注意:若当前请求满足@RequestMapping注解的value和method属性,但是不满足headers属性,
* 此时页面显示404错误,即资源未找到
3.6 SpringMVC支持ant风格的路径
在@RequestMapping注解的value属性中设置一些特殊字符
?
:任意的单个字符(不包括?和/)
*
:表示0个或多个任意字符(不包括?和/)
**
:表示任意层数的任意目录,注意使用方式只能将**
写在双斜线中,前后不能有任何的其他字符
3.7 使用路径中的占位符(重点)?
对比:
- 传统:/deleteUser?id=1
- rest: /deleteUser/1
使用:
需要在@RequestMapping注解的value属性中所设置的路径中,使用{xxx}的方式表示路径中的数据
再通过@PathVariable注解,将占位符的值和控制器方法的形参进行绑定
<a th:href="@{/test/rest/2374234/admin}">测试value属性中的占位符</a>
@RequestMapping("/test/rest/{id}/{username}")
public String testRest(@PathVariable(value = "id") Integer id,@PathVariable(value = "username")String username){
System.out.println("id:"+id+",username:"+username);
return "success";
}
11:01:50.534 [http-nio-8080-exec-4] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped to com.zylai.controller.TestRequestMappingController#testRest(Integer, String)
id:2374234,username:admin
4 SpringMVC获取请求参数?
4.1 通过servletAPI获取
很少用
只需要在控制其方法的形参位置设置HttpServletRequest类型的形参,就可以在控制器方法中使用request对象获取请求参数。
这里DispatcherServlet会根据request参数去匹配,会把request对象传过来(具体实现可以参考JavaWeb中的myssm实现)
@RequestMapping("/servletapi")
// 直接在参数中写HttpServletRequest即可,DispatcherServlet会根据参数去匹配
public String getParamByServletAPI(HttpServletRequest request){
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println("username:"+username+",password:"+password);
return "success";
}
4.2 通过控制器方法的形参获取
只需要在控制器方法的形参位置设置好形参,其名字和请求参数的名字一致即可。
表单:
<form th:action="@{/param/params}" method="post">
用户名:<input type="text" name="username"><br/>
密码:<input type="password" name="password"><br/>
<input type="submit" value="登录"><br/>
</form>
控制器方法:
@RequestMapping("/params")
public String getParams(String username,String password){
System.out.println("username:"+username+",password:"+password);
return "success";
}
4.3 @RequestParam注解?
将请求参数和控制器方法的形参绑定,三个属性为:
- value:设置和形参绑定的请求参数的名字
- required:设置是否必须传输value设置的请求参数,默认值为true,表示必须传输,否则报错400
- defaultValue:当未传输value对应的请求参数时,为形参设置默认值,此时和required的值无关
比如,这里把username改为userName,和控制器方法的形参名字不一致
<form th:action="@{/param/params}" method="post">
用户名:<input type="text" name="userName"><br/>
密码:<input type="password" name="password"><br/>
<input type="submit" value="登录"><br/>
</form>
这时只需要在控制器方法的形参上加上注解即可。
实际中,一般只用到value这一个属性就行,这里为了演示
@RequestMapping("/params")
public String getParams( @RequestParam(value = "userName",required = false,defaultValue = "hello") String username, String password){
System.out.println("username:"+username+",password:"+password);
return "success";
}
4.4 @RequestHeader注解
参考@RequestParam注解,这里是获取请求头
4.5 @CookieValue注解
参考@RequestParam注解,这里是根据名称获取cookie
@RequestMapping("/params")
public String getParams(
@RequestParam(value = "userName",required = false,defaultValue = "hello") String username,
String password,
@RequestHeader("referer") String referer,
@CookieValue("JSESSIONID") String jsessionId){
System.out.println("username:"+username+",password:"+password);
System.out.println("referer:"+referer+"\ncookie-jsessionId:"+jsessionId);
return "success";
}
4.6 通过控制器方法的实体类类型的形参获取✌️
需要保证实体类中的属性的属性名和请求参数的名字一致,才可以通过实体类类型的形参获取请求参数。
实体类User:
public class User {
private Integer id;
private String username;
private String password;
}
表单:
<form th:action="@{/param/pojo}" method="post">
用户名:<input type="text" name="userName"><br/>
密码:<input type="password" name="password"><br/>
<input type="submit" value="登录"><br/>
</form>
控制器方法:
@RequestMapping("/pojo")
public String getParamByPojo(User user){
System.out.println(user);
return "success";
}
4.7 设置编码 ?
在web.xml
中设置
设置两个属性encoding
和forceEncoding
注意:在SpringMVC中,编码过滤器一定配置到其他过滤器之前
<!--配置SpringMVC的编码过滤器-->
<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>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
源码解析
这是倒着的关系。CharacterEncodingFilter中设置编码的方法是doFilterInternal。CharacterEncodingFilter的父类OncePerRequestFilter重写了doFilter方法,在doFilter方法中,调用了设置编码的方法doFilterInternal
调用这个构造器
public CharacterEncodingFilter(String encoding, boolean forceEncoding) {
this(encoding, forceEncoding, forceEncoding);
}
this用三个参数的构造器,给三个成员变量赋值
public CharacterEncodingFilter(String encoding, boolean forceRequestEncoding, boolean forceResponseEncoding) {
Assert.hasLength(encoding, "Encoding must not be empty");
this.encoding = encoding;
this.forceRequestEncoding = forceRequestEncoding;
this.forceResponseEncoding = forceResponseEncoding;
}
在这个方法中,设置request和response的参数
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String encoding = getEncoding();
if (encoding != null) {
if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
request.setCharacterEncoding(encoding);
}
if (isForceResponseEncoding()) {
response.setCharacterEncoding(encoding);
}
}
filterChain.doFilter(request, response);
}
5 域对象共享数据?
SpringMVC往域对象中共享数据,最终都是通过ModelAndView实现
5.1 通过ModelAndView向请求域共享数据
- 用其Model功能向请求域中共享数据
- 使用View功能设置逻辑视图,但是控制器方法一定要将ModelAndView作为方法的返回值
@RequestMapping("/test/mav")
public ModelAndView testMAV(){
/**
* ModelAndView包含Model和View的功能
* Model:向请求域中共享数据
* View:设置逻辑视图实现页面跳转
*/
ModelAndView mav = new ModelAndView();
//向请求域中设置数据
mav.addObject("testRequestScope","hello,ModelAndView");
//设置视图名称
mav.setViewName("success");
return mav;
}
5.2 通过Model向请求域共享数据
注意:5.2 5.3 5.4 这三个选择一个用就可以,一般选择Model
将Model作为方法的形参,之后设置域共享数据即可
@RequestMapping("/test/model")
public String testModel(Model model){
System.out.println(model.getClass().getName());
//org.springframework.validation.support.BindingAwareModelMap
model.addAttribute("testRequestScope","hello,Model");
return "success";
}
5.3 通过ModelMap向请求域共享数据
@RequestMapping("/test/modelMap")
public String testModel(ModelMap modelMap){
System.out.println(modelMap.getClass().getName());
//org.springframework.validation.support.BindingAwareModelMap
modelMap.addAttribute("testRequestScope","hello,ModelMap");
return "success";
}
5.4 通过Map向请求域共享数据
@RequestMapping("/test/map")
public String testMap(Map<String,Object> map){
System.out.println(map.getClass().getName());
//org.springframework.validation.support.BindingAwareModelMap
map.put("testRequestScope","hello,Map");
return "success";
}
5.5 Model和ModelMap和Map的关系
其实在底层中,这些类型的形参最终都是通过BindingAwareModelMap
创建的
几个类的关系:
public class BindingAwareModelMap extends ExtendedModelMap {}
public class ExtendedModelMap extends ModelMap implements Model {}
public class ModelMap extends LinkedHashMap<String, Object> {}
最终都是返回一个ModelAndView实例
5.6 共享session域和application域中的数据
使用servletAPI即可
@RequestMapping("/test/session")
public String testSession(HttpSession session){
session.setAttribute("testSessionScope","hello,session");
return "success";
}
@RequestMapping("/test/application")
public String testApplication(HttpSession session){
ServletContext application = session.getServletContext();
application.setAttribute("testApplicationScope","hello,application");
return "success";
}
6 SpringMVC视图?
SpringMVC中的视图是View接口,视图的作用渲染数据,将模型Model中的数据展示给用户
SpringMVC视图的种类很多,默认有转发视图InternalResourceView和重定向视图RedirectView
当工程引入jstl的依赖,转发视图会自动转换为JstlView
若使用的视图技术为Thymeleaf,在SpringMVC的配置文件中配置了Thymeleaf的视图解析器,由此视图解析器解析之后所得到的是ThymeleafView
6.1 Thymeleaf视图
当控制器方法中所设置的视图名称没有任何前缀时,此时的视图名称会被SpringMVC配置文件中所配置的视图解析器解析,视图名称拼接视图前缀和视图后缀所得到的最终路径,会通过转发的方式实现跳转
@RequestMapping("/testHello")
public String testHello(){
return "hello";
}
6.2 转发视图
转发和重定向:
- 转发浏览器发送一次请求,在服务器中处理一次,重定向浏览器发送两次请求。
- 转发由于只发送一次请求,地址栏地址不变。重定向改变
- 转发由于只发送一次request请求,可以使用request共享域中的数据,而重定向不可
SpringMVC中默认的转发视图是InternalResourceView
SpringMVC中创建转发视图的情况:
当控制器方法中所设置的视图名称以"forward:"为前缀时,创建InternalResourceView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"forward:"去掉,剩余部分作为最终路径通过转发的方式实现跳转。
例如"forward:/",“forward:/employee”。
@RequestMapping("/testForward")
public String testForward(){
return "forward:/testHello";//跳转到路径为/testHello的控制器方法中,之后再执行这个方法
}
6.3 重定向视图
SpringMVC中默认的重定向视图是RedirectView
当控制器方法中所设置的视图名称以"redirect:"为前缀时,创建RedirectView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"redirect:"去掉,剩余部分作为最终路径通过重定向的方式实现跳转
例如"redirect:/",“redirect:/employee”
@RequestMapping("/testRedirect")
public String testRedirect(){
return "redirect:/testHello";
}
6.4 视图控制器
当控制器方法中,仅仅用来实现页面跳转,即只需要设置视图名称时,可以将处理器方法使用viewcontroller标签进行表示。
注意:
当SpringMVC中设置任何一个view-controller时,其他控制器中的请求映射将全部失效,此时需
要在SpringMVC的核心配置文件中设置开启mvc注解驱动的标签:
<mvc:annotation-driven />
7 RESTful?
7.1 RESTful简介
将服务器看做由很多资源组成,一切皆资源。对于同一个资源,要求其访问路径是一样的,不论是增删改查,路径都是一样。
但是如何区别增删改查呢?使用请求方式修改。
下面是比较详细的介绍
7.2 RESTful的实现
具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE用来删除资源。
REST 风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性。
以操作用户对象为例:
@Controller
public class TestRestController {
// @RequestMapping(value = "/user",method = RequestMethod.GET)
@GetMapping("/user")
public String getAllUser(){
System.out.println("查询所有的用户信息-->/user-->get");
return "success";
}
// @RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
@GetMapping("/user/{id}")
public String getUserById(@PathVariable("id")Integer id){
System.out.println("根据id查询用户信息-->/user/"+id+"-->get");
return "success";
}
// @RequestMapping(value = "/user",method = RequestMethod.POST)
@PostMapping("/user")
public String insertUser(){
System.out.println("添加用户信息-->/user-->post");
return "success";
}
// @RequestMapping(value = "/user",method = RequestMethod.PUT)
@PutMapping("/user")
public String updateUser(){
System.out.println("修改用户信息-->/user-->put");
return "success";
}
// @RequestMapping(value = "/user/{id}",method = RequestMethod.DELETE)
@DeleteMapping("/user/{id}")
public String updateUser(@PathVariable("id") Integer id){
System.out.println("删除用户信息-->/user/"+id+"-->delete");
return "success";
}
}
7.3 处理PUT和DELETE请求
浏览器目前只能发送get和post请求
若要发送put和delete请求,需要在web.xml中配置一个过滤器HiddenHttpMethodFilter,配置了过滤器之后,发送的请求要满足两个条件,才能将请求方式转换为put和delete
1、当前请求的请求方式必须为post
2、当前请求必须传输请求参数_method,_method的值才是最终的请求方式
源码分析
HiddenHttpMethodFilter:
请求方式必须是post,获取参数_method的值
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request;
//如果当前请求是post且没有异常就进入
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
//得到_method参数的值,然后再根据值指定的方法创建一个对应方法的request
String paramValue = request.getParameter(this.methodParam);
//判断是否有值
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter(requestToUse, response);
}
//这个内部类,传入request和method,重写了getMethod方法,之后获取method都是PUT或者DELETE
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}
@Override
public String getMethod() {
return this.method;
}
}
8 RESTful案例?
这一部分就是实现一个增删改查,使用RESTful风格实现
8.1 功能
- 查询所有的员工信息 -->/employee–>get
- 跳转到添加页面 -->/to/add–>get
- 新增员工信息 -->/employee–>post
- 跳转到修改页面 -->/employee/1–>get
- 修改员工信息 -->/employee–>put
- 删除员工信息 -->/employee/1–>delete
8.2 处理静态资源
由css文件引入
在员工列表的页面引入css文件
<link rel="stylesheet" th:href="@{/static/css/index_work.css}">
DispatcherServlet会拦截除了jsp之外的所有请求,当请求静态资源时,该请求也会被DispatcherServlet处理,然而他处理不了,需要交给Tomcat处理。
DispatcherServlet和Tomcat的DefaultServlet
当前工程的web.xml配置的前端控制器DispatcherServlet的url-pattern是/
而Tomcat的web.xml配置的默认的控制器DefaultServlet的url-pattern也是/
此时,浏览器发送的请求优先会被DispatcherServlet进行处理,但是DispatcherServlet无法处理静态资源。因此需要把静态资源的处理交给默认的servlet,进行如下配置:
若配置了<mvc:default-servlet-handler/>
,此时浏览器发送的所有请求都会被DefaultServlet处理,DispatcherServlet无法进行处理
因此还需要开启mvc的注解驱动<mvc:annotation-driven />
,这样浏览器发送的请求会先被DispatcherServlet处理,无法处理再交给DefaultServlet处理
如果Tomcat中的web.xml的配置和工程中的web.xml配置冲突,会以当前工程的配置为准。
8.3 功能实现
这一部分没啥的,正常的crud
控制器:
@Controller
public class EmployeeController {
@Autowired
private EmployeeDao employeeDao;
@GetMapping("/employee")
public String getAllEmployee(Model model){
//获取所有的员工信息
Collection<Employee> employeeList = employeeDao.getAll();
//将所有的员工信息在请求域中共享
model.addAttribute("employeeList",employeeList);
//跳转到列表页面
return "employee_list";
}
@PostMapping("/employee")
public String addEmployee(Employee employee){
//保存员工信息
employeeDao.save(employee);
//访问列表功能,调用查询数据
//重定向
return "redirect:/employee";
}
@GetMapping("/employee/{id}")
public String toUpdate(@PathVariable("id") Integer id,Model model){
//根据id查询信息
Employee employee = employeeDao.get(id);
//将员工信息共享到请求域中
model.addAttribute("employee",employee);
//跳转
return "employee_update";
}
@PutMapping("/employee")
public String updateEmployee(Employee employee){
//更新信息
employeeDao.save(employee);
//重定向
return "redirect:/employee";
}
@DeleteMapping("/employee/{id}")
public String deleteEmployeeById(@PathVariable("id") Integer id){
employeeDao.delete(id);
return "redirect:/employee";
}
}
前端页面,就以员工列表页为例
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>employee list</title>
<link rel="stylesheet" th:href="@{/static/css/index_work.css}">
</head>
<body>
<div id="app">
<table>
<tr>
<th colspan="5">employee list</th>
</tr>
<tr>
<th>id</th>
<th>lastName</th>
<th>email</th>
<th>gender</th>
<th>options(<a th:href="@{/to/add}">add</a> )</th>
</tr>
<tr th:each="employee : ${employeeList}">
<td th:text="${employee.id}"></td>
<td th:text="${employee.lastName}"></td>
<td th:text="${employee.email}"></td>
<td th:text="${employee.gender}"></td>
<td>
<a @click="deleteEmployee()" th:href="@{|/employee/${employee.id}|}">delete</a>
<a th:href="@{|/employee/${employee.id}|}">update</a>
</td>
</tr>
</table>
<form method="post">
<input type="hidden" name="_method" value="delete"/>
</form>
</div>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script type="text/javascript">
var vue = new Vue({
el:"#app",
methods:{
deleteEmployee(){
//获取表单
var form = document.getElementsByTagName("form")[0]
//将超链接的href属性赋值给form表单的action属性
form.acton = event.target.href;
//表单提交
form.submit();
//阻止超链接的默认行为
event.preventDefault();
}
}
})
</script>
</body>
</html>
9 SpringMVC处理ajax请求?
更详细的介绍见https://heavy_code_industry.gitee.io/code_heavy_industry/pro001-javaweb/lecture/chapter12/
9.1 ajax介绍?
服务端渲染与ajax渲染
服务端渲染
ajax渲染(局部更新)
前后端分离
彻底舍弃服务器端渲染,数据全部通过Ajax方式以JSON格式来传递。
异步和同步
Ajax本身就是Asynchronous JavaScript And XML的缩写,直译为:异步的JavaScript和XML。在实际应用中Ajax指的是:不刷新浏览器窗口,不做页面跳转,局部更新页面内容的技术。
axios
发送请求的方式之一:(一般不使用这个,这里用这个详细介绍一下axios)
参数用params方式,参数拼接到地址栏中。参数用data方式,参数放在请求体中。
axios({
url:"",//请求路径
method:"",//请求方式
//请求参数,以name=value&name=value的方式,
//不管使用的请求方式是get或者post,请求参数都会被拼接到请求地址后
// 此种方式可以通过request.getParameter()获取
params:{},
//请求参数,以json格式发送,
// 请求参数会被保存到请求报文的请求体传输到服务器,因此不能使用get,get没有请求体
// 需要获取请求体,之后把请求体中的json数据进行处理,会使用一些处理json的jar包处理
data:{}
}).then(res=>{
console.log(res.data)
})
发送请求的方式二:
axios.post(
//url,要加上上下文路径
"/SpringMVC/test/ajax?id=1001",
//json类型的数据,会被封装在请求体中
{username:"admin",password:"123456"}
).then(res=>{//请求成功的响应
//服务器返回的结果封装到data属性中
console.log(res.data)
})
9.2 @RequestBody注解
该注解将请求体中的内容和形参进行绑定。由于axios发送请求把请求参数放到了请求体中,所以需要这个注解来获取请求体中的内容。
testAjax(){
axios.post(
//url,要加上上下文路径
"/SpringMVC/test/ajax?id=1001",
//json类型的数据,会被封装在请求体中
{username:"admin",password:"123456"}
).then(res=>{//请求成功的响应
//服务器返回的结果封装到data属性中
console.log(res.data)
})
},
@RequestMapping("/test/ajax")
public void testAjax(Integer id, @RequestBody String requestBody, HttpServletResponse response) throws IOException {
System.out.println("id:"+id);
System.out.println("requestBody:"+requestBody);
response.getWriter().write("hello.axios");
}
控制台打印:
id:1001
requestBody:{"username":"admin","password":"123456"}
9.3 @RequestBody注解将json格式的数据转为java对象?
由于axios的请求体中是json格式,我们需要进行转换
步骤:
1、导入jackson的依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.1</version>
</dependency>
2、在SpringMVC配置文件中设置开启mvc的注解驱动
<mvc:annotation-driven />
3、在控制器方法的形参前设置@RequestBody注解
@RequestMapping("/test/requestbody/json")
public void testRequestBody(@RequestBody User user, HttpServletResponse response) throws IOException {
System.out.println(user);
response.getWriter().write("hello,requestBody");
}
9.4 @ResponseBody注解
就是把所标识的方法的返回值作为响应报文的响应体响应到浏览器。
@RequestMapping("/test/responsebody")
//这个注解就是把方法的返回值作为请求的返回体返回给浏览器
@ResponseBody
public String testResponseBody(){
return "success";
}
这里直接将内容显示到浏览器中
9.5 @ResponseBody注解响应给浏览器json格式数据
java --> json
如何将java对象响应过去?如果直接如果直接response.getWriter().write,加入java对象,是返回的内存地址或者返回了一个toString方法的返回值,再者,如果将java类型的对象返回了,那么浏览器也不能处理java类型的数据。
一般都是奖java转换为json值返回
步骤:(和上面的差不多)
- 导入jackson的依赖
- 在SpringMVC配置文件中设置开启mvc的注解驱动
- 将需要转换为json字符串的Java对象作为控制器方法的返回值,使用@ResponseBody注解标识控制器方法就可以将java对象直接转化为json字符串,并响应到浏览器
非常方便,省略了将返回的java对象转换为json的过程,由框架完成
//非常方便,省略了将返回的java对象转换为json的过程,由框架完成
@RequestMapping("/test/responsebody/json")
@ResponseBody
public User testResponseBodyJson(){
User user = new User(1001,"admin","23232",20,"男");
return user;
}
转换关系
常用的java对象转化为json的结果:
- 实体类–>json对象
- map–>json对象
- List–>json数组
9.6 @RestController注解
@RestController注解是springMVC提供的一个复合注解,标识在控制器的类上,就相当于为类添加了
@Controller注解,并且为其中的每个方法添加了@ResponseBody注解
10 文件上传和下载
10.1 文件下载
ResponseEntity:可以作为控制器方法的返回值,表示响应到浏览器的完整的响应报文
如果不设置返回,返回值为void,那么会默认将请求地址作为逻辑视图
下面这个,一般当成一个模板用就可以了
@RequestMapping("/test/down")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws
IOException {
//获取ServletContext对象
ServletContext servletContext = session.getServletContext();
//获取服务器中文件的真实路径,其实就是直接把这个路径拼接到项目的后面
String realPath = servletContext.getRealPath("img");
//E:\code_workspace\study_SSM\spring_mvc04_ajax\target\spring_mvc04_ajax-1.0-SNAPSHOT\
realPath = realPath + File.separator+"avatar.jpg";
//创建输入流
InputStream is = new FileInputStream(realPath);
//创建字节数组,is.available()获取输入流所对应的文件的字节数。
// 对于流,可以设置响应的读取策略,可以适量多次读取
byte[] bytes = new byte[is.available()];
//将流读到字节数组中
is.read(bytes);
//创建HttpHeaders对象设置响应头信息
MultiValueMap<String, String> headers = new HttpHeaders();
//设置要下载方式以及下载文件的名字,attachment:以附件的方式下载
headers.add("Content-Disposition", "attachment;filename=avatar.jpg");
//设置响应状态码
HttpStatus statusCode = HttpStatus.OK;
//创建ResponseEntity对象
ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers,
statusCode);
//关闭输入流
is.close();
return responseEntity;
}
10.2 文件上传
要求:
- form表单的请求方式必须是post
- form表单必须设置属性enctype=“multipart/form-data”,保证以二进制方式上传
步骤:
添加依赖
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload --> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version> </dependency>
配置bean
注意设置id,id名固定
<!--必须通过文件解析器的解析才能将文件转换为MultipartFile对象 这里的id必须是multipartResolver,因为这里是根据id获取bean的--> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!--<property name="defaultEncoding" value="UTF-8"/>--> </bean>
控制器方法
注意点:使用MultipartFile类型参数接收,注意把他当成一个普通参数就可,需要匹配请求的参数
@RequestMapping("/test/up") //注意:这里的MultipartFile不是由框架自动填充(就像request和session那样赋值), // 而是像普通参数一样从请求参数中获取 public String testUp(@RequestParam("photo") MultipartFile photo, HttpSession session) throws IOException { //获取上传的文件的文件名 String filename = photo.getOriginalFilename(); //处理文件重名问题 //获取上传的文件的后缀名 String fileType = filename.substring(filename.lastIndexOf(".")); filename = UUID.randomUUID().toString() + fileType; System.out.println(filename); //获取ServletContext对象 ServletContext servletContext = session.getServletContext(); //获取当前工程下photo目录的真实路径 String photoPath = servletContext.getRealPath("photo"); //创建photoPath所对应的File对象 File file = new File(photoPath); //判断file对应的目录是否存在 if(!file.exists()){ file.mkdir(); } String finalPath = photoPath + File.separator + filename; //文件上传 photo.transferTo(new File(finalPath)); return "success"; }
10.3 文件上传的重命名问题
使用UUID命名,UUID加上文件后缀名即可。
//处理文件重名问题
//获取上传的文件的后缀名
String fileType = filename.substring(filename.lastIndexOf("."));
filename = UUID.randomUUID().toString() + fileType;
System.out.println(filename);
下面这些知识点用的不多,了解即可
11 拦截器
请求经过过滤器,过滤器放行之后DispatcherServlet处理请求,而拦截器用于拦截控制器方法的执行
11.1 拦截器的配置
SpringMVC中的拦截器用于拦截控制器方法的执行
SpringMVC中的拦截器需要实现HandlerInterceptor接口
SpringMVC的拦截器必须在SpringMVC的配置文件中进行配置:
<mvc:interceptor>
<bean class="com.atguigu.interceptor.FirstInterceptor"> </bean>
<ref bean="firstInterceptor"></ref>
</mvc:interceptor>
<!-- 以上两种配置方式都是对DispatcherServlet所处理的所有的请求进行拦截 -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/testRequestEntity"/>
<ref bean="firstInterceptor"></ref>
</mvc:interceptor>
<!--
以上配置方式可以通过ref或bean标签设置拦截器,通过mvc:mapping设置需要拦截的请求(/**匹配所有的请求),通过mvc:exclude-mapping设置需要排除的请求,即不需要拦截的请求
-->
总结:bean和ref的配置方式都是对所有请求进行拦截,而mvc:mapping可以设置拦截的请求
11.2 拦截器的三个抽象方法
SpringMVC中的拦截器有三个抽象方法:
preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法
postHandle:控制器方法执行之后执行postHandle()
afterComplation:处理完视图和模型数据,渲染视图完毕之后执行afterComplation()
拦截器的类要实现HandlerInterceptor接口,并重写其中的三个默认的抽象方法(使用Ctrl+O快捷键)
public class FirstInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("FirstInterceptor --> preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("FirstInterceptor --> postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("FirstInterceptor --> afterCompletion");
}
}
11.3 多个拦截器的执行顺序
a>若每个拦截器的preHandle()都返回true
此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关:
preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行
b>若某个拦截器的preHandle()返回了false
preHandle()返回false和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,返回false的拦截器之前的拦截器的afterComplation()会执行
12 异常处理器
1、基于配置的异常处理
SpringMVC提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver
HandlerExceptionResolver接口的实现类有:DefaultHandlerExceptionResolver和SimpleMappingExceptionResolver
SpringMVC提供了自定义的异常处理器SimpleMappingExceptionResolver
,使用方式:
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<!--
properties的键表示处理器方法执行过程中出现的异常
properties的值表示若出现指定异常时,设置一个新的视图名称,跳转到指定页面
-->
<prop key="java.lang.ArithmeticException">error</prop>
</props>
</property>
<!--
exceptionAttribute属性设置一个属性名,将出现的异常信息在请求域中进行共享
-->
<property name="exceptionAttribute" value="ex"></property>
</bean>
2、基于注解的异常处理
//@ControllerAdvice将当前类标识为异常处理的组件
@ControllerAdvice
public class ExceptionController {
//@ExceptionHandler用于设置所标识方法处理的异常
@ExceptionHandler(ArithmeticException.class)
//ex表示当前请求处理中出现的异常对象
public String handleArithmeticException(Exception ex, Model model){
model.addAttribute("ex", ex);
return "error";
}
}
13 注解配置SpringMVC
使用配置类和注解代替web.xml和SpringMVC配置文件的功能
1、创建初始化类,代替web.xml
在Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的话就用它来配置Servlet容器(就是Tomcat)。可以用这个类代替web.xml。
Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。Spring3.2引入了一个便利的WebApplicationInitializer基础实现,名为AbstractAnnotationConfigDispatcherServletInitializer,当我们的类扩展了AbstractAnnotationConfigDispatcherServletInitializer并将其部署到Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文。
总结一句话:如果用类代替web.xml文件,需要这个类去继承AbstractAnnotationConfigDispatcherServletInitializer
public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 指定spring的配置类
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringConfig.class};
}
/**
* 指定SpringMVC的配置类
* @return
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
/**
* 指定DispatcherServlet的映射规则,即url-pattern
* @return
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
/**
* 添加过滤器
* @return
*/
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
encodingFilter.setEncoding("UTF-8");
encodingFilter.setForceRequestEncoding(true);
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
return new Filter[]{encodingFilter, hiddenHttpMethodFilter};
}
}
2、创建SpringConfig配置类,代替spring的配置文件
@Configuration
public class SpringConfig {
//ssm整合之后,spring的配置信息写在此类中
}
3、创建WebConfig配置类,代替SpringMVC的配置文件
/**
* @Author: zyl
* @Date: 2021/08/24/17:14
* @Description: 代替SpringMVC的配置文件
* 1、扫描组件
* 2、视图解析器
* 3、view-controller
* 4、default-servlet-handler
* 5、mvc注解驱动
* 6、文件上传解析器
* 7、异常处理
* 8、拦截器
*/
//将当前类表示为一个配置类
@Configuration
//1、扫描组件
@ComponentScan("com.atguigu.mvc")
//5、mvc注解驱动
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
//4、default-servlet-handler
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
//8、拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TestInterceptor())
.addPathPatterns("/**") //配置拦截的路径
.excludePathPatterns("/");//排除的的路径
}
//3、view-controller
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/hello")
.setViewName("hello");
}
//6、文件上传解析器
@Bean
public MultipartResolver multipartResolver(){
CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();
return commonsMultipartResolver;
}
//7、异常处理
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
Properties prop = new Properties();
//不能使用put方法,使用setProperty,使键值对都是字符串
prop.setProperty("java.lang.ArithmeticException","error");//异常类型和跳转的页面名称
exceptionResolver.setExceptionMappings(prop);
exceptionResolver.setExceptionAttribute("exception");//把异常信息放到request共享域
resolvers.add(exceptionResolver);
}
//========配置Thymeleaf视图解析器==============
//配置生成模板解析器
@Bean
public ITemplateResolver templateResolver() {
WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
// ServletContextTemplateResolver需要一个ServletContext作为构造参数,可通过WebApplicationContext 的方法获得
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(
webApplicationContext.getServletContext());
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setTemplateMode(TemplateMode.HTML);
return templateResolver;
}
//生成模板引擎并为模板引擎注入模板解析器
//方法的形参是自动装配,按照类型自动装配
@Bean
public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
//生成视图解析器并未解析器注入模板引擎
@Bean
public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setCharacterEncoding("UTF-8");
viewResolver.setTemplateEngine(templateEngine);
return viewResolver;
}
//====================================
}
4、测试功能
@RequestMapping("/")
public String index(){
return "index";
}
14 SpringMVC执行流程
1、SpringMVC常用组件
- DispatcherServlet:前端控制器,不需要工程师开发,由框架提供
作用:统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求
- HandlerMapping:处理器映射器,不需要工程师开发,由框架提供
作用:根据请求的url、method等信息查找Handler,即控制器方法
- Handler:处理器,需要工程师开发
作用:在DispatcherServlet的控制下Handler对具体的用户请求进行处理
- HandlerAdapter:处理器适配器,不需要工程师开发,由框架提供
作用:通过HandlerAdapter对处理器(控制器方法)进行执行
- ViewResolver:视图解析器,不需要工程师开发,由框架提供
作用:进行视图解析,得到相应的视图,例如:ThymeleafView、InternalResourceView、RedirectView
- View:视图
作用:将模型数据通过页面展示给用户
2、DispatcherServlet初始化过程
DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet 生命周期来进行调度。
在FrameworkServlet的initServletBean()方法中,调用了initWebApplicationContext()方法,下面就需要看如何初始化IOC容器
a>初始化WebApplicationContext
所在类:org.springframework.web.servlet.FrameworkServlet
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
// 创建WebApplicationContext
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
synchronized (this.onRefreshMonitor) {
// 刷新WebApplicationContext
onRefresh(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
// 将IOC容器在应用域共享
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
b>创建WebApplicationContext
所在类:org.springframework.web.servlet.FrameworkServlet
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
Class<?> contextClass = getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
// 通过反射创建 IOC 容器对象
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(getEnvironment());
// 设置父容器
wac.setParent(parent);
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
configureAndRefreshWebApplicationContext(wac);
return wac;
}
c>DispatcherServlet初始化策略
FrameworkServlet创建WebApplicationContext后,刷新容器,调用onRefresh(wac),此方法在DispatcherServlet中进行了重写,调用了initStrategies(context)方法,初始化策略,即初始化DispatcherServlet的各个组件
所在类:org.springframework.web.servlet.DispatcherServlet
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
3、DispatcherServlet调用组件处理请求
a>processRequest()
FrameworkServlet重写HttpServlet中的service()和doXxx(),这些方法中调用了processRequest(request, response)
所在类:org.springframework.web.servlet.FrameworkServlet
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 {
// 执行服务,doService()是一个抽象方法,在DispatcherServlet中进行了重写
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();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
b>doService()
所在类:org.springframework.web.servlet.DispatcherServlet
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(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);
}
RequestPath requestPath = null;
if (this.parseRequestPath && !ServletRequestPathUtils.hasParsedRequestPath(request)) {
requestPath = ServletRequestPathUtils.parseAndCache(request);
}
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);
}
}
if (requestPath != null) {
ServletRequestPathUtils.clearParsedRequestPath(request);
}
}
}
c>doDispatch()
所在类:org.springframework.web.servlet.DispatcherServlet
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 {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
/*
mappedHandler:调用链
包含handler、interceptorList、interceptorIndex
handler:浏览器发送的请求所匹配的控制器方法
interceptorList:处理控制器方法的所有拦截器集合
interceptorIndex:拦截器索引,控制拦截器afterCompletion()的执行
*/
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 通过控制器方法创建相应的处理器适配器,调用所对应的控制器方法
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 调用拦截器的preHandle()
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 由处理器适配器调用具体的控制器方法,最终获得ModelAndView对象
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 调用拦截器的postHandle()
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);
}
// 后续处理:处理模型数据和渲染视图
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);
}
}
}
}
d>processDispatchResult()
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.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
// 调用拦截器的afterCompletion()
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
4、SpringMVC的执行流程
用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获。
DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI),判断请求URI对应的映射:
a) 不存在
i. 再判断是否配置了mvc:default-servlet-handler
ii. 如果没配置,则控制台报映射查找不到,客户端展示404错误
iii. 如果有配置,则访问目标资源(一般为静态资源,如:JS,CSS,HTML),找不到客户端也会展示404错误
b) 存在则执行下面的流程
根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回。
DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。
如果成功获得HandlerAdapter,此时将开始执行拦截器的preHandler(…)方法【正向】
提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)方法,处理请求。在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:
a) HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
b) 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等
c) 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
d) 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象。
此时将开始执行拦截器的postHandle(…)方法【逆向】。
根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的ViewResolver进行视图解析,根据Model和View,来渲染视图。
渲染视图完毕执行拦截器的afterCompletion(…)方法【逆向】。
将渲染结果返回给客户端。