Again today to introduce you to a Spring components – HandlerMethodReturnValueHandler in the Boot.

In the previous article (how to elegantly implement the Spring Boot interface parameters encryption and decryption?) , Songo has already shown you how to pre-process/re-process request/response data, and we used responseBodyAdvice and RequestBodyAdvice. The responseBodyAdvice is a secondary processing of the response data, where you can encrypt/wrap the response data, etc. But that is not the only solution, loose elder brother today to and introduce a more flexible – HandlerMethodReturnValueHandler, we take a look at.

1.HandlerMethodReturnValueHandler

HandlerMethodReturnValueHandler role is the processing results of the processor and then to a secondary processing, the interface is punctuated with two methods:

public interface HandlerMethodReturnValueHandler {
    boolean supportsReturnType(MethodParameter returnType);
    void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
  • SupportsReturnType: Whether the corresponding return value type is supported by this handler.
  • HandleTurnValue: Handles the return value of the method.

HandlerMethodReturnValueHandler has a lot of the default implementation class, we’ll look at:

Let’s take a look at what these implementation classes do:

ViewNameMethodReturnValueHandler

This handler is used to handle cases where the return values are void and String. If the return value is void, no processing is done. If the return value is String, we assign String to the viewName property of mavContainer, and determine if the String is a redirected String. If so, we assign String to the viewName property of mavContainer The redirectModelScenario property of the MavContainer is set to true, which is the signal that the processor returns the redirect view.

ViewMethodReturnValueHandler

This handler is used to handle cases where the return value is View. If the return value is View, we assign the View to the MavContainer’s View property and determine if the View is a redirected View. If so, we assign the View to the MavContainer’s View property. The redirectModelScenario property of the MavContainer is set to true, which is the signal that the processor returns the redirect view.

MapMethodProcessor

This handler is used to handle cases where the return value type is Map by adding the Map to the model property of the MavContainer.

StreamingResponseBodyReturnValueHandler

This handles the return value of type > of streamingResponseBody or responseEntity < streamingResponseBody.

DeferredResultMethodReturnValueHandler

This handles the return value of DeferredResult, ListenableFuture, and CompletionStage types for asynchronous requests.

CallableMethodReturnValueHandler

Handles the return value of type Callable, also for asynchronous requests.

HttpHeadersReturnValueHandler

This handles the return value of the HttpHeaders type by setting the requestHttpHeaders property in the MavContainer to true, which indicates whether the request has been processed (if so, stop there, I won’t go back to the view), and then add HttpHeaders to the response header.

ModelMethodProcessor

This is used to handle cases where the return value type is Model by adding the Model to the Model of MavContainer.

ModelAttributeMethodProcessor

This was used for handling return value types with the @ModelAttribute annotation added, and could also be used for handling return values of other non-generic types if the AnnotaionNotreguidance attribute was true.

ServletModelAttributeMethodProcessor

As above, this class only modifies the way parameters are resolved.

ResponseBodyEmitterReturnValueHandler

This is used when the return value type is responseBodyEmitter.

ModelAndViewMethodReturnValueHandler

This is used to handle the case where the return value type is ModelAndView, and the Model and View in the return value are set to the corresponding properties of the MavContainer.

ModelAndViewResolverMethodReturnValueHandler

The supportsReturnType method for this one returns true, meaning it can handle all types of return values, which is usually left at the back of the bag.

AbstractMessageConverterMethodProcessor

This is an abstract class whose subclasses are used when the return value needs to be transformed through HttpMessageConverter. This abstract class mainly defines some utility methods.

RequestResponseBodyMethodProcessor

This handles the return value type with the @responseBody annotation added.

HttpEntityMethodProcessor

This is used to handle cases where the return value type is HttpEntity and not RequestEntity.

AsyncHandlerMethodReturnValueHandler

This is an empty interface and no typical usage scenarios have been found yet.

AsyncTaskMethodReturnValueHandler

This handles cases where the return value type is WeBasyncTask.

HandlerMethodReturnValueHandlerComposite

If you look at Composite, it’s a Composite processor. There’s nothing to say.

This is the system default HandlerMethodReturnValueHandler.

In the previous introduction, you saw a component called MavContainer repeatedly mentioned, which I would like to introduce to you.

2.ModelAndViewContainer

The Model and ViewContainer is a data shuttle bus that transfers data throughout the request. As the name implies, it holds Model and View data, but there are more than two types. Let’s look at the definition of ModelAndViewContainer:

public class ModelAndViewContainer {
    private boolean ignoreDefaultModelOnRedirect = false;
    @Nullable
    private Object view;
    private final ModelMap defaultModel = new BindingAwareModelMap();
    @Nullable
    private ModelMap redirectModel;
    private boolean redirectModelScenario = false;
    @Nullable
    private HttpStatus status;
    private final Set<String> noBinding = new HashSet<>(4);
    private final Set<String> bindingDisabled = new HashSet<>(4);
    private final SessionStatus sessionStatus = new SimpleSessionStatus();
    private boolean requestHandled = false;
}

With these properties in mind, you can basically see what modelAndViewContainer does:

