ControllerAdvice分析说明

ControllerAdvice分析说明

@ControllerAdvice见的最多的就是同于对异常的处理。比如下面的这个样子

@ControllerAdvice
public class TestControllerAdvised {
	@ExceptionHandler(value = Exception.class)
	public String modelAndViewException(){
		// 做一些异常的处理逻辑
	}
}

在阅读SpringMvc源码的时候,发现除了异常处理之外,还有别的功能。就有了这篇文章

一切的开始还是从源码的注释开始

ControllerAdvice源码

在这里插入图片描述

他是一个特殊的@Component。可以在它标注的Bean里面探测到@ExceptionHandle,@InitBinder,@ModelAttribute标注的方法,可以在多个@Controller标注得类上面做操作。除此之外,还可以通过它来配置RequestBodyAdviceResponseBodyAdvice

先说说这三个注解的作用

  1. @InitBinder,它可以自定义DataBinder,DataBinder是SpringMvc用来给目标的Bean设置属性值的,包括支持验证,和参数绑定结果扽西,并且还可以自定义可以字段,那些字段是可以绑定的,那些字段是必须的。等等。此外,它标注的方法的返回值必须为null。

    • 获取@InitBinder注解标注方法 RequestMappingHandlerAdapter#getDataBinderFactory,@InitBinder可以在@Controller和@ControllerAdvice里面都可以用。
    • 调用@InitBinder注解方法来自定义DataBinder的操作。InitBinderDataBinderFactory#initBinder(WebDataBinder,NativeWebRequest)
    • 校验返回值必须为void的操作。InitBinderDataBinderFactory#initBinder(WebDataBinder,NativeWebRequest)
  2. @ModelAttribute,它可以在调用对应的Controller处理之前,先前一步操作Model,MVC,是Model,View,COntroller,Model是SpringMvc里面很重要的概念,对应在真实的代码逻辑上就是一个大Map(ModelAndViewContainer#ModelMap属性)。模型渲染是需要Model的。所以可以在先前一步处理Model。会将它所标注的方法返回值放在Model中,Key就是注解中value()的值。

    它还可以添加到@ModelAttribute标注的方法中。用来表示该方法依赖那个model的值。如果在model中没有找到,每次循环默认取第一个。对应的代码如下: ModelFactory#invokeModelAttributeMethods

    // 这个方法是在循环中调用的, 注意下面的remove操作,如果找到依赖了,就会移除找到的这个,否则就会获取第一个,然后将它移除。	
    private ModelMethod getNextModelMethod(ModelAndViewContainer container) {
    		for (ModelMethod modelMethod : this.modelMethods) {
    			if (modelMethod.checkDependencies(container)) {
    				this.modelMethods.remove(modelMethod);
    				return modelMethod;
    			}
    		}
    		ModelMethod modelMethod = this.modelMethods.get(0);
    		this.modelMethods.remove(modelMethod);
    		return modelMethod;
    	}
    
    	public boolean checkDependencies(ModelAndViewContainer mavContainer) {
    			for (String name : this.dependencies) {
    				if (!mavContainer.containsAttribute(name)) {
    					return false;
    				}
    			}
    			return true;
    		}
    

    还可以添加到RequestMapping标注的方法的参数里面。@ModelAttribute value字段表示模型的名字,如果有,会直接从model中找出来赋值,如果没有就会尝试从Request中找,赋值,将对应的值放在model中,key 如果指定了value就是value属性的值,如果没有,就默认是参数类型小写。对应的代码如下: ModelAttributeMethodProcessor#resolveArgument

    	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
    			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    
    		Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
    		Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
            // 获取参数名字,取值为@ModelAttribute中value的值,如果没有就是类型名字小写
    		String name = ModelFactory.getNameForParameter(parameter);
            // 获取注解的属性值
    		ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
    		if (ann != null) {
    			mavContainer.setBinding(name, ann.binding());
    		}
    
    		Object attribute = null;
    		BindingResult bindingResult = null;
    			// 看是否有对应的key。下面的都是获取对应的值了
    		if (mavContainer.containsAttribute(name)) {
    			attribute = mavContainer.getModel().get(name);
    		}
    		else {
    			// Create attribute instance
    			try {
    				attribute = createAttribute(name, parameter, binderFactory, webRequest);
    			}
    			catch (BindException ex) {
    				if (isBindExceptionRequired(parameter)) {
    					// No BindingResult parameter -> fail with BindException
    					throw ex;
    				}
    				// Otherwise, expose null/empty value and associated BindingResult
    				if (parameter.getParameterType() == Optional.class) {
    					attribute = Optional.empty();
    				}
    				else {
    					attribute = ex.getTarget();
    				}
    				bindingResult = ex.getBindingResult();
    			}
    		}
    
    		if (bindingResult == null) {
    			// Bean property binding and validation;
    			// skipped in case of binding failure on construction.
    			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
    			if (binder.getTarget() != null) {
    				if (!mavContainer.isBindingDisabled(name)) {
    					bindRequestParameters(binder, webRequest);
    				}
    				validateIfApplicable(binder, parameter);
    				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
    					throw new BindException(binder.getBindingResult());
    				}
    			}
    			// Value type adaptation, also covering java.util.Optional
    			if (!parameter.getParameterType().isInstance(attribute)) {
    				attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
    			}
    			bindingResult = binder.getBindingResult();
    		}
    
    		// Add resolved attribute and BindingResult at the end of the model
            // 将处理过的添加到Model中。
    		Map<String, Object> bindingResultModel = bindingResult.getModel();
    		mavContainer.removeAttributes(bindingResultModel);
    		mavContainer.addAllAttributes(bindingResultModel);
    
    		return attribute;
    	}
    

    还可以在返回值的类型上面添加注解,会将该值添加到Model中去。对应的代码在 ModelAttributeMethodProcessor#handleReturnValue中,同样的,如果value属性指定,key的名字就是它,否则就是类名小写。

    	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
    			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
    
    		if (returnValue != null) {
    			String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
    			mavContainer.addAttribute(name, returnValue);
    		}
    	}
    
    • 获取@ModelAttribute注解的方法 RequestMappingHandlerAdapter#getModelFactory(HandlerMethod WebDataBinderFactory),和上面是一样的, 先获取当前Controller里面的,在获取全局的(@ControllerAdvice里面的)
  3. @ExceptionHandle

    用来处理异常的,在指定的class中的方法,或者当前Controller中的方法中。他有两种使用方式

    1. 写在@ControllerAdvice类中。
    2. 写在当前处理请求的Controller中。

    具体的一些信息建议直接看它的注释。清晰方便。

    • 参数

