Spring boot implements RESTful API to return unified data format

Keywords: Spring JSON

There are two aspects to Spring's global processing:

  1. Unified data return format

  2. 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

 

115 original articles published, 58 praised, 240000 visitors+
Private letter follow

Posted by plugnz on Mon, 02 Mar 2020 19:35:20 -0800