基于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
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
log.info("returnType:"+returnType);
log.info("converterType:"+converterType);
return true;
}
@Override
public 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格式的body
if(body instanceof ResponseResult){ // 如果响应返回的对象为统一响应体,则直接返回body
return 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做了一些调整,主要有:
- 链表长度>=8时,链表会转换为红黑树,<=6时又会恢复成链表;
- 1.7及以前,链表采用的是头插法,1.8后改成了尾插法;
- 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