• 使用Spring AOP 和自定义注解统一API返回值格式


    摘要:统一接口返回值格式后,可以提高项目组前后端的产出比,降低沟通成本。因此,在借鉴前人处理方法的基础上,通过分析资料,探索建立了一套使用Spring AOP和自定义注解无侵入式地统一返回数据格式的方法。

    §前言

      我们封装所有的Controller中接口返回结果,将其处理为统一返回数据结构后,可以提高前后端对接效率,降低沟通成本。而使用Spring AOP和自定义注解无侵入式地统一返回数据格式,则可以避免在每一个api上都处理返回格式,从而使后端开发节约开发时间,更加专注于开发业务逻辑。

      后端返回给前端的自定义返回格式如下:

    {
        "code":200,
        "message":"OK",
        "data":{
        }
    }
    

    其中的三个参数的含义如下:

    • code: 返回状态码;
    • message: 返回信息的描述;
    • data: 返回值。

    §直接修改API返回值类型

      Spring AOP和自定义注解的基本概念可以分别参考《Spring AOP 面向切面编程之AOP是什么》和《Spring注解之自定义注解入门》。下面定义返回数据格式的封装类:

    package com.eg.wiener.config.result;
    
    import lombok.Getter;
    import lombok.ToString;
    
    import java.io.Serializable;
    
    @Getter
    @ToString
    public class ResultData<T> implements Serializable {
    
        private static final long serialVersionUID = 4890803011331841425L;
    
        /** 业务错误码 */
        private Integer code;
        /** 信息描述 */
        private String message;
        /** 返回参数 */
        private T data;
    
        private ResultData(ResultStatus resultStatus, T data) {
            this.code = resultStatus.getCode();
            this.message = resultStatus.getMessage();
            this.data = data;
        }
    
        /** 业务成功返回业务代码和描述信息 */
        public static ResultData<Void> success() {
            return new ResultData<Void>(ResultStatus.SUCCESS, null);
        }
    
        /** 业务成功返回业务代码,描述和返回的参数 */
        public static <T> ResultData<T> success(T data) {
            return new ResultData<T>(ResultStatus.SUCCESS, data);
        }
    
        /** 业务成功返回业务代码,描述和返回的参数 */
        public static <T> ResultData<T> success(ResultStatus resultStatus, T data) {
            if (resultStatus == null) {
                return success(data);
            }
            return new ResultData<T>(resultStatus, data);
        }
    
        /** 业务异常返回业务代码和描述信息 */
        public static <T> ResultData<T> failure() {
            return new ResultData<T>(ResultStatus.INTERNAL_SERVER_ERROR, null);
        }
    
        /** 业务异常返回业务代码,描述和返回的参数 */
        public static <T> ResultData<T> failure(ResultStatus resultStatus) {
            return failure(resultStatus, null);
        }
    
        /** 业务异常返回业务代码,描述和返回的参数 */
        public static <T> ResultData<T> failure(ResultStatus resultStatus, T data) {
            if (resultStatus == null) {
                return new ResultData<T>(ResultStatus.INTERNAL_SERVER_ERROR, null);
            }
            return new ResultData<T>(resultStatus, data);
        }
    }
    
     === 我是分割线 === 
     package com.eg.wiener.config.result;
    
    import lombok.Getter;
    import lombok.ToString;
    import org.springframework.http.HttpStatus;
    
    @ToString
    @Getter
    public enum ResultStatus {
        SUCCESS(HttpStatus.OK.value(), "OK"),
        BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), "Bad Request"),
        INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Internal Server Error"),
        TOO_MANY_REQUESTS(HttpStatus.TOO_MANY_REQUESTS.value(), "请勿重复请求"),
        USER_NOT_FIND(-99, "请登陆");
    
        /**
         * 业务状态码
         */
        private Integer code;
        /**
         * 业务信息描述
         */
        private String message;
    
        ResultStatus(Integer code, String message) {
            this.code = code;
            this.message = message;
        }
    }
    

      在UserController类中添加测试函数,其返回值直接使用上面定义的ResultData:

    /**
     * @author Wiener
     */
    @RestController
    @RequestMapping("/user")
    public class UserController {
        private static Logger logger = LoggerFactory.getLogger(UserController.class);
    
        /**
         * 示例地址 xxx/user/getResultById?userId=1090330
         *
         * @author Wiener
         */ 
        @GetMapping(value ="/getResultById", produces = "application/json; charset=utf-8")
        public ResultData getResultById(Long userId) {
            User user = new User();
            user.setId(userId);
            user.setAddress("测试地址是 " + UUID.randomUUID().toString());
            logger.info(user.toString());
            return ResultData.success(user);
        }
    }
    

      启动项目,在浏览器中输入URL,得到如下结果:

    {
        "code":200,
        "message":"OK",
        "data":{
            "id":1090330,
            "userName":null,
            "age":null,
            "address":"测试地址是 f057181c-e7e2-41ec-9066-0e72619f0f86",
            "mobilePhone":null
        }
    }
    

      说明已经成功定义返回数据格式,但是,这里并没有使用自定义注解和Spring AOP这两个知识点。客官莫急,待我喝完这杯咖啡,且听我娓娓道来。

    §设置全局返回值类型

      下面首先定义一个用于设置全局切入点的自定义注解@ResponseResultBody。参考文献中强调,此自定义注解需要添加@ResponseBody注解,这是画蛇添足,下文的测试用例将给大家演示不添加也可以达到同样的目的。

    package com.eg.wiener.aop;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface ResponseResultBody {
    }
    

      通过实现ResponseBodyAdvice接口,基于上述自定义注解@ResponseResultBody获取切面,封装返回值格式:

    package com.eg.wiener.config.result;
    
    import com.eg.wiener.aop.ResponseResultBody;
    import org.springframework.core.MethodParameter;
    import org.springframework.core.annotation.AnnotatedElementUtils;
    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 java.lang.annotation.Annotation;
    
    @RestControllerAdvice
    public class ResultBodyAdvice implements ResponseBodyAdvice<Object> {
    
        private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseResultBody.class;
    
        /**
         * 基于自定义注解判断类或者方法是否使用了注解 @ResponseResultBody,使用就拦截
         * @return boolean. true:执行函数beforeBodyWrite;否则,不执行
         */
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE)
                    || returnType.hasMethodAnnotation(ANNOTATION_TYPE);
        }
    
        /**
         * 当类或者方法使用了 @ResponseResultBody 就会调用这个方法
         */
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
              Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            // 避免重复封装响应报文
            if (body instanceof ResultData) {
                return body;
            }
            // 使用约定全局返回格式封装响应报文
            return ResultData.success(body);
        }
    
    }
    

      修改测试用例,添加自定义注解@ResponseResultBody:

    
    /**
     * @author Wiener
     */
    @RestController
    @RequestMapping("/user")
    @ResponseResultBody
    public class UserController {
        private static Logger logger = LoggerFactory.getLogger(UserController.class);
    
         * 示例地址 /user/getUserTestById?userId=1090330
         *
         * @author Wiener
         */
        @GetMapping(value ="/getUserTestById", produces = "application/json; charset=utf-8")
        public User getUserTestById(Long userId) throws Exception {
            User user = new User();
            user.setId(userId);
            user.setAddress("测试地址是 " + UUID.randomUUID().toString());
            logger.info(user.toString());
            return user;
        }
    
        /**
         * /user/getResult?userId=1090330
         * @param user
         * @return
         */
        @PostMapping(value ="/getResult", produces = "application/json; charset=utf-8")
        public ResultData getResult(@RequestBody User user) {
            user.setAddress("测试地址是 " + UUID.randomUUID().toString());
            logger.info(user.toString());
            return ResultData.success(user);
        }
    }
    

      启动服务,在Postman中请求getResult函数,可以得到如下结果:

    在浏览器中请求API getUserTestById,可以得到如下结果:

    说明成功统一返回值格式。

    §小结

      本篇文章的内容基本上就是这些,我们来总结一下。在控制层添加自定义注解@ResponseResultBody后,我们就可以通过Spring AOP定义基于此注解的切面。如果一个代理类使用了此注解,那么就封装其返回值;否则,不处理。

    §Reference


      读后有收获,小礼物走一走,请作者喝咖啡。

    赞赏支持

  • 相关阅读:
    面试精选:链表问题集锦
    经典排序算法总结与实现 ---python
    Python高级编程–正则表达式(习题)
    Python面试题汇总
    Python正则表达式
    Linux下的Libsvm使用历程录
    在 linux(ubuntu) 下 安装 LibSVM
    过拟合
    百度历年笔试面试150题
    MATLAB 的数据类型
  • 原文地址:https://www.cnblogs.com/east7/p/14433111.html
Copyright © 2020-2023  润新知