  • DefaultModel: The Model used by default. When we reuse Model, ModelMap, or Map in interface parameters, we end up using BindingAwareModelMap as the implementation class and DefaultModel as the corresponding one.
  • RedirectModel: The redirectModel. If we used a redirectAttributes type parameter in the interface parameter, the redirectModel will eventually be passed in.

As you can see, there are two models. Which Model do we use? This returns the appropriate Model in the getModel method based on the condition:

public ModelMap getModel() { if (useDefaultModel()) { return this.defaultModel; } else { if (this.redirectModel == null) { this.redirectModel = new ModelMap(); } return this.redirectModel; } } private boolean useDefaultModel() { return (! this.redirectModelScenario || (this.redirectModel == null && ! this.ignoreDefaultModelOnRedirect)); }

Here, the RedirectModelScenario indicates whether or not the handler returns the Redirect view; Whether ignoreDefaultModelOnRedirect said ignore defaultModel when redirected, so the logic is this:

  1. If the RedirectModelScenario is true, that is, the processor returns a redirect view, then the RedirectModel is used. If the RedirectModelScenario is false, that is, the processor does not return a redirected view, then DefaultModel is used.
  2. If redirectModel is null, and ignoreDefaultModelOnRedirect to false, then use redirectModel, otherwise use defaultModel.

This leaves the following parameters:

  • View: The returned view.
  • Status: HTTP status code.
  • NoBinding: Whether to bind the corresponding attribute of the data model declared by @ModelAttribute(binding=true/false).
  • BindingDisabled: A property that does not require data binding.
  • SessionStatus: SessionAttribute uses the completed identity.
  • RequestHandled: Indicates that the request has been processed (for example, added)@ResponseBodyThis attribute is true, so the request doesn’t go to the view again.

We will talk about this component again in the source code analysis later.

Next, we also customize a HandlerMethodReturnValueHandler, to feel the HandlerMethodReturnValueHandler basic usage.

3.API interface data packaging

Let’s say I have a requirement that I want to wrap another layer around the original returned data. For a simple example, the interface would look like this:

@RestController
public class UserController {
    @GetMapping("/user")
    public User getUserByUsername(String username) {
        User user = new User();
        user.setUsername(username);
        user.setAddress("www.javaboy.org");
        return user;
    }
}

The format of the returned data is as follows:

{"username":"javaboy","address":"www.javaboy.org"}

Now I want to return data in a format like this:

{"status":"ok","data":{"username":"javaboy","address":"www.javaboy.org"}}

With such a simple requirement, let’s take a look at how to implement it.

3.1 RequestResponseBodyMethodProcessor

Before start defining introduce RequestResponseBodyMethodProcessor for everyone, this is one of HandlerMethodReturnValueHandler implementation class, this is mainly used to process the return JSON.

Let’s look at it a little bit:

@Override
public boolean supportsReturnType(MethodParameter returnType) {
    return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
            returnType.hasMethodAnnotation(ResponseBody.class));
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    mavContainer.setRequestHandled(true);
    ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
  • SupportsReturnType: As you can see from this method, it is supported@ResponseBodyThe interface to the annotation.
  • HandleReturnValue: So here’s the processing logic. First of all, in MavContainer, we set the RequestHandLed property to true, which means we’re done, we don’t have to look for the view anymore, and then we get the InputMessage and the OutputMessage, respectively. Call writeWithMessageConverters method for output, writeWithMessageConverters method is defined in the parent class method, this method is longer, the core logic is invoked to determine output data, determine the MediaType, Then write the JSON data out via HttpMessageConverter.

With this knowledge in hand, then we can implement it ourselves.

3.2 Specific Implementation

First of all a HandlerMethodReturnValueHandler custom:

public class MyHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler { private HandlerMethodReturnValueHandler handler; public MyHandlerMethodReturnValueHandler(HandlerMethodReturnValueHandler handler) { this.handler = handler; } @Override public boolean supportsReturnType(MethodParameter returnType) { return handler.supportsReturnType(returnType); } @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { Map<String, Object> map = new HashMap<>(); map.put("status", "ok"); map.put("data", returnValue); handler.handleReturnValue(map, returnType, mavContainer, webRequest); }}

Because we need to do function is implemented based on RequestResponseBodyMethodProcessor, because support @ ResponseBody, output JSON those things are the same, we just modify the data before output. So I here defines an attribute HandlerMethodReturnValueHandler directly, this attribute instance is RequestResponseBodyMethodProcessor, SupportsReturnType method according to the request of RequestResponseBodyMethodProcessor, in handleReturnValue approach, we first, a pretreatment of return value, Then call RequestResponseBodyMethodProcessor# handleReturnValue methods continue to output a JSON.

Then there is configuration MyHandlerMethodReturnValueHandler into law. Due to the HandlerAdapter for SpringMVC at the time of loading have configured HandlerMethodReturnValueHandler (after the scene and the analysis of all relevant source code). So we can through the following ways to configured RequestMappingHandlerAdapter, modified as follows:

@Configuration public class ReturnValueConfig implements InitializingBean { @Autowired RequestMappingHandlerAdapter requestMappingHandlerAdapter; @Override public void afterPropertiesSet() throws Exception { List<HandlerMethodReturnValueHandler> originHandlers = requestMappingHandlerAdapter.getReturnValueHandlers(); List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<>(originHandlers.size()); for (HandlerMethodReturnValueHandler originHandler : originHandlers) { if (originHandler instanceof RequestResponseBodyMethodProcessor) { newHandlers.add(new MyHandlerMethodReturnValueHandler(originHandler)); }else{ newHandlers.add(originHandler); } } requestMappingHandlerAdapter.setReturnValueHandlers(newHandlers); }}

A custom ReturnValueConfig implements the InitializingBean interface, and the afterPropertiesSet method is called automatically. In this method, We will be configured RequestMappingHandlerAdapter HandlerMethodReturnValueHandler carry out each check, If the type is RequestResponseBodyMethodProcessor, rebuild, use our custom MyHandlerMethodReturnValueHandler instead of it, Finally give requestMappingHandlerAdapter resetting HandlerMethodReturnValueHandler can.

Finally, provide a test interface:

@RestController public class UserController { @GetMapping("/user") public User getUserByUsername(String username) { User  user = new User(); user.setUsername(username); user.setAddress("www.javaboy.org"); return user; } } public class User { private String username; private String address; // omit other}

After the configuration is complete, you are ready to start the project.

After successful startup of the project, access the /user interface as follows:

Perfect.

4. Summary

In fact, there are many ways to unify the API interface response format, you can refer to Songge previously shared how to achieve the elegant Spring Boot interface parameters encryption and decryption? , you can use the scheme in this article, or you can even customize the filter implementation.

This article is a bit too much, I don’t know if you have noticed that Songgo has posted a lot of SpringMVC source code analysis recently, yes, this article is actually part of the source parsing of Songgo SpringMVC, in order to make the source parsing not boring, so force a case to come in, wish you have fun learning ~