1.概述
最近在梳理Spring MVC
相关扩展点时发现了@ControllerAdvice这个注解,用于定义全局的异常处理、数据绑定、数据预处理等功能。通过使用 @ControllerAdvice
,可以将一些与控制器相关的通用逻辑提取到单独的类中进行集中管理,从而减少代码重复,提升代码的可维护性。
定义如下
/*** Specialization of {@link Component @Component} for classes that declare* {@link ExceptionHandler @ExceptionHandler}, {@link InitBinder @InitBinder}, or* {@link ModelAttribute @ModelAttribute} methods to be shared across* multiple {@code @Controller} classes.* ........*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {@AliasFor("basePackages")String[] value() default {};@AliasFor("value")String[] basePackages() default {};Class<?>[] basePackageClasses() default {};Class<?>[] assignableTypes() default {};Class<? extends Annotation>[] annotations() default {};}
从定义来看,@ControllerAdvice
是@Component
的一个派生注解,这就意味着使用该注解的类会被Spring扫描到放入bean容器中。从上面注释也可以得知@ControllerAdvice
一般与这三个注解@ExceptionHandler
,@InitBinder
和@ModelAttribute
配合使用,从而作用于所有的@Controller
类的接口上。@ExceptionHandler
想来我们并不陌生,是用来全局异常统一处理的,但另外两个注解@InitBinder
和@ModelAttribute
在日常中个人感觉并不常用,我们稍后会浅浅分析下它们是做什么用的。
2.@ExceptionHandler
这个注解我们并不陌生,进行统一异常处理使用的,程序由于运行时异常导致报错的结果,有些异常我们可能无法提前预知,接口不能正常返回结果,因此我们需要定义一个统一的全局异常来捕获这些信息,并作为一种结果返回给控制层。
先来看看没有进行全局异常处理的报错,搞一个Java常出现的示例如下:
@GetMapping("/111")public void test111() {User user = null;String userNo = user.getUserNo();System.out.println(userNo);}
调接口报错如下:
{"timestamp": "2024-06-13T06:25:01.508+00:00","status": 500,"error": "Internal Server Error","path": "/test/111"
}
这对于前端来说是不太友好的。下面来看看全局统一异常处理
package com.shepherd.basedemo.advice;import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;import java.util.HashMap;
import java.util.Map;/*** @author fjzheng* @version 1.0* @date 2024/6/13 14:41*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {/*** 全局异常处理* @param e* @return*/@ResponseBody@ResponseStatus(HttpStatus.OK)@ExceptionHandler(Exception.class)public ResponseVO exceptionHandler(Exception e){// 处理业务异常if (e instanceof BizException) {BizException bizException = (BizException) e;if (bizException.getCode() == null) {bizException.setCode(ResponseStatusEnum.BAD_REQUEST.getCode());}return ResponseVO.failure(bizException.getCode(), bizException.getMessage());} else if (e instanceof MethodArgumentNotValidException) {// 参数检验异常MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) e;Map<String, String> map = new HashMap<>();BindingResult result = methodArgumentNotValidException.getBindingResult();result.getFieldErrors().forEach((item)->{String message = item.getDefaultMessage();String field = item.getField();map.put(field, message);});log.error("数据校验出现错误:", e);return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST, map);} else if (e instanceof HttpRequestMethodNotSupportedException) {log.error("请求方法错误:", e);return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求方法不正确");} else if (e instanceof MissingServletRequestParameterException) {log.error("请求参数缺失:", e);MissingServletRequestParameterException ex = (MissingServletRequestParameterException) e;return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数缺少: " + ex.getParameterName());} else if (e instanceof MethodArgumentTypeMismatchException) {log.error("请求参数类型错误:", e);MethodArgumentTypeMismatchException ex = (MethodArgumentTypeMismatchException) e;return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数类型不正确:" + ex.getName());} else if (e instanceof NoHandlerFoundException) {NoHandlerFoundException ex = (NoHandlerFoundException) e;log.error("请求地址不存在:", e);return ResponseVO.failure(ResponseStatusEnum.NOT_EXIST, ex.getRequestURL());} else {//如果是系统的异常,比如空指针这些异常log.error("【系统异常】", e);return ResponseVO.failure(ResponseStatusEnum.SYSTEM_ERROR.getCode(), ResponseStatusEnum.SYSTEM_ERROR.getMsg());}}}
再次调用接口结果如下:
这时候正常返回统一的格式,方便前端处理。关于接口返回结果格式全局统一和异常统一处理详解请看之前总结的:Spring Boot如何优雅实现结果统一封装和异常统一处理
3.@InitBinder
该注解作用于方法上,用于将前端请求的特定类型的参数在到达controller之前进行处理,从而达到转换请求参数格式的目的。
先来看看我们接口示例:
@GetMapping("/222")public void test222(User user) {System.out.println(user);}
@Data
public class User {private Long id;private Date birthday;
}
postman调接口:
报错了:
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'user' on field 'birthday': rejected value [2024-06-30 12:00:00]; codes [typeMismatch.user.birthday,typeMismatch.birthday,typeMismatch.java.util.Date,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.birthday,birthday]; arguments []; default message [birthday]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'birthday'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@com.fasterxml.jackson.annotation.JsonFormat @com.alibaba.excel.annotation.ExcelProperty java.util.Date] for value [2024-06-30 12:00:00]; nested exception is java.lang.IllegalArgumentException]
这时候使用@InitBinder
就能解决了
@ControllerAdvice
public class GlobalAdviceHandler {@InitBinderpublic void initBinder(WebDataBinder binder) {// 自定义数据绑定逻辑binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), false));}
}
重新调接口控制台就正常输出了。但请注意@InitBinder仅作用于get接口,对于post接口的@RequestBody接收参数并不起效
@PostMapping("/111")public void test111(@RequestBody User user) {System.out.println(user);}
针对于json传参我们可以在接参的实体日期字段上添加@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”, timezone = “GMT+8”)
@Data
public class User {private Long id;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private Date birthday;
}
或者在配置文件配置如下:
spring:jackson:date-format: yyyy-MM-dd HH:mm:sslocale: zh_CNtime-zone: GMT+8default-property-inclusion: non_null
4.@ModelAttribute
该注解作用于方法和请求参数上,在方法上时设置一个值,可以直接在进入controller后传入该参数。全局绑定登录上下文参数:
@ControllerAdvice
public class GlobalAdviceHandler {@ModelAttribute("loginUser")public LoginUser setLoginUser() {return RequestUserHolder.getCurrentUser();}
}
接口方法就能使用@ModelAttribute
绑定获取参数了
// 使用@PostMapping("/student")public ResponseVO<Long> addStudent(@ModelAttribute("loginUser") LoginUser loginUser, @RequestBody Student student){return ResponseVO.success(studentService.addStudent(loginUser, student));}
其实完全没必要这么做,需要登录上下文信息时候直接使用RequestUserHolder.getCurrentUser()
获取即可,看你怎么选择啦,是喜欢通过方法参数传递登录信息上下文,还是用的地方再获取。
5.@ControllerAdvice实现原理
我们都知道Spring MVC的核心处理器是DispatcherServlet
,项目启动时会调用 DispatcherServlet
的 #initStrategies(ApplicationContext context)
方法,初始化 Spring MVC 的各种组件
protected void initStrategies(ApplicationContext context) {// 初始化 MultipartResolverinitMultipartResolver(context);// 初始化 LocaleResolverinitLocaleResolver(context);// 初始化 ThemeResolverinitThemeResolver(context);// 初始化 HandlerMappingsinitHandlerMappings(context);// 初始化 HandlerAdaptersinitHandlerAdapters(context);// 初始化 HandlerExceptionResolvers initHandlerExceptionResolvers(context);// 初始化 RequestToViewNameTranslatorinitRequestToViewNameTranslator(context);// 初始化 ViewResolversinitViewResolvers(context);// 初始化 FlashMapManagerinitFlashMapManager(context);
}
一次请求会通过DispatcherServlet
的#doDispatch(HttpServletRequest request, HttpServletResponse response)
方法,执行请求的分发
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.// 获得请求对应的 HandlerExecutionChain 对象mappedHandler = getHandler(processedRequest);if (mappedHandler == null) {noHandlerFound(processedRequest, response);return;}// Determine handler adapter for the current request.// 获得当前 handler 对应的 HandlerAdapter 对象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;}}if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// Actually invoke the handler.mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {dispatchException = ex;}catch (Throwable err) {// As of 4.3, we're processing Errors thrown from handler methods as well,// making them available for @ExceptionHandler methods and other scenarios.dispatchException = new NestedServletException("Handler dispatch failed", err);}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 afterCompletionif (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}}else {// Clean up any resources used by a multipart request.if (multipartRequestParsed) {cleanupMultipart(processedRequest);}}}}
Spring MVC是通过处理器适配器来进行具体方法的调用执行的,这时候来到适配器RequestMappingHandlerAdapter
@Override
public void afterPropertiesSet() {// Do this first, it may add ResponseBody advice beans// 初始化 ControllerAdvice 相关initControllerAdviceCache();// 初始化 argumentResolvers 属性if (this.argumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}// 初始化 initBinderArgumentResolvers 属性if (this.initBinderArgumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}// 初始化 returnValueHandlers 属性if (this.returnValueHandlers == null) {List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);}
}
private void initControllerAdviceCache() {if (getApplicationContext() == null) {return;}// <1> 扫描 @ControllerAdvice 注解的 Bean 们,并将进行排序List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());AnnotationAwareOrderComparator.sort(adviceBeans);List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();// 遍历 ControllerAdviceBean 数组for (ControllerAdviceBean adviceBean : adviceBeans) {Class<?> beanType = adviceBean.getBeanType();if (beanType == null) {throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);}// 扫描有 @ModelAttribute ,无 @RequestMapping 注解的方法,添加到 modelAttributeAdviceCache 中Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);if (!attrMethods.isEmpty()) {this.modelAttributeAdviceCache.put(adviceBean, attrMethods);}// 扫描有 @InitBinder 注解的方法,添加到 initBinderAdviceCache 中Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);if (!binderMethods.isEmpty()) {this.initBinderAdviceCache.put(adviceBean, binderMethods);}// 如果是 RequestBodyAdvice 或 ResponseBodyAdvice 的子类,添加到 requestResponseBodyAdviceBeans 中if (RequestBodyAdvice.class.isAssignableFrom(beanType)) {requestResponseBodyAdviceBeans.add(adviceBean);}if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {requestResponseBodyAdviceBeans.add(adviceBean);}}// 将 requestResponseBodyAdviceBeans 添加到 this.requestResponseBodyAdvice 属性种if (!requestResponseBodyAdviceBeans.isEmpty()) {this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);}
}
这就是@ControllerAdvice的实现原理底层分析咯。