基于Spring AOP的统一响应体的实现(注解版)

一、前言

在上一篇系列中 我们 统一参数校验,统一结果响应,统一异常处理,统一错误处理,统一日志记录,统一生成api文档,

对于统一数据响应返回规范那里(5. 统一结果响应
),我们写的方式不采用注解的

介于springboot中注解的使用较为频繁,特意增加一个自定义注解版本来完成的统一响应的操作。

二、思路

使用Spring的Controller增强机制,其中关键的类为以下3个:

  • @ControllerAdvice:类注解,用于指定Controller增强处理器类。
  • ResponseBodyAdvice:接口,实现beforeBodyWrite()方法后可以对响应的body进行修改,需要结合@ControllerAdvice使用。
  • @ExceptionHandler:方法注解,用于指定异常处理方法,需要结合@ControllerAdvice和@ResponseBody使用。

三、代码

本示例使用的Spring Boot版本为2.1.6.RELEASE,同时需要开发工具安装lombok插件。

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.6.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.rjh</groupId><artifactId>spring-web-unified-response-demo</artifactId><version>0.0.1-SNAPSHOT</version><name>spring-web-unified-response-demo</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><!--web-starter--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--test-starter--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

1.统一的公共响应体:(统一结果类)

Controller增强后统一响应体对应的对象

新建ResponseResult.java

package com.zoutao.web.response;import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;/*** @title: ResponseResult* @Description:  1.统一的公共响应体(统一结果类)* @Author: ZouTao* @Date: 2020/4/15*/
@Data
@AllArgsConstructor
public class ResponseResult implements Serializable {/*** 返回的状态码*/private Integer code;/*** 返回的信息*/private String msg;/*** 返回的数据*/private Object data;
}

2. 统一响应注解:自定义注解

统一响应注解是一个标记是否开启统一响应增强的注解
@Retention(RetentionPolicy.RUNTIME) //运行时生效
@Target({ElementType.METHOD, ElementType.TYPE}) // 用于描述注解的使用范围

BaseResponse.java:

package com.zoutao.web.response;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @title: BaseResponse* @Description:  2.统一响应注解--自定义注解。* 添加注解后,统一响应体才能生效* @Author: ZouTao* @Date: 2020/4/15*/
@Retention(RetentionPolicy.RUNTIME)  //运行时生效
@Target({ElementType.METHOD, ElementType.TYPE}) // 用于描述注解的使用范围
public @interface BaseResponse {}

3. 响应码枚举

统一响应体中返回的状态码code和状态信息msg对应的枚举类

ResponseCode枚举类:

package com.zoutao.web.response;/*** @title: ResponseCode* @Description:  3.状态信息枚举* @Author: ZouTao* @Date: 2020/4/15*/
public enum ResponseCode {/*** 成功返回的状态码*/SUCCESS(10000, "success"),/*** 资源不存在的状态码*/RESOURCES_NOT_EXIST(10001, "资源不存在"),/*** 所有无法识别的异常默认的返回状态码*/SERVICE_ERROR(50000, "服务器异常");/*** 状态码*/private int code;/*** 返回信息*/private String msg;ResponseCode(int code, String msg) {this.code = code;this.msg = msg;}public int getCode() {return code;}public String getMsg() {return msg;}
}

4. 业务异常类

继承运行异常,确保事务正常回滚

BaseException.java类:

package com.zoutao.web.exception;import com.zoutao.web.response.ResponseCode;
import lombok.Data;
import lombok.EqualsAndHashCode;/*** @title: BaseException* @Description:  4.业务异常类,继承运行异常,确保事务正常回滚* @Author: ZouTao* @Date: 2020/4/15*/
@Data
@EqualsAndHashCode(callSuper = false)
public class BaseException extends RuntimeException{private ResponseCode code;  // 枚举对象public BaseException(ResponseCode code) {this.code = code;}public BaseException(Throwable cause, ResponseCode code) {super(cause);this.code = code;}
}

5.异常处理类(使用到了自定义注解)

用于处理Controller运行时未捕获的异常的处理类。

ExceptionHandlerAdvice.java:

package com.zoutao.web.response;import com.zoutao.web.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;/*** @title: ExceptionHandlerAdvice* @Description:  5.异常处理器* 用于处理Controller中所有运行时未捕获到的异常的处理类。* @Author: ZouTao* @Date: 2020/4/15*/
@ControllerAdvice(annotations = BaseResponse.class)
@ResponseBody
@Slf4j
public class ExceptionHandlerAdvice {/*** 处理未捕获的Exception* @param e 异常* @return 统一响应体* data:null*/@ExceptionHandler(Exception.class)public ResponseResult handleException(Exception e){log.error(e.getMessage(),e);return new ResponseResult(ResponseCode.SERVICE_ERROR.getCode(),ResponseCode.SERVICE_ERROR.getMsg(),null);}/*** 处理未捕获的RuntimeException* @param e 运行异常* @return 统一响应体* data:null*/@ExceptionHandler(RuntimeException.class)public ResponseResult handleRuntimeException(RuntimeException e){log.error(e.getMessage(),e);return new ResponseResult(ResponseCode.SERVICE_ERROR.getCode(),ResponseCode.SERVICE_ERROR.getMsg(),null);}/*** 处理业务异常BaseException* @param e 业务异常* @return 统一响应体* data:null*/@ExceptionHandler(BaseException.class)public ResponseResult handleBaseException(BaseException e){log.error(e.getMessage(),e);ResponseCode code=e.getCode();return new ResponseResult(code.getCode(),code.getMsg(),null);}
}

6.响应体增强类(使用到了自定义注解)

Conrtoller增强的统一响应体处理类,需要注意异常处理类已经进行了增强,所以需要判断一下返回的对象是否为统一响应体对象。

ResponseResultHandlerAdvice.java类:

package com.zoutao.web.response;import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;/*** @title: ResponseResultHandlerAdvice* @Description:  6.统一响应体处理器--响应增强类* 对Conrtoller增强的统一响应体处理类,需要注意异常处理类已经进行了增强,* 所以需要判断一下返回的对象是否为统一响应体对象。* @Author: ZouTao* @Date: 2020/4/15*/
@ControllerAdvice(annotations = BaseResponse.class)
@Slf4j
public class ResponseResultHandlerAdvice implements ResponseBodyAdvice {// 如果接口返回的类型本身就是统一响应体的格式,那就没有必要进行额外的操作,返回true@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {log.info("returnType:"+returnType);log.info("converterType:"+converterType);return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {if(MediaType.APPLICATION_JSON.equals(selectedContentType) || MediaType.APPLICATION_JSON_UTF8.equals(selectedContentType)){ // 判断响应的Content-Type为JSON格式的bodyif(body instanceof ResponseResult){ // 如果响应返回的对象为统一响应体,则直接返回bodyreturn body;}else{// 只有正常返回的结果才会进入这个判断流程,返回正常成功的状态码+信息+数据。ResponseResult responseResult =new ResponseResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),body);return responseResult;}}// 非JSON格式body直接返回即可return body;}
}

7.使用接口示例

准备一个User实体类。

User.java:

package com.zoutao.web.entity;import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;/*** @title: User* @Description:  7.用户类用来测试* @Author: ZouTao* @Date: 2020/4/15*/
@Data
@EqualsAndHashCode
public class User implements Serializable {private Integer id;private String name;}

然后是准备一个简单的UserController,使用@BaseResponse自定义注解标识。

UserController.java:

