Detailed explanation of RequestBodyAdvice and ResponseBodyAdvice, @ ControllerAdvice annotation

Posted by hookit on Sat, 27 Nov 2021 05:25:33 +0100

overview

RequestBodyAdvice
RequestBodyAdvice is an interface provided by spring mvc4.2, which allows the request body to be read and converted into an object, and takes the processing result object as the @ RequestBody parameter or the @ HttpEntity method parameter. It can be seen that its scope of action is:

Parameters marked with @ RequestBody
The parameter is HttpEntity

ResponseBodyAdvice
ResponseBodyAdvice is an interface provided by spring mvc4.1. It allows you to customize the returned data after executing @ ResponseBody, or use HttpMessageConverter to customize the Controller Method that returns @ ResponseEntity before writing to the body. It can be seen that its scope of action is:

Tag with @ ResponseBody annotation
Return @ ResponseEntity

Source code

These are two new interfaces added to spring 4.2

1,RequestBodyAdvice

public interface RequestBodyAdvice {
    boolean supports(MethodParameter var1, Type var2, Class<? extends HttpMessageConverter<?>> var3);

    HttpInputMessage beforeBodyRead(HttpInputMessage var1, MethodParameter var2, Type var3, Class<? extends HttpMessageConverter<?>> var4)       throws IOException;
    Object afterBodyRead(Object var1, HttpInputMessage var2, MethodParameter var3, Type var4, Class<? extends HttpMessageConverter<?>> var5);

    @Nullable
    Object handleEmptyBody(@Nullable Object var1, HttpInputMessage var2, MethodParameter var3, Type var4,             Class<? extends HttpMessageConverter<?>> var5);
}

The readWithMessageConverters() method of AbstractMessageConverterMethodArgumentResolver calls the method of this interface.

@Nullable
    protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType)               throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
        boolean noContentType = false;

        MediaType contentType;
        try {
            contentType = inputMessage.getHeaders().getContentType();
        } catch (InvalidMediaTypeException var16) {
            throw new HttpMediaTypeNotSupportedException(var16.getMessage());
        }

        if (contentType == null) {
            noContentType = true;
            contentType = MediaType.APPLICATION_OCTET_STREAM;
        }

        Class<?> contextClass = parameter.getContainingClass();
        Class<T> targetClass = targetType instanceof Class ? (Class)targetType : null;
        if (targetClass == null) {
            ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
            targetClass = resolvableType.resolve();
        }

        HttpMethod httpMethod = inputMessage instanceof HttpRequest ? ((HttpRequest)inputMessage).getMethod() : null;
        Object body = NO_VALUE;

        AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage message;
        try {
            label94: {
                message = new AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage(inputMessage);
                Iterator var11 = this.messageConverters.iterator();

                HttpMessageConverter converter;
                Class converterType;
                GenericHttpMessageConverter genericConverter;
                while(true) {
                    if (!var11.hasNext()) {
                        break label94;
                    }

                    converter = (HttpMessageConverter)var11.next();
                    converterType = converter.getClass();
                    genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;
                    if (genericConverter != null) {
                        if (genericConverter.canRead(targetType, contextClass, contentType)) {
                            break;
                        }
                    } else if (targetClass != null && converter.canRead(targetClass, contentType)) {
                        break;
                    }
                }

                if (message.hasBody()) {
                    HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
                    body = genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :                 converter.read(targetClass, msgToUse);
                    body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
                } else {
                    body = this.getAdvice().handleEmptyBody((Object)null, message, parameter, targetType, converterType);
                }
            }
        } catch (IOException var17) {
            throw new HttpMessageNotReadableException("I/O error while reading input message", var17, inputMessage);
        }

        if (body != NO_VALUE) {
            LogFormatUtils.traceDebug(this.logger, (traceOn) -> {
                String formatted = LogFormatUtils.formatValue(body, !traceOn);
                return "Read \"" + contentType + "\" to [" + formatted + "]";
            });
            return body;
        } else if (httpMethod != null && SUPPORTED_METHODS.contains(httpMethod) && (!noContentType || message.hasBody())) {
            throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
        } else {
            return null;
        }
    }

The HttpMessageConverter does some processing before and after processing the request body and when the body is empty.

It can be seen from this that when using these handlermethodargumentresolvers, we can perform pre-processing and post-processing on the request body

ResponseBodyAdvice

public interface ResponseBodyAdvice<T> {
    boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2);

    @Nullable
    T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4,             ServerHttpRequest var5, ServerHttpResponse var6);
}

This can process the returned result of @ ResponseBody before outputting it to the response.

