Spring MVC
- MVC设计思想:
- 核心处理流程:DispatcherServlet
- 参数校验:Bean Validation 2.0
- 统一全局异常处理:@ControllerAdvice + @ExceptionHandler
- 自定义异常实现原理:https://mp.weixin.qq.com/s/tyYr1Kt0jppcBn5fkJgBqA
1.核心架构
经典MVC设计架构
- M model 模型
- V view 视图
- C controller 控制器
spring mvc 的 核心分为两部分
- 前端控制器: DispatcherServlet 处理http请求
- 后端控制器: controller(一般情况下),处理业务
spring mvc 是 spring framework的一个核心模块,并不是单独的项目。延续了 J2EE 标准,通过一个Servlet暴露接口, 完成对所有请求的收敛,主要的功能如下:
- 处理http请求
- 拦截器
- 视图解析器
- 国际化
- SpEL
2.请求处理过程
2.1.DispatcherServlet
DispatcherServlet 也称为 前端控制器,在Spring IOC容器初始化过程中,就会实例化一个DispatcherServlet实例到容器中。
作用:作为统一访问点,进行全局的请求控制。DispatcherServlet 收到请求后自己不进行处理,而是委托给其他的解析器进行处理;
2.2.请求流程
SpringMVC请求流程详解: https://www.cnblogs.com/leskang/p/6101368.html
https://blog.csdn.net/u010859650/article/details/81167223

- 首先用户发送请求->DispatcherServlet
- HandlerMapping->HandlerExecutionChain: 根据请求的URI 从 HandlerMapping中 获得 HandlerExecutionChain对象(包含一个Handler处理器 以及 handler对应的Interceptor拦截器),
- HandlerExecutionChain->HandlerAdapter: 根据Handler的类型,适配器会适配成不同的HandlerAdapter(即适配器设计模式的应用,从而很容易支持很多类型的处理器)
- 调用拦截器的preHandler方法
- 调用Handler的请求处理方法: HandlerAdapter调用对应handler的方法,过程中会提取Request中的模型数据,填充Handler入参,涉及以下功能:
- HttpMessageConveter:将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。
- 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等。
- 数据根式化:对请求消息进行数据格式化。如将字符串转换成格式化数字或格式化日期等。
- 数据验证:验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中。
- Handler处理完请求,返回ModelAndView对象(包含模型数据、逻辑视图名),如果Handler的响应类型是ResponseBody则直接响应请求。
- 通过ViewResolver将ModelAndView中的逻辑视图名解析为具体的View,通过这种策略模式,很容易更换其他视图技术;
- 渲染View会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构,因此很容易支持其他视图技术;
- 返回控制权给DispatcherServlet,由DispatcherServlet返回响应给用户,到此一个流程结束。
调用过程中,比较重要的是HandlerAdapter,根据controller的不同的实现方式,会适配到不同的handler处理方式。 因为不同的实现方式,HandlerExecutionChain中保存实际需要调用的对象,也就是handler字段,会被封装成不用的对象。 注解方式都是HandlerMethod。

