I have two things to say about Spring’s global handling:

  1. Unified data return format
  2. Unified exception Handling In order to explain the two problems clearly, we will divide them into two chapters respectively. This chapter mainly focuses on the first point

Some people say that our project does this kind of processing, that is, in each API separate utility class to encapsulate the return value, but this is not elegant; I want to do this with minimal code. Some people may say that a few comments are enough to solve the problem, and that’s true, but this article is mainly about explaining why a few comments are enough to solve the problem, and hopefully you’ll understand why.

To better illustrate the problem, this article explains how to implement it first, then dissects the implementation principle in detail (which is critical)

Why uniform data return format

The separation of front and back ends is the mainstream of today’s service forms. How to design a good RESTful API and how to enable front-end partners to deal with the standard Response JSON data structure are of great importance. In order to enable front-end partners to have better logical display and page interaction processing, Each RESTful request should contain the following information:

The name of the

describe

status

Status code, indicating whether the request is successful, for example, [1: successful; -1: failed]

errorCode

Error code: provides clear error codes to better deal with service exceptions. The value can be null if the request succeeds

errorMsg

Error messages, which correspond to error codes, more specifically describe the exception information

resultBody

Return the result, usually the JSON data corresponding to the Bean object, which is usually declared as a generic type to accommodate the different return value types

implementation

Generic return value class definition

Using Java beans to represent this structure, as described above, looks like this:

@Data public final class CommonResult<T> { private int status = 1; private String errorCode = ""; private String errorMsg = ""; private T resultBody; public CommonResult() { } public CommonResult(T resultBody) { this.resultBody = resultBody; }}Copy the code

configuration

Yes, we need a few key annotations to complete the configuration:

@EnableWebMvc
@Configuration
public class UnifiedReturnConfig {

    @RestControllerAdvice("com.example.unifiedreturn.api")
    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 methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
            if (body instanceof CommonResult){
                return body;
            }

            return new CommonResult<Object>(body);
        }
    }
}Copy the code

That’s the end of it, and we can write any RESTful API we like, all the return values will have a uniform JSON structure

test

Create a new UserController, add the corresponding RESTful API, and write a simple test case to illustrate the handling of the return value

@RestController @RequestMapping("/users") public class UserController { @GetMapping("") public List<UserVo> getUserList(){ List<UserVo> userVoList = Lists.newArrayListWithCapacity(2); UserVoList. Add (UserVo. Builder (). The id (1 l). The name (" day GongYiBing "). The age (18). The build ()); userVoList.add(UserVo.builder().id(2L).name("tan").age(19).build()); return userVoList; }}Copy the code

Open a browser testing input address: http://localhost:8080/users/, we can see the JSON data returned to the List

Continue to add RESTful apis to query user information based on user ids

@GetMapping("/{id}") public UserVo getUserByName(@PathVariable Long id){ return UserVo. Builder (). The id (1 l). The name (" day GongYiBing "). The age (18). The build (); }Copy the code

Open a browser testing input address: http://localhost:8080/users/1, we can see that the return to a single User JSON data

Add an API that returns a value of type ResponseEntity

@GetMapping("/testResponseEntity") public ResponseEntity getUserByAge(){ return new ResponseEntity(uservo.builder ().id(1L).responseEntity (uservo.builder ().id(1L).responseEntity (uservo.builder ().id(1L).age(18).build(), httpstatus.ok); }Copy the code

Open a browser testing input address: http://localhost:8080/users/testResponseEntity, we can see the same returned to the single User JSON data

Anatomical realization process

I will explain the key parts clearly one by one. You need to go to the crime scene (open your OWN IDE) to solve the case.

The story begins with the @enableWebMVC annotation, which opens:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}Copy the code

By introducing the DelegatingWebMvcConfiguration @ Import annotations. The class, then to see this class:

@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    ...
}Copy the code

@ the Configuration notes, you should be very familiar with, but in this class is the superclass WebMvcConfigurationSupport hidden a key code:

@Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter(); . return adapter; }Copy the code