Through generics, specify the type of response body object that needs to be "intercepted". The implementation of this interface will be executed after the data returned by the Controller method is matched to the HttpMessageConverter and before the HttpMessageConverter is serialized. You can modify the response body uniformly by overriding beforeBodyWrite.

application

Use of RequestBodyAdvice

First, an implementation class is implemented RequestBodyAdvice,Annotate the class after@ControllerAdvice,Both of them are indispensable. For example, some request bodies have been encrypted and can be decrypted here.

The core method is supports,The value returned by this method boolean Value, which determines whether to execute beforeBodyRead method.

And our main logic is beforeBodyRead Method, decrypt the request body of the client.

Note: no need to add@Component annotation
//@Component
@ControllerAdvice
public class RequestBodyDecrypt implements RequestBodyAdvice {
    @Reference
    private EquipmentService equipmentService;
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;  // Must be true to execute the beforeBodyRead and afterBodyRead methods
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type,                                     Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        String equipmentNo = httpInputMessage.getHeaders().getFirst("equipmentNo"); // Get equipmentNo from the request header, and the getFirst() method gets the value according to the name of the request header
        String privateKey = null;
        List<EquipmentDTO> mapList = equipmentService.getByEquipmentNo(equipmentNo);
        if (mapList.size() > 0) {
            EquipmentDTO equipmentDTO = mapList.get(0);
            privateKey = equipmentDTO.getProdPriKey();
        }

        // Extract data
        InputStream is = httpInputMessage.getBody(); // Get the request body from the HTTPInputMessage to get the byte input stream
        byte[] data = new byte[is.available()];
        is.read(data);
        String dataStr = new String(data, StandardCharsets.UTF_8);
        JSONObject json = JSONObject.parseObject(dataStr);
        String decrypt = null;
        try {
            decrypt = RSAUtils.decryptByPrivateKey(json.getString("applyData"), privateKey); // Request body after decryption of private key
        } catch (Exception e) {
            throw new RuntimeException("data error");
        }     // Encapsulate the decrypted request body into HttpInputMessage and return
        return new DecodedHttpInputMessage(httpInputMessage.getHeaders(), new ByteArrayInputStream(decrypt.getBytes(StandardCharsets.UTF_8)));
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type,                                               Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type,                                                Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    static class DecodedHttpInputMessage implements HttpInputMessage {
        HttpHeaders headers;
        InputStream body;

        public DecodedHttpInputMessage(HttpHeaders headers, InputStream body) {
            this.headers = headers;
            this.body = body;
        }

        @Override
        public InputStream getBody() throws IOException {
            return body;
        }

        @Override
        public HttpHeaders getHeaders() {
            return headers;
        }
    }
}

decryptByPrivateKey:

public static String decryptByPrivateKey(String encryptData, String priKey) throws NoSuchAlgorithmException,    InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, IOException {
        byte[] sourceBytes = Base64.getDecoder().decode(encryptData);
        byte[] keyBytes = Base64.getDecoder().decode(priKey);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        Key priK = keyFactory.generatePrivate(pkcs8KeySpec);
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, priK);
        int inputLen = sourceBytes.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // Decrypt data segments
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
                cache = cipher.doFinal(sourceBytes, offSet, MAX_DECRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(sourceBytes, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_DECRYPT_BLOCK;
        }
        String decryptedData = out.toString("UTF-8");
        out.close();
        return decryptedData;
    }

Use of ResponseBodyAdvice

First, an implementation class implements ResponseBodyAdvice, and then adds the annotation @ ControllerAdvice to the class,

@ControllerAdvice
public class MyResponseBodyAdvice implements ResponseBodyAdvice<Result> {
 
    /**
     * Encryption string 1
     */
    private static String md5_keyone;
    /**
     * Encryption string II
     */
    private static String md5_keytwo;
 
    @PostConstruct
    public void init() throws Exception {
        md5_keyone = Utils.PT.getProps("md5_keyone");
        md5_keytwo = Utils.PT.getProps("md5_keytwo");
    }
 
    /**
     * Determine the type of support
     * 
     * @param returnType
     * @param converterType
     * @return
     * @see org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice#supports(org.springframework.core.MethodParameter,
     *      java.lang.Class)
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return returnType.getMethod().getReturnType().isAssignableFrom(Result.class);
    }
 
    /**
     * Encrypt results
     * 
     * @param body
     * @param returnType
     * @param selectedContentType
     * @param selectedConverterType
     * @param request
     * @param response
     * @return
     * @see org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice#beforeBodyWrite(java.lang.Object,
     *      org.springframework.core.MethodParameter,
     *      org.springframework.http.MediaType, java.lang.Class,
     *      org.springframework.http.server.ServerHttpRequest,
     *      org.springframework.http.server.ServerHttpResponse)
     */
    @Override
    public Result beforeBodyWrite(Result body, MethodParameter returnType,org.springframework.http.MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,ServerHttpResponse response) {
        String jsonString = JSON.toJSONString(body.getData());
        System.out.println(jsonString);
        // First encryption
        String data_encode_one = MD5.md5(md5_keyone + jsonString);
        // Second encryption
        String data_encode_two = MD5.md5(data_encode_one + md5_keytwo);
        body.setToken(data_encode_two);
        return body;
    }
 
}

