基于注解实现接口幂等机制防止数据重复提交

1:什么是接口幂等性? 能解决什么问题?

接口幂等性是指无论调用接口的次数是一次还是多次,对于同一资源的操作都只会产生相同的效果。 比如: 一个订单记账的时候,会同步扣减库存数量,如果记账的按钮被用户多次触发,就会导致一个订单库存却被多次扣减的情况. 如果对每一个接口都进行特殊的处理会导致工作量增大,并增加了代码可维护性.我下面的代码将通过实现幂等性来避免多次扣减库存.

2:实现接口幂等的步骤

本文采用 注解+切面+reids锁+装饰者模式来实现的.

1:新建一个EqualityAnnotation注解类 (用来对接口进行标识需要实现幂等)

2:新建一个HttRequestWrapper类(该类继承HttpServletRequestWrapper,并基于装饰者设计模式的理念.对每个需要实现幂等的接口进行数据读取操作)

3:新建一个EqualityAspect.java切面类 (用来识别被注解所修饰的接口.并将参数加入redis限时锁起来.)

4:对需要实现幂等的接口方法上面加该注解. 

3:实现接口幂等的详细代码及原理

1:新建一个EqualityAnnotation注解类 

import java.lang.annotation.*;

/**
 * 保持幂等性注解
 */

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public  @interface EqualityAnnotation {
    int seconds()  default 5;  
}

代码解释:  

@Target({ElementType.PARAMETER, ElementType.METHOD})  代表该注解可以修饰在 接口、类、枚举、注解,方法参数

@Retention(RetentionPolicy.RUNTIME)   代表该注解会在class字节码文件中存在,在运行时可以通过反射获取到

@Documented 说明该注解将被包含在javadoc中

2:新建一个HttRequestWrapper类

import org.apache.poi.util.IOUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

public class HttRequestWrapper extends HttpServletRequestWrapper {
    //参数的字节流
    private byte[] requestBoyd;
    //当前请求的http对象
    private HttpServletRequest request;
    public HttRequestWrapper(HttpServletRequest request) {
        super(request);
        this.request=request;
    }

    /**
     * 此方法通过读取出参数的IO流,然后重新填回去,防止@RequestBoyd参数不能重复读取
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException{
        if (null==this.requestBoyd){
            ByteArrayOutputStream byteArrayInputStream=new ByteArrayOutputStream();
            IOUtils.copy(request.getInputStream(),byteArrayInputStream);
            this.requestBoyd=byteArrayInputStream.toByteArray();
        }
        final  ByteArrayInputStream bais=new ByteArrayInputStream(requestBoyd);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException{
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

代码解释: 

      对于form-data的入参只需要调用HttpServletRequest的API读取,但是对于@RequestBody标注的入参是通过IO流读取数据,且IO流只能被读取一次,如果在AOP中读取了,在调到接口的时候则会报错.

     所以本文首先在构造 RepeatedlyRequestWrapper 的时候,就通过 IO 流将数据读取出来并存入到一个 byte 数组中,然后重写 getReader 和 getInputStream 方法,在这两个读取 IO 流的方法中,都从 byte 数组中返回 IO 流数据出来,这样就实现了反复读取了。(这个地方采用了装饰者模式的思想向一个现有的对象添加新的功能,同时又不改变其原有的结构。)

3:新建一个EqualityAspect.java切面类

import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.HttpUtil;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.text.MessageFormat;
import java.util.Objects;
@Component
@Aspect
@Order(1)
public class equalityAspect {
    @Pointcut("@annotation(com.wm.assets.web.config.EqualityAnnotation)")
    public void equalityAspect(){
    }

    @Around("equalityAspect() && @annotation(equalityAnnotation)")
    public Object around(ProceedingJoinPoint joinPoint,EqualityAnnotation equalityAnnotation) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
       // 请求的URL
         String requestURI = URLUtil.getPath(request.getRequestURI());
        // 请求的IP地址
        String clientIp = ServletUtil.getClientIP(request);
        // 请求的参数
        String params = getParams(request);
        // 获取参数
        String key = MessageFormat.format(requestURI, Math.abs(clientIp.hashCode()),Math.abs(MD5Util.getMd5Str(params).hashCode()));
        // 存在即返回false 不存在即返回true
        Boolean ifAbsent = JedisUtils.setNxPx(key,requestURI,equalityAnnotation.seconds());
        Assert.isTrue(ifAbsent, "提交频率过快,请在五秒后重试! ! !");
        return joinPoint.proceed();
    }


    private String getParams(HttpServletRequest request) throws Exception {
        String params;
        // 判断是否封装的request
        if (request instanceof HttpRequestWrapper) {
            params = request.getReader().readLine();
        } else {
            // 非@RequestBody入参读取
            params = HttpUtil.toParams(request.getParameterMap());
        }
        return params;
    }


}

代码解释:

1: @Pointcut("@annotation(com.xxx.xxx.xxx.EqualityAnnotation)") 

 这个aop类定义了一个之前创建好的注解切点.  具体的路径要写你自己的类路径

2:  Boolean ifAbsent = JedisUtils.setNxPx(key,requestURI,equalityAnnotation.seconds());

这个JedisUtils工具类是我自己封装好.如果你没有封装可以直接调用redis的

jedis.set(key, requestURI,new SetParams().nx().ex(equalityAnnotation.seconds()));

 nx()就是指如果值不存在就插入,存在就返回false.所以这个方法很多地方也用来做分布式锁.

3:  Assert.isTrue(ifAbsent, "提交频率过快,请在五秒后重试! ! !");

这个类是我封装的异常抛出类,如果没有封装可以使用 throw new Exception("提交频率过快,请在五秒后重试! ! ");

4: 对需要实现幂等的接口方法上面加该注解. 

基于注解实现接口幂等机制防止数据重复提交_第1张图片

 此时调用接口就能实现接口幂等了.在五秒钟内多次请求就会提示提交频率过快,请在五秒后重试! ! !

你可能感兴趣的:(java,注解,幂等机制)