There are two aspects to Spring's global processing:
-
Unified data return format
-
Unified exception handling
General return value class definition:
public class GlobalResponse<T> implements POJO { /** * */ private static final long serialVersionUID = 1L; @ApiModelProperty(notes = "data") private T data; @ApiModelProperty(notes = "Not empty. When it is equal to 200, it means that the business is successful; otherwise, it means that the business fails") private int code = 200; @ApiModelProperty(notes = "Error message. If it is not empty, it will be displayed to the user") private String msg; public GlobalResponse() { } }
To configure
Yes, we need to use several key notes to complete the following configuration:
@EnableWebMvc public class UnifiedResponseHandler { @RestControllerAdvice static class CommonResultResponseAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof GlobalResponse) { // It is compatible with the data of the old version, which has been encapsulated with GlobalResponse, so it is unnecessary to deal with it return body; } else if (body instanceof POJO) { // Currently, only the return objects of POJO are encapsulated return new GlobalResponse<Object>(body); } else { return body; } } } }
This is the end. We can write any RESTful API, and all return values will have a unified JSON structure
Anatomical realization process
Starting from the annotation @ EnableWebMvc, open the annotation to see:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(DelegatingWebMvcConfiguration.class) public @interface EnableWebMvc { }
DelegatingWebMvcConfiguration.class is introduced through @ Import annotation. Let's look at this class:
@Configuration public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); @Autowired(required = false) public void setConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); } } @Override protected void configurePathMatch(PathMatchConfigurer configurer) { this.configurers.configurePathMatch(configurer); } @Override protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) { this.configurers.configureContentNegotiation(configurer); } @Override protected void configureAsyncSupport(AsyncSupportConfigurer configurer) { this.configurers.configureAsyncSupport(configurer); } @Override protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { this.configurers.configureDefaultServletHandling(configurer); } @Override protected void addFormatters(FormatterRegistry registry) { this.configurers.addFormatters(registry); } @Override protected void addInterceptors(InterceptorRegistry registry) { this.configurers.addInterceptors(registry); } @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { this.configurers.addResourceHandlers(registry); } @Override protected void addCorsMappings(CorsRegistry registry) { this.configurers.addCorsMappings(registry); } @Override protected void addViewControllers(ViewControllerRegistry registry) { this.configurers.addViewControllers(registry); } @Override protected void configureViewResolvers(ViewResolverRegistry registry) { this.configurers.configureViewResolvers(registry); } @Override protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { this.configurers.addArgumentResolvers(argumentResolvers); } @Override protected void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) { this.configurers.addReturnValueHandlers(returnValueHandlers); } @Override protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { this.configurers.configureMessageConverters(converters); } @Override protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { this.configurers.extendMessageConverters(converters); } @Override protected void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { this.configurers.configureHandlerExceptionResolvers(exceptionResolvers); } @Override protected void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { this.configurers.extendHandlerExceptionResolvers(exceptionResolvers); } @Override @Nullable protected Validator getValidator() { return this.configurers.getValidator(); } @Override @Nullable protected MessageCodesResolver getMessageCodesResolver() { return this.configurers.getMessageCodesResolver(); } }
With @ Configuration annotation, you should be familiar with it. However, there is a key code hidden in WebMvcConfigurationSupport, the parent class of this class:
@Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter(); adapter.setContentNegotiationManager(mvcContentNegotiationManager()); adapter.setMessageConverters(getMessageConverters()); adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer()); adapter.setCustomArgumentResolvers(getArgumentResolvers()); adapter.setCustomReturnValueHandlers(getReturnValueHandlers()); if (jackson2Present) { adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice())); adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice())); } AsyncSupportConfigurer configurer = new AsyncSupportConfigurer(); configureAsyncSupport(configurer); if (configurer.getTaskExecutor() != null) { adapter.setTaskExecutor(configurer.getTaskExecutor()); } if (configurer.getTimeout() != null) { adapter.setAsyncRequestTimeout(configurer.getTimeout()); } adapter.setCallableInterceptors(configurer.getCallableInterceptors()); adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors()); return adapter; }
RequestMappingHandlerAdapter is the key to each request processing. Let's see the definition of this class:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... }
This class implements the InitializingBean interface. The afterpropertieset method of the InitializingBean interface is one of the keys. This method is also overridden in the RequestMappingHandlerAdapter class:
@Override public void afterPropertiesSet() { // Do this first, it may add ResponseBody advice beans initControllerAdviceCache(); if (this.argumentResolvers == null) { List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.initBinderArgumentResolvers == null) { List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers(); this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.returnValueHandlers == null) { List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } }
The contents of this method are very critical, but let's first look at the initControllerAdviceCache method, and then separately explain the other contents:
private void initControllerAdviceCache() { ... if (logger.isInfoEnabled()) { logger.info("Looking for @ControllerAdvice: " + getApplicationContext()); } List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); AnnotationAwareOrderComparator.sort(beans); List<Object> requestResponseBodyAdviceBeans = new ArrayList<Object>(); for (ControllerAdviceBean bean : beans) { ... if (ResponseBodyAdvice.class.isAssignableFrom(bean.getBeanType())) { requestResponseBodyAdviceBeans.add(bean); } } }
Scan controlleradvise annotation through ControllerAdviceBean static method, but we use @ restcontrolleradvise annotation in the actual implementation of UnifiedResponseHandler. Open the annotation:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @ControllerAdvice @ResponseBody public @interface RestControllerAdvice {
This annotation is marked by @ controlleradvise and @ ResponseBody, just like the familiar @ RestController annotation is marked by @ Controller and @ ResponseBody
Now that you know how the bean marked with @ RestControllerAdvice is loaded into the Spring context, you need to know how Spring uses our bean and processes the returned body
Actually in This is how HttpMessageConverter converts data This article has explained part of it. I hope you can read this article first, and the next part will be understood in seconds. Let's make further explanation here
In the writeWithMessageConverters method of AbstractMessageConverterMethodProcessor, there is a core code:
if (messageConverter instanceof GenericHttpMessageConverter) { if (((GenericHttpMessageConverter) messageConverter).canWrite( declaredType, valueType, selectedMediaType)) { outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); ... return; } }
You can see that the beforeBodyWrite method is called through getAdvice(), and we are close to the truth
protected RequestResponseBodyAdviceChain getAdvice() { return this.advice; }
RequestResponseBodyAdviceChain, with the name of Chain, obviously uses the "responsibility Chain design mode", but it passes the responsibility Chain in a circular way:
class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice<Object> { @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType contentType, Class<? extends HttpMessageConverter<?>> converterType, ServerHttpRequest request, ServerHttpResponse response) { return processBody(body, returnType, contentType, converterType, request, response); } @SuppressWarnings("unchecked") private <T> Object processBody(Object body, MethodParameter returnType, MediaType contentType, Class<? extends HttpMessageConverter<?>> converterType, ServerHttpRequest request, ServerHttpResponse response) { for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) { if (advice.supports(returnType, converterType)) { body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType, contentType, converterType, request, response); } } return body; } }
Our rewritten "beforeBodyWrite" method will be called after all, the truth is this!!!
In fact, it's not over yet. Have you ever thought that if the return value of our API method is org. Springframework. Http. ResponseEntity < T > type, we can specify the HTTP return status code, but this return value will be directly put into the body parameter of our beforeBodyWrite method? If this is obviously wrong, because the ResponseEntity contains a lot of our non business data, how does Spring help us deal with it?
Before our method gets the return value and calls the beforeBodyWrite method, we also need to select HandlerMethodReturnValueHandler to handle different types of return values
In the handleReturnValue method in the HandlerMethodReturnValueHandlerComposite class
@Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); if (handler == null) { throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); } handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); }
By calling the selectHandler method to select the appropriate handler, Spring has many built-in handlers. Let's look at the class diagram:
HttpEntityMethodProcessor is one of them. It overrides the supportsParameter method and supports HttpEntity type, that is, it supports ResponseEntity type:
@Override public boolean supportsParameter(MethodParameter parameter) { return (HttpEntity.class == parameter.getParameterType() || RequestEntity.class == parameter.getParameterType()); }
So when the returned type is ResponseEntity, we need to process our results through the handleReturnValue method of HttpEntityMethodProcessor:
@Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { ... if (responseEntity instanceof ResponseEntity) { int returnStatus = ((ResponseEntity<?>) responseEntity).getStatusCodeValue(); outputMessage.getServletResponse().setStatus(returnStatus); if (returnStatus == 200) { if (SAFE_METHODS.contains(inputMessage.getMethod()) && isResourceNotModified(inputMessage, outputMessage)) { // Ensure headers are flushed, no body should be written. outputMessage.flush(); // Skip call to converters, as they may update the body. return; } } } // Try even with null body. ResponseBodyAdvice could get involved. writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage); // Ensure headers are flushed even if no body was written. outputMessage.flush(); }
This method extracts responseEntity.getBody(), passes a MessageConverter, and then continues to call the beforeBodyWrite method, which is the truth