package com.zoutao.web.controller;import com.zoutao.web.entity.User;
import com.zoutao.web.exception.BaseException;
import com.zoutao.web.response.BaseResponse;
import com.zoutao.web.response.ResponseCode;
import org.springframework.web.bind.annotation.*;import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;/*** @title: UserController* @Description:  8.测试用的Controller* 用了一些高并发场景下的类型* @Author: ZouTao* @Date: 2020/4/15*/
@BaseResponse
@RestController
@RequestMapping("users")
public class UserController {/*** 当前ID* AtomicInteger 并发下保证原子性*/private AtomicInteger currentId = new AtomicInteger(1);/*** 用户列表* ConcurrentHashMap 并发散列映射表* 在并发情况下不能使用HashMap。* https://www.jianshu.com/p/d0b37b927c48*/private Map<Integer,User> users = new ConcurrentHashMap<>();/*** 根据用户ID获取用户* @param userId 用户ID* @return*/@GetMapping("/{userId}")public User getUserById(@PathVariable Integer userId){// 用的是json的containsKey()函数来判断json串中是否存在key
//        if(users.containsKey(userId)){//            return users.get(userId);
//        }else{//            throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
//        }// 测试用的if(userId.equals(0)){throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);}if(userId.equals(1)){throw new RuntimeException();}User user=new User();user.setId(userId);user.setName("test");return user;}/*** 列出所有用户* @return*/@GetMapping("/allUser")public Map<String, List<User>> listAllUsers(){System.out.println("进入列出所有用户"+users.values()); 获取Map的value集合User user1 = new User();user1.setId(1);user1.setName("张三");User user2 = new User();user2.setId(2);user2.setName("李四");List<User> list = new ArrayList<>();list.add(user1);list.add(user2);Map<String, List<User>> map = new HashMap<>();map.put("items", list);return map;}/*** 新增用户* @param user 用户实体* @return*/@PostMapping("/addUser")public User addUser(@RequestBody User user){System.out.println("进入新增用户"+currentId.getAndIncrement());user.setId(currentId.getAndIncrement());users.put(user.getId(),user);return user;}/*** 更新用户信息* @param userId* @param user* @return*/@PutMapping("/{userId}")public User updateUser(@PathVariable Integer userId,@RequestBody User user){if(users.containsKey(userId)){User newUser=users.get(userId);newUser.setName(user.getName());return newUser;}else{throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);}}/*** 删除用户* @param userId 用户ID* @return*/@DeleteMapping("/{userId}")public User deleteUserById(@PathVariable Integer userId){User user=users.remove(userId);if(user!=null){return user;}else{throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);}}
}

UserController写了一个普通版,也写了一个并发版,用了一些高并发场景下的数据类型。

8. 测试结果:

在postman中访问http://localhost:8080/users/0,则返回结果如下:

http://localhost:8080/users/1:

http://localhost:8080/users/allUser:


由运行结果可以得知统一响应增强已经生效,而且能够很好的处理异常。

9. 总结:

  • 注解版本,通过自定义注解,来完成统一规范的构建。
  • 高并发下的统一规范写法。

两篇文章中,两种版本对比:

  • 在异常处理器中,分别使用的是 @ExceptionHandler@ControllerAdvice 来实现统一处理异常。

  • @ExceptionHandler,可以处理异常, 但是仅限于当前Controller中处理异常,

  • @ControllerAdvice,大体意思是控制器增强,可以配置basePackage下的所有controller. (或者是标识了自定义注解@BaseResponse的controller),所以结合两者使用,就可以处理全局的异常了。

总结就是:在@ControllerAdvice注解下的类,里面的方法用@ExceptionHandler注解修饰的方法,会将对应的异常交给对应的方法处理。

比如:

@ControllerAdvice
public class GlobalExceptionHandle {@ExceptionHandler({IOException.class})
public Result handleException(IOExceptione) {log.error("[handleException] ", e);return ResultUtil.failureDefaultError();}
}

整体项目的下载地址:喜欢就给个评论吧~


其他就是一些本文中,使用到的并发下的数据类型的知识点:(仅做参考)

知识点:ConcurrentHashMap

HashMap虽然性能好,可它是非线程安全的,在多线程并发下会出现问题,那么有没有解决办法呢? 当然有,可以使用Collections.synchronizedMap()将hashmap包装成线程安全的,底层其实使用的就是synchronized关键字。但是前面说了,synchronized是重量级锁,独占锁,它会对hashmap的put、get整个都加锁,显然会给并发性能带来影响,类似hashtable。

简单解释一下。
hashmap的底层是哈希表(数组+链表,java1.8后又加上了红黑树),若使用synchronizedMap(),那么在线程对哈希表做put/get时,相当于会对整个哈希表加上锁,那么其他线程只能等锁被释放才能争夺锁并操作哈希表,效率较低。
hashtable虽是线程安全的,但其底层也是用synchronized实现的线程安全,效率也不高。
对此,JUC(java并发包)提供了一种叫做ConcurrentHashMap的线程安全集合类,它使用分段锁来实现较高的并发性能。

在java1.7及以下,ConcurrentHashMap使用的是Segment+ReentrantLock,ReentrantLock相比于synchronized的优点更多。

在java1.8后,对ConcurrentHashMap做了一些调整,主要有:

