preface

A project already has a fixed message format for external users. However, it needs to be compatible with the interface request and return format of another system. The resolution of the request is also inconsistent, but the point here is to deal with the return value, so the request is ignored. Let’s look at the differences between the original and current compatible return values:

  • Old: JSON plaintext, RSA signature
  • New: Base6 + URLEncode formatting (hereafter called encryption), RSA signature

Spring (SpringMVC) provides a number of entry points to implement this requirement. I’ll briefly talk about the various implementations using my own experience and source code. Here are a few things you might learn after reading this article:

  • What steps does an HTTP request go through in SpringMVC?
  • In which class does SpringMVC transform the message?
  • How do I customize the implementationHandlerMethodReturnValueHandler,HttpMessageConverter? What should I pay attention to?
  • How do I customize the implementation@ResponseBodyAnnotate the corresponding functionality
  • ControllerAdviceWhat is the function? And which step is implemented in the source code?
  • How do I implement custom output using Filter, AOP, and so on

You can probably get an idea of how Java Web implementations can respond to changes in content, form, and so on, and pick the right way to meet the actual business in a similar scenario.

Fixed call implementation

The simplest and most straightforward approach we can think of is to implement this in code with logical calls, with a unified parent class that accepts the return from the business layer, and a fixed call to the signing and encryption methods in each Controller method. As follows:

public class NewOpenApiController {
    @Autowired
    private NewService newService;

    @PostMapping(value = {"/var/{action}"})
    @ResponseBody
    public NewBaseResponseVo webapi(@PathVariable("action") String action) {
        return doSignAndCodec(service.progress());
    }

    private NewBaseResponseVo doSignAndCodec(NewBaseResponseVo responseVo) {
        // do sign
        // do codec
        returnresponseVo; }}Copy the code

If our Controller provides 100 methods, we need to write the call at least 100 times, although we can use @pathvariable to minimize the number of interface methods on the Controller layer. But calling this action is still repetitive, which is really not elegant, so we’ll leave it as a final compromise.

Second, AOP interceptor

We imagine that we can separate the signature and encryption methods from the business logic, the business layer just returns itself, and the rest of the signature encryption is implemented in another class. Spring’s AOP capabilities (logging facets, permission controls, and so on) come to mind. Enhancements to specified methods can be made with simple annotations to isolate some common aspect logic from the business. As follows:

@Aspect
@Component
public class NewOpenApiAspect {
    @Around("execution(* com.xxx.xxx.Controller.. *. * (..) )"
    public String handleResponse(ProceedingJoinPoint pjp) {
        // Customize signature and encryption logic
        return "Processed response message"; }}Copy the code

This is not enough. In Spring, Aspect interception takes precedence over SpringMVC’s convert operation on the result value, and the output and formatting of the result is ultimately done in SpringMVC’s built-in converter.

The convert operation on the result value is already bound when the Spring container starts. If the return value of our Controller method is still declared as NewBaseResponseVo, we dynamically change it to String at run time using AOP. That will generate an error when executed. To avoid this problem, we also need to change the return value of the Controller method to String as follows:

public class NewOpenApiController {
    @Autowired
    private NewService newService;

    @PostMapping(value = {"/var/{action}"})
    @ResponseBody
    public String webapi(@PathVariable("action") String action) {
        return doformat(service.progress());
    }

    private String doformat(NewBaseResponseVo responseVo) {
        // Format the entity class to String
        returnresponseVo; }}Copy the code

At this point we are ready to implement the requirements. Although the signature encryption logic is isolated, there are format operations at the Controller layer in addition to calling the business layer logic, which is not much more elegant than the first approach. Let’s look at some other methods.

Third, based on@ControllerAdvice

If the requirement is just to sign the return message, the @ControllerAdvice annotation will suffice

@ControllerAdvice(assignableTypes = {NewOpenApiController.class})
public class NewOpenApiResponseAdvice implements ResponseBodyAdvice<NewBaseResponseVo> {
    @Override
    public boolean supports(MethodParameter t, Class
       > c) {
        return true;
    }
    
