基於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