@ControllerAdvice:你可以没用过,但是不能不了解

2024-06-24 11:12

本文主要是介绍@ControllerAdvice:你可以没用过,但是不能不了解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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的实现原理底层分析咯。

这篇关于@ControllerAdvice:你可以没用过,但是不能不了解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1089986

相关文章

【从0实现React18】 (四) 如何触发更新 带你了解react触发更新的流程以及更新后如何触发render

常见的触发更新的方式 创建 React 应用的根对象 ReactDOM.creatRoot().render();类组件 this.setState();函数组件 useState useEffect; 我们希望实现一套统一的更新机制,他的特点是: 兼容上述触发更新的方式方便后续拓展(优先级机制) 更新机制的组成部分 代表更新的数据结构 Update消费update的数据结构——Up

简单了解ESD模型与TLP曲线

上文讲了ESD和EOS的区别,说实话远不止那些。今日再稍加深入的介绍ESD。 一 ESD原理 ESD-Electro Static Discharge静电放电,具有不同静电电位的物体互相靠近或者直接接触引起的电荷转移。正常情况下,物体内部的正负电荷是相等的,对外表现不带电。当任何两种不同材质的物体接触后再分离就会产生静电。当正负电荷逐渐累计到一定程度时,将与周围环境产生电位差,从而使电荷经由放

了解Play

偶然看到这篇文章,写的不错,拿来分享一下。 版权所有©转载必须以链接形式注明作者和原始出处 原文地址:http://freewind.me/blog/20120728/965.html 我要捐助(Donate)博主,鼓励他写出更多好文章 =======================原文========================= 为了方便群中的Play初学者们,写了一

【理论了解】接口测试简介以及接口测试用例设计思路

接口测试简介 1.什么是接口 接口就是内部模块对模块,外部系统对其他服务提供的一种可调用或者连接的能力的标准,就好比usb接口,他是系统向外接提供的一种用于物理数据传输的一个接口,当然仅仅是一个接口是不能进行传输的,我们还的对这个接口怎么进行传输进行进行一些设置和定义。开发所谓的接口是模块模块之间的一种连接,而测试眼中的接口是一种协议(对接口的功能的一种定义) 2.接口的种类和分类 外部接

了解 Spring RequestMapping

1. 概述   这篇文章会集中讨论 Spring MVC 的一个重要注解 @RequestMapping。   简要地说,该注解用于把 Web 请求映射到 Spring Controller 方法。   2. @RequestMapping 基础   先从一个简单的示例开始:通过设置基本条件把 HTTP 请求映射到某个方法。   2.1. 路径映射   @RequestMa

【Linux】了解冯诺伊曼体系结构

文章目录 冯诺依曼体系结构概念冯诺依曼体系结构的推导过程理解冯诺依曼体系 冯诺依曼体系结构概念 冯·诺依曼结构是现代计算机发展所遵循的基本结构形式之一,其特点是“程序存储,共享数据,顺序执行”。冯·诺依曼结构消除了原始计算机体系中,只能依靠硬件控制程序的状况,将程序编码存储在存储器中,实现了可编程的计算机功能,实现了硬件设计和程序设计的分离,大大促进了计算机的发展。冯·诺依曼结构

什么是扎实的基本功?MySQL 基础知识看看你了解多少

本文首发于公众平台:腐烂的橘子 当前很多同学沉迷于“碎片化学习”,问题在于获取到的都是零碎的知识,没有体系化的知识框架,这对于练就扎实的基本功是极其不利的。 怎么办?这时要懂得中庸之道“慢即是快”的道理,系统学一遍,查漏补缺,不要觉得有些你知道就学不下去了,要耐得住性子,系统学习。 下面就来检验下这些知识点你是否都掌握了。 关于 join 的那些事 Inner join 冷知识:

简单了解html常用的标签

HTML 一、基础认知 1、注释 1.1、注释的作用和写法 1.1.1、作用 为代码添加解释性,描述性的信息,主要用来帮助开发人员理解代码,浏览器执行代码时回忽略所有注释。 1.1.2、注释的快捷键 在VS Code中:Ctrl + / 2、HTML标签的结构 2.1、结构说明 标签由<、>、/、英文单词或者字母组成。并且把标签中<>包括起来的英文单词或者字母称为标签名常见的标

【vue3|第13期】深入了解Vue3生命周期:管理组件的诞生、成长与消亡

日期:2024年6月22日 作者:Commas 签名:(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释:如果您觉得有所帮助,帮忙点个赞,也可以关注我,我们一起成长;如果有不对的地方,还望各位大佬不吝赐教,谢谢^ - ^ 1.01365 = 37.7834;0.99365 = 0.0255 1.02365 = 1377.4083;0.98365 = 0.0006 说在最前面:本文

了解UBOOT添加命令的执行流程

BootLoader(引导装载程序)是嵌入式系统软件开发的第一个环节,它把操作系统和硬件平台衔接在一起。U-BOOT是当前比较流行、功能强大的BootLoader,可以支持多种体系结构。 BootLoader(引导装载程序)是嵌入式系统软件开发的第一个环节,它把操作系统和硬件平台衔接在一起,对于嵌入式系统的后续软件开发十分重要,在整个开发中也占有相当大的比例。U-BOOT是当前比较流行、功