springboot aop实现接口防重复操作

一、前言
有时在项目开发中某些接口逻辑比较复杂,响应时间长,那么可能导致重复提交问题。

二、如何解决
1.先定义一个防重复提交的注解。

import java.lang.annotation.*;

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {

    /**
     * 防重复操作限时标记数值(存储redis限时标记数值)
     */
    String value() default "value" ;

    /**
     * 防重复操作过期时间(借助redis实现限时控制)
     */
    int expireSeconds() default 10;
}

2.编写防重复操作的AOP

import lombok.extern.slf4j.Slf4j;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;

@Slf4j
@Component
@Aspect
@Order(0)
public class NoRepeatSubmitAspect  {
 private static final String TOKENAuthorization = "Authorization";

    private static final String TOKENUSERNAME = "api-userName";

    private static final String PREVENT_DUPLICATION_PREFIX = "PREVENT_DUPLICATION_PREFIX:";

    @Autowired
    private RedisService redisService;

    @Pointcut("@annotation(com.dp.aop.annotation.RepeatSubmit)")
    public void preventDuplication() {}
    @Around("preventDuplication()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        if (Objects.isNull(request)) {
            return joinPoint.proceed();
        }
        //获取执行方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //获取防重复提交注解
        RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
        //获取token以及方法标记,生成redisKey
        String header = request.getHeader(TOKENAuthorization);
        String token = header == null ? "" : header;
        String requestHeader = request.getHeader(TOKENUSERNAME);
        String headerToken = requestHeader == null ? "" : requestHeader;
        token = token + headerToken;
        String url = request.getRequestURI();
        // 通过前缀 + url + token + 函数参数签名 来生成redis上的 key
        String redisKey = PREVENT_DUPLICATION_PREFIX
                .concat(url)
                .concat(token)
                .concat(getMethodSign(method, joinPoint.getArgs()));
        RedisLock redisLock = null;
        try {
            try {
                redisLock = redisService.tryLock(redisKey, annotation.expireSeconds());
            } catch (Exception e) {
                log.error("tryLock error  ", e);
                throw new BizException(CommonMsgConstants.NoRepeatSubmitMsg);
            }
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            log.error("throwable trace is ", throwable);
            throw new RuntimeException(throwable);
        } finally {
            if (Objects.nonNull(redisLock)) {
                redisLock.unlock();
            }
        }
    }
    /**
     * 生成方法标记:采用数字签名算法SHA1对方法签名字符串加签
     *
     * @param method
     * @param args
     * @return
     */
    private String getMethodSign(Method method, Object... args) {
        StringBuilder sb = new StringBuilder(method.toString());
        for (Object arg : args) {
            sb.append(toString(arg));
        }
        return DigestUtil.sha1Hex(sb.toString());
    }

    private String toString(Object arg) {
        if (Objects.isNull(arg)) {
            return "null";
        }
        if (arg instanceof Number) {
            return arg.toString();
        }
        return JSONObject.toJSONString(arg);
    }

}

3.接下来定义redisService类

@Component
public class RedisService {
	public RedisLock tryLock(String lockKey, int expireTime) {
        String lockValue = UUID.randomUUID().toString();
        Boolean hasLock = (Boolean)this.redisTemplate.execute((connection) -> {
            Object nativeConnection = connection.getNativeConnection();
            String status = null;
            if (nativeConnection instanceof Jedis) {
                Jedis jedis = (Jedis)nativeConnection;
                status = jedis.set(lockKey, lockValue, "nx", "ex", expireTime);
            } else {
                JedisCluster jedisx = (JedisCluster)nativeConnection;
                status = jedisx.set(lockKey, lockValue, "nx", "ex", (long)expireTime);
            }

            return "OK".equals(status);
        });
        if (hasLock) {
            return new RedisService.RedisLockInner(this.redisTemplate, lockKey, lockValue);
        } else {
            throw new RuntimeException("获取锁失败,lockKey:" + lockKey);
        }
    }
 private class RedisLockInner implements RedisLock {
        private RedisTemplate redisTemplate;
        private String key;
        private String expectedValue;

        protected RedisLockInner(RedisTemplate redisTemplate, String key, String expectedValue) {
            this.redisTemplate = redisTemplate;
            this.key = key;
            this.expectedValue = expectedValue;
        }

       public Object unlock() {
            final List<String> keys = new ArrayList();
            keys.add(this.key);
            final List<String> values = new ArrayList();
            values.add(this.expectedValue);
            Object result = this.redisTemplate.execute(new RedisCallback<Long>() {
                public Long doInRedis(RedisConnection connection) throws DataAccessException {
                    Object nativeConnection = connection.getNativeConnection();
                    return nativeConnection instanceof JedisCluster ? (Long)((JedisCluster)nativeConnection).eval("if redis.call('get',KEYS[1])==ARGV[1]\n then\n   return redis.call('del',KEYS[1])\n else\n   return 0\n end", keys, values) : (Long)((Jedis)nativeConnection).eval("if redis.call('get',KEYS[1])==ARGV[1]\n then\n   return redis.call('del',KEYS[1])\n else\n   return 0\n end", keys, values);
                }
            });
           return result;
        }

        public void close() throws Exception {
            this.unlock();
        }
    }
}

4.最后在Controller接口加上注解就行了。

你可能感兴趣的:(spring,boot,后端,java)