Mastering Spring MVC’s RequestBodyAdvice and ResponseBodyAdvice: Usage and Implementation Details

This article explains the purpose, core methods, and practical implementation of Spring MVC’s RequestBodyAdvice and ResponseBodyAdvice extension points, including code examples for request decryption, signature verification, response encryption, unified result wrapping, and the underlying request‑processing flow within the framework.

Shepherd Advanced Notes
Shepherd Advanced Notes
Shepherd Advanced Notes
Mastering Spring MVC’s RequestBodyAdvice and ResponseBodyAdvice: Usage and Implementation Details

RequestBodyAdvice

Spring MVC defines the RequestBodyAdvice interface to allow custom processing before and after the HTTP request body is read and converted to a Java object.

public interface RequestBodyAdvice {
    boolean supports(MethodParameter methodParameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType);
    HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage,
        MethodParameter parameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType) throws IOException;
    Object afterBodyRead(Object body, HttpInputMessage inputMessage,
        MethodParameter parameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType);
    @Nullable Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage,
        MethodParameter parameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType);
}

Key methods:

supports() – returns true when the advice should be applied to the current request.

beforeBodyRead() – invoked before the request body is converted; useful for wrapping or modifying the incoming HttpInputMessage.

afterBodyRead() – invoked after the body has been converted; allows modification or replacement of the resulting object.

handleEmptyBody() – invoked when the request body is empty; can provide a default object.

Practical Implementation

The class RequestBodyHandlerAdvice demonstrates decryption of request parameters and signature verification.

@RestControllerAdvice
public class RequestBodyHandlerAdvice implements RequestBodyAdvice {
    @Resource private ApiSecurityProperties apiSecurityProperties;
    @Resource private StringRedisTemplate stringRedisTemplate;
    private static final String SIGN_KEY = "X-Sign";
    private static final String NONCE_KEY = "X-Nonce";
    private static final String TIMESTAMP_KEY = "X-Timestamp";

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.hasMethodAnnotation(ApiSecurity.class)
                || AnnotatedElementUtils.hasAnnotation(
                        methodParameter.getDeclaringClass(), ApiSecurity.class);
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage,
            MethodParameter parameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        ApiSecurity apiSecurity = getApiSecurity(parameter);
        if (!apiSecurity.decryptRequest()) {
            return inputMessage;
        }
        String body = IoUtil.read(inputMessage.getBody(), StandardCharsets.UTF_8);
        if (StringUtils.isBlank(body)) {
            throw new BizException("请求参数body不能为空");
        }
        ApiSecurityParam apiSecurityParam = JSON.parseObject(body, ApiSecurityParam.class);
        String aesKey = RSAUtil.decryptByPrivateKey(apiSecurityParam.getKey(),
                apiSecurityProperties.getRsaPrivateKey());
        String data = AESUtil.decrypt(apiSecurityParam.getData(), aesKey);
        HttpHeaders headers = inputMessage.getHeaders();
        if (StringUtils.isNotBlank(apiSecurityParam.getTimestamp())) {
            headers.set(TIMESTAMP_KEY, apiSecurityParam.getTimestamp());
        }
        if (StringUtils.isNotBlank(apiSecurityParam.getNonce())) {
            headers.set(NONCE_KEY, apiSecurityParam.getNonce());
        }
        if (StringUtils.isNotBlank(apiSecurityParam.getSign())) {
            headers.set(SIGN_KEY, apiSecurityParam.getSign());
        }
        return new PtcHttpInputMessage(headers, data);
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
            MethodParameter parameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) {
        ApiSecurity apiSecurity = getApiSecurity(parameter);
        if (!apiSecurity.isSign()) {
            return body;
        }
        verifySign(inputMessage.getHeaders(), body);
        return body;
    }

    @Override
    public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage,
            MethodParameter parameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) {
        return null;
    }

    private ApiSecurity getApiSecurity(MethodParameter methodParameter) {
        return methodParameter.getMethodAnnotation(ApiSecurity.class);
    }

    private void verifySign(HttpHeaders headers, Object body) {
        String sign = headers.getFirst(SIGN_KEY);
        if (StringUtils.isBlank(sign)) {
            throw new BizException("签名不能为空");
        }
        String nonce = headers.getFirst(NONCE_KEY);
        if (StringUtils.isBlank(nonce)) {
            throw new BizException("唯一标识不能为空");
        }
        String timestamp = headers.getFirst(TIMESTAMP_KEY);
        if (StringUtils.isBlank(timestamp)) {
            throw new BizException("时间戳不能为空");
        }
        long time = Long.parseLong(timestamp);
        long now = System.currentTimeMillis() / 1000;
        if (now - time > apiSecurityProperties.getValidTime()) {
            throw new BizException("签名已过期");
        }
        if (stringRedisTemplate.hasKey(NONCE_KEY + nonce)) {
            throw new BizException("唯一标识nonce已存在");
        }
        SortedMap sortedMap = SignUtil.beanToMap(body);
        String content = SignUtil.getContent(sortedMap, nonce, timestamp);
        boolean flag = RSAUtil.verifySignByPublicKey(content, sign,
                apiSecurityProperties.getRsaPublicKey());
        if (!flag) {
            throw new BizException("签名验证不通过");
        }
        stringRedisTemplate.opsForValue().set(NONCE_KEY + nonce, "1",
                apiSecurityProperties.getValidTime(), TimeUnit.SECONDS);
    }
}

The custom annotation used to mark secured endpoints:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface ApiSecurity {
    @Alias("isSign") boolean value() default true;
    @Alias("value") boolean isSign() default true;
    boolean decryptRequest() default false;
    boolean encryptResponse() default false;
}

ResponseBodyAdvice

