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: 对需要实现幂等的接口方法上面加该注解.
此时调用接口就能实现接口幂等了.在五秒钟内多次请求就会提示提交频率过快,请在五秒后重试! ! !