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

发布了212 篇原创文章 · 获赞 934 · 访问量 106万+

猜你喜欢

转载自blog.csdn.net/ITBigGod/article/details/105567916