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

目录
相关文章
|
5月前
|
XML Java 关系型数据库
面试一口气说出Spring的声明式事务@Transactional注解的6种失效场景
面试一口气说出Spring的声明式事务@Transactional注解的6种失效场景
125 0
|
6月前
|
Java 编译器 Spring
一文搞懂:什么是注解?
一文搞懂:什么是注解?
39 0
|
7月前
|
Java
SpringAop切面编程(一看就会用)
SpringAop切面编程(一看就会用)
28 0
|
前端开发 Java API
SpringMVC注解完全解析(上)
SpringMVC注解完全解析(上)
|
安全 Java 大数据
一文搞懂什么是“注解”
一文搞懂什么是“注解”
318 0
一文搞懂什么是“注解”
|
JSON 数据格式
@JsonProperty与@JSONField注解用处
@JsonProperty与@JSONField注解用处
|
开发框架 Java Linux
这类注解都不知道,还好意思说用过Spring Boot?
这类注解都不知道,还好意思说用过Spring Boot?
这类注解都不知道,还好意思说用过Spring Boot?
|
JSON 前端开发 Java
|
Java Spring 容器
|
Java 测试技术 Spring
@Aysnc注解其实也就这么回事! (下)
@Aysnc注解其实也就这么回事! (下)
430 1
@Aysnc注解其实也就这么回事! (下)