preface

The responseBodyAdvice interface can process the return value of the Handler method before writing it to the Response, for example by encapsulating the return value as an object agreed with the client for the client to process the response data. This article will learn how to use responseBodyAdvice and how it works.

SpringBoot version: 2.4.1

The body of the

1. Use of ResponseBodyAdvice

Suppose a Controller already exists, as shown below.

@RestController
public class LoginController {

    private static final String DATE_STRING = "20200620";

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");

    private final Student student;

    public LoginController() {
        student = new Student();
        student.setName("Lee");
        student.setAge(20);
        student.setSex("male");
        try {
            student.setDate(dateFormat.parse(DATE_STRING));
        } catch (ParseException e) {
            System.out.println(e.getMessage());
        }
    }

    @RequestMapping(value = "/api/v1/student/name", method = RequestMethod.GET)
    public ResponseEntity<Object> getStudentByName(@RequestParam(name = "name") String name) {
        if (student.getName().equals(name)) {
            return new ResponseEntity<>(student, HttpStatus.OK);
        } else {
            return new ResponseEntity<>(String.format("get student failed by name: %s", name), HttpStatus.BAD_REQUEST);
        }
    }

    @RequestMapping(value = "/api/v1/student/age", method = RequestMethod.GET)
    public Student getStudentByAge(@RequestParam(name = "age") int age) {
        if (student.getAge() == age) {
            return student;
        } else {
            return null;
        }
    }

}

@Data
public class Student {

    private String name;
    private int age;
    private String sex;
    private Date date;

}

There are two methods in the Controller above and the return values are responseEntity and Student respectively. At this time, after the client receives the response, the processing for the response body becomes very inconvenient. If more methods are added and the return value is different, the client will need to process the response body specifically according to different requests. Therefore, in order to facilitate the client to process the response data, the server side specially creates a ReturnResult class, ReturnResult, and stipulates that all the response body written into the response after the execution of the handler method on the server side must be ReturnResult. In this case, the ResponseBodyAdvice can be used to easily implement the above requirement without modifying the existing business code. Assume that the custom ReturnResult class, ReturnResult, looks like this.

@Data public class ReturnResult<T> { private int statusCode; private T body; public ReturnResult() {} public ReturnResult(T body) { this.body = body; }}

The body of returnResult is the response content that originally needs to be written in response. Now the whole returnResult is the response content that needs to be written in response, which is equivalent to returnResult wrapping the return value of Handler method.

Now create a ReturnResultAdvice class and implement the ResponseBodyAdvice interface, as shown below.

@ControllerAdvice public class ReturnResultAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(@Nullable MethodParameter returnType, @Nullable Class converterType) { return true; } @Override public Object beforeBodyWrite(Object body, @Nullable MethodParameter returnType, @Nullable MediaType selectedContentType, @Nullable Class selectedConverterType, @Nullable ServerHttpRequest request, @Nullable ServerHttpResponse response) { if (body == null) { return null; } if (body instanceof ReturnResult) { return body; } return new ReturnResult<>(body); }}

The beforeBodyWrite() method of returnResultAdvice is called before the return value of the handler method is written to the response.

The next step is to simulate a client making a request in a unit test.

@ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles class LoginControllerTest { private static final ObjectMapper MAPPER = new ObjectMapper(); private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd"); @Autowired private TestRestTemplate restTemplate; @Test void givenName_whenGetStudentByNameAndStudentConvertedToReturnResultByResponseBodyAdvice_thenGetStudentSuccess() throws Exception { String name = "Lee"; String url = "/api/v1/student/name? name=" + name; ResponseEntity<ReturnResult> response = restTemplate.getForEntity(url, ReturnResult.class); assertThat(response.getBody() ! = null, is(true)); ReturnResult<Student> returnResult = MAPPER.readValue( MAPPER.writeValueAsString(response.getBody()), new TypeReference<ReturnResult<Student>>() {}); Student student = returnResult.getBody(); assertThat(student ! = null, is(true)); assertThat(student.getName(), is("Lee")); assertThat(student.getAge(), is(20)); assertThat(student.getSex(), is("male")); assertThat(student.getDate(), is(DATE_FORMAT.parse("20200620"))); } @Test void givenAge_whenGetStudentByAgeAndStudentConvertedToReturnResultByResponseBodyAdvice_thenGetStudentSuccess() throws Exception { int age = 20; String url = "/api/v1/student/age? age=" + age; ResponseEntity<ReturnResult> response = restTemplate.getForEntity(url, ReturnResult.class); assertThat(response.getBody() ! = null, is(true)); ReturnResult<Student> returnResult = MAPPER.readValue( MAPPER.writeValueAsString(response.getBody()), new TypeReference<ReturnResult<Student>>() {}); Student student = returnResult.getBody(); assertThat(student ! = null, is(true)); assertThat(student.getName(), is("Lee")); assertThat(student.getAge(), is(20)); assertThat(student.getSex(), is("male")); assertThat(student.getDate(), is(DATE_FORMAT.parse("20200620"))); }}

Run the test program and the assertions all pass.

Finally, two points are given to explain the whole example.

  • althoughLoginControllerthegetStudentByName()The return value of theResponseEntity<Object>, but the actual response body content written to response isResponseEntityIn this example, the body corresponds to createResponseEntityObject is passed in as Student. soReturnResultAdviceTo deal withgetStudentByName()Method is actually processed when the return value is returnedStudentObject. The principle will be explained in the second section;
  • In the unit test program,MAPPER.readValue(MAPPER.writeValueAsString(...) ,...).This is used because deserializing the response content from Response to an object with a generic parameter deserializes the generic content in the object toLinkHashMap, so with the help ofObjectMapperandTypeReferenceTo get it directlyStudentObject.

Section:@ControllerAdviceAnnotations are decorated and implementedResponseBodyAdviceClass of the interfacebeforeBodyWrite()Method is called before the handler method return value is written to response, and the handler method return value is passed in as an input parameterbeforeBodyWrite(), so that some customization can be done to the return value before it is written to Response, such as a layer of encapsulation of the return value.

2. The principle of ResponseBodyAdvice

Let’s first explain why the getStudentByName() method of the LoginController in the first section returns a value of type responseEntity , However, the body of response written to response is the body of responseEntity. First all ResponseBodyAdvice interface call is occurred in AbstractMessageConverterMethodProcessor writeWithMessageConverters (), the method statement as shown below.

protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
            ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
            throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException

The value is the value that needs to be written to the response body and is the value that the responseBodyAdvice will process. Then if the return value of the handler method is a non-responseEntity object and the handler method is decorated with the @responseBody annotation, Then writeWithMessageConverters () call in RequestResponseBodyMethodProcessor# handleReturnValue (); If the return value of the handler method is ResponseEntity object, then writeWithMessageConverters () call in HttpEntityMethodProcessor# handleReturnValue (), Respectively, have a look at these two methods invokes the writeWithMessageConverters () of the incoming parameters, can explain questions before.

RequestResponseBodyMethodProcessor#handleReturnValue()

public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

    ...

    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

HttpEntityMethodProcessor#handleReturnValue()

public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { ...... HttpEntity<? > responseEntity = (HttpEntity<? >) returnValue; . writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage); . }

We now begin our analysis of the principles of the responseBodyAdvice. All known ResponseBodyAdvice interface call is occurred in AbstractMessageConverterMethodProcessor writeWithMessageConverters (), some of its source code is shown below.

protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { ...... if (selectedMediaType ! = null) { selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter<? > converter : this.messageConverters) { GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<? >) converter : null); if (genericConverter ! = null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, SelectedMediaType)) {// The responseBodyAdvice call occurs here body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<? >>) converter.getClass(), inputMessage, outputMessage); if (body ! = null) { Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); addContentDispositionHeader(inputMessage, outputMessage); if (genericConverter ! = null) { genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug("Nothing to write: null body"); } } return; }}}... }

AbstractMessageConverterMethodProcessor getAdvice () method returns the loading good RequestResponseBodyAdviceChain object in the constructor, Look at the below RequestResponseBodyAdviceChain beforeBodyWrite () method.

public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType, Class<? extends HttpMessageConverter<? >> converterType, ServerHttpRequest request, ServerHttpResponse response) { return processBody(body, returnType, contentType, converterType, request, response); } private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType, Class<? extends HttpMessageConverter<? >> converterType, ServerHttpRequest request, ServerHttpResponse) {// Get the responseBodyAdvice for (responseBodyAdvice <? > advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) { if (advice.supports(returnType, // Execute the beforeBodyWrite() method of responseBodyAdvice to handle the return value of handler method body = ((responseBodyAdvice <T>)) {// Execute the beforeBodyWrite() method of responseBodyAdvice to handle the return value of handler method body = ((responseBodyAdvice <T>) advice).beforeBodyWrite((T) body, returnType, contentType, converterType, request, response); } } return body; } private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? Extends A availableAdvice {List<Object availableAdvice = GetAdvice (adviceType); if (CollectionUtils.isEmpty(availableAdvice)) { return Collections.emptyList(); } List<A> result = new ArrayList<>(availableAdvice.size()); for (Object advice : AvailableAdvice {// Advice if (advice instanceof controllerAdviceBean) { ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice; // determine if the responseBodyAdvice is applicable to the current handler if (! adviceBean.isApplicableToBeanType(parameter.getContainingClass())) { continue; } advice = adviceBean.resolveBean(); } if (adviceType.isAssignableFrom(advice.getClass())) { result.add((A) advice); } } return result; }

In RequestResponseBodyAdviceChain beforeBodyWrite () method calls the processBody () method, The processBody() method iterates through all responseBodyAdvice that is loaded and applicable to the current handler and executes, so that all responseBodyAdvice interfaces modified by the @ControllerAdvice annotation are executed here.

Section:@ControllerAdviceannotatedResponseBodyAdviceThe interface is loaded into the SpringMVC frameworkRequestResponseBodyMethodProcessorandHttpEntityMethodProcessorOf the two return value handlers, when the two return value handlers write the return value to response, applied to the current handlerResponseBodyAdviceThe interface is called so that the return value can be customized.

ResponseBodyAdvice is loaded

As can be seen from the second section, Because RequestResponseBodyMethodProcessor and HttpEntityMethodProcessor both will return value processor by @ ControllerAdvice annotation modified ResponseBodyAdvice interface to load Before writing the return value to response, we can call these ResponseBodyAdvice interfaces to do something about the return value before writing it to response. This section will then learn about loading the responseBodyAdvice interface.

First gives the conclusion that ResponseBodyAdvice loading in RequestMappingHandlerAdapter afterPropertiesSet () method.

Known, RequestMappingHandlerAdapter InitializingBean interface is achieved, thus RequestMappingHandlerAdapter implements the afterPropertiesSet () method. This method is implemented as follows.

Public void afterPropertiesSet() {// Load ControllerAdviceBean (@ControllerAdvice) 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) { Here will finish RequestResponseBodyMethodProcessor and HttpEntityMethodProcessor initialization, Initialization will be completed at the same time ResponseBodyAdvice interface loading List < HandlerMethodReturnValueHandler > handlers = getDefaultReturnValueHandlers (); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); }}

The above implementation, initControllerAdviceCache () loads ControllerAdviceBean related content to the RequestMappingHandlerAdapter, This includes the responseBodyAdvice interface decorated with the @ControllerAdvice annotation. Then in getDefaultReturnValueHandlers () method will create a return value in the processor, They have been used in creating RequestResponseBodyMethodProcessor and HttpEntityMethodProcessor loading good ResponseBodyAdvice interface to complete the two return values processor initialization. The partial source code for both methods is shown below.

initControllerAdviceCache()

private void initControllerAdviceCache() { if (getApplicationContext() == null) { return; } // Get the @ControllerAdvice annotation for the bean List<ControllerAdviceBean> AdviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); List<Object> requestResponseBodyAdviceBeans = new ArrayList<>(); for (ControllerAdviceBean adviceBean : adviceBeans) { Class<? > beanType = adviceBean.getBeanType(); if (beanType == null) { throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean); } Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS); if (! attrMethods.isEmpty()) { this.modelAttributeAdviceCache.put(adviceBean, attrMethods); } Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS); if (! binderMethods.isEmpty()) { this.initBinderAdviceCache.put(adviceBean, binderMethods); } // If ControllerAdviceBean implements the responseBodyAdvice interface, Then the ControllerAdviceBean needs to be loaded into the requestResponseBodyAdvice if (RequestBodyAdvice. Class. IsAssignableFrom (beanType) | | ResponseBodyAdvice.class.isAssignableFrom(beanType)) { requestResponseBodyAdviceBeans.add(adviceBean); } } if (! requestResponseBodyAdviceBeans.isEmpty()) { this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans); }... }

getDefaultReturnValueHandlers()

private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() { List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20); . / / create and load HttpEntityMethodProcessor handlers. Add (new HttpEntityMethodProcessor (getMessageConverters (), this.contentNegotiationManager, this.requestResponseBodyAdvice)); . / / create and load RequestResponseBodyMethodProcessor handlers. Add (new RequestResponseBodyMethodProcessor (getMessageConverters (), this.contentNegotiationManager, this.requestResponseBodyAdvice)); . return handlers; }

According to getDefaultReturnValueHandlers () method, in creating HttpEntityMethodProcessor or RequestResponseBodyMethodProcessor, Will load RequestMappingHandlerAdapter good ResponseBodyAdvice into the constructor, and, Whether HttpEntityMethodProcessor or RequestResponseBodyMethodProcessor, Its constructor can eventually call to the parent class AbstractMessageConverterMethodArgumentResolver constructors, And in which initializes a RequestResponseBodyAdviceChain to complete ResponseBodyAdvice loading. The constructor source is shown below.

HttpEntityMethodProcessor#HttpEntityMethodProcessor()

public HttpEntityMethodProcessor(List<HttpMessageConverter<? >> converters, @Nullable ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) { super(converters, manager, requestResponseBodyAdvice); }

AbstractMessageConverterMethodProcessor#AbstractMessageConverterMethodProcessor()

protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<? >> converters, @Nullable ContentNegotiationManager manager, @Nullable List<Object> requestResponseBodyAdvice) { super(converters, requestResponseBodyAdvice); this.contentNegotiationManager = (manager ! = null ? manager : new ContentNegotiationManager()); this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions()); this.safeExtensions.addAll(SAFE_EXTENSIONS); }

