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

In the previous article (how to elegantly implement Spring Boot interface parameters encryption and decryption?) We used ResponseBodyAdvice and RequestBodyAdvice when we preprocessed/reprocessed request/response data. ResponseBodyAdvice can realize the secondary processing of the response data, where the response data can be encrypted/packaged and other operations. 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;
}
Copy the code
  • SupportsReturnType: Whether the handler supports the corresponding return value type.
  • HandleReturnValue: Processes the method return value.

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, set String to the viewName property of mavContainer and determine if the String is a redirected String. If so, The redirectModelScenario property of mavContainer is set to true, which is a sign that the processor returns the redirected view.

ViewMethodReturnValueHandler

This handler is used to handle cases where the return value is View. If the return value is View, set the View to the View property of the mavContainer and determine whether the View is redirected. If so, The redirectModelScenario property of mavContainer is set to true, which is a sign that the processor returns the redirected view.

MapMethodProcessor

This handler handles cases where the return value type is Map by adding the Map to the Model property of mavContainer.

StreamingResponseBodyReturnValueHandler

This is used to handle the return value of StreamingResponseBody or ResponseEntity

type.

DeferredResultMethodReturnValueHandler

This handles the return values of type DeferredResult, ListenableFuture, and CompletionStage for asynchronous requests.

CallableMethodReturnValueHandler

Handles return values of type Callable, also used for asynchronous requests.

HttpHeadersReturnValueHandler

This handles the return value of HttpHeaders by setting true to the requestHandled property in mavContainer, which indicates whether the request has been processed (if so, stop there, I won’t look for the view later), and then add HttpHeaders to the response header.

ModelMethodProcessor

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

ModelAttributeMethodProcessor

This is used for handling return value types with the @ModelAttribute added and, if the annotaionNotRequired property is true, for the guidance of other non-generic types of return values.

ServletModelAttributeMethodProcessor

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

ResponseBodyEmitterReturnValueHandler

This is used when the return value is ResponseBodyEmitter.

ModelAndViewMethodReturnValueHandler

This handles the case where the return value type is ModelAndView and sets Model and View in the return value to the corresponding property of the mavContainer.

ModelAndViewResolverMethodReturnValueHandler

The supportsReturnType method of this returns true, meaning it can handle all types of return values, which is usually left in the back pocket.

AbstractMessageConverterMethodProcessor

This is an abstract class whose subclasses are used when a return value needs to be converted via 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 is not RequestEntity.

AsyncHandlerMethodReturnValueHandler

This interface is empty, and no typical application scenario is found.

AsyncTaskMethodReturnValueHandler

This is used to handle cases where the return value type is WebAsyncTask.

HandlerMethodReturnValueHandlerComposite

Look at Composite, it’s a Composite processor, nothing to say.

This is the system default HandlerMethodReturnValueHandler.

So in the introduction above, you’ve seen a component called mavContainer repeatedly, and I’m going to tell you about that as well.

2.ModelAndViewContainer

The ModelAndViewContainer is a data shuttle bus that transfers data throughout the request process. The name of the container indicates that it holds two types of data, Model and View, 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;
}
Copy the code

With these attributes in mind, ModelAndViewContainer is basically what it does:

  • DefaultModel: The Model used by default. When we reuse Model, ModelMap, or Map in interface parameters, the final implementation class we use is BindingAwareModelMap, corresponding to defaultModel.
  • RedirectModel: The Model for redirection. If we use a parameter of type RedirectAttributes in the interface parameter, the redirectModel will eventually be passed in.

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

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

RedirectModelScenario indicates whether the processor returns the Redirect view. Whether ignoreDefaultModelOnRedirect said ignore defaultModel when redirected, so the logic is this:

  1. If redirectModelScenario is true, that is, the processor returns a redirected view, then redirectModel is used. If redirectModelScenario is false, that is, if the processor is not returning a redirected view, then defaultModel is used.
  2. If redirectModel is null, and ignoreDefaultModelOnRedirect to false, then use redirectModel, otherwise use defaultModel.

The remaining parameters are as follows:

  • View: The returned view.
  • Status: indicates the HTTP status code.
  • NoBinding: Whether to bind the corresponding attributes of the data model declared by @modelAttribute (binding=true/false).
  • BindingDisabled: An attribute that does not require data binding.
  • SessionStatus: SessionAttribute Use completion identifier.
  • RequestHandled: Indicates that the request processing is complete (for example, added@ResponseBodyAnnotated interface, this property is true, the request will not go to the view).

This ModelAndViewContainer friends just do an understanding, Songko in the later source analysis, and we will talk about this component again.

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

3.API interface data packaging

Suppose I have a requirement that I want to wrap a layer around the original returned data. For a simple example, the original 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");
        returnuser; }}Copy the code

The returned data format is as follows:

{"username":"javaboy"."address":"www.javaboy.org"}
Copy the code

Now I want the data format returned to look like this:

{"status":"ok"."data": {"username":"javaboy"."address":"www.javaboy.org"}}
Copy the code

Let’s take a look at how to implement this simple requirement.

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);
}
Copy the code
  • SupportsReturnType: As you can see from this method, it is supported@ResponseBodyInterface for annotations.
  • HandleReturnValue: So here’s the logic. You set the requestHandled property to true in mavContainer, which means we’re done, we don’t have to go to the view anymore, and then you get the inputMessage and outputMessage, 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, The JSON data is then written out using HttpMessageConverter.

With this knowledge in hand, 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); }}Copy the code

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

Custom ReturnValueConfig implements the InitializingBean interface, and the afterPropertiesSet method is automatically called. 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");
        returnuser; }}public class User {
    private String username;
    private String address;
    // omit others
}
Copy the code

Once the configuration is complete, you can start the project.

After the project is successfully started, 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 songgo before sharing how to gracefully implement Spring Boot interface parameters encryption and decryption? , you can also use the scheme in this article, or you can even implement custom filters.

The content of this article is a little bit more, I don’t know if you have found that Songgo recently sent a lot of SpringMVC source code related things, yes, this article is actually a part of Songgo SpringMVC source code analysis, in order to source code analysis is not so boring, so forced to add a case in, I wish friends happy learning ~