基於Spring AOP的統一響應體的實現(註解版)

基於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做了一些調整,主要有:

  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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章