问题:Spring mvc如何统计URL调用耗时?
- 使用动态代理,自定义aop。最合适
- 拦截器,使用request属性或者threadLocal都可以
- 过滤器,很简单的实现,但是调用时间为整个spring MVC的处理时间
- request的监听器很简单的实现,但是调用时间为整个spring MVC和tomcat的处理时间
- 重新实现DispatcherServlet的方法,想监控哪里就监控哪里
2.3.【源码】核心处理逻辑
DispatcherServlet中通过adapter调用handle执行controller方法,核心逻辑入口在ServletInvocableHandlerMethod中
// 以下是伪代码
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 1、调用controller,获得返回值。
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// 2、处理返回值。比如统一数据格式、数据加密、数据格式转换(pojo转json)
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
// 1.1.InvocableHandlerMethod#invokeForRequest
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 预处理请求参数。调用HandlerMethodArgumentResolverComposite#resolveArgument
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
// 通过反射调用controller【没有什么看的】
return doInvoke(args);
}
// 1.1.1.HandlerMethodArgumentResolverComposite#resolveArgument
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// getArgumentResolver 获得参数处理器
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
// 调用参数处理器的resolver方法,获得参数。
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
以下是常用的参数处理器:HandlerMethodArgumentResolver 解析 request 获取请求参数。
一、模型参数处理器,也就是controller方法中某个参数是对象类型。ModelAttributeMethodProcessor
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
// a)通过反射拿到controller方法中的参数和注解等信息,并封装为参数对象。
Object attribute = createAttribute(name, parameter, binderFactory, webRequest);
// b)创建一个数据绑定器,内部会根据方法中参数的类型创建不同类型的绑定器。就是类型转化器,帮助把参数转为指定的类型。
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
// c)绑定参数,从request中获得数据,通过WebDataBinder类型转变,并使用反射将数据写入到attribute中
bindRequestParameters(binder, webRequest);
// d)参数校验,就是使用@Vail相关校验注解
validateIfApplicable(binder, parameter);
return attribute;
}
二、json参数处理器。也就是controller方法中的参数整体是个json。并使用了@RequestBody注解。
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
// a)使用消息转换器从Web请求中读取参数值。
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
// b)创建一个数据绑定器,内部会根据方法中参数的类型创建不同类型的绑定器。就是类型转化器,帮助把参数转为指定的类型。
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
// c)绑定参数,从request中获得数据,通过WebDataBinder类型转变,并使用反射将数据写入到attribute中
validateIfApplicable(binder, parameter);
return arg;
}
3.常用操作
3.1.接口实现方式
1.实现Controller接口
/**
* 实现 Controller 接口,springMVC的HandlerAdapter是SimpleControllerHandlerAdapter
*/
@Component("/a")
public class AController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("aaaa");
return new ModelAndView("index");
}
}
2.实现HttpRequestHandler接口
/**
* 实现 HttpRequestHandler 接口,springMVC的HandlerAdapter是SimpleControllerHandlerAdapter
*/
@Component("/b")
public class BController implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("bbbb");
response.getWriter().write("bbbb");
}
}
3.继承Servlet
/**
* 继承HttpServlet,springMVC的HandlerAdapter是 SimpleServletHandlerAdapter。
* 但是,需要自己注册 SimpleServletHandlerAdapter,否则会报错
*/
@Component("/c")// 需要自己注册
public class CController extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("cccc");
resp.getWriter().write("cccc");
}
@Bean
public SimpleServletHandlerAdapter simpleServletHandlerAdapter() {
return new SimpleServletHandlerAdapter();
}
}
4.使用@RequestMapping注解
解析为 HandlerMethod,适配器 RequestMappingHandlerAdapter
@Controller
public class DController {
@RequestMapping("/d")
@ResponseBody
public String d() {
return "d";
}
}
3.2.Valid参数校验
校验放在哪?Controller层 VS Service层 去网上查阅了一些资料,一般推荐与业务无关的放在Controller层中进行校验,而与业务有关的放在Service层中进行校验。 早在 Java 2009 年就提出了 Bean Validation 规范,并且已经历经 JSR303、JSR349、JSR380 三次标准的置顶,发展到了 2.0 。
Bean Validation 规范的数据校验框架,主要有:Hibernate Validator 和 Apache BVal 而我们在使用spring的时候,也可以使用spring提供的注解。
3.2.1.使用案例
Spring的校验通常使用Hibernate Validate。
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
注意:它支持el表达式解析,需要配合Tomcat等web容器使用。如果想要单独使用,需要添加这个依赖
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>3.0.3</version>
</dependency>
常用案例:
@Data
public class DataSetSaveVo {
//唯一标识符为空
@NotBlank(message = "user uuid is empty")
//用户名称只能是字母和数字
@Pattern(regexp = "^[a-z0-9]+$", message = "user names can only be alphabetic and numeric")
@Length(max = 48, message = "user uuid length over 48 byte")
private String userUuid;
//数据集名称只能是字母和数字
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "data set names can only be letters and Numbers")
//文件名称过长
@Length(max = 48, message = "file name too long")
//文件名称为空
@NotBlank(message = "file name is empty")
private String name;
//数据集描述最多为256字节
@Length(max = 256, message = "data set description length over 256 byte")
//数据集描述为空
@NotBlank(message = "data set description is null")
private String description;
}
Controller写法
@PostMapping
public ResponseVO createDataSet(@Valid @RequestBody DataSetSaveVO dataSetVO) {
return ResponseUtil.success(dataSetService.saveDataSet(dataSetVO));
}
3.4.2.自定义校验器
有两种方式:1. 使用java官方标准【推荐】;2. 使用spring提供的扩展
第一种:使用java官方标准的自定义工具。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Objects;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
/**
* 自定义校验规则,增强枚举值的校验【非spring功能,是javax.validation提供的能力】
* @author zhangxue
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValidator.EnumValidatorRule.class)
public @interface EnumValidator {
String message() default " 枚举值不存在";
Class<? extends Enum<?>> enumClass();
// 自定义校验规则
public static class EnumValidatorRule implements ConstraintValidator<EnumValidator, Object> {
private EnumValidator enumValidator;
@Override
public void initialize(EnumValidator EnumValidator) {
this.enumValidator = EnumValidator;
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
// 校验逻辑
}
}
}
第二种:使用spring提供的扩展。必须通过WebDataBinder添加SpringValidateUserValidator才能生效, javax.validation要麻烦很多,好处是不需要灵活不需要注解。
@Data
public class SpringValidateUser {
private String userName;
private Integer age;
}
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
public class SpringValidateUserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return SpringValidateUser.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
// 校验name是否为空
ValidationUtils.rejectIfEmpty(errors, "userName", "userName不能为空");
SpringValidateUser p = (SpringValidateUser) target;
if (p.getAge() < 1) {
errors.rejectValue("age", "年龄不能小于1");
} else if (p.getAge() > 110) {
errors.rejectValue("age", "年龄不能大于110");
}
}
}
@RestController
@RequestMapping("valid")
public class ValidController {
// 使用spring的自定义校验器
@InitBinder
private void initBinder(WebDataBinder binder) {
binder.addValidators(new SpringValidateUserValidator());
}
// http://127.0.0.1:8080/valid/my?userName=1&age=999
@RequestMapping("/my")
public SpringValidateUser my(@Valid SpringValidateUser user, BindingResult result) {
// 参数校验
if (result.hasErrors()) {
List<FieldError> fieldErrors = result.getFieldErrors();
fieldErrors.forEach(e -> {
System.out.println(e.getField() + e.getCode());
});
throw new IllegalArgumentException("参数输入错误");
}
return user;
}
}
3.3.统一异常处理
- 1.全局级别的异常处理器,实现HandlerExceptionResolver接口
- 2.在Controller层面使用注解@ExceptionHandler(不推荐,需要在每个controller类中增加异常处理类,非常冗余)
- 3.全局级别的异常处理器,使用注解@ControllerAdvice + @ExceptionHandler (可用定义多个异常处理类,但是处理的异常类型不能重复)
3.4.拦截器
我们都知道aop的代表是拦截器。但事实上,spring mvc 的 拦截器 HandlerInterceptor,本质上其实不是正真aop。
DispatcherServlet 中的代码如下
// 1. 执行拦截器的 前置方法preHandle
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 执行controller方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 2. 将响应结果提交给视图解析器
applyDefaultViewName(processedRequest, mv);
// 3. 执行拦截器的 后执法postHandle
mappedHandler.applyPostHandle(processedRequest, response, mv);
// 4. 处理响应结果(转换为json、渲染视图模板、异常处理)
// 包括 执行拦截器的 完成执行方法afterCompletion
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
而拦截器的执行方式为遍历执行所有拦截器的某个方法
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = 0; i < interceptors.length; i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
}
return true;
}
拦截器的使用方式
@Slf4j
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
log.info(">>interceptor 进入controller之前的处理<<, url={}", request.getRequestURI());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
log.info(">>interceptor postHandle请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)<<, url={}", request.getRequestURI());
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
response.setHeader("setheader333", "setheader333");
log.info(">>interceptor 页面渲染之后<<, url={}", request.getRequestURI());
}
}
注意事项:
- postHandle:这是页面的渲染之前,如果controller方法中使用@ResponseBody,则在这里就无法处理response。 因为response已经调用了flush,接口的数据已经返回到过去了。这里的response状态是commited。
- afterCompletion。只能关闭当前服务的资源,读取参数的数据,无法进行修改。
为什么页面不会出问题,是因为返回页面也是在服务器端处理的,在flush之后,outputStream还没有断开,可以在渲染页面的过程中继续操作。
解决方案:使用RestControllerAdvice
/**
* 只作用于@ResponseBody修饰过的方法和类
*/
@RestControllerAdvice
public class WebAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class converterType) {
// 只单独处理 ResponseBody的方法
//boolean flag = methodParameter.hasMethodAnnotation(ResponseBody.class);
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
response.getHeaders().set("setheader111", "setheader111");
return body;
}
}
原因分析
// DispatcherServlet 调用controller方法的过程中
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 将controller方法返回的数据进行转换
AbstractMessageConverterMethodProcessor#writeWithMessageConverters(ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
// 方法中调用write方法
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
@Override
public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}));
}
else {
// 我们正常的请求到这里。writeInternal 方法和转换器有关。默认是StringHttpMessageConverter
writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
}
// StringHttpMessageConverter 中的实现
@Override
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
HttpHeaders headers = outputMessage.getHeaders();
if (this.writeAcceptCharset && headers.get(HttpHeaders.ACCEPT_CHARSET) == null) {
headers.setAcceptCharset(getAcceptedCharsets());
}
Charset charset = getContentTypeCharset(headers.getContentType());
// 将结果复制到 OutputStream 中,到这里,我们的接口的数据已经返回过去。后面的操作都不会影响
StreamUtils.copy(str, charset, outputMessage.getBody());
}
// 上面提到的 代码 outputMessage.getBody().flush();
// 也会将httpServletResponse的状态修改为完成,防止后面有人继续操作OutputStream
3.5.RequestBodyAdvice 和 ResponseBodyAdvice
- RequestBodyAdvice:是请求体的拦截器,可以对请求体进行修改或者添加自定义的处理逻辑。
- supports:用于判断该拦截器是否支持当前请求的方法
- beforeBodyRead:在体内容被读取之前调用,可以对请求体进行修改。
- afterBodyRead:在请求体内容被读取之后调用,可以对请求体进行修改。
- 作用:请求解码、数据解码、日志记录等
- 注意:这个只作用于请求参数中使用了@RequestBody注解的
- ResponseBodyAdvice:是响应体的拦截器,可以对响应体进行修改或者添加自定义的处理逻辑。
- supports:用于判断该拦截器是否支持当前响应的方法
- beforeBodyWrite:在响应体内容被写入之前用,可以对响应体进行修改。
- 作用:统一封装结果类型、数据加密等
ResponseBodyAdvice作用位置:DispatcherServlet调用handle->ServletInvocableHandlerMethod调用controller方法后返回数据, 如果使用了@ResponseBody,则会将数据交给RequestResponseBodyMethodProcessor,在数据转为json之前进行处理。

而RequestBodyAdvice路径相似,是在调用controller之前进行处理。RequestResponseBodyMethodProcessor#resolveArgument
3.6.WebDataBinder
WebDataBinder作用:自定义类型转换器 以及 数据校验器。
- 帮助我们使用 request.getParameter("xx")获得一个String类型的数据,将数据封装到Java对象中,也就是数据类型转换
- 数据校验。包括spring 和 jsr标准的校验器。
使用方式:
- controller 中生效:controller类中增加@InitBinder相关代码
- 全局生效:@ControllerAdvice 的配置类中增加@InitBinder相关代码
- 本文作者:zhangxue
- 本文链接:https://lucksnow1989.github.io/article/9.开源/spring/6.Spring MVC.html
- 版权声明:本文为本人原创文章,遵循 CC 4.0 BY-NC-SA版权协议,转载请附上原文出处链接和本声明。