一、需求
为了解决重复提交造成数据冗余出现误差,防止前端提交过快造成服务器不必要的压力过大
二、源码解析
采用技术spring AOP、反射动态代理、spring EL表达式、redis同步锁、java自定义注解
1.注解
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoReSubmit {
/**
* 限制接口再次提交时间(秒)
*
* @return
*/
int waiting() default 3;
/**
* 提示错误码,默认请勿重复提交,可自定义为字符串直接提醒
*
* @return
*/
int error() default 1006;
/**
* 前缀属性,自定义防止redis的key重复
*
* @return
*/
String prefix() default "noReSubmit";
/**
* 需要的参数名数组
*
* @return
*/
String[] keys();
}
2.AOP
@Aspect
@Component
public class NoReSubmitAspect {
@Resource
private ExtendJedisService extendJedisService;
/**
* 切入点
*/
@Pointcut("@annotation(com.cxp.common.annotation.NoReSubmit)")
public void pointcut() {
}
/**
* 前置处理器
*
* @param joinPoint
*/
@Before("pointcut()")
public void joinPoint(JoinPoint joinPoint) {
// 获取参数对象列表
Object[] args = joinPoint.getArgs();
// 获取方法签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 得到方法
Method method = methodSignature.getMethod();
// 得到方法名
String methodName = method.getName();
// 获取参数数组
Parameter[] parameters = method.getParameters();
// 得到注解实例
NoReSubmit noReSubmit = method.getAnnotation(NoReSubmit.class);
// 获取该注解属性值
int waiting = noReSubmit.waiting();
int error = noReSubmit.error();
String prefix = noReSubmit.prefix();
String[] keys = noReSubmit.keys();
// 初始化springEL表达式解析器实例
ExpressionParser parser = new SpelExpressionParser();
// 初始化解析内容上下文
EvaluationContext context = new StandardEvaluationContext();
// 把参数名和参数值放入解析内容上下文里
for (int i = 0; i < parameters.length; i++) {
if (args[i] != null) {
// 添加解析对象目标
context.setVariable(parameters[i].getName(), args[i]);
}
}
// 解析定义key对应的值,拼接成key
StringBuffer noReSubmitKey = new StringBuffer(prefix + ":" + methodName);
for (int i = 0; i < keys.length; i++) {
// 解析对象
Expression expression = parser.parseExpression(keys[i]);
noReSubmitKey.append(":" + expression.getValue(context));
}
// 使用redis的锁实现重复,同步且原子
boolean bool = extendJedisService.setnx(noReSubmitKey.toString(), System.currentTimeMillis() + "", waiting);
if (!bool) {
throw new BusinessException(ErrorCode.getErrorCode(error));
}
}
}
3.方法使用
@NoReSubmit(prefix = "prepare", keys = {"#TopicPO.interactBO.interactId", "#TopicPO.userBO.userId"}, waiting = 3 * 60 * 60)
public ResultData prepare(@RequestBody TopicPO topicPO) {
log.info("准备参数TopicPO={}", TopicPO);
return new ResultData();
}
key的定义遵循spring EL表达式规则,解析方便,可以参考spring的@Cacheable注解,也是使用spring EL解析的。
三、踩坑
1.开始使用想用json解析来解析key(之前key定义不是这种格式),但是要自己写解析方法,比较麻烦而且效率不是很好,最后组内大哥推荐@Cacheable实现方案,后来看源码是spring EL实现,最终采用这种方式
2.还有一个java8的问题,java8获取参数名,正常应该是写的什么是什么,如我这topicPO,但是反射的动态代理出来的参数名是arg0、arg1…,所以我的这里:
context.setVariable(parameters[i].getName(), args[i]);
放入的set的变量名是arg0,解析的时候找不到注解里写的key。本地idea会默认编译成写的方法名,发布到环境就不会生效。
解决办法:(我用的第一种)
1.pom文件添加注释那个配置,编译就OK,
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF8</encoding>
<!-- java8把方法参数编译成我们写的名字,不然默认是arg0,arg1... -->
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
2.用spring自带的获取方法名
//获取方法参数名
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
3.编译时加上 javac -parameters 参数,不管是本地还是环境的jenkins部署
四、总结
网上也有通过注解来解决防重问题,我参考过一些,但是对我的现有业务不太合适,所以自己就干脆自己整一个吧,也学到不少知识,aop加深、动态代理加深、spring EL学习,后来了解到好多解析都用到spring EL。