在前文 《在 Spring Boot 中通过 RequestBodyAdvice 统一解码请求体》 中,我们介绍了如何通过 RequestBodyAdvice
组件统一地解码客户端编码后的请求体。
有请求,就有响应,本文将会带你了解如何使用 ResponseBodyAdvice
来统一对响应体进行编码。
这种场景比较常见,常用于 APP 客户端和服务器之间的通信。为了避免中间人通过抓包直接获取到服务器的响应数据,所以会对数据进行加密(AES、3DES、RSA 等等)。
客户端获取到加密数据后,使用本地存储的密钥进行解密从而得到原始数据。由于密钥没有经过网络传输,意味着加密数据不会轻易被人破解。当然,密钥由于存储在客户端,需要通过混淆等手段保证它不会被轻易的破解、获取(这种安全话题超出了本文的范畴)!
ResponseBodyAdvice {#responsebodyadvice}
ResponseBodyAdvice
spring mvc 提供的一个增强接口,用于在返回对象被 HttpMessageConverter
执行序列化之前对其进行一些自定义的操作。
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);
}
这是一个带泛型 <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("/demo")
@EncodeBody
public ResponseEntity<Object> demo() {
Map<String, Object> response = new HashMap<>();
response.put("title", "spring 中文网");
response.put("url", "springdoc.cn");
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<? extends HttpMessageConverter<?>> converterType) {
// 方法上注解了 @EncodeBody,才对返回对象进行编码
return returnType.hasMethodAnnotation(EncodeBody.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> 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("data", 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("eyJ0aXRsZSI6InNwcmluZyDkuK3mlofnvZEiLCJ1cmwiOiJzcHJpbmdkb2MuY24ifQ==");
System.out.println(new String(raw, StandardCharsets.UTF_8));
}
}
输出如下:
{"title":"spring 中文网","url":"springdoc.cn"}
一切 OK,解码后的结果正是在 Controller 中定义的返回对象。客户端也只要遵循这种格式,统一地对响应体进行解码就可以获取到原文。
而这一切对 Controller 都是透明的,Controller 不需要进行任何修改。
总结 {#总结}
本文介绍了 Spring 中的 ResponseBodyAdvice
接口,开发人员可以对 Controller 方法返回的响应体进行处理,例如添加统一的响应头、修改响应体的内容格式、对响应体进行编码、加密等。这能够更加方便地实现全局的响应处理逻辑,提升代码的可维护性和复用性。