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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/33075.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WinForm 2048

WinForm 2048 是一个基于 Windows 窗体应用程序&#xff08;WinForms&#xff09;实现的经典益智游戏——2048。这个游戏通过简单的滑动或点击操作&#xff0c;将相同数字的方块合并&#xff0c;以生成更大的数字方块&#xff0c;最终目标是创造出一个数字为 2048 的方块。 游…

电商爬虫API的定制开发:满足个性化需求的解决方案

一、引言 随着电子商务的蓬勃发展&#xff0c;电商数据成为了企业决策的重要依据。然而&#xff0c;电商数据的获取并非易事&#xff0c;特别是对于拥有个性化需求的企业来说&#xff0c;更是面临诸多挑战。为了满足这些个性化需求&#xff0c;电商爬虫API的定制开发成为了解决…

【杂记-浅谈IPv6地址】

IPv6地址 一、IPv6地址概述二、IPv6地址结构三、IPv6地址分类四、IPv6地址配置五、IPv6的应用场景 一、IPv6地址概述 IPv6&#xff0c;Internet Protocol version 6&#xff0c;是互联网协议的第六版&#xff0c;旨在克服IPv4地址耗尽的挑战&#xff0c;并为互联网的未来发展提…

Apache Tomcat 10.1.25 新版本发布 java 应用服务器

Tomcat 是一个小型的轻量级应用服务器&#xff0c;在中小型系统和并发访问用户不是很多的场合下被普遍使用&#xff0c;是开发和调试 JSP 程序的首选。对于一个初学者来说&#xff0c;可以这样认为&#xff0c;当在一台机器上配置好 Apache 服务器&#xff0c;可利用它响应对 H…

uniapp 使用uview 插件

看创建项目版本vue2 、 vue3 Button 按钮 | uView 2.0 - 全面兼容 nvue 的 uni-app 生态框架 - uni-app UI 框架 1. npm install uview-ui2.0.36 2. // main.js&#xff0c;注意要在use方法之后执行 import uView from uview-ui Vue.use(uView) // 如此配置即可 uni.$u.c…

服务治理入门

服务治理的生命周期 在微服务架构中&#xff0c;服务治理是确保服务正常运行和高效协作的关键。服务治理的生命周期包括以下五个阶段&#xff1a;服务注册、服务发现、服务续约/心跳、服务被动剔除和服务主动剔除。 服务注册 服务提供者在启动时&#xff0c;需要将其服务信…

Prometheus的infratest、UAT、PRE、PRD分别代表什么

