@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 {

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

  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您好!关于`@ControllerAdvice`注解未生效的问题,可能有以下几个原因: 1. 未正确配置`@ControllerAdvice`注解:请确保您的`@ControllerAdvice`注解被正确地放置在一个类上,并且这个类被正确地扫描到。一般来说,您需要将它放置在一个被Spring容器管理的组件上,比如一个带有`@Component`或`@RestController`注解的类。 2. 扫描包路径不正确:Spring需要扫描指定的包路径以找到`@ControllerAdvice`注解所在的类。请确保您已经正确配置了包扫描路径,或者将`@ControllerAdvice`注解所在的类放置在默认的扫描路径下。 3. 异常处理方法签名不正确:在`@ControllerAdvice`注解所在的类中,您需要编写异常处理方法,并且这些方法需要遵循一定的签名规则。例如,对于处理所有异常的方法,可以使用`@ExceptionHandler(Exception.class)`注解进行标注,并且方法的参数可以是`Exception`类型或其子类型。 4. 异常处理方法未生效:如果您的异常处理方法没有被调用,可能是因为抛出的异常类型与方法签名中定义的异常类型不匹配。请确保异常类型与方法签名中定义的异常类型相符合。 5. Spring配置问题:如果上述解决方法都没有生效,那么可能是您的Spring配置出现了问题。请确保您的Spring配置文件中包含了正确的配置项,比如`<context:component-scan>`用于扫描组件。 希望以上解决方案能对您有所帮助!如果问题仍然存在,请提供更多的详细信息,我将尽力协助您解决问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值