注解实现接口幂等防重

一、需求

为了解决重复提交造成数据冗余出现误差,防止前端提交过快造成服务器不必要的压力过大

二、源码解析

采用技术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。

你可能感兴趣的:(java相关)