如何优雅地给一堆服务统一加上返回结果包装?
有一堆服务,现有的接口都是直接将实体对象返回,如:
{
"name": "张三",
"city": "上海"
}
复制代码
现在要求所有接口支持统一包装,并且兼容之前的版本。为了达到这个目的,我们约定,只要接口带有App-Adapter=open
请求头,就按新的包装格式返回,否则还是按之前的格式返回。包装格式如下:
{
"code": 0,
"message": "ok",
"timestamp": 1627384576680,
"data": {
"name": "张三",
"city": "上海"
}
}
复制代码
传统解决方案
Spirng提供了ResponseBodyAdvice
接口,支持在消息转换器执行转换之前,对接口的返回结果进行处理,再结合@RestControllerAdvice
注解即可轻松支持上述功能。代码示例如下:
@RestControllerAdvice
public class AppResponseBodyAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
final String header = request.getHeader(AdapterRestResponseMessageProvider.HEADER);
// 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false
return Objects.equals(header, AdapterRestResponseMessageProvider.HEADER_VALUE) &&
!returnType.getGenericParameterType().equals(Result.class);
}
@Override
@SneakyThrows
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// String类型不能直接包装,所以要进行些特别的处理
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(Result.ok(body));
}
// 将原本的数据包装在ResultVO里
return Result.ok(body);
}
@Data
public static class Result<T> {
public static final int OK = 0;
/**
* 返回code 200 为正常
*/
private int code;
/**
* 消息
*/
private String message;
/**
* 返回时间戳
*/
private long timestamp;
/**
* 返回数据
*/
private T data;
public static <T> Result<T> ok(T data) {
Result<T> re = new Result<T>();
re.code = OK;
re.message = "ok";
re.data = data;
re.timestamp = System.currentTimeMillis();
return re;
}
}
}
复制代码
这种方式的确非常简单,但是缺点也很明显,需要在各个服务中都copy这段代码。作为一个有追求的程序员,这种实现方案肯定是不能容忍的。必须找到一种通用方案,让所有服务直接引入依赖即可支持。
分析
实现思路上,肯定还是用ResponseBodyAdvice
接口进行实现,关键点在于如何将其织入进Spring的逻辑中去。
我们先从源码的角度看看ResponseBodyAdvice
接口具体是如何生效的。首先可以判定,这段转换逻辑肯定是在处理方法返回值的地方执行的。直接查看ServletInvocableHandlerMethod.invokeAndHandle()
实现:
SpringMVC源码阅读可参考:【Spring源码阅读】MVC实现原理
继续跟进HandlerMethodReturnValueHandlerComposite.handleReturnValue()
继续跟进RequestResponseBodyMethodProcessor.handleReturnValue()
writeWithMessageConverters()
方法的实现在父类AbstractMessageConverterMethodProcessor
中。
可以发现,ResponseBodyAdvice
是在图中红框的位置进行执行的。接下来,我们就得找到这些Advice
是如何注入进去的,并且想办法手动将我们的实现也注入进去。
advice
实际上是父类AbstractMessageConverterMethodArgumentResolver
的一个字段,并且在构造方法中进行了实例化。
private final RequestResponseBodyAdviceChain advice;
复制代码
该构造方法的调用方是AbstractMessageConverterMethodProcessor
的构造方法,再往上一级是 RequestResponseBodyMethodProcessor
。
继续分析RequestResponseBodyMethodProcessor
是在哪里实例化的,根据调用关系,可以判定是在我们熟悉的RequestMappingHandlerAdapter.getDefaultReturnValueHandlers()
方法中。而该方法是在afterPropertiesSet()
中调用的。
这里的requestResponseBodyAdvice
是RequestMappingHandlerAdapter
的一个属性。
private List<Object> requestResponseBodyAdvice = new ArrayList<>();
复制代码
分析到这里,我们可以得出,ResponseBodyAdvice
是在RequestMappingHandlerAdapter
实例进行初始化的时候注入进去的。
解决方案
经过上面的分析,我们要做的事情就十分明显了,那就是在RequestMappingHandlerAdapter
实例化之后,初始化之前,往requestResponseBodyAdvice
列表中注入我们实现的ResponseBodyAdvice
。
查看Bean实例化源码,在AbstractAutowireCapableBeanFactory.initializeBean()
方法中:
可以看到,在初始化之前,调用了applyBeanPostProcessorsBeforeInitialization()
方法,支持在初始化之前做一些操作。
在它的实现里面,调用了BeanProcessor.postProcessBeforeInitialization(result, beanName)
方法。
Spring容器启动原理可参考【Spring源码阅读】Spring容器启动原理(下)-Bean实例的创建和依赖注入
Spring钩子方法可参考:熟悉Spring钩子方法和钩子接口使用,简化你的开发
因此,我们只需要实现BeanProcessor
接口,并且重写postProcessBeforeInitialization()
方法,在这里面将我们实现的ResponseBodyAdvice
注入进去即可。为了方便起见,我们将其放在一起。代码实现如下:
public class AppResponseBodyAdvice implements ResponseBodyAdvice, BeanPostProcessor {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
final String header = request.getHeader(AdapterRestResponseMessageProvider.HEADER);
// 带有App-Adapter=open请求头,且返回类型不是Result,返回true
return Objects.equals(header, AdapterRestResponseMessageProvider.HEADER_VALUE) &&
!returnType.getGenericParameterType().equals(Result.class);
}
@Override
@SneakyThrows
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// String类型不能直接包装,所以要进行些特别的处理
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(Result.ok(body));
}
// 将原本的数据包装在Result里
return Result.ok(body);
}
@Override
@SneakyThrows
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 注入到RequestMappingHandlerAdapter的requestResponseBodyAdvice属性中
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter requestMappingHandlerAdapter = (RequestMappingHandlerAdapter) bean;
final Field requestResponseBodyAdvice = FieldUtils.getField(RequestMappingHandlerAdapter.class, "requestResponseBodyAdvice", true);
final List<Object> list = (List<Object>) FieldUtils.readField(requestResponseBodyAdvice, requestMappingHandlerAdapter);
List<Object> temp = new ArrayList<>(list);
list.clear();
list.add(this);
list.addAll(temp);
}
return bean;
}
@Data
public static class Result<T> {
public static final int OK = 0;
/**
* 返回code 200 为正常
*/
private int code;
/**
* 消息
*/
private String message;
/**
* 返回时间戳
*/
private long timestamp;
/**
* 返回数据
*/
private T data;
public static <T> Result<T> ok(T data) {
Result<T> re = new Result<T>();
re.code = OK;
re.message = "ok";
re.data = data;
re.timestamp = System.currentTimeMillis();
return re;
}
}
}
复制代码
最后配置成自动装配,后续所有服务只要引入该依赖即可。
@Configuration
public class AppResponseBodyAdviceAutoConfiguration {
@Bean
public AppResponseBodyAdvice appResponseBodyAdvice() {
return new AppResponseBodyAdvice();
}
}
复制代码
在spring.factories
中配置:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lianjia.confucius.somehelp.response.AppResponseBodyAdviceAutoConfiguration
复制代码
完美收工!!!
原创不易,觉得文章写得不错的小伙伴,点个赞👍 鼓励一下吧~
欢迎关注我的开源项目:一款适用于SpringBoot的轻量级HTTP调用框架