AbstractMessageConverterMethodArgumentResolver#AbstractMessageConverterMethodArgumentResolver()

public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<? >> converters, @Nullable List<Object> requestResponseBodyAdvice) { Assert.notEmpty(converters, "'messageConverters' must not be empty"); this.messageConverters = converters; this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters); this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice); }

Section:RequestMappingHandlerAdapterIt will be implemented thereafterPropertiesSet()Method to load by@ControllerAdviceannotatedResponseBodyAdviceThe interface will then be created and the return value handler will be loaded during the creationRequestResponseBodyMethodProcessorandHttpEntityMethodProcessorThese two return values will be passed in by the processor when loadedResponseBodyAdvice, which completes the loading of responseBodyAdvice.

conclusion

If you need to use responseBodyAdvice to handle the return value of the handler method, you need to create a class and implement the responseBodyAdvice interface, and the class needs to be decorated with the @ControllerAdvice annotation. Such a ResponseBodyAdvice interface will be RequestMappingHandlerAdapter load, And the initialization RequestResponseBodyMethodProcessor and HttpEntityMethodProcessor both return value when the processor is loaded, finally passed the two return values processor will return the value written response before, The beforeBodyWrite() method of the responseBodyAdvice interface loaded is called by the return value handler, completing the customization of the return value.