    @Override
    public NewBaseResponseVo beforeBodyWrite(NewBaseResponseVo body, MethodParameter mp, MediaType mt, Class
       > t, ServerHttpRequest r, ServerHttpResponse s) {
        // Customize signature and encryption logic
        returnbody; }}Copy the code

Custom class that implements the ResponseBodyAdvice interface and abstracts the return value into a unified parent, OpenApiBaseResponse. In order not to affect the other Controller classes. The assignableTypes attribute on the annotation specifies that the Controller class needs to be intercepted.

As you can see from the beforeBodyWrite method name, this is what SpringMVC does before writing the response entity to the response stream. It only changes the content of the response, not the format of the response message. This method returns the ResponseBodyAdvice generic, which cannot be changed when the class is created.

Not only do we need to change the response content, but we also need to Base64 and other operations on the response format, so using this annotation alone is not enough for us here.

What if we combine AOP with intercepting the defined Advice to change the response format? This is fine in theory, but it’s so “barbaric” that it’s like doing AOP again, and it’s not recommended at all.

Is there any other way to do that? Keep reading.

Four, the use ofFilterintercept

When we first learned Severlet, we learned the Filter technology, which is not in the SpringMVC category, but it does serve our purpose. As follows:

public class NewOpenApiFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
    throws IOException, ServletException {
        // Wrap response
        ServletResponseWrapper custResponse = new ServletResponseWrapper((HttpServletResponse) response);
        chain.doFilter(request, custResponse);
        // Go to the Controller layer
        String result = custResponse.getResult();

        // Customize signature and encryption logic
        
        // External outputresponse.getWriter().write(result); }}public class ServletResponseWrapper extends HttpServletResponseWrapper {
    private PrintWriter cachedWriter;
    private CharArrayWriter bufferedWriter;

    public ServletResponseWrapper(HttpServletResponse response) {
        super(response);
        bufferedWriter = new CharArrayWriter();
        cachedWriter = new PrintWriter(bufferedWriter);
    }

    @Override
    public PrintWriter getWriter(a) {
        return cachedWriter;
    }

    public String getResult(a) {
        return newString(bufferedWriter.toCharArray()); }}Copy the code

A few things to note here:

  • ServletResponseWrapperIs a custom wrapper class that can be used inIf the output buffer is not clearedGet the output of the Controller
  • filterSeverlet container, not SpringMVC, so those Spring annotations (@Autowired) is invalid here
  • The example omits the action of registering a custom filter into the Web container and fills it in yourself

If this is the case, the Controller response needs to change

public class NewOpenApiController {
    @Autowired
    private NewService newService;

    @PostMapping(value = {"/var/{action}"})
    @ResponseBody
    public void webapi(@PathVariable("action") String action, HttpServletResponse response) {
        return doWrite(service.progress(), response);
    }

    private void doWrite(NewBaseResponseVo responseVo, HttpServletResponse response) {
        // Write custom bufferresponse.getWriter().write(JSON.toJSONString(responseVo)); }}Copy the code

In this case, the Controller method returns no value, and the business-layer response is formatted as JSON and output to the custom buffer. The filter can then be taken to the buffer for further processing.

This method still doesn’t meet our requirements. Is there any more? Of course there is, but let’s dig a little deeper and look at the source code for SpringMVC.

Fifth, useHandlerMethodReturnValueHandlerImplementation of + part of the source code analysis

We make a breakpoint on the Controller method to get the following call stack

From DispatcherServlet# doDispatch began to eventually RequestMappingHandlerAdapter# invokeHandlerMethod began to make actual method call. Look closely at the invokeHandlerMethod method.

protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    try {
        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
        
        ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
        if (this.argumentResolvers ! =null) {
            invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); / / 1
        }
        if (this.returnValueHandlers ! =null) {
            invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); / / 2
        }
        
        / / to omit...

        invocableMethod.invokeAndHandle(webRequest, mavContainer); / / 3
        if (asyncManager.isConcurrentHandlingStarted()) {
                return null;
        }
        return getModelAndView(mavContainer, modelFactory, webRequest);
    }
    finally{ webRequest.requestCompleted(); }}Copy the code

The extraneous parts have been omitted. Action 1 sets the parameter parser, action 2 sets the return value handler, and action 3 makes the actual Controller layer method call. Let’s click on invokeAndHandle again:

Go to the handle EreturnValue method

