统一接口返回数据格式(二)—自定义注解控制是否启用

1 整体说明

上一篇博客《搭建公共服务实现统一后端接口的返回数据格式》介绍了如何搭建公共服务完成统一后端接口的返回数据格式,并在业务服务中引入公共服务并使用。公共服务引入并配置生效后,默认对所有的Rest接口生效。为了提供更多的灵活性,我们对公共服务进行改造,提供自定义注解用于代替@RestController,只有引用了自定义注解的controller接口统一封装返回数据格式,对@RestController注解标识的controller接口不做处理。

在上一篇我们提到通过@RestControllerAdvice来拦截所有的@RestController@RestControllerAdvice注解搭配ResponseBodyAdvice接口可以实现在接口调用正常时的封装返回数据。ResponseBodyAdvice接口的support方法返回值决定是否对当前controller接口应用ResponseBodyAdvice接口的beforeBodyWrite方法,`ResponseBodyAdvice接口的也是我们封装返回数据格式的逻辑所在。

为了实现开篇提到的以只拦截封装用自定义注解修饰的controller接口的返回结果,我们可以在ResponseBodyAdvice#support中判断当前controller是否带自定义注解,如果是那么返回true,否则返回false不再经过ResponseBodyAdvice#beforeBodyWrite方法去封装。

那现在的问题变成了如何在ResponseBodyAdvice#support中知道controller接口的注解信息,网上搜一把发现可以通过WebMvcConfigurerHandlerInterceptorAdapter接口拿到controller的注解信息。那我们可以把拿到的注解信息作为请求属性塞到HttpServletRequest实例request中,然后在ResponseBodyAdvice#support直接获取requst的对应属性值即可。

基本逻辑梳理完成,开干。当然WebMvcConfigurerHandlerInterceptorAdapter接口的工作原理、HttpServletRequest的生命周期还需要进一步梳理,不过不影响目前使用。

参考资料:

SpringMVC拦截器中获得Controller方法名和注解信息(用于验证权限)

精通SpringBoot——第三篇:详解WebMvcConfigurer接口

2 公共服务改造

2.1 注解定义

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@Documented
@RestController
@RequestMapping
public @interface ComRestController {
​
    @AliasFor(annotation = RequestMapping.class) String[] value() default {};
​
    @AliasFor(annotation = RequestMapping.class) String[] path() default {};
​
    @AliasFor(annotation = RequestMapping.class) String[] produces() default {"application/json;charset=UTF-8"};
​
    @AliasFor(annotation = RequestMapping.class) String[] consumes() default {};
}
复制代码

2.2 WebMvcConfigurer接口实现

@Configuration  //必须加这个注解, Spring才能统一管理当前的拦截器实例
public class ComRestControllerConfigure implements WebMvcConfigurer {
    /* 在应用启动阶段就执行 */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ComRestControllerInterceptor());
    }
}
复制代码

2.3 HandlerInterceptor接口实现

public class ComRestControllerInterceptor implements HandlerInterceptor {
​
    public static final String REST_CONTROLLER_ANNO = "REST_CONTROLLER_ANNO";
​
    /* 在rest接口前执行该方法 */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Class<?> clazz = handlerMethod.getBeanType();
            request.setAttribute(REST_CONTROLLER_ANNO, clazz.isAnnotationPresent(ComRestController.class));
        }
        // 返回true时, 执行拦截器链上下一个拦截器, 直到所有拦截器的preHandle方法执行完成后, 再执行被拦截的请求
        // 返回false时, 不再执行拦截器链后续的preHandle方法以及被拦截的请求
        return true;
    }
}
复制代码

2.4 封装接口异常的返回结果

只需要修改support方法,间接判断当前controller是否带有ComRestController注解即可。

@RestControllerAdvice  //拦截Controller方法的返回值, 统一处理返回值/响应体, 一般用于统一返回格式、加解密、签名等等
public class RestResponseAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;
​
    /*
    * 是否支持advice功能
    * true支持, false不支持
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 带有自定义的ComRestController注解则启用封装返回结果数据, 否则不启用封装逻辑
        if(requestAttributes instanceof ServletRequestAttributes) {
            ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
            return Boolean.parseBoolean(String.valueOf(servletRequestAttributes.getRequest().
                    getAttribute(ComRestControllerInterceptor.REST_CONTROLLER_ANNO)));
        }
        return true;
    }
​
    /*
     * 处理返回的结果数据
     */
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if(o instanceof String){
            return objectMapper.writeValueAsString(ResultData.success(o));
        }
        if (o instanceof ResultData) {
            return o;
        }
        return ResultData.success(o);
    }
}
复制代码

2.5 项目结构

image-20211107104006324.png

3 业务服务引用

公共服务的安装、业务服务引入公共服务及配置参考上一篇博客,这里不再赘述。

3.1 接口验证

(1)使用SpringBoot的@RestController注解

@RestController
@RequestMapping("/api/original")
public class RestOriginal {
​
    @GetMapping("/success")
    public String GetSuccess() {
        return "hello, don!";
    }
​
    @GetMapping("/error")
    public int GetError() {
        return 6/0;
    }
}
复制代码

接口调用结果没有进行封装处理,直接返回字符串。

image-20211107104242314.png

(2)使用自定义的的@ComRestController注解

@ComRestController  //自定义注解
@RequestMapping("/api/anno")
public class RestAnno {
​
    @GetMapping("/success")
    public String GetSuccess() {
        return "hello, don!";
    }
​
    @GetMapping("/error")
    public int GetError() {
        return 6/0;
    }
}
复制代码

接口调用结果经过统一的封装处理

image-20211107104345094.png