在这里插入图片描述

  • 返回值

在这里插入图片描述

在出现异常之后,会通过HandlerExceptionResolver来处理异常,发现@ExceptionHandler对应的代码在 ExceptionHandlerExceptionResolver#getExceptionHandlerMethod,还是先从当前的Controller里面找,再从@ControllerAdvice标注的类里面找。

看到这里有个问题,@ControllerAdvice标注的类是在哪里加载进来的,是怎么发现的?

这种发现模式是通用的,获取Bean工厂里面所有的Bean。遍历这些Bean,找对应标注了这个注解的类。模板如下:

	public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
		List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
        // 找到所有的Bean,
		for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) {
			if (!ScopedProxyUtils.isScopedTarget(name)) {
                // 通过这种方法来发现
				ControllerAdvice controllerAdvice = context.findAnnotationOnBean(name, ControllerAdvice.class);
				if (controllerAdvice != null) {
					adviceBeans.add(new ControllerAdviceBean(name, context, controllerAdvice));
				}
			}
		}
		OrderComparator.sort(adviceBeans);
		return adviceBeans;
	}

一般来说,在Spring中,这种自动发现Bean的这种功能都是写在 InitializingBean#afterPropertiesSet的。

RequestBodyAdvice,ResponseBodyAdvice作用