@Override
public void handleReturnValue(@Nullable 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

Code is very clear, select the appropriate HandlerMethodReturnValueHandler instance, and then call instance handleReturnValue method to deal with the return value of the Controller layer.

Here we can know, realize HandlerMethodReturnValueHandler interface to create custom return value processors, and to bring the processor register for SpringMVC container. You can implement a custom return. To get started, let’s create our own processing class

@Component
public class NewOpenApiReturnValueHandler implements HandlerMethodReturnValueHandler {

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return false;
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {}}Copy the code

There are two methods to implement. Look at the supportsReturnType method first, recalling the selectHandler method above. Yes! This method is used to select suitable HandlerMethodReturnValueHandler. You need to decide what you currently support based on the parameters passed in.

In the current situation, the difference between us and other controllers is the return value. We have unified the return value of NewBaseResponseVo, so if the method returns a value of NewBaseResponseVo class, that is what we can support. How can we get it from this method? I don’t know yet. Put that aside and look at another handler return Value method.

So returnValue looks like the returnValue of our Controller, and you can just twist it and get the response instance, so that’s easy. Then the operation of signature, encryption, etc., is also simple. ServletResponse = ServletResponse = ServletResponse

To solve these two problems, debugging is the only option. Register first! The registration method is also very convenient, and SpringMVC provides external extension points that make it easy to add custom processing instances to the list of processors

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private NewOpenApiReturnValueHandler returnValueHandler;
    
    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) { handlers.add(returnValueHandler); }}Copy the code

We get excited about typing breakpoints into our custom return value handlers, only to find that breakpoints never come in. In the way HandlerMethodReturnValueHandlerComposite# selectHandler technique, we have a look at the source

@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
    boolean isAsyncValue = isAsyncReturnValue(value, returnType);
    for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
        if(isAsyncValue && ! (handlerinstanceof AsyncHandlerMethodReturnValueHandler)) {
                continue;
        }
        if (handler.supportsReturnType(returnType)) {
                returnhandler; }}return null;
}
Copy the code

It used to put all the processors in a list, use a for loop, and return the first one it could match. In handlers. Add, the list is always appended back to the default handler, and because the first one is matched, the next one is not executed. So we need to rewrite the registration method, adjust the handler location, and put the custom handler first:

@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
    if (handlers instanceof ArrayList) {
        handlers.add(0, returnValueHandler);
    }
    else {
        throw new RuntimeException("Type not ArrayList cannot initialize return value handler"); }}Copy the code

Redebugging discoveryIt’s not breaking yet. Wondering if our custom instance is not added to the processor list, debugselectHandlermethodsCustom processors are in the list, but not at the top. Why is that? It was the first place to register. Continue the break point on the registration methodfoundSpring the provide registered foreign mouth, just add the processor to customReturnValueHandlers listIn fact, it’s calledreturnValueHandlersIn the list. And in theRequestMappingHandlerAdapter#getDefaultReturnValueHandlersIn the method, ** takes precedencereturnValueHandlersPut the default processor in there, and put it againcustomReturnValueHandlers, ** so there is clearly adjusted into the first actual effect is not. The code is as follows:

private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers(a) {
    List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();
    // Put the default processor first
    handlers.add(new ModelAndViewMethodReturnValueHandler());
    / / to omit
    // Add the custom processor
    if(getCustomReturnValueHandlers() ! =null) {
            handlers.addAll(getCustomReturnValueHandlers());
    }
    / / to omit
    return handlers;
}
Copy the code

A look at the WebMvcConfigurer interface shows that Spring does not provide an extension point to operate returnValueHandlers. Since there’s no way to adjust the order, how on earth do we get into our own processor?

Well, it’s really easy to just take the @ResponseBody annotation off the Controller method. This annotation part’s needs in the current method statement need to handle with RequestResponseBodyMethodProcessor, observe its supportsReturnType was to be seen.

Charge! Finally back to the problem we need to solve. To look at firstsupportsReturnTypeMethod parametersMethodParameterWhat the hell is that, debugging

To get the response instance type from getParameterType(), we just need to determine whether NewBaseResponseVo is of the same type or its superclass or superinterface, as shown below

NewBaseResponseVo.class.isAssignableFrom(returnType.getParameterType());
Copy the code

Then how to solve the problem of no ServletResponse, no output stream. Remember the above mentioned RequestResponseBodyMethodProcessor class? In fact, our implementation should be roughly similar to it, so we just copy the code

protected ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequest) { HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); Assert.state(response ! =null."No HttpServletResponse");
    return new ServletServerHttpResponse(response);
}
Copy the code

With the output stream in hand, the second problem is solved.

Then you might think that our custom processor is only inRequestResponseBodyMethodProcessorSome features have been added to theWhy not inherit this class? This is a question that you’ll find if you try, and this sort of thingConstructors must have converters arguments and no no-parameter constructorsAnd we can’t get the parameters it needsconverters, so can not be implemented, can only achieve the next step interfaceHandlerMethodReturnValueHandler

With both problems solved, let’s look at the final implementation of the custom processor

@Component
public class NewOpenApiReturnValueHandler implements HandlerMethodReturnValueHandler {

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return NewBaseResponseVo.class.isAssignableFrom(returnType.getParameterType());
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        / / strong
        NewBaseResponseVo responseVo = (NewBaseResponseVo) returnValue;