  1. 链表长度>=8时,链表会转换为红黑树,<=6时又会恢复成链表;
  2. 1.7及以前,链表采用的是头插法,1.8后改成了尾插法;
  3. Segment+ReentrantLock改成了CAS+synchronized。
    在java1.8后,对synchronized进行了优化,优化后的synchronized甚至比ReentrantLock性能要更好。

不过即使有了ConcurrentHashMap,也不能忽略HashMap,因为各自适用于不同场景,如HashMap适合于单线程,ConcurrentHashMap则适合于多线程对map进行操作的环境下。
参考地址:https://www.sohu.com/a/205451532_684445

知识点:AtomicInteger

高并发的情况下,i++无法保证原子性,往往会出现问题,所以引入AtomicInteger类。

TestAtomicInteger.java测试类:

package com.zoutao.web.entity;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;/*** @Description: 通过多次运行测试,可以看到只有AtomicInteger能够真正保证最终结果永远是2000。* @Author: Zoutao* @Date: 2020/4/15*/
public class TestAtomicInteger {private static final int THREADS_COUNT = 2;  //线程数public static int count = 0;                // 传统变量public static volatile int countVolatile = 0; // volatile标识为并发变量public static AtomicInteger atomicInteger = new AtomicInteger(0); // AtomicInteger变量public static CountDownLatch countDownLatch = new CountDownLatch(2); //countDownLatch是一个计数器,线程完成一个记录一个,递减,只能用一次public static void increase() {count++;countVolatile++;atomicInteger.incrementAndGet();}public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[THREADS_COUNT];System.out.println("主线程开始执行…… ……");for(int i = 0; i< threads.length; i++) {threads[i] = new Thread(() -> {for(int i1 = 0; i1 < 1000; i1++) {increase();  //调用递增方法}/*** 每次减少一个容量*/countDownLatch.countDown();System.out.println("thread counts = " + (countDownLatch.getCount()));//线程计数});threads[i].start();}countDownLatch.await();System.out.println("concurrency counts = " + (100 - countDownLatch.getCount())); //并发计数System.out.println(count);System.out.println(countVolatile);System.out.println(atomicInteger.get());}
}

volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

count++是一个非原子性的操作,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致的情况:

假如某个时刻变量count的值为10,线程1对变量进行自增操作,线程1先读取了变量count的原始值,然后线程1被阻塞了;线程2也对变量进行自增操作,线程2先读取了count的原始值,线程1只是进行了读取操作,没有进行写的操作,所以不会导致线程2中的本地缓存无效,因此线程2进行++操作,在把结果刷新到主存中去,此时线程1在还是读取原来的10 的值在进行++操作,所以线程1和线程2对于count=10进行两次++操作,结果都为11.。

上述问题解决方法:
1.采用add方法中加入sychnorized.,或者同步代码块
2.采用AtomicInteger

参考:
https://www.jianshu.com/p/4ed887664b13
https://www.cnblogs.com/startSeven/p/10223736.html
https://www.cnblogs.com/ziyue7575/p/12213729.html

知识点:countDownLatch

countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。一个计数器的作用。
是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

参考:https://www.jianshu.com/p/e233bb37d2e6

知识点:volitile关键字

被volatile修饰的共享变量,就具有了以下两点特性:

1 . 保证了不同线程对该变量操作的内存可见性;

2 . 禁止指令重排序。

参考地址:
https://mp.weixin.qq.com/s?__biz=MzI4MDYwMDc3MQ==&mid=2247486266&idx=1&sn=7beaca0358914b3606cde78bfcdc8da3&chksm=ebb74296dcc0cb805a45ca9c0501b7c2c37e8f2586295210896d18e3a0c72b01bea765924ce5&mpshare=1&scene=24&srcid=&key=c8fbfa031bd0c4166acd110fd54b85e9b3568f80a3f4c2d80add2f4add0ced46d1d3a0cf139c0ca64877a98635727a7fc593b850f8082d1fcf77a5ebf067fc1476285146d13d691f80b64b930006a341&ascene=0&uin=MjYwNzAzMzYzNw%3D%3D&devicetype=iMac+MacBookAir6%2C2+OSX+OSX+10.14.2+build(18C54)&version=12020810&nettype=WIFI&lang=zh_CN&fontScale=100&pass_ticket=hbg9AwR77rok2jxxdwyHyTHBDzwwC7lR8aEfF6HfW4KgJwsj0ruOpw8iNsUK%2B5kK

参考:https://blog.csdn.net/zzzgd_666/article/details/81544098

基于Spring AOP的统一响应体的实现(注解版)相关推荐

