ControllerAdvice analysis description

ControllerAdvice analysis description

@What controller advice sees most is the same as exception handling. Like this one below

@ControllerAdvice
public class TestControllerAdvised {
	@ExceptionHandler(value = Exception.class)
	public String modelAndViewException(){
		// Do some exception handling logic
	}
}

When reading the source code of spring MVC, I found that there are other functions besides exception handling. With this article

Everything starts with the comments of the source code

ControllerAdvice source code

He is a special @ Component. You can detect @ ExceptionHandle, @ InitBinder, @ ModelAttribute marked methods in the Bean marked by it, and you can operate on multiple @ Controller marked classes. In addition, it can also be used to configure RequestBodyAdvice and ResponseBodyAdvice.

Let's talk about the role of these three annotations first

  1. @InitBinder, which can customize the DataBinder. The DataBinder is used by SpringMvc to set the attribute value of the target Bean, including supporting validation and parameter binding results. It can also customize the fields. Those fields can be bound and those fields are required. wait. In addition, the return value of the method it labels must be null.

    • Get @ InitBinder annotation annotation method requestmappinghandleradapter#getdatabindfactory, @ InitBinder can be used in @ Controller and @ ControllerAdvice.
    • Call the @ InitBinder annotation method to customize the operation of DataBinder. InitBinderDataBinderFactory#initBinder(WebDataBinder´╝îNativeWebRequest).
    • Verify that the return value must be void. InitBinderDataBinderFactory#initBinder(WebDataBinder´╝îNativeWebRequest).
  2. @ModelAttribute, which can operate model and MVC in the previous step before calling the corresponding controller for processing. Model is a very important concept in SpringMvc. In real code logic, it is a large Map (ModelAndViewContainer#ModelMap attribute). Model is required for model rendering. So you can handle the model in the previous step. The return value of the marked method will be placed in the model, and the Key is the value() in the annotation.

    It can also be added to the method of the @ ModelAttribute annotation. Used to indicate that the method depends on the value of that model. If it is not found in the model, the first one is selected by default for each cycle. The corresponding code is as follows: ModelFactory#invokeModelAttributeMethods

    // This method is called in the loop. Notice the following remove operation. If you find a dependency, you will remove the found one, otherwise you will get the first one and then remove it.	
    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;
    		}
    

    It can also be added to the parameters of the method marked by RequestMapping@ The ModelAttribute value field indicates the name of the model. If there is one, it will find the assignment directly from the model. If there is no one, it will try to find the assignment from the Request and put the corresponding value in the model. If the key specifies the value, it is the value of the value attribute. If there is no one, it is the default parameter type in lowercase. The corresponding code is as follows: 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");
            // Get the parameter name. The value is the value in @ ModelAttribute. If not, the type name is lowercase
    		String name = ModelFactory.getNameForParameter(parameter);
            // Gets the attribute value of the annotation
    		ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
    		if (ann != null) {
    			mavContainer.setBinding(name, ann.binding());
    		}
    
    		Object attribute = null;
    		BindingResult bindingResult = null;
    			// See if there is a corresponding key. The following are to obtain the corresponding values
    		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
            // Add the processed to the Model.
    		Map<String, Object> bindingResultModel = bindingResult.getModel();
    		mavContainer.removeAttributes(bindingResultModel);
    		mavContainer.addAllAttributes(bindingResultModel);
    
    		return attribute;
    	}
    

    You can also add comments on the type of return value, which will be added to the Model. The corresponding code is in ModelAttributeMethodProcessor#handleReturnValue. Similarly, if the value attribute is specified, the name of the key is it; otherwise, the class name is lowercase.

    	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);
    		}
    	}
    
    • The method RequestMappingHandlerAdapter#getModelFactory(HandlerMethod WebDataBinderFactory) to obtain the @ ModelAttribute annotation is the same as above. First obtain the method in the current Controller, and then obtain the global method (@ ControllerAdvice)
  3. @ExceptionHandle

    It is used to handle exceptions in the methods in the specified class or in the current Controller. He has two ways to use it

    1. Written in the @ ControllerAdvice class.
    2. It is written in the Controller currently processing the request.

    For some specific information, it is recommended to directly look at its notes. Clear and convenient.

    • parameter

  • Return value