Prometheus的infratest、UAT、PRE、PRD分别代表什么 在Prometheus监控系统中,infratest、UAT、PRE和PRD通常指的是不同阶段的测试环境,分别对应基础设施测试(Infrastructure Test)、用户验收测试(User Acceptance Test)、预生产环境(Pre-production)和生产环境(Produ…

构建RISC-V工具链:基本步骤

在这一节内容中&#xff0c;我们将介绍如何构建一个64位的RISC-V工具链。在这个过程中&#xff0c;我们将编译默认的RISC-V工具链&#xff0c;而不修改指令集。 1. 安装必要的软件包 首先&#xff0c;需要安装一些必要的软件包。在终端中运行以下命令&#xff1a; sudo apt-g…

vue3-cropperjs图片裁剪工具-用户上传图片截取-(含预览视频)

效果图 上传图片弹窗预览 对于这个上传图片样式可以参考 官方原代码 官网传送入口 Upload 上传 | Element Plus (element-plus.org) <template><el-uploadclass"upload-demo"dragaction"https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6…

免费分享一套SpringBoot+Vue电影院售票管理系统【论文+源码+SQL脚本】,帅呆了~~

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的SpringBootVue电影院售票管理系统&#xff0c;分享下哈。 项目视频演示 【免费】SpringBootVue电影院售票管理系统 Java毕业设计_哔哩哔哩_bilibili【免费】SpringBootVue电影院售票管理系统 Java毕业设计…

DriverManager.getConnection用法总结

DriverManager.getConnection用法总结 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 在Java编程中&#xff0c;DriverManager.getConnection是一个用于建立与…

《Windows API每日一练》5.5 插入符号

当你向程序中输入文本时&#xff0c;通常会有下划线、竖线或方框指示你输入的下一个字符将出现在屏幕上的位置。你也许认为这是“光标”&#xff0c;但在编写Windows程序时&#xff0c;你必须避免这种习惯。在Windows中&#xff0c;它被称为“插入符号”&#xff08;caret&…

市政道路乙级资质申报的筹备与执行

一、筹备阶段 1. 政策研读与自我评估 详细了解资质标准&#xff1a;仔细阅读最新的资质申报指南和相关法规&#xff0c;明确乙级资质的具体要求&#xff0c;包括企业资本、技术人员配置、过往业绩等。自我评估&#xff1a;对照资质标准&#xff0c;对企业现状进行全面评估&am…

河南省乙级建筑设计资质标准案例分析

河南省乙级建筑设计资质标准案例分析 虽然我没有具体的河南省乙级建筑设计资质的详细案例分析&#xff0c;但我可以根据一般性的资质标准和流程&#xff0c;构建一个简化的案例分析框架&#xff0c;帮助理解乙级建筑设计资质的获取和应用。 案例背景&#xff1a; 假设“华豫…

如何评估LabVIEW需求中功能的必要性和可行性

评估LabVIEW需求中功能的必要性和可行性涉及多个方面的分析&#xff0c;包括需求的重要性、技术可行性、资源需求以及潜在风险。以下是一个详细的评估方法&#xff1a; ​ 一、功能必要性评估 需求来源和目的&#xff1a; 来源&#xff1a;需求来自哪里&#xff1f;是客户、市…

Lua 绕过元表

Lua 绕过元表&#xff0c;直接访问 table 的字段。 绕过元表 rawset(table, index, value)&#xff0c;在不触发元方法的情况下&#xff0c;设置 table[index] 的值为 value。 rawget(table, index)&#xff0c;在不触发元方法的情况下&#xff0c;获取 table[index] 的值。…

写一个坏越的个人天地(二)

小红书上搜了下博客,感觉好像没有让自己喜欢的。昨天刚好学了点grid布局,来试试 菜单栏直接使用el-menu 下边布局就用grid局部了,这块初步想法是轮播+你的天气和我的天气+自我介绍 天气的话,这边要先找一下有没有天气的api 我这边百度搜了个聚合的api,一天可以免费调用5…

Linux系统iptables应用SNAT和DNAT

一、SNAT 1.SNAT应用环境 局域网主机共享单个公网IP地址接入Internet (私有IP不能在Internet中正常路由) 2.SNAT原理 源地址转换&#xff0c;根据指定条件修改数据包的源IP地址&#xff0c;通常被叫做源映谢 数据包从内网发送到公网时&#xff0c;SNAT会把数据包的源IP由私…

网页抓取和网页爬取之间有何区别?

随着互联网的发展和信息的爆炸式增长&#xff0c;数据收集和处理已成为企业和个人不可或缺的需求。在此背景下&#xff0c;网页抓取和网络爬虫已成为两种常见的数据收集方法。虽然这两种方法看似相似&#xff0c;但它们的方法和目标存在显著差异。本文将为您详细介绍网页抓取和…

H4020 12V24V36V40V1A 同步降压芯片IC Buck-DCDC 低功耗,高效率 100%占空比

H4020是一款12V24V36V40V1A的同步降压&#xff08;Buck&#xff09;DC-DC转换器&#xff0c;专为需要高效率、低功耗和精确电压/电流控制的应用而设计。它内置了高压MOSFET&#xff0c;支持宽范围的输入电压&#xff08;5V-36V&#xff09;&#xff0c;并能提供高达1A的持续输出…