SpringMVC
MVC理论基础
MVC架构:
三层架构:
- Model包括数据访问层和业务层
- View属于表示层
- Controllert通常被视为表示层的一部分,但也可能包含一些轻量级的业务逻辑
三层架构中,最关键的是表示层:
- 它直接与用户交互,所有的请求都经过表示层解析,再告知业务层处理。
- 所有页面的返回和数据的填充也靠表示层来完成。
SpringMVC就是一个优秀的表示层框架,将业务逻辑和表示逻辑解耦,更加精细地划分对应的职责,最后将View和Model进行渲染,得到最终的页面并返回给前端。
配置环境并搭建项目
新建Jakarta EE项目,Template选择
Web application
,Application server
选择本地的Tomcat直接创建
编辑Tomcat Server的配置。
修改
Application context
的默认名称,点击Apply
之后,Server
标签页中的URL
也会同步变更(两者应保持一致)。修改
On 'Update' action
为Restart server
。
添加SpringMVC依赖
1 | <dependency> |
传统XML配置形式
添加Spring配置文件,并配置为应用程序上下文(IoC容器)
使用DispatcherServlet替换Tomcat自带的Servlet,并指定Spring配置文件
web.xml(属于web应用程序的一部分,部署描述符),用于找到请求路径对应的Servlet
web.xml
文件中添加:
1 | <servlet> |
删除项目自带的Servlet类和index.jsp,创建一个MVC中使用的Controller类,并注册为Bean
1 | package com.hunter.springmvc_demo.controller; |
Spring配置文件配置包扫描,将HelloController注册为Bean:
1 |
|
可以看到该Spring配置文件上方显示File is included in 4 contexts
,不是错误,不用理会。
启动tomcat,浏览器就能出现HelloController
中返回的Hello World!
。
全注解配置形式
创建Spring配置类
1 | package com.hunter.springmvc_demo.config; |
创建 注解配置DispatcherServlet初始化器,并指定Spring配置类
Tomcat会在类路径中查找实现了ServletContainerInitializer
接口的类,如果发现的话,就用它来配置Servlet容器。
Spring内置了实现类SpringServletContainerInitializer
,通过@HandlesTypes(WebApplicationInitializer.class)
设置,这个类反过来会查找实现WebApplicationInitializer
的类,并将配置的任务交给他们来完成。
因此,创建一个实现WebApplicationInitializer
接口的类,就能管理Spring配置。
1 | public class MainInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { |
Controller控制器
SpringMVC使用DispatcherServlet
替代Tomcat提供的默认的静态资源Servlet,这样,所有的请求都会经过DispatcherServlet
处理(jsp除外,tomcat还提供了jsp的servlet)。
请求到达Tomcat服务器之后,会交给当前的Web应用程序进行处理,SpringMVC采用DispatcherServlet
作为统一的访问点,所有的请求交给它来调度。
当一个请求经过
DispatcherServlet
之后,会先走HandlerMapping
,它会将请求映射为HandlerExecutionChain
,依次经过HandlerInterceptor
,然后再交给HandlerAdapter
,根据请求的路径选择合适的控制器进行处理,控制器处理完成之后,会返回一个ModelAndView
对象,包括数据模型和视图,通俗的讲就是页面中数据和页面本身(只包含视图名称即可)。返回ModelAndView
之后,会交给ViewResolver
(视图解析器)进行处理(SpringMVC自带了一些视图解析器,但是只适用于JSP页面,也可以使用Thymeleaf作为视图解析器,根据给定的视图名称,直接读取HTML编写的页面),解析为一个真正的View。解析完成后,就需要将页面中的数据全部渲染到View中,最后返回给DispatcherServlet
一个包含所有数据的成形页面,再响应给浏览器,完成整个过程。
因此,实际上整个过程只需要编写对应请求路径的的Controller以及配置好我们需要的ViewResolver即可,之后还可以继续补充添加拦截器,而其他的流程已经由SpringMVC帮助我们完成了。
创建Controller只需要在一个类上添加@Controller
注解,它会被Spring扫描并自动注册为Contrller类型的Bean,只需要在其中编写方法用于处理对应地址的请求即可。
配置视图解析器 ViewResolver
Thymeleaf(百里香叶)是一个既能实现模板,又能兼顾前后端分离的服务器端Java模板引擎,可以替代JSP作为视图层。
Note that Thymeleaf has integrations for both versions 5.x and 6.x of the Spring Framework, provided by two separate libraries called
thymeleaf-spring5
andthymeleaf-spring6
. These libraries are packaged in separate.jar
files (thymeleaf-spring5-{version}.jar
andthymeleaf-spring6-{version}.jar
) and need to be added to your classpath in order to use Thymeleaf’s Spring integrations in your application.
1 | <dependency> |
1 |
|
Controller的返回类型
ModelAndView
1 |
|
创建一个简单的html文件src/main/resources/index.html
,启动tomcat就能正常访问。
1 |
|
String
为了简便,可以用String类型直接返回View的名称,SpringMVC会自动将其包装成ModelAndView对象。
1 |
|
方法参数可以是Model
、Map
、ModelMap
,SpringMVC通过依赖注入会自动传递对应的实例对象。
静态资源交给Tomcat提供的默认Servlet解析
页面中可能还会包含一些静态资源,比如js、css,让静态资源通过Tomcat提供的默认Servlet进行解析,需要让Spring配置类实现一下WebMvcConfigurer
接口:
1 |
|
创建src/main/resources/static/test.js
:
1 | window.alert("欢迎来到测试网站") |
修改src/main/resources/index.html
为:
1 |
|
这样就能在页面加载时,显示一个弹窗。以上,就完成了最基本的页面配置。
@RequestMapping 详解
最关键的属性是
path
(等价于value),它决定了当前方法处理的请求路径。路径必须全局唯一,任何一个路径只能由一个方法进行处理。也可以直接将
@RequestMapping
添加到类名上,表示为此类中的所有请求映射添加一个路径前缀。路径还支持通配符进行匹配:
?
:表示任意1个字符。@RequestMapping("/index/x?")
可以匹配/index/xa、/index/xb等等。*
:表示任意0-n个字符。@RequestMapping("/index/*")
可以匹配/index、/index/yyds等。**
:表示当前目录或基于当前目录的多级目录。比如@RequestMapping("/index/**")
可以匹配/index、/index/xxx等。
method
属性设置请求的方法类型。默认支持所有方法,浏览器默认使用GET方法获取页面。params
用于指定请求必须携带的参数,未携带则无法访问。还支持表达式,**
!
表示请求不允许携带的参数**。甚至可以直接设定值:params = {"username!=test", "password=123"}
1
2
3
4
public ModelAndView index() {
return new ModelAndView("index");
}headers
用于指定请求头中携带的内容。同样,**!
表示请求不允许携带的属性**。1
2
3
4
public ModelAndView index() {
return new ModelAndView("index");
}consumes
指定处理请求的提交内容类型(Content-Type)。例如application/json
,text/html
。produces
指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回。
@RequestParam 、@RequestHeader 和 @RequestBody
想要获取到请求中的参数,为方法添加一个形参,并在形参的类型之前添加@RequestParam
即可。
1 |
|
我们需要在@RequestParam
中填写请求中的参数名称,参数的值会自动传递给形式参数,可以直接在方法中使用。一旦添加@RequestParam
,请求必须携带指定参数,也可以将require
属性设定为false
非必须,也能通过defaultValue
设定一个默认值。
1 |
|
如果需要使用Servlet原本的一些类,比如HttpServletReuqest
、HttpServletResponse
、HttpSession
,直接将其添加为参数即可。
1 |
|
还可以将请求参数传递给一个实体类。实体类必须携带set
方法或者有包含所有参数的构造方法,请求参数会自动根据类中的字段名称进行匹配。
1 |
|
1 |
|
@RequestHeader
与@RequestParam
用法一致,不过它是用于获取请求头参数的。
如果我们需要读取前端发送给我们的JSON格式数据,就需要添加@RequestBody
注解:
1 |
|
@CookieValue 和 @SessionAttribute
通过使用@CookieValue
注解,我们也可以快速获取请求携带的Cookie信息:
1 |
|
同样的,Session也能使用@SessionAttribute
注解快速获取:
1 |
|
重定向 和 请求转发
重定向和请求转发非常简单,只需要在视图名称前面添加一个前缀即可。
重定向添加
redirect:
前缀1
2
3
4
5
6
7
8
9
public String index(){
return "redirect:home";
}
public String home(){
return "home";
}请求转发添加
forward:
前缀1
2
3
4
5
6
7
8
9
public String index(){
return "forward:home";
}
public String home(){
return "home";
}
@ResponseBody 和 @RestController
Controller默认返回的类型是视图。通过在方法上添加@ResponseBody
注解,表示返回的是字符串数据,而不是视图名称,将不会经过ViewResolver解析。在类上添加@RestController
表示此Controller默认返回的是字符串数据。
而且返回类型可以直接是对象类型,SpringMVC会自动转换成字符串。
1 |
|
Bean的作用域
默认情况下,通过IoC容器进行管理的Bean都是单例模式。
在Bean默认的作用域singleton(单例)模式下,配置文件加载的时候,容器中管理的所有对象就已经完成了初始化。后续getBean的操作直接获取对象。
prototype(原型)模式下,只有在获取对象时才会被创建。
在SpringMVC中,Bean的作用域被继续细分:
@RequestScope
:对于每次HTTP请求,使用request作用域定义的Bean都将产生一个新实例,请求结束后Bean也消失。@SessionScope
:对于每一个会话,使用session作用域定义的Bean都将产生一个新实例,会话过期后Bean也消失。global session:不常用,不做讲解。
示例:
- 创建一个实体类,添加
@Component
。 - 在Spring配置类中增加
@ComponentScan
将其注册为Bean。 - 在实体类上增加
@SessionScope
注解,限制其作用域为每次会话。 - 自动注入到Controller中。
1 | package com.hunter.springmvc_demo.entity; |
1 |
|
1 |
|
RestFul风格
中文释义为“表现层状态转换”,它是一种设计风格。它的主要作用是充分并正确利用HTTP协议的特性,规范资源获取的URI路径。通俗的讲,RESTful风格的设计允许将参数通过URL拼接传到服务端,让URL看起来更简洁实用,并且我们可以充分使用多种HTTP请求方式(POST/GET/PUT/DELETE),来执行相同请求地址的不同类型操作。
因此,这种风格的连接可以直接从请求路径中读取参数,比如http://localhost:8080/mvc/index/123456
,可以直接将index的下一级路径作为请求参数进行处理。
请求路径可以手动添加类似占位符一样的信息,占位符位置的所有内容都会被作为请求参数,方法的形参列表中必须包括一个与占位符同名的并且添加了@PathVariable
注解的参数,或是由@PathVariable
注解指定为占位符名称:
1 |
|
Interceptor 拦截器
拦截器是整个SpringMVC的一个重要内容。拦截器与过滤器类似,都是用于拦截一些非法请求,但是过滤器Filter是作用于Servlet之前,而拦截器在Servlet与RequestMapping之间,相当于DispatcherServlet在将请求交给对应Controller中的方法之前进行拦截处理,它只会拦截所有Controller中定义的请求映射对应的请求(不会拦截静态资源)。
创建拦截器
创建一个拦截器我们需要实现一个HandlerInterceptor
接口:
1 | package com.hunter.springmvc_demo.interceptor; |
在Spring配置类中进行注册:
1 |
|
如果在Controller处理过程中抛出异常,就不会执行拦截器的postHandle
方法,但是会执行afterCompletion
方法,可以在该方法中获取到抛出的异常。
多级拦截器
1 | public class SubInterceptor implements HandlerInterceptor { |
在Spring配置类中进行注册,拦截器会根据注册顺序依次执行。
1 |
|
和多级Filter相同,在Controller处理之前,按照顺序从前向后进行拦截,但是Controller处理完成之后,就按照倒序执行处理后方法,DispatcherServlet完全处理完请求后也是以倒序方式执行。
异常处理
当请求映射方法中出现异常时,会直接展示在前端页面。这是因为SpringMVC为我们提供了默认的异常处理页面,请求会被直接转交给专门用于异常处理的控制器进行处理。
可以通过@ControllerAdvice
和@ExceptionHandler
注解自定义一个异常处理控制器,一旦出现指定异常,就会转接到此控制器执行:
1 | // 表明统一处理异常 |
编写src/main/resources/error.html
:
1 |
|
JSON数据格式 与 Axios请求
JSON (JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。
现在推崇的是前后端分离的开发模式,而不是所有的内容全部交给后端渲染再发送给浏览器。也就是说,整个Web页面的内容在一开始就编写完成了,而其中的数据由前端执行JS代码来向服务器动态获取,再到前端进行渲染(填充),这样可以大幅度减少后端的压力。
JSON数据格式
JSON非常容易理解,并且与前端的兼容性极好,因此现在比较主流的数据传输方式是通过JSON格式承载的。
JSON格式的数据,以学生对象为例:
1 | {"name": "杰哥", "age": 18} |
多个学生可以以数组的形式表示:
1 | [{"name": "杰哥", "age": 18}, {"name": "阿伟", "age": 18}] |
嵌套关系可以表示为:
1 | {"studentList": [{"name": "杰哥", "age": 18}, {"name": "阿伟", "age": 18}], "count": 2} |
JSON语法格式如下:
- 属性表示为键值对,键名在前,用双引号
""
包裹,使用冒号:
分隔,之后跟着对应的值。 - 花括号
{}
保存对象 - 方括号
[]
保存数组 - 数据之间由逗号分隔
JSON字符串到达前端后,可以直接转换为对象,以对象的形式进行操作和内容的读取,相当于以字符串形式表示了一个JS对象。
在浏览器的控制台操作:
1 | let obj = JSON.parse('{"studentList": [{"name": "杰哥", "age": 18}, {"name": "阿伟", "age": 18}], "count": 2}') |
也可以将JS对象转换为JSON字符串:
1 | JSON.stringify(obj) |
Jackson框架
JSON解析框架广为熟知的有Jackson
、Gson
、Fastjson
。Fastjson,为了快而有很多bug,并且经常有安全漏洞,故不予考虑学习使用。
Jackson的依赖:
1 | <!-- JSON解析 --> |
ObjectMapper
ObjectMapper
用于JSON字符串和Java对象的相互转换。
使用
writeValueAsString()
方法和ObjectNode
对象,将Java对象转为JSON字符串。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// produces指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回。在其中指定编码类型,避免中文乱码
public String index2() {
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put("name", "测试");
objectNode.put("age", 18);
try {
return objectMapper.writeValueAsString(objectNode);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}使用
writeValueAsString()
方法、ObjectNode
对象和ArrayNode
对象,将Java对象数组转为JSON字符串。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public String index3() {
ObjectMapper objectMapper = new ObjectMapper();
ArrayNode arrayNode = objectMapper.createArrayNode();
ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put("name", "测试");
objectNode.put("age", 18);
arrayNode.add(objectNode);
try {
return objectMapper.writeValueAsString(arrayNode);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}也可以直接将实体类转换为JSON字符串。
1
2
3
4
5
6
public class User {
private String username;
private int age;
}1
2
3
4
5
6
7
8
9
10
11
public String index2() {
User user = new User("测试", 18);
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(user);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
Controller的返回类型
SpringMVC非常智能,Controller中的方法,搭配@ResponseBody
,可以直接返回一个对象类型,它会被自动转换为JSON字符串。
1 |
|
Axios异步请求
常见的前端异步请求方式包括使用XMLHttpRequest
对象、Fetch API、以及使用jQuery库中的AJAX方法,以及目前最常用的Axios框架等。
前端页面src/main/resources/index.html
:
1 |
|
后端Controller中编写处理请求的方法:
1 |
|
实现文件上传和下载
利用SpringMVC,可以轻松实现文件上传和下载,需要在继承了AbstractDispatcherServletInitializer
的类上重写customizeRegistration()
方法。
1 | public class MainInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { |
1 |
|
前端页面src/main/resources/index.html
:
1 |
|
下载使用HttpServletResponse
,向输出流中传输数据即可。
需要依赖Commons IO
1 | <!-- 包含实用程序类、流实现、文件过滤器、 文件比较器、字节序变换类 --> |
1 |
|
1 | <a href="download" download="test.png">下载最新资源</a> |
DispatcherServlet源码解析
初始化
采用全注解配置形式使用SpringMVC,需要继承AbstractAnnotationConfigDispatcherServletInitializer
,指定DispathcerServlet作为默认的Servlet是父类AbstractDispatcherServletInitializer
指定的。
调度
首先请求肯定会经过HttpServlet
,交给对应的doGet
、doPost
方法进行处理,但**FrameworkServlet
重写了这些方法,并使用processRequest
方法进行处理**。
1 | package org.springframework.web.servlet; |