Spring MVC defines the ResponseBodyAdvice interface to allow processing of the response body before it is written.

public interface ResponseBodyAdvice<T> {
    boolean supports(MethodParameter returnType,
        Class<? extends HttpMessageConverter<?>> converterType);
    @Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType,
        MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
        ServerHttpRequest request, ServerHttpResponse response);
}

Key methods:

supports() – decides whether the advice applies to a given controller method or return type.

beforeBodyWrite() – can modify or replace the response body before it is written.

Practical Implementation

The class ResponseResultBodyAdvice demonstrates unified result wrapping and optional encryption.

@RestControllerAdvice
@Slf4j
public class ResponseResultBodyAdvice implements ResponseBodyAdvice<Object> {
    @Resource private ObjectMapper objectMapper;
    @Resource private ApiSecurityProperties apiSecurityProperties;

    @Override
    public boolean supports(MethodParameter returnType,
            Class<? extends HttpMessageConverter<?>> converterType) {
        return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseResultBody.class)
                || returnType.hasMethodAnnotation(ResponseResultBody.class)
                || AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ApiSecurity.class)
                || returnType.hasMethodAnnotation(ApiSecurity.class);
    }

    @Override
    @SneakyThrows
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
            MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
            ServerHttpRequest request, ServerHttpResponse response) {
        Method method = returnType.getMethod();
        Class<?> returnClass = method.getReturnType();
        Boolean enable = apiSecurityProperties.getEnable();
        ApiSecurity apiSecurity = method.getAnnotation(ApiSecurity.class);
        if (apiSecurity == null) {
            apiSecurity = method.getDeclaringClass().getAnnotation(ApiSecurity.class);
        }
        if (enable && apiSecurity != null && apiSecurity.encryptResponse() && body != null) {
            if (body instanceof ResponseVO) {
                body = ((ResponseVO) body).getData();
            }
            JSONObject jsonObject = encryptResponse(body);
            body = jsonObject;
        } else {
            if (body instanceof String || Objects.equals(returnClass, String.class)) {
                String value = objectMapper.writeValueAsString(ResponseVO.success(body));
                return value;
            }
            if (body instanceof ResponseVO) {
                return body;
            }
        }
        return ResponseVO.success(body);
    }

    private JSONObject encryptResponse(Object result) {
        String aesKey = AESUtil.generateAESKey();
        String content = JSONObject.toJSONString(result);
        String data = AESUtil.encrypt(content, aesKey);
        String key = RSAUtil.encryptByPublicKey(aesKey, apiSecurityProperties.getRsaPublicKey());
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("key", key);
        jsonObject.put("data", data);
        return jsonObject;
    }
}

Data class used for encrypted request payloads:

@Data
public class ApiSecurityParam {
    private String appId;
    private String key;      // RSA‑encrypted AES key
    private String data;     // AES‑encrypted JSON parameters
    private String sign;
    private String timestamp;
    private String nonce;
}

Example controller method:

@PostMapping("/security")
@ApiSecurity(encryptResponse = true, decryptRequest = true)
public User testApiSecurity(@RequestBody User user) {
    System.out.println(user);
    return user;
}

Spring MVC Processing Flow

When Spring MVC receives an HTTP request, the processing steps are:

DispatchServlet receives the request.

HandlerMapping finds the matching controller.

HandlerAdapter invokes the controller method.

HttpMessageConverter converts the request body to a Java object.

The controller method executes and returns a result.

HttpMessageConverter converts the result to the response body.

The response is sent back to the client.

During steps 4 and 6, Spring MVC checks for registered RequestBodyAdvice and ResponseBodyAdvice instances. The core logic resides in RequestResponseBodyAdviceChain, which iterates over all matching advice objects and invokes their methods.

private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {
    List<Object> availableAdvice = getAdvice(adviceType);
    if (CollectionUtils.isEmpty(availableAdvice)) {
        return Collections.emptyList();
    }
    List<A> result = new ArrayList<>(availableAdvice.size());
    for (Object advice : availableAdvice) {
        if (advice instanceof ControllerAdviceBean) {
            ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;
            if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) {
                continue;
            }
            advice = adviceBean.resolveBean();
        }
        if (adviceType.isAssignableFrom(advice.getClass())) {
            result.add((A) advice);
        }
    }
    return result;
}

The initControllerAdviceCache() method scans the application context for beans annotated with @ControllerAdvice, collects those that implement RequestBodyAdvice or ResponseBodyAdvice, and stores them in internal advice lists.

private void initControllerAdviceCache() {
    if (getApplicationContext() == null) {
        return;
    }
    List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
    AnnotationAwareOrderComparator.sort(adviceBeans);
    List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();
    for (ControllerAdviceBean adviceBean : adviceBeans) {
        Class<?> beanType = adviceBean.getBeanType();
        if (RequestBodyAdvice.class.isAssignableFrom(beanType) ||
                ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
            requestResponseBodyAdviceBeans.add(adviceBean);
        }
    }
    if (!requestResponseBodyAdviceBeans.isEmpty()) {
        this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
    }
}

Both RequestBodyAdvice and ResponseBodyAdvice follow the same registration and invocation pattern, enabling developers to plug in cross‑cutting concerns such as encryption, decryption, signature verification, logging, and unified response formatting.

Conclusion

RequestBodyAdvice

and ResponseBodyAdvice are powerful extension points in Spring MVC. By implementing these interfaces developers can inject custom logic around request deserialization and response serialization, achieving request decryption, signature verification, response encryption, and consistent result packaging while keeping application code clean and maintainable.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

EncryptionAPI SecuritySpring MVCsignature verificationResponseBodyAdviceRequestBodyAdvice
Shepherd Advanced Notes
Written by

Shepherd Advanced Notes

Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.