接口幂等性

幂等性是什么:一个接口在入参相同的情况下,被多次发起请求,多次调用产生的结果与一次调用是相同的,简单说其实就是必须保证只有一次请求操作被执行
主要原理:定义切面拦截请求,利用Redis的分布式锁Redisson,对接口的入参比如用户token设置成一个key,或者基于业务去组合一个唯一key,再给这个key设置一个过期时间,这样就保证了在一段时间内同样的请求只能取到一个锁,从而保证接口的幂等性。具体实现主要通过切面织入设置redis的key以及加锁等逻辑。
实现:

1、自定义注解

import java.lang.annotation.*;
/**
 * @description: 幂等注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
    /**
     * 参数的表达式,用来确定key值,例如:"#req.userInfo.userId,#req.seqId"
     * @return
     */
    String key();
    /**
     * 幂等过期时间,即:在此时间段内,对API进行幂等处理。
     */
    long expireTime();
}

2、幂等性切面

/**
 * @description: 幂等性切面
 */
@Slf4j
@Aspect
@Component
public class IdempotentAspect {
    /**
     * redis缓存key的模板
     */
    private static final String KEY_TEMPLATE = "idempotent:%s";
    @Resource
    RedissonDistributedLocker redissonDistributedLocker;
    @Autowired
    RedisRepository redisRepository;
    /**
     * 定义切点
     */
    @Pointcut("@annotation(cn.com.bluemoon.spring.annotation.Idempotent)")
    public void executeIdempotent() {
    }
    @Around("executeIdempotent()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //获取幂等注解
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
        // joinPoint获取参数值
        Object[] args = joinPoint.getArgs();
        String expressKey = idempotent.key();
        if (!expressKey.contains("#")) {
            //注解的值非SPEL表达式
            throw new Exception("表达式错误",new  Throwable() );
        }
        String key = parseKey(expressKey, method, args);
        String cacheKey = String.format("MYAPP" + KEY_TEMPLATE, key);
        //通过加锁确保只有一个接口能够正常访问
        //尝试加锁
        //这里是不限制时间,知道接口执行为止才释放 boolean tryLock = redissonDistributedLocker.tryLock(cacheKey, TimeUnit.SECONDS, 0, -1);
        //这里根据入参设置锁过期时间,注意应该得确保在这个时间内接口能执行完
        boolean tryLock = redissonDistributedLocker.tryLock(cacheKey, TimeUnit.SECONDS, 0,    idempotent.expireTime());
        if (tryLock) {
            log.debug("get tryLock ,key:{}", cacheKey);
            try {
                Object proceed = joinPoint.proceed();
                return proceed;
            } finally {
                log.debug("lease lock");
                redissonDistributedLocker.unlock(cacheKey);
            }
        } else {
            log.debug("get tryLock fail,key:{}", cacheKey);
            throw new RuntimeException("请求已接收,请不要重复操作!");
        }
    }
    /**
     * 获取缓存的key
     * key 定义在注解上,支持SPEL表达式
     *
     * @return
     */
    private String parseKey(String key, Method method, Object[] args) {
        if (StringUtils.isEmpty(key)) return null;
        //获取被拦截方法参数名列表(使用Spring支持类库)
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);
        //使用SPEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        //把方法参数放入SPEL上下文中
        for (int i = 0; i < paraNameArr.length; i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }
        return parser.parseExpression(key).getValue(context, String.class);
    }
}

3、使用:

在controller上,通过接口入参判断,加锁时间为2秒

@Idempotent(key = "#dto.userId", expireTime = 2L)

AOP-面向切面编程

特征:多个步骤间的隔离性、原代码无关性
相关注解:

1、@Aspect

切面,作用在定义的切面类上,在里面做需要织入的增强逻辑,一个Java类加了这个注解之后就声明这是一个切面类

2、@Pointcut

切点、连接点,在哪个层面使用的你的增强逻辑。可通配路径到所有controller,也可自定义一个注解,切点通过注解去作用于某个“点”。

3、@Before 前置通知,在某连接点(JoinPoint)之前执行的通知

4、@After 前置通知,在某连接点(JoinPoint)之后执行的通知

5、@Around 环绕通知,等价于@After 和 @Before,但又有点区别

@Around和@Before+@After的区别

@Around用这个注解的方法入参传的是ProceedingJionPoint pjp,可以决定当前线程能否进入核心方法中——通过调用pjp.proceed(),
而@After 和 @Before的方法入参只能是JoinPoint joinPoint,这个类没有proceed()方法,所以不能决定线程是否进入核心方法中
eg:

(1)自定义一个注解

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

    /**
     * 参数的表达式,用来确定key值
     * @return
     */
    String key();

    /**
     * 幂等过期时间,即:在此时间段内,对API进行幂等处理
     */
    long expireTime() default -1;
}

(2)aop切面

@Component
public class MyAspect {
    /**
     * 定义切点,此处切点为自定义注解。
     * 【如果不通过注解进行切面织入,也可以使用通配对所有controller进行切面加强,比如@Pointcut("execution(public * com.myproject..*.*(..))"),
     * 如果使用这种,直接@Before("Pointcut()")、@After("Pointcut()")、@Around("Pointcut()")即可】
     */
    @Pointcut("@annotation(cn.com.myproject.annotation.MyAnnotation)")
    public void executeMyAnnotation() {
    }

    @Before("executeMyAnnotation()")
    public void beforeMethod(JoinPoint joinPoint) {
        //dosomething
    }
 
    @After("executeMyAnnotation()")
    public void afterMethod(JoinPoint joinPoint) {
        //dosomething
    }
 
    /**
     * @Around注解 环绕执行,就是在调用目标方法之前和调用之后都会执行你的代码逻辑,划分点是执行Object proceed = joinPoint.proceed()。等价于@After 和 @Before
     */
    @Around("executeMyAnnotation()")
    public Object Around(ProceedingJoinPoint pjp) throws Throwable {
        //dosomething=>@Before
        // 调用执行目标方法(result为目标方法执行结果)
        Object result = pjp.proceed();
        //dosomething=>@After
        return result;
    }