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

简介: `@ControllerAdvice` 是 Spring MVC 中用于定义全局行为的注解,如异常处理、数据绑定和预处理。它从 `@Component` 派生,确保被扫描并纳入容器。`@ExceptionHandler` 用于全局异常处理,提供统一的错误响应。例如,当处理不当的异常时,它能返回友好的错误信息。`@InitBinder` 在数据绑定前对参数进行处理,如格式转换。`@ModelAttribute` 可以用于全局绑定模型属性,如登录用户信息。Spring MVC 通过 `DispatcherServlet` 和 `HandlerAdapter` 在请求处理流程中应用这些全局配置。

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 {
   
   

    @InitBinder
    public 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:ss
    locale: zh_CN
    time-zone: GMT+8
    default-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) {
   
   
    // 初始化 MultipartResolver
    initMultipartResolver(context);
    // 初始化 LocaleResolver
    initLocaleResolver(context);
    // 初始化 ThemeResolver
    initThemeResolver(context);
    // 初始化 HandlerMappings
    initHandlerMappings(context);
    // 初始化 HandlerAdapters
    initHandlerAdapters(context);
    // 初始化 HandlerExceptionResolvers 
    initHandlerExceptionResolvers(context);
    // 初始化 RequestToViewNameTranslator
    initRequestToViewNameTranslator(context);
    // 初始化 ViewResolvers
    initViewResolvers(context);
    // 初始化 FlashMapManager
    initFlashMapManager(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 afterCompletion
                if (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的实现原理底层分析咯。

目录
相关文章
|
缓存 Java Spring
07HandlerMapping中用到的RequestMappingInfo和RequestCondition
RequestCondition(请求匹配条件)体系。 上面提到的RequestMapping注解申明的属性与之呼应的就是spring中RequestCondition的实现体系。 RequestMappingInfo体系 RequestMappingInfo是请求映射信息的描述,维护了一个请求所匹配的各种条件。即一个请求是有很多匹配条件的都放在了RequestMappingInfo中 RequestMappingInfo的生成、存放、和获取
1726 0
|
5月前
|
JSON Java 数据库
第08课:Spring Boot中的全局异常处理
第08课:Spring Boot中的全局异常处理
734 0
|
前端开发 数据库 微服务
JavaWeb - 我们的开发规范(VO、DTO、BO、PO、DO、POJO)
JavaWeb - 我们的开发规范(VO、DTO、BO、PO、DO、POJO)
2174 0
JavaWeb - 我们的开发规范(VO、DTO、BO、PO、DO、POJO)
|
Java API Spring
Springfox Swagger2从入门到精通
本文详细介绍了如何使用Springfox Swagger2在Spring Boot项目中生成API文档,包括引入依赖、配置Swagger2、启用Swagger2、编写API文档注释、访问Swagger UI以及常用注解分析和高级配置。
1063 0
Springfox Swagger2从入门到精通
|
前端开发
使用Postman导出excel
在本文档中,作者分享了如何使用Postman测试导出Excel接口的两种方法。配以四张图片说明了设置步骤,包括选择接口请求方式、设置Header(Content-Type: multipart/form-data)、Body中选取form-data类型以及指定文件。尽管代码指定了文件名,但在Postman的响应中不会显示,提示需要前端进一步处理。
1650 0
|
Java 数据库连接 Apache
全网首发一IoTDB数据库整合MyBatis实现SpringBoot项目CRUD
最近用到IoTDB数据库,经过对一些相关文档的搜集,大概了解到了该数据库的主要应用场景和使用方法,本篇就讲一下如何利用IoTDB并结合SpringBoott和Mybatis进行项目整合。经过一番查找全网都没有一篇完整的关于该数据库采用Mybatis做持久化框架的文章,那么就由我来开辟一下先河。
1336 0
|
网络协议 前端开发 算法
配置之道:深入研究Netty中的Option选项
配置之道:深入研究Netty中的Option选项
825 0
|
XML 缓存 Java
Spring高手之路5——彻底掌握Bean的生命周期
在这篇文章中,我们将深入研究Spring Framework的核心部分——Spring Bean的生命周期。我们将探讨初始化和销毁方法,了解如何使用@PostConstruct和@PreDestroy注解,以及实现InitializingBean和DisposableBean接口。我们还将详细讨论原型Bean的生命周期,并最后总结Spring中控制Bean生命周期的三种方式。无论你是Spring新手,还是想进一步提高你的Spring技能,这篇文章都能给你提供有价值的 insights。
2592 3
Spring高手之路5——彻底掌握Bean的生命周期
|
缓存 Java uml
SpringBoot2 | Spring AOP 原理深度源码分析(八)
SpringBoot2 | Spring AOP 原理深度源码分析(八)
305 0