After an exception occurs, it will be handled through the HandlerExceptionResolver. It is found that the code corresponding to @ ExceptionHandler is in the ExceptionHandlerExceptionResolver#getExceptionHandlerMethod, or first from the current Controller and then from the class marked by @ ControllerAdvice.

There is a problem here. Where is the class marked by @ ControllerAdvice loaded and how is it found?

This discovery mode is universal, which can obtain all beans in the Bean factory. Traverse these beans and find the corresponding class marked with this annotation. The template is as follows:

	public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
		List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
        // Find all the beans,
		for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) {
			if (!ScopedProxyUtils.isScopedTarget(name)) {
                // In this way to find
				ControllerAdvice controllerAdvice = context.findAnnotationOnBean(name, ControllerAdvice.class);
				if (controllerAdvice != null) {
					adviceBeans.add(new ControllerAdviceBean(name, context, controllerAdvice));
				}
			}
		}
		OrderComparator.sort(adviceBeans);
		return adviceBeans;
	}

Generally speaking, in Spring, the function of automatically discovering beans is written in initializing Bean #afterpropertieset.

RequestBodyAdvice, ResponseBodyAdvice

Look at this name, that is, to is a notification class, which operates before the request is processed and before the request is returned.

  1. ResponseBodyAdvice

    Used on methods marked with @ ResponseBody and return value ResponseEntity. The operation before writing the response value.

    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

    Used on methods marked with @ RequestBody and parameter value HttpEntity.

    public interface RequestBodyAdvice {
        //Support
    	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
        // The request has no request body to call it.
    	Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
    			Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
    }
    

    How did they register with spring MVC?

    Two ways.

    1. Add directly to RequestMappingHandlerAdapter
    2. The annotation @ ControllerAdvice will be found automatically.

    How to do automatic discovery?

    Because it is directly added to the RequestMappingHandlerAdapter, when building the RequestMappingHandlerAdapter, you can get all classes marked with @ ControllerAdvice annotation from Spring, traverse the acquisition, and judge with isAssignableFrom. The corresponding code is in the RequestMappingHandlerAdapter#initControllerAdviceCache.

give an example

  1. @InitBinder annotation

    Like adding Validator in DataBinder

  1. ResponseBodyAdvice

    Add a shell to the original return value and wrap it with Result

    1. Controller

      	@GetMapping("/test")
      	@ResponseBody
      	public Message listResponseBody(@ModelAttribute(name = "age") int age) {
      		Message message = new Message();
      		message.setId(33L);
      		return message;
      	}
      
    2. Responsebodyadvice implementation class

      @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()?
                  // If it's 200, use ok, otherwise it's 500
      				new Result(HttpStatus.OK.value(),body,HttpStatus.OK.getReasonPhrase()):
      				new Result(HttpStatus.INTERNAL_SERVER_ERROR.value(),body,HttpStatus.OK.getReasonPhrase());
      	}
      }
      
    3. result

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

    It can be seen that instead of directly nesting in the processing method, the object is returned directly and processed uniformly through Advice.

Write at the end (a little important)

@Controller advice also has properties. Through these attributes, you can specify packages, annotations, or types to indicate which controllers these @ ControllerAdvice can be applied to. The default is global, and only one of these conditions is satisfied

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {

	 // Specify package
	@AliasFor("basePackages")
	String[] value() default {};

	@AliasFor("value")
	String[] basePackages() default {};

    // Based on which classes are in the package
	Class<?>[] basePackageClasses() default {};

 // Specify type
	Class<?>[] assignableTypes() default {};

 // Specify annotation
	Class<? extends Annotation>[] annotations() default {};

}

The corresponding judgment logic is in handlertypepredict #test. As mentioned earlier, it will be obtained from the global. All @ ControllerAdvice will be obtained during scanning and judged during application. Similar operations are as follows:

This is the operation in judging @ modelattribute.

About the blog, I take it as my notes. There are a lot of contents in it that reflect my thinking process. Because my thinking is limited, there are some differences in some contents. If there are any problems, please point them out. Discuss together. thank you.

Keywords: Java Spring Spring MVC

Added by robocop on Sat, 19 Feb 2022 16:32:20 +0200