Why add @ ControllerAdvice annotation and implement RequestBodyAdvice or ResponseBodyAdvice interface

Now let's analyze why we need to implement the RequestBodyAdvice interface and add the ControllerAdvice annotation at the same time.
Simply put, to execute the afterBodyRead method, you must implement the ResponseBodyAdvice interface.

afterBodyRead method of RequestResponseBodyAdviceChain: call getMatchingAdvice method to get advice of RequestBodyAdvice type

Where: class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice

public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,     Class<? extends HttpMessageConverter<?>> converterType) {
        Iterator var6 = this.getMatchingAdvice(parameter, RequestBodyAdvice.class).iterator();

        while(var6.hasNext()) {
            RequestBodyAdvice advice = (RequestBodyAdvice)var6.next(); // This advice is the class we define
            if (advice.supports(parameter, targetType, converterType)) { //If the return value of the supports method is true, the afterBodyRead method of RequestBodyAdvice is executed
                body = advice.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
            }
        }

        return body;
    }

getMatchAdvice: get the advice of RequestBodyAdvice type (this advice is defined by us). If it is not the RequestBodyAdvice type, it will not be added to the result set, so this is why we implement RequestBodyAdvice

private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {
     List<Object> availableAdvice = this.getAdvice(adviceType);//Get the advice of RequestBodyAdvice type (this advice is the class we define to implement the RequestBodyAdvice interface)
        if (CollectionUtils.isEmpty(availableAdvice)) {
            return Collections.emptyList();
        } else {
            List<A> result = new ArrayList(availableAdvice.size());
            Iterator var5 = availableAdvice.iterator();

            while(true) {
                Object advice;
                while(true) {
                    if (!var5.hasNext()) {
                        return result;
                    }

                    advice = var5.next();
                    if (!(advice instanceof ControllerAdviceBean)) {
                        break;
                    }

                    ControllerAdviceBean adviceBean = (ControllerAdviceBean)advice;
                    if (adviceBean.isApplicableToBeanType(parameter.getContainingClass())) {
                        advice = adviceBean.resolveBean(); // The Advice we defined is returned, that is, the Bean object is obtained from the BeanFactory according to the name of the Bean
                        break;
                    }
                }
          // Judge whether this class is of RequestBodyAdvice type. If not, it will not be added to the result set, so this is why we implement RequestBodyAdvice
                if (adviceType.isAssignableFrom(advice.getClass())) {
                    result.add(advice);
                }
            }
        }
    }
private List<Object> getAdvice(Class<?> adviceType) {
        if (RequestBodyAdvice.class == adviceType) {
            return this.requestBodyAdvice;
        } else if (ResponseBodyAdvice.class == adviceType) {
            return this.responseBodyAdvice;
        } else {
            throw new IllegalArgumentException("Unexpected adviceType: " + adviceType);
        }
    }
public Object resolveBean() {
        if (this.resolvedBean == null) {
            Object resolvedBean = this.obtainBeanFactory().getBean((String)this.beanOrName); // obtainBeanFactory() returns the BeanFactory object
            if (!this.isSingleton) {
                return resolvedBean;
            }

            this.resolvedBean = resolvedBean;
        }

        return this.resolvedBean;
    }

Add @ ControllerAdvice annotation

Simply put, @ ControllerAdviceBean can be found only by adding it.

HandlerAdapter literally means processing adapter. Its function is summarized in one sentence, which is to call specific methods to process requests sent by users. When the handler mapping obtains the controller executing the request, the dispatcher servlet will call the corresponding handler adapter according to the controller type corresponding to the controller for processing.

RequestMappingHandlerAdapter is more complex. It can be said that this class is the most complex class in the whole spring MVC, but it is also the most frequently used class in spring MVC. At present, in the use of spring MVC, the processing of requests mainly depends on the combination of RequestMappingHandlerMapping and RequestMappingHandlerAdapter classes. The following focuses on the RequestMappingHandlerAdapter class

RequestMappingHandlerAdapter initialization process:
RequestMappingHandlerAdapter implements the InitializingBean interface, and the Spring container will automatically call its afterpropertieset method.

Topics: Spring Boot data structure Spring Cloud list