看这个名字,就是到是一个通知类,在请求处理前,和请求返回前的操作。

  1. ResponseBodyAdvice

    用在标注了 @ResponseBody和返回值为ResponseEntity的方法上的。在写响应值之前的操作。

    public interface ResponseBodyAdvice<T> {
        
    	boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
    rn the body that was passed in or a modified (possibly new) instance
    	  
    	@Nullable
    	T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
    			Class<? extends HttpMessageConverter<?>> selectedConverterType,
    			ServerHttpRequest request, ServerHttpResponse response);
    
    }
    
  2. RequestBodyAdvice

    用在标注了@RequestBody和参数值为HttpEntity的方法上的。

    public interface RequestBodyAdvice {
        //是否支持
    	boolean supports(MethodParameter methodParameter, Type targetType,
    			Class<? extends HttpMessageConverter<?>> converterType);
    
    
    	HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
    			Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;
    
        
    	Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
    			Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
    	@Nullable
        // 请求没有请求体调用它。
    	Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
    			Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
    }
    

    他俩是怎么注册到Springmvc中的呢?

    两种方式。

    1. 直接添加在RequestMappingHandlerAdapter中
    2. 标注@ControllerAdvice注解会自动发现。

    自动发现是怎么做的呢?

    因为它是直接添加到RequestMappingHandlerAdapter中,那么在构建RequestMappingHandlerAdapter的时候,就可以从Spring中获取所有标注了@ControllerAdvice注解的类,遍历获取,用isAssignableFrom 判断就好,对应的代码在RequestMappingHandlerAdapter#initControllerAdviceCache

    在这里插入图片描述

举例

  1. @InitBinder注解

    像DataBinder中增加Validator

在这里插入图片描述

  1. ResponseBodyAdvice

    在原来的返回值中增加外壳,用Result来包装一下

    1. Controller

      	@GetMapping("/test")
      	@ResponseBody
      	public Message listResponseBody(@ModelAttribute(name = "age") int age) {
      		Message message = new Message();
      		message.setId(33L);
      		return message;
      	}
      
    2. ResponseBodyAdvices实现类

      @ControllerAdvice
      public class TestRequestBodyAdvice implements ResponseBodyAdvice<Object> {
      	@Override
      	public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
      		return true;
      	}
      
      	@Override
      	public Object beforeBodyWrite(Object body,
      								   MethodParameter returnType,
      								   MediaType selectedContentType,
      								   Class<? extends HttpMessageConverter<?>> selectedConverterType,
      								   ServerHttpRequest request,
      								   ServerHttpResponse response) {
      		ServletServerHttpResponse response1 = (ServletServerHttpResponse) response;
      		int status = response1.getServletResponse().getStatus();
      		return status==HttpStatus.OK.value()?
                  // 如果是200,就用ok,否则就是500
      				new Result(HttpStatus.OK.value(),body,HttpStatus.OK.getReasonPhrase()):
      				new Result(HttpStatus.INTERNAL_SERVER_ERROR.value(),body,HttpStatus.OK.getReasonPhrase());
      	}
      }
      
    3. 结果

      {
        "code": 200,
        "data": {
          "id": 33,
          "text": null,
          "summary": null,
          "created": "2022-02-19T08:46:33.913+00:00"
        },
        "msg": "OK"
      }
      

    可以看到,没有直接在处理的方法中嵌套,而是直接返回对象,通过Advice来统一处理。

写在最后(有点重要)

@ControllerAdvice也是有属性的。通过这些属性可以指定包,或者注解,或者类型表示这些@ControllerAdvice可以应用于哪些Controller。默认都是全局的,这些条件只要满足一个就好

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

}

对应的判断逻辑在HandlerTypePredicate#test中,前面已经说了,会从全局中获取,在扫描的时候会将所有的@ControllerAdvice拿到,在应用的时候在判断。类似的操作如下:

在这里插入图片描述

这是在判断@ModelAttitude中的操作。

关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值