  1. 基于spring boot的统一异常处理

    基于spring boot的统一异常处理 参考文章: (1)基于spring boot的统一异常处理 (2)https://www.cnblogs.com/knyel/p/7804237.html 备 ...

  2. 注解参数获取不到_scm-springboot基于spring boot的统一注解缓存

    scm-springboot 基于spring boot的统一注解缓存,支持mencached.redis.ehcache的缓存无缝切换.支持单个缓存设置过期时间,灵活的key设置规则,采用fastj ...

  3. 基于Spring AOP实现权限控制

    基于Spring AOP实现一个简单的接口权限 1.定义自定义注解 //注解的作用类型:方法.类 @Target({ElementType.METHOD,ElementType.TYPE}) @Ret ...

  4. Spring boot异常统一处理方法:@ControllerAdvice注解的使用、全局异常捕获、自定义异常捕获

    Spring boot异常统一处理方法:@ControllerAdvice注解的使用.全局异常捕获.自定义异常捕获 参考文章: (1)Spring boot异常统一处理方法:@ControllerAd ...

  5. 基于Spring AOP的JDK动态代理和CGLIB代理

    一.AOP的概念  在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.AOP是OOP的 ...

  6. s2sh框架搭建(基于spring aop)

    对于spring aop 是如何管理事务的,请看一下:http://bbs.csdn.net/topics/290021423 1.applicationContext.xml <?xml ve ...

  7. java aop管理权限_基于spring aop 权限管理系统原型 - andyj2ee - BlogJava

    此权限管理系统把待访问的业务层方法做为权限管理中的资源,通过spring aop 对接口方法进行拦截,来实现权限的管理,可以实现细粒度的权限控制. 在上文体验了spring aop 一些特性,aop ...

  8. 基于Spring Boot 的统一错误处理

    我将展示一下如何在Spring Boot 下实现统一错误处理,使用的工具是IDEA. 首先,我们需要定义一个自定义名称的错误控制类,我把它叫做WebErrorController,这个类需要实现Err ...

  9. Spring Boot2.x-09 基于Spring Boot 2.1.2 + Mybatis使用自定义注解实现数据库切换

    文章目录 概述 场景说明:读写分离 操作步骤 工程结构 Step1 自定义注解 Step2 数据源定义 Step3 配置文件配置数据源 Step4 数据源实例化DatasourceConfig Ste ...

最新文章

  1. 万物皆为向量:在线向量召回工程服务化实践
  2. python excel行数_使用python计算excel中的行数
  3. OpenShift — 核心概念
  4. 一不小心就踩坑的fail-fast是个什么鬼?
  5. layer:好看的弹出窗口
  6. 大众CEO迪斯承认芯片仍短缺
  7. 2、Shiro的认证
  8. self = [super init]的解释
  9. UML简单介绍(五)——用例图的使用案例与分析
  10. bootstrap-select 通过拼音搜索汉字下拉框方法
  11. fast无线路由器设置服务器,迅捷(FAST)路由器静态ip上网设置方法
  12. 三维地质建模数据处理
  13. Android 常用颜色列表
  14. 传递函数的幅值计算公式_设积分环节的传递函数为G(s)=1/s ,则其频率特性幅值M(ω)=( )...
  15. 【C语言入门】SDUT《程序设计基础I 》实验2-选择结构程序设计题解 c语言语法讲解
  16. 小米盒子4c android,小米盒子4/4C规格曝光 有一定升级
  17. sklearn preprocessing 数据预处理 OneHotEncoder
  18. (zhuan) 126 篇殿堂级深度学习论文分类整理 从入门到应用
  19. VMware Horizon 8 2111 部署系列(十三)创建应用程序池
  20. Linux常用命令——lftp命令

热门文章

  1. c语言闹钟程序源代码,用c语言实现的立体闹钟源代码
  2. Python_四舍五入
  3. java 连接 sqlserver 2000
  4. (java)Poj2503:Babelfish(在线翻译)(通天鱼)
  5. 浏览器语言列表(语种名称代码)
  6. 王小草【机器学习】笔记--EM算法
  7. CSS_09_盒子模型(二)
  8. 10.17面试类题1:V23双向数据绑定,2:defineProperty,3.sass和scss
  9. 焊接技术操作要领!焊工必须得掌握
  10. 出于安全考虑,Velodyne Lidar将不参加CES 2022现场活动