SpringMVC
一、概述
1、三层架构
层级 | 作用 |
---|---|
表现层(web) | 接收请求,返回响应。 |
服务层(service) | 业务逻辑的实现。 |
数据层(dao) | 数据存储相关。 |
2、MVC
MVC,即 Model 模型、View 视图,及 Controller 控制器。
MVC是软件工程中的一种软件架构模式,它是一种分离业务逻辑与显示界面的设计方法。
- Model 模型:承载数据,并对用户提交的请求进行计算(处理)的模块。分为两类:
- 数据承载 Bean:专门用于承载业务数据的,实体类,如User
- 业务处理 Bean:专门用于处理业务逻辑的,Service 或 Dao 对象
- View 视图:为用户提供使用界面,与用户直接进行交互。
- Controller 控制器:将用户请求转发给相应的 Model 处理,并根据 Model 的处理结果给用户返回响应。
目的:高内聚,低耦合。
3、MVC与三层架构
MVC 架构程序的工作流程:
- 用户通过 View 页面向服务端提出请求,可以是表单请求、超链接请求、AJAX 请求等。
- 服务端 Controller 控制器接收到请求后对请求进行解析,找到相应的 Model 对用户请求进行处理。
- Model 处理后,将处理结果再交给 Controller。
- Controller 接到处理结果后,找到要响应的 View 页面,页面经渲染(数据填充)后,再发送给客户端。
4、SpringMVC
MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。
Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。
- SpringMVC主要负责 View 和 Controller 的交互
- 请求路径的映射规则
- 请求参数的接收
- 返回参数的处理
- 视图的渲染
二、SpringMVC 原理
在Web阶段,处理用户请求,我们需要编写相关的Servlet,并且要进行配置。
在 SpringMVC 框架中,提供了一个统一的 DispatcherServlet
来 接收 和 处理 用户请求。
1、SpringMVC 的 组件
-
前端控制器:DispatcherServlet
核心组件,负责接收请求、分发,并给予客户端响应。
-
处理器映射器:HandlerMapping
根据 请求URL 匹配查找能处理的
Handler
,并会将请求涉及到的拦截器和Handler
一起封装。 -
处理器适配器:HandlerAdapter
根据
HandlerMapping
找到的Handler
,适配执行对应的Handler
; -
请求处理器:Handler
处理来自用户的请求,执行业务逻辑并返回相应的数据或视图。(就是Controller控制器)
-
视图解析器:ViewResolver
根据
Handler
返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给DispatcherServlet
响应客户端。 -
拦截器:HandlerInterceptor
在请求到达 Controller 之前和响应发送给客户端之前进行一些操作,例如记录日志、用户认证等。
2、组件初始化
项目启动,初始化DispatcherServlet
时,会通过 initStrategies
方法初始化各个组件
// org.springframework.web.servlet.DispatcherServlet
/**
* 初始化各个组件
*/
protected void initStrategies(ApplicationContext context) {
this.initMultipartResolver(context);
this.initLocaleResolver(context);
this.initThemeResolver(context);
// 初始化`处理器映射器`,定义了 `请求路径` 和 `方法` 之间的关系
this.initHandlerMappings(context);
// 初始化`处理器适配器`,制定 `请求参数` 和 `返回参数` 的解析规则。
this.initHandlerAdapters(context);
// 初始化异常处理器
this.initHandlerExceptionResolvers(context);
this.initRequestToViewNameTranslator(context);
// 初始化`视图解析器`,指定 `前缀` 和 `后缀` 和其他一些配置(文件、文件类型)
this.initViewResolvers(context);
this.initFlashMapManager(context);
}
3、核心方法 doService
DispatcherServlet
的核心方法为 doService()
,doService()
中的核心逻辑由 doDispatch()
实现
// org.springframework.web.servlet.DispatcherServlet
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
try {
this.doDispatch(request, response);
} finally {
// ...
}
}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 初始化
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
// 最终目标:构建ModelAndView
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 检测是否是文件上传内容
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 处理器映射器
// 通过 HandlerMapping 找到 HandlerExecutionChain 返回
// HandlerExecutionChain 封装了 handler 和 interceptors
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
// 处理器解析器
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
// ...
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 构建ModelAndView
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
// 解析View,返回最终内容
this.processDispatchResult(processedRequest, response, mappedHandler, mv,
(Exception)dispatchException);
} catch (Exception var22) {
// ...
}
} finally {
// ...
}
}
private void processDispatchResult(...) throws Exception {
// ...
if (mv != null && !mv.wasCleared()) {
// 渲染
this.render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
} // ...
}
protected void render(...) throws Exception {
// ...
View view; // 具体的View
if (viewName != null) {
// 根据viewName获取
view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
} else {
view = mv.getView();
}
try {
// ...
// 将Model传给view渲染
view.render(mv.getModelInternal(), request, response);
} // ...
}
// org.springframework.web.servlet.view.AbstractView
public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
// 渲染输出结果
this.renderMergedOutputModel(mergedModel, this.getRequestToExpose(request), response);
}
protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception {
model.forEach((name, value) -> {
if (value != null) {
// 将对象封装到request域中
request.setAttribute(name, value);
} else {
request.removeAttribute(name);
}
});
}
// org.springframework.web.servlet.view.InternalResourceView
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 将模型对象设置为请求属性
this.exposeModelAsRequestAttributes(model, request);
// Expose helpers as request attributes, if any.
this.exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = this.prepareForRendering(request, response);
// 获取请求转发对象
RequestDispatcher rd = this.getRequestDispatcher(request, dispatcherPath);
// ...
// 请求转发
rd.forward(request, response);
// ...
}
4、请求流程
-
客户端(浏览器)发送请求,被
前端控制器 DispatcherServlet
拦截。 -
DispatcherServlet
调用处理器映射器 HandlerMapping
。HandlerMapping
根据request
匹配HandlerExecutionChain
(封装了handler
和interceptors
) -
DispatcherServlet
根据handler
找到对应的处理器适配器 HandlerAdapter
。 -
HandlerAdapter
解析参数,调用对应的handler
(即Controller控制器)构建ModelAndView
-
ModelAndView
返回给DispatcherServlet
。 -
视图解析器 ViewResolver
解析ModelAndView
(数据对象 + 逻辑View) 后返回具体的 View 。 -
根据
Model
渲染View
-
将渲染过后的
View
返回给请求者(浏览器)
三、SpringMVC 使用
1、RESTful风格
强调 HTTP 应当以 资源 为中心,并且规范了 资源URL 的风格。
- 路径规范:路径一般都是资源名称,尽量不出现动词
- 请求规范:请求方式必须遵循规定,要使用GET、POST、PUT、DELETE、PATCH等,具有不同含义。
Restful风格 | 请求方式 | 描述 | SpringMVC注解支持 |
---|---|---|---|
/customer | POST | 新增用户 | @PostMapping |
/customer | GET | 获取用户列表 | @GetMapping |
/customer/1 | GET | 获取id=1的用户信息 | @GetMapping |
/customer/1 | DELETE | 删除id=1的用户信息 | @DeleteMapping |
/customer/1 | PUT | 修改id=1的用户信息(整体) | @PutMapping |
/customer/1 | PATCH | 修改id=1的用户信息(部分) | @PatchMapping |
总结:面向资源的,带有动作表达的一种规则和标准(底层会将请求方式拼接到路径中)。
2、Controller 控制器
注解 | 说明 |
---|---|
@Controller | 添加在类上,标记一个类是控制器,用于 处理请求 和 返回响应。 |
@ResponseBody | 添加在方法上,将方法返回值写入 HTTP 响应体中。(一般用于返回 JSON 数据) |
@RestController | 添加在类上,相当于 @Controller + @ResponseBody |
- 前后端不分离,ModelAndView 应该是最最常见的返回值类型。
- 前后端分离后,基本都是以 JSON 数据为主了。
因此,前后端分离之后,一般都是使用 @RestController
注解加在 Controller 类上
3、请求映射
@RequestMapping
用于映射 请求路径 到 Controller 方法上,可以配置 HTTP 请求类型、请求参数、请求头等。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String name() default "";
@AliasFor("path")
String[] value() default {};
// 请求路径
@AliasFor("value")
String[] path() default {};
// 指定请求的 HTTP 方法(GET、POST、PUT、DELETE等)
RequestMethod[] method() default {};
// 指定请求必须包含的参数
String[] params() default {};
// 指定请求中必须包含某些指定的header值,才能够让该方法处理请求
String[] headers() default {};
// 指定处理请求的提交内容类型(为指定类型才能够让该方法处理请求)
// 示例:consumes = "application/json"
String[] consumes() default {};
// 指定返回的内容类型,必须是request请求头(Accept)中所包含的类型
// 示例:produces = "application/json,charset=utf-8"
String[] produces() default {};
}
以下注解是 @RequestMapping
的扩展,可以看做指定了 method 属性的 @RequestMapping
注解 | 等价于 | 适用场景 |
---|---|---|
@GetMapping | @RequestMapping(method = RequestMethod.GET) | 获取资源 |
@PostMapping | @RequestMapping(method = RequestMethod.POST) | 新增内容 |
@DeleteMapping | @RequestMapping(method = RequestMethod.DELETE) | 删除内容 |
@PutMapping | @RequestMapping(method = RequestMethod.PUT) | 修改整体内容 |
@PatchMapping | @RequestMapping(method = RequestMethod.PATCH) | 修改部分内容 |
4、参数绑定
@RequestBody
:将 请求体 中的 JSON数据 转化为 方法参数。(Json 的 Key 与 Java对象 的 Field 对应)
@PostMapping("/hello")
public String hello(@RequestBody User user) {
return "Hello, " + user.getName();
}
@PathVariable
:用于从 URL 路径中提取参数。(占位符 {xxx}
)
@GetMapping("/hello/{name}")
public String hello(@PathVariable("name") String name) {
return "Hello, " + name;
}
@RequestParam
:用于从请求中获取参数(可以是 URL参数、表单数据、文件等)。
@GetMapping("/hello")
public String hello(@RequestParam("name") String name) {
return "Hello, " + name;
}
/**
* 文件上传
*/
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file) {
//...
return "File uploaded successfully";
}
四、HTTP请求示例
注解 | 请求类型 | 使用场景 |
---|---|---|
@RequestBody | POST | 接收 Json对象,对象的属性名要与json 的key 保持一致 |
@PathVariable | GET 或 POST | 接收 URL中的参数值,可以接收多个 |
@RequestParam | GET 或 POST | 接收 文件/表单数据 |
1、GET - URL键值对参数
请求格式: GET /user?name=jack&age=18 HTTP/1.1
/**
* 用字段接收
*/
@GetMapping("/user")
public String getUserInfo(@RequestParam("name") String name, @RequestParam("age") Integer age) {
return "姓名:" + name + ",年龄:" + age;
}
/**
* 用对象接收
*/
@GetMapping("/user")
public String getUserInfo(User user) {
return "姓名:" + user.getName() + ",年龄:" + user.getAge();
}
注意:用 对象 接收 前端数据 时,@RequestParam
注解省略不写(写了会报错)
- 编译工具会根据对象的
set,get
方法自动生成@RequestParam
。 - 有几个属性就会生成几个
@RequestParam
,其中required
属性默认为false
2、GET - URL拼接参数
请求格式: GET /user/jack/18 HTTP/1.1
@GetMapping("/user/{name}/{age}")
public String getUserInfo(@PathVariable("name") String name, @PathVariable("age") Integer age) {
return "姓名:" + name + ",年龄:" + age;
}
3、POST - URL拼接参数
POST请求也支持URL拼接参数
请求格式: POST /user/jack/18 HTTP/1.1
@PostMapping("/user/{name}/{age}")
public String getUserInfo(@PathVariable("name") String name, @PathVariable("age") Integer age) {
return "姓名:" + name + ",年龄:" + age;
}
4、POST - raw
处理 JSON
数据
{
"name": "John",
"age": 30
}
@PostMapping("/processJson")
public ResponseEntity<String> processJson(@RequestBody MyModel myModel) {
// myModel 对象自动填充来自请求体的 JSON 数据
return ResponseEntity.ok("Processed " + myModel.getName());
}
处理 纯文本
数据
123
@PostMapping("/processText")
public ResponseEntity<String> processText(@RequestBody String text) {
return ResponseEntity.ok("Received: " + text);
}
5、POST - x-www-form-urlencoded
处理 表单
数据
name=John&age=30
@PostMapping("/processForm")
public ResponseEntity<String> processForm(@RequestParam String name, @RequestParam int age) {
return ResponseEntity.ok("Name: " + name + ", Age: " + age);
}
@PostMapping("/processForm")
public ResponseEntity<String> processForm(User user) {
return ResponseEntity.ok("Name: " + user.getName() + ", Age: " + user.getAge());
}
6、POST - multipart/form-data
这个请求体是由多部分组成的,每个部分包含不同的内容(比如 文本字段
和 上传文件
)。
----------------------------------------------------7de1a433602ac
Content-Disposition: form-data; name="remark"
123
----------------------------------------------------7de1a433602ac
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg
(binary data of the file)
----------------------------------------------------7de1a433602ac--
@PostMapping("/uploadFile")
public ResponseEntity<String> uploadFile(
@RequestParam("remark") String remark,
@RequestParam("file") MultipartFile file) {
// ...
return ResponseEntity.ok("File uploaded successfully: " + file.getOriginalFilename());
}
Spring 默认通过 CommonsMultipartResolver
或 StandardServletMultipartResolver
处理文件上传。
可以通过配置 Spring 的 multipart
解析器来启用文件上传功能。
spring:
servlet:
multipart:
# 启用文件上传支持(默认为 true)
enabled: true
# 单个文件的最大大小限制(默认为 1MB)
max-file-size: 2MB
# 请求的最大大小(包括所有文件和表单数据,默认为 1MB)
max-request-size: 2MB
7、POST - binary
处理 原始的二进制数据
Content-Type: application/octet-stream
(binary data)
@PostMapping("/uploadBinary")
public ResponseEntity<String> uploadBinary(@RequestBody byte[] data) {
// 处理二进制数据(例如存储文件等)
return ResponseEntity.ok("Binary data received: " + data.length + " bytes");
}
Spring 不需要额外的配置来处理二进制数据,可以通过 @RequestBody
来确保接收原始的二进制数据。
五、异常统一处理
1、原理
// org.springframework.web.servlet.DispatcherServlet
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
try {
// ...
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
// doDispatch中的异常,都会被 catch 到 processDispatchResult 进行统一处理
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
}
private void processDispatchResult(..., @Nullable Exception exception) throws Exception {
boolean errorView = false;
// 优先对异常进行统一处理
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
this.logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException)exception).getModelAndView();
} else {
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
// 非 ModelAndViewDefiningException 都会进到这里处理
mv = this.processHandlerException(request, response, handler, exception);
errorView = mv != null;
}
}
// ...
}
@Nullable
protected ModelAndView processHandlerException(..., Exception ex) throws Exception {
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
Iterator var6 = this.handlerExceptionResolvers.iterator();
while(var6.hasNext()) {
// 最终,异常都由 HandlerExceptionResolver 处理
HandlerExceptionResolver resolver = (HandlerExceptionResolver)var6.next();
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
// ...
}
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(HttpServletRequest var1, HttpServletResponse var2,
@Nullable Object var3, Exception var4);
}
2、异常统一处理
1)继承方式处理(了解)
既然异常最终都会由 HandlerExceptionResolver 处理,只要自定义异常解析类继承它就可以了
@Component
public class ProjectExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) {
// 自定义的异常
ProjectException projectException = null;
// 如果是自定义的异常,强制类型转换
if (ex instanceof ProjectException){
projectException = (ProjectException) ex;
} else {
// 如果不是,给出提示
projectException = new ProjectException("未知异常", "-1");
}
// 如果发生异常,跳转到error页面并显示页面
ModelAndView modelAndView = new ModelAndView("error");
modelAndView.addObject("message", projectException.getMessage());
modelAndView.addObject("code", projectException.getCode());
return modelAndView;
}
}
2)注解方式处理(推荐)
使用 @ControllerAdvice
+ @ExceptionHandler
注解,为所有的 Controller 添加异常处理。
// @ControllerAdvice 为所有 Controller 添加增强效果处理
@ControllerAdvice
public class BaseController {
// 异常处理方法上添加注解 @ExceptionHandler,可以指定处理的异常类型
@ExceptionHandler(Exception.class)
@ResponseBody
public BaseResponse exceptionHandler(Exception e) {
errorLogger(e);
return new BaseResponse(500, "系统出小差了");
}
@ExceptionHandler(RedisException.class)
@ResponseBody
public BaseResponse exceptionHandler(RedisException e) {
errorLogger(e);
return new BaseResponse(ResponseCodeEnum.INTERNAL_SERVER_ERROR, e.getMessage());
}
// ...
}
@ControllerAdvice
注解为所有 Controller 添加增强效果处理(相当于给所有Controller加了try)
- 当
Controller
中的方法抛出异常的时候,由被@ExceptionHandler
注解修饰的方法进行处理。
六、SpringMVC 拦截器
SpringMVC拦截器用于对请求信息进行拦截处理的机制,是切面思想的一个实现。
1、执行流程
【执行时机】
- Controller方法执行之前(权限的认证)
- Controller方法执行之后(敏感信息的处理)
- 视图方法执行完成之后(资源的回收)
2、拦截器实现
1)自定义拦截器
HandlerInterceptor
接口下有三个方法,用于不同时机的拦截处理。
public interface HandlerInterceptor {
// 预处理回调方法,实现处理器的预处理
default boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler)throws Exception {
return true;
}
// 后处理回调方法,实现处理器的后处理(在渲染视图之前)
default void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
// 整个请求处理完毕回调方法,即在视图渲染完毕时回调
default void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable Exception ex) throws Exception {
}
}
自定义拦截器只要实现HandlerInterceptor
接口,重写方法即可。(default方法,按需手动重写)
@Slf4j
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
try {
String staffId = request.getHeader("STAFF_ID");
String tenantId = request.getHeader("TENANT_ID");
UserInfo userInfo = new UserInfo(staffId, tenantId);
UserHelper.setUserInfo(userInfo);
} catch (Exception e) {
log.error("get userInfo error", e);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
UserHelper.clear();
}
}
public class UserHelper {
private static TransmittableThreadLocal<UserInfo> USER_INFO = new TransmittableThreadLocal<>();
public static UserInfo getUserInfo() {
return USER_INFO.get();
}
public static void setUserInfo(UserInfo userinfo) {
USER_INFO.set(userinfo);
}
public static void clear() {
USER_INFO.remove();
}
}
2)注册自定义拦截器
继承WebMvcConfigurer
接口,重写addInterceptors
方法,注册拦截器
@Configuration
public class WebConfig extends WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加拦截器实现类
registry.addInterceptor(new UserInfoInterceptor())
// 配置拦截路径
.addPathPatterns("/**")
// 配置放过路径
.excludePathPatterns();
}
}