如何优雅地给一堆服务统一加上返回结果包装?

2021/07/28 16:23
阅读数 133

如何优雅地给一堆服务统一加上返回结果包装?

有一堆服务,现有的接口都是直接将实体对象返回,如:

{
    "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()实现:

image.png

SpringMVC源码阅读可参考:【Spring源码阅读】MVC实现原理

继续跟进HandlerMethodReturnValueHandlerComposite.handleReturnValue()

image.png

继续跟进RequestResponseBodyMethodProcessor.handleReturnValue()

image.png

writeWithMessageConverters()方法的实现在父类AbstractMessageConverterMethodProcessor中。

image.png

可以发现,ResponseBodyAdvice是在图中红框的位置进行执行的。接下来,我们就得找到这些Advice是如何注入进去的,并且想办法手动将我们的实现也注入进去

advice实际上是父类AbstractMessageConverterMethodArgumentResolver的一个字段,并且在构造方法中进行了实例化。

private final RequestResponseBodyAdviceChain advice;
复制代码

image.png

该构造方法的调用方是AbstractMessageConverterMethodProcessor的构造方法,再往上一级是 RequestResponseBodyMethodProcessor

image.png

image.png

继续分析RequestResponseBodyMethodProcessor是在哪里实例化的,根据调用关系,可以判定是在我们熟悉的RequestMappingHandlerAdapter.getDefaultReturnValueHandlers()方法中。而该方法是在afterPropertiesSet()中调用的。

image.png

这里的requestResponseBodyAdviceRequestMappingHandlerAdapter的一个属性。

private List<Object> requestResponseBodyAdvice = new ArrayList<>();
复制代码

分析到这里,我们可以得出,ResponseBodyAdvice是在RequestMappingHandlerAdapter实例进行初始化的时候注入进去的

解决方案

经过上面的分析,我们要做的事情就十分明显了,那就是RequestMappingHandlerAdapter实例化之后,初始化之前,往requestResponseBodyAdvice列表中注入我们实现的ResponseBodyAdvice

查看Bean实例化源码,在AbstractAutowireCapableBeanFactory.initializeBean()方法中:

image.png

可以看到,在初始化之前,调用了applyBeanPostProcessorsBeforeInitialization()方法,支持在初始化之前做一些操作。

image.png

在它的实现里面,调用了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调用框架

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部