/*
 * Copyright (c) 2018-2999 广州市蓝海创新科技有限公司 All rights reserved.
 *
 * https://www.mall4j.com/
 *
 * 未经允许，不可做商业用途！
 *
 * 版权所有，侵权必究！
 */

package com.yami.shop.common.aspect;

import cn.hutool.core.util.StrUtil;
import com.yami.shop.common.annotation.RedisLock;
import com.yami.shop.common.util.SpelUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * Redis锁切面类
 * 该类利用Spring AOP（面向切面编程）机制，通过定义切面和切点，实现了基于Redis锁的并发控制功能。
 * 主要作用是在被标注了`@RedisLock`注解的方法执行前后进行加锁和解锁操作，以保证在同一时刻只有一个线程能够执行被标注的方法，避免并发冲突。
 *
 * @author lgh
 */
@Aspect
// 使用 @Aspect 注解声明该类是一个切面类，用于定义切点和增强逻辑（如前置通知、后置通知、环绕通知等）
@Component
// 使用 @Component 注解将该类注册为Spring容器中的一个组件，方便进行依赖注入等操作
public class RedisLockAspect {

    @Autowired
    // 通过依赖注入获取RedissonClient对象，用于与Redis进行交互，操作Redis锁相关的功能
    private RedissonClient redissonClient;

    // 定义Redis锁的键名前缀，用于在Redis中区分不同的锁，方便管理和识别
    private static final String REDISSON_LOCK_PREFIX = "redisson_lock:";

    /**
     * 环绕通知方法，用于在被标注`@RedisLock`注解的方法执行前后添加加锁和解锁逻辑
     * 该方法在目标方法执行前获取Redis锁，在目标方法执行完毕后释放锁，确保在同一时刻只有一个线程能够执行被标注的方法，实现并发控制。
     *
     * @param joinPoint 连接点对象，代表了目标方法的执行点，通过它可以获取目标方法的相关信息，如方法签名、参数等。
     * @param redisLock 从目标方法上获取的`@RedisLock`注解对象，通过它可以获取注解中定义的锁相关的配置信息，如锁的键表达式、锁名称、过期时间等。
     * @return 返回目标方法执行的结果，即被标注`@RedisLock`注解的方法正常执行后的返回值。
     * @throws Throwable 如果在目标方法执行过程中或者获取、释放锁的过程中出现异常，会将异常向上抛出。
     */
    @Around("@annotation(redisLock)")
    // 使用 @Around 注解定义环绕通知，该通知会在目标方法执行前后进行额外的逻辑处理，这里就是围绕着被标注`@RedisLock`注解的方法进行加锁和解锁操作
    public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
        // 获取`@RedisLock`注解中定义的用于生成锁键的SPEL表达式，SPEL表达式可以根据方法参数等动态生成锁键
        String spel = redisLock.key();
        // 获取`@RedisLock`注解中定义的锁名称，用于更直观地标识锁的用途等信息
        String lockName = redisLock.lockName();

        // 根据连接点、锁名称以及SPEL表达式生成最终的Redis锁的键名，并通过RedissonClient获取对应的Redis锁对象
        RLock rLock = redissonClient.getLock(getRedisKey(joinPoint, lockName, spel));

        // 使用获取到的Redis锁对象进行加锁操作，设置锁的过期时间和时间单位，按照`@RedisLock`注解中定义的配置来进行
        rLock.lock(redisLock.expire(), redisLock.timeUnit());

        Object result = null;
        try {
            // 执行被标注`@RedisLock`注解的目标方法，即让原本的业务逻辑正常执行
            result = joinPoint.proceed();

        } finally {
            // 无论目标方法执行是否成功，最终都要释放锁，确保锁资源能够被正确释放，避免死锁等问题
            rLock.unlock();
        }
        return result;
    }

    /**
     * 将SPEL表达式转换为具体的Redis锁键字符串的方法
     * 该方法根据连接点（包含目标方法、目标对象以及方法参数等信息）、锁名称以及SPEL表达式，通过解析SPEL表达式，结合相关信息生成最终用于在Redis中标识锁的键字符串。
     *
     * @param joinPoint 切点对象，代表了目标方法的执行点，从中可以获取目标方法、目标对象以及方法参数等信息，用于解析SPEL表达式。
     * @param lockName 锁的名称，作为生成的Redis锁键的一部分，用于更直观地标识锁的用途等信息。
     * @param spel 用于生成锁键的SPEL表达式，通过解析该表达式，结合目标方法、目标对象以及方法参数等信息来生成最终的锁键字符串。
     * @return 返回生成的Redis锁键字符串，格式为"redisson_lock:锁名称:具体解析后的表达式内容"，用于在Redis中唯一标识一个锁。
     */
    private String getRedisKey(ProceedingJoinPoint joinPoint, String lockName, String spel) {
        // 获取连接点对应的方法签名信息，用于后续获取目标方法对象
        Signature signature = joinPoint.getSignature();
        // 将方法签名信息转换为方法签名的具体实现类对象，方便获取目标方法的详细信息
        MethodSignature methodSignature = (MethodSignature) signature;
        // 获取目标方法对象，后续可用于解析SPEL表达式等操作
        Method targetMethod = methodSignature.getMethod();
        // 获取目标对象，即被代理的实际业务对象，同样可用于解析SPEL表达式等操作
        Object target = joinPoint.getTarget();
        // 获取目标方法的参数数组，这些参数在解析SPEL表达式时可能会作为变量参与计算
        Object[] arguments = joinPoint.getArgs();

        // 通过工具类方法解析SPEL表达式，结合锁名称等信息生成最终的Redis锁键字符串，并添加前缀，形成完整的用于在Redis中标识锁的键名
        return REDISSON_LOCK_PREFIX + lockName + StrUtil.COLON + SpelUtil.parse(target, spel, targetMethod, arguments);
    }
}