        // Perform signature and encryption

        ServletServerHttpResponse serverHttpResponse = this.createOutputMessage(webRequest);
        serverHttpResponse.getServletResponse().getWriter().write("Signed, encrypted content");
    }

    private ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequest) { HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); Assert.state(response ! =null."No HttpServletResponse");
        return newServletServerHttpResponse(response); }}Copy the code

Also, our Controller just gets rid of the @ResponseBody, doesn’t have to format or call any other methods. So far, this is a relatively elegant way to do it. But it gets better. And finally, let’s go on.

Think about! Now that we’ve implemented a custom return value handler, can we implement a custom annotation like @responseBody and use our own handler when typing this annotation?

Six, the use ofHttpMessageConverterimplementation

HandlerMethodReturnValueHandler deeper inside, Spring also provides a kind of implement custom output extension point, is the HttpMessageConverter, we’ll look at the source code.

So let’s seeRequestResponseBodyMethodProcessor#handleReturnValueMethod, click to find that the parent class is calledAbstractMessageConverterMethodProcessor#writeWithMessageConvertersMethods. Skip the methods you don’t care about and see below:

Action 1, 2 are familiar, and above in selecting HandlerMethodReturnValueHandler and decide whether to support the current output action is the same. The object is different, so pick an appropriate HttpMessageConverter. (PS: Is there a feeling of extrapolating one from another, understand one point, the other point unexpectedly also can)

If you define your own HttpMessageConverter and register it as this.messageConverters, you can create a custom output. And then to

@Component
public class NewOpenApiMessageConverter extends AbstractHttpMessageConverter<NewBaseResponseVo> {

    @Override
    protected boolean supports(Class
        clazz) {
        return NewBaseResponseVo.class.isAssignableFrom(clazz);
    }

    /** * The WebapiBaseResponseVo type will not be used as an argument * so we will return null */ for now
    @Override
    protected NewBaseResponseVo readInternal(Class<? extends NewBaseResponseVo> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        return null;
    }


    @Override
    protected void writeInternal( responseVo, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        Writer writer = new OutputStreamWriter(outputMessage.getBody(), StandardCharsets.UTF_8);
        try {
            writer.write(this.signAndSerialize(responseVo));
        }
        catch (Exception ex) {
            throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
        }
        writer.flush();
    }

    private String signAndSerialize(NewBaseResponseVo responseVo) {
        // Signature and encryption logic
        return ""; }}Copy the code

Here we inherited AbstractHttpMessageConverter class, copy a few methods. Note that the SUPPORTS method is valid for both inputs and outputs.

Suppose you have a method in the Controller that uses the NewBaseResponseVo type to receive arguments, which goes into readInternal logic, Similarly, if the Controller method returns NewBaseResponseVo, it goes to the writeInternal method. Since we are not using this type as an input parameter at this point, we simply return the read method to NULL. Let’s look at registration

/ / advice
@Override
public void extendMessageConverters(List
       
        > converters)
       > {
    if (converters instanceof ArrayList) {
        // As always, it needs to be placed first, otherwise it will be returned first by the default converter
        converters.add(0, newOpenApiMessageConverter);
    }
    else {
        throw new RuntimeException("Type not ArrayList cannot initialize message converter"); }}// Not recommended
@Override
public void configureMessageConverters(List
       
        > converters)
       > {}Copy the code

Note that WebMvcConfigurer provides two extension methods. As named literally, extendMessageConverters used to extend, configureMessageConverters used to configure. Difference between registered extendMessageConverters operation will not affect the default Converter, and configureMessageConverters once you add a new Converter the default Converter registration will be closed (PS: Try deleting a Converter! Use carefully!

Note that adding converters to the list, turns off default converter registration. To simply add a converter without impacting default registration, consider using the method

Look again at our Controller, because we don’t have to change HandlerMethodReturnValueHandler does not need to remove the @ ResponseBody annotations, or according to the normal development.

public class NewOpenApiController {
    @Autowired
    private NewService newService;
    @PostMapping(value = {"/var/{action}"}, produces = MediaType.TEXT_PLAIN_VALUE)
    @ResponseBody
    public NewBaseResponseVo webapi(@PathVariable("action") String action, HttpServletResponse response) {
        returndoSignAndCodec(service.progress()); }}Copy the code

Very clean and elegant. When we add a method or a new Controller instance, as long as the return value is NewBaseResponseVo, we can always go to our custom MessageConverter. This is highly recommended in this scenario!

At this point! All 6 ways are introduced!

reference

Web on Servlet Stack (spring.io)