51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 Spring Boot 中通过 ResponseBodyAdvice 统一编码响应体

在前文 《在 Spring Boot 中通过 RequestBodyAdvice 统一解码请求体》 中,我们介绍了如何通过 RequestBodyAdvice 组件统一地解码客户端编码后的请求体。

有请求,就有响应,本文将会带你了解如何使用 ResponseBodyAdvice 来统一对响应体进行编码。

这种场景比较常见,常用于 APP 客户端和服务器之间的通信。为了避免中间人通过抓包直接获取到服务器的响应数据,所以会对数据进行加密(AES、3DES、RSA 等等)。

客户端获取到加密数据后,使用本地存储的密钥进行解密从而得到原始数据。由于密钥没有经过网络传输,意味着加密数据不会轻易被人破解。当然,密钥由于存储在客户端,需要通过混淆等手段保证它不会被轻易的破解、获取(这种安全话题超出了本文的范畴)!

ResponseBodyAdvice {#responsebodyadvice}

ResponseBodyAdvice spring mvc 提供的一个增强接口,用于在返回对象被 HttpMessageConverter 执行序列化之前对其进行一些自定义的操作。

public interface ResponseBodyAdvice <T> {
boolean supports(MethodParameter returnType, Class&lt;? extends HttpMessageConverter&lt;?&gt;&gt; converterType);

@Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class&lt;? extends HttpMessageConverter&lt;?&gt;&gt; selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);

}

这是一个带泛型 <T> 的接口,T 表示返回的对象类型,这决定了它会对哪些返回的对象生效。另外,它比 RequestBodyAdvice 简单,它只有 2 个方法。

  • supports:用于确定该实现类是否支持对响应体进行处理,通过 returnType 参数可以获取到 Controller 方法的返回类型等信息。
  • beforeBodyWrite:该方法在响应体写入之前被调用,在这个方法中可以通过参数获取到最终要响应给客户端的对象,我们可以对这个对象进行一些操作,最后返回修改后的对象。

接下来我们通过一个示例来了解如何使用 ResponseBodyAdvice

假设,服务器返回的所有 JSON 数据都会被编码为 Base64 格式。

定义 @EncodeBody 注解 {#定义-encodebody-注解}

定义一个 @EncodeBody 注解,可用于 Controller 方法。

package cn.springdoc.demo.annotations;

import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention; import java.lang.annotation.Target;

@Retention(RUNTIME) @Target(METHOD) public @interface EncodeBody {

}

只有使用 @EncodeBody 注解标识的 API 方法返回的对象,才会被编码。

Controller {#controller}

package cn.springdoc.demo.web.controller;

import java.util.HashMap; import java.util.Map;

import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;

import cn.springdoc.demo.annotations.EncodeBody;

@RestController @RequestMapping public class DemoController {

@GetMapping(&quot;/demo&quot;)
@EncodeBody
public ResponseEntity&lt;Object&gt; demo() {
Map&amp;lt;String, Object&amp;gt; response = new HashMap&amp;lt;&amp;gt;();
response.put(&amp;quot;title&amp;quot;, &amp;quot;spring 中文网&amp;quot;);
response.put(&amp;quot;url&amp;quot;, &amp;quot;springdoc.cn&amp;quot;);

return ResponseEntity.ok(response);

}

}

这是一个非常普通的 API 端点,它返回一个 Map 对象,并且注解了 @EncodeBody

ResponseBodyEncodeAdvice {#responsebodyencodeadvice}

定义 ResponseBodyAdvice 的实现:ResponseBodyEncodeAdvice,用于把响应的 JSON 数据编码为 Base64。

package cn.springdoc.demo.web.advice;

import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Collections;

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper;

import cn.springdoc.demo.annotations.EncodeBody;

@RestControllerAdvice public class ResponseBodyEncodeAdvice implements ResponseBodyAdvice<Object> {

// 系统中默认使用的 ObjectMapper
@Autowired
private ObjectMapper objectMapper;

@Override public boolean supports(MethodParameter returnType, Class&lt;? extends HttpMessageConverter&lt;?&gt;&gt; converterType) { // 方法上注解了 @EncodeBody,才对返回对象进行编码 return returnType.hasMethodAnnotation(EncodeBody.class); }

@Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class&lt;? extends HttpMessageConverter&lt;?&gt;&gt; selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

if (body == null) {
    return body;
}

String jsonText = null;

try { // 把响应体序列化为 JSON 字符串 jsonText = this.objectMapper.writeValueAsString(body); } catch (JsonProcessingException e) { throw new RuntimeException(e); }

// 对原始数据进行 Base64 编码 String encodedText = Base64.getEncoder().encodeToString(jsonText.getBytes(StandardCharsets.UTF_8));

// 以 Map 形式响应给客户端,保持 JSON 格式 return Collections.singletonMap(&amp;quot;data&amp;quot;, encodedText);

}

}

ResponseBodyEncodeAdvice 实现了 ResponseBodyAdvice 接口,并且指定了泛型为 Object,意味着该实现对所有返回对象都生效。

还注入了 ObjectMapper,用于把返回的对象序列化为 JSON 字符串。spring mvc 默认使用 Jackson 作为 JSON 的序列化、反序列化库,所以 ObjectMapper 是已经预置的,不需要自己创建。当然,你如果对序列化方式有特殊需求,可以自己实例化一个 ObjectMapper,甚至是换一个 JSON 库,比如:FastJson 都可以。

beforeBodyWrite 方法中,先使用 ObjectMapper 把返回对象序列化为 JSON 字符串,也就是响应的原文。然后再把原文编码为 Base64 字符串。最后通过一个通用的 Map 返回到客户端。

测试 {#测试}

启动应用,使用 cURL 进行测试:

$ curl localhost:8080/demo
{"data":"eyJ0aXRsZSI6InNwcmluZyDkuK3mlofnvZEiLCJ1cmwiOiJzcHJpbmdkb2MuY24ifQ=="}

如你所见,返回的 JSON 格式正是我们在 ResponseBodyEncodeAdvice 中定义的。其中 data 字段的值就是对响应原文进行 Base64 编码后的密文。

解码 data:

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Main { public static void main(String[] args) throws Exception {

    byte[] raw = Base64.getDecoder().decode(&quot;eyJ0aXRsZSI6InNwcmluZyDkuK3mlofnvZEiLCJ1cmwiOiJzcHJpbmdkb2MuY24ifQ==&quot;);
System.out.println(new String(raw, StandardCharsets.UTF_8));

}

}

输出如下:

{"title":"spring 中文网","url":"springdoc.cn"}

一切 OK,解码后的结果正是在 Controller 中定义的返回对象。客户端也只要遵循这种格式,统一地对响应体进行解码就可以获取到原文。

而这一切对 Controller 都是透明的,Controller 不需要进行任何修改。

总结 {#总结}

本文介绍了 Spring 中的 ResponseBodyAdvice 接口,开发人员可以对 Controller 方法返回的响应体进行处理,例如添加统一的响应头、修改响应体的内容格式、对响应体进行编码、加密等。这能够更加方便地实现全局的响应处理逻辑,提升代码的可维护性和复用性。

赞(5)
未经允许不得转载:工具盒子 » 在 Spring Boot 中通过 ResponseBodyAdvice 统一编码响应体