RequestMappingHandlerAdapter is the key to every request processing, to see the class definition:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
        implements BeanFactoryAware, InitializingBean {
    ...
}Copy the code

This class implements the InitializingBean interface, and MY “Where did I come from” in the Spring Bean lifecycle? Several keys to the initialization of Spring beans have been specified in this article. One of the keys is the afterPropertiesSet method of the InitializingBean interface. Rewrite the same method in 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); }}Copy the code

The method content is critical, but let’s see initControllerAdviceCache method, other content follow-up again separately:

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); }}}Copy the code

The ControllerAdvice annotation is scanned through the ControllerAdviceBean static method, but we are implementing the @RestControllerAdvice annotation. Open this annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {Copy the code

This annotation is marked by @ControllerAdvice and @responseBody, just as the @RestController annotation you’re familiar with is marked by @Controller and @responseBody

Now that you know how our Bean tagged with @RestControllerAdvice is loaded into the Spring context, how does Spring use our Bean and process the returned body

How does HttpMessageConverter convert data? This article has explained part of the article, I hope partners first read this article, the following part will be seconds to understand, we do further explanation here

In AbstractMessageConverterMethodProcessor writeWithMessageConverters method, 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; }}Copy the code

You can see that we are close to the truth by calling the beforeBodyWrite method with getAdvice()

protected RequestResponseBodyAdviceChain getAdvice() {
    return this.advice;
}Copy the code

RequestResponseBodyAdviceChain, see name with Chain, obviously with the help of the Chain of responsibility design pattern, the content is not clever in the Chain of responsibility design pattern Specified in article, only pass the chain of responsibility 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; }}Copy the code

The beforeBodyWrite method we overwrote will be called eventually, that’s it!!

Actually yet, have you ever wondered, if the return value is our API method org. Springframework. HTTP. ResponseEntity type, we can specify the HTTP status code returned, But will this return value be placed directly in the body argument of our beforeBodyWrite method? If doing so is clearly wrong because ResponseEntity contains a lot of our non-business data in it, what does Spring do for us?

In our approach to obtain the return value and before call beforeBodyWrite method, also select HandlerMethodReturnValueHandler used to handle different Handler to handle the return value

In the class HandlerMethodReturnValueHandlerComposite handleReturnValue method

@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);
}Copy the code

Call selectHandler to select the appropriate handler. Spring has a number of built-in handlers.

HttpEntityMethodProcessor is one of them, it overrides the supportsParameter method, supporting HttpEntity types, namely support ResponseEntity type:

@Override
public boolean supportsParameter(MethodParameter parameter) {
    return (HttpEntity.class == parameter.getParameterType() ||
            RequestEntity.class == parameter.getParameterType());
}Copy the code

So when we return types for ResponseEntity by HttpEntityMethodProcessor handleReturnValue method to deal with our results:

@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(); }Copy the code

This method extracts responseEntity.getBody() and passes a MessageConverter, then continues calling beforeBodyWrite method, this is the truth!!

This is where RESTful apis normally return content. In the next article, let’s take a look at how unified exception handling works and how it works

Soul asking

  1. What handler is used when the return value is of non-responseEntity type? What return value types does it support? You might know why you’re using it, right@ResponseBodyNote the
  2. Have you tracked the whole process of DispatchServlet requests?

Efficiency tools

Recommended reading

  • Can only use Git pull? Sometimes you can try something a little more elegant
  • Parental assignment model: large factory high frequency interview questions, easy to fix
  • The interview doesn’t know the difference between BeanFactory and ApplicationContext.
  • How to design a good RESTful API
  • Program ape why to see the source code?

——–

Welcome to continue to pay attention to the public account: “One soldier of The Japanese Arch”

– Sharing cutting-edge Java technology dry goods

– efficient tool summary | back “tool”

– Interview question analysis and solution

– technical data to receive reply | “data”

To read detective novel thinking easy fun learning Java technology stack related knowledge, in line with the simplification of complex problems, abstract problems concrete and graphical principles of gradual decomposition of technical problems, technology continues to update, please continue to pay attention to……