通用分布式锁组件

news2024/10/10 6:19:17

通用分布式锁组件

  • 1 Redisson
    • 1.1介绍
    • 1.2 快速了解
    • 1.3 项目集成
  • 2 定义通用分布式锁组件
    • 2.1 实现思路分析
    • 2.2 定义注解
    • 2.3 定义切面
    • 2.4 使用锁
    • 2.5.工厂模式切换锁类型
      • 2.5.1 锁类型枚举
      • 2.5.2 锁对象工厂
      • 2.5.3 改造切面代码
    • 2.6 锁失败策略
      • 2.6.1 策略分析
      • 2.6.2 策略实现
    • 2.7 基于SPEL的动态锁名
      • 2.7.1 SPEL表达式
      • 2.7.2 解析SPEL
    • 2.8 完整代码

自定义注解实现通用分布式锁组件。

1 Redisson

Redisson官网:https://redisson.org/

1.1介绍

在这里插入图片描述

Redisson是一个基于Redis的工具包,功能非常强大。将JDK中很多常见的队列、锁、对象都基于Redis实现了对应的分布式版本。

1.2 快速了解

首先引入依赖:

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
</dependency>

然后是配置:

 @Configuration
 public class RedisConfig {
    @Bean
    public RedissonClient redissonClient() {
        // 配置类
        Config config = new Config();
        // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址 
        config.useSingleServer()
            .setAddress("redis://192.168.150.101:6379")
            .setPassword("123456");
        // 创建客户端
        return Redisson.create(config);
    }
 }

最后是基本用法:

 @Autowired
 private RedissonClient redissonClient;

 @Test
 void testRedisson() throws InterruptedException {
    // 1.获取锁对象,指定锁名称
    RLock lock = redissonClient.getLock("anyLock");
    try {
        // 2.尝试获取锁,参数:waitTime、leaseTime、时间单位
        boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
        if (!isLock) {
            // 获取锁失败处理 ..
        } else {
            // 获取锁成功处理
        }
    } finally {
        // 4.释放锁
        lock.unlock();
    }
 }

利用Redisson获取锁时可以传3个参数:

  • waitTime:获取锁的等待时间。当获取锁失败后可以多次重试,直到waitTime时间耗尽。waitTime默认-1,即失败后立刻返回,不重试。
  • leaseTime:锁超时释放时间。默认是30,同时会利用WatchDog来不断更新超时时间。需要注意的是,如果手动设置leaseTime值,会导致WatchDog失效。
  • TimeUnit:时间单位

1.3 项目集成

关键基础配置:

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.tianji.common.autoconfigure.redisson.aspect.LockAspect;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@ConditionalOnClass({RedissonClient.class, Redisson.class})
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class RedissonConfig {
    private static final String REDIS_PROTOCOL_PREFIX = "redis://";
    private static final String REDISS_PROTOCOL_PREFIX = "rediss://";

    @Bean
    @ConditionalOnMissingBean
    public LockAspect lockAspect(RedissonClient redissonClient){
        return new LockAspect(redissonClient);
    }

    @Bean
    @ConditionalOnMissingBean
    public RedissonClient redissonClient(RedisProperties properties){
        log.debug("尝试初始化RedissonClient");
        // 1.读取Redis配置
        RedisProperties.Cluster cluster = properties.getCluster();
        RedisProperties.Sentinel sentinel = properties.getSentinel();
        String password = properties.getPassword();
        int timeout = 3000;
        Duration d = properties.getTimeout();
        if(d != null){
            timeout = Long.valueOf(d.toMillis()).intValue();
        }
        // 2.设置Redisson配置
        Config config = new Config();
        if(cluster != null && !CollectionUtil.isEmpty(cluster.getNodes())){
            // 集群模式
            config.useClusterServers()
                    .addNodeAddress(convert(cluster.getNodes()))
                    .setConnectTimeout(timeout)
                    .setPassword(password);
        }else if(sentinel != null && !StrUtil.isEmpty(sentinel.getMaster())){
            // 哨兵模式
            config.useSentinelServers()
                    .setMasterName(sentinel.getMaster())
                    .addSentinelAddress(convert(sentinel.getNodes()))
                    .setConnectTimeout(timeout)
                    .setDatabase(0)
                    .setPassword(password);
        }else{
            // 单机模式
            config.useSingleServer()
                    .setAddress(String.format("redis://%s:%d", properties.getHost(), properties.getPort()))
                    .setConnectTimeout(timeout)
                    .setDatabase(0)
                    .setPassword(password);
        }
        // 3.创建Redisson客户端
        return Redisson.create(config);
    }

    private String[] convert(List<String> nodesObject) {
        List<String> nodes = new ArrayList<>(nodesObject.size());
        for (String node : nodesObject) {
            if (!node.startsWith(REDIS_PROTOCOL_PREFIX) && !node.startsWith(REDISS_PROTOCOL_PREFIX)) {
                nodes.add(REDIS_PROTOCOL_PREFIX + node);
            } else {
                nodes.add(node);
            }
        }
        return nodes.toArray(new String[0]);
    }
}

几个关键点:

  • 这个配置上添加了条件注解@ConditionalOnClass({RedissonClient.class, Redisson.class}) 也就是说,只要引用了配置所在模块,并且引用了Redisson依赖,这套配置就会生效。不引入Redisson依赖,配置自然不会生效,从而实现按需引入。
  • RedissonClient的配置无需自定义Redis地址,而是直接基于SpringBoot中的Redis配置即可。而且不管是Redis单机、Redis集群、Redis哨兵模式都可以支持

2 定义通用分布式锁组件

Redisson的分布式锁使用并不复杂,基本步骤包括:

  • 1)创建锁对象
  • 2)尝试获取锁
  • 3)处理业务
  • 4)释放锁

但是,除了第3步以外,其它都是非业务代码,对业务的侵入较多:

在这里插入图片描述

可以发现,非业务代码格式固定,每次获取锁总是在重复编码。我们可不可以对这部分代码进行抽取和简化呢?

2.1 实现思路分析

要优化这部分代码,需要通过整个流程来分析:

在这里插入图片描述
可以发现,只有红框部分是业务功能,业务前、后都是固定的锁操作。既然如此,我们完全可以基于AOP的思想,将业务部分作为切入点,将业务前后的锁操作作为环绕增强。

但是,我们该如何标记这些切入点呢?
不是每一个service方法都需要加锁,因此我们不能直接基于类来确定切入点;另外,需要加锁的方法可能也较多,我们不能基于方法名作为切入点,这样太麻烦。因此,最好的办法是把加锁的方法给标记出来,利用标记来确定切入点。如何标记呢?

最常见的办法就是基于注解来标记了。同时,加锁时还有一些参数,比如:锁的key名称、锁的waitTime、releaseTime等等,都可以基于注解来传参。

因此,注解的核心作用是两个:

  • 标记切入点
  • 传递锁参数

综上,我们计划利用注解来标记切入点,传递锁参数。同时利用AOP环绕增强来实现加锁、释放锁等操作。

2.2 定义注解

注解本身起到标记作用,同时还要带上锁参数:

  • 锁名称
  • 锁等待时间
  • 锁超时时间
  • 时间单位
  • 方法结束是否释放锁
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLock {
    /**
     * 加锁key的表达式,支持SPEL表达式
     */
    String name();

    /**
     * 阻塞超时时长,不指定 waitTime 则按照Redisson默认时长
     */
    long waitTime() default 1;

    /**
     * 锁自动释放时长,默认是-1,其实是30秒 + watchDog模式
     */
    long leaseTime() default -1;

    /**
     * 时间单位,默认为秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 如果设定了false,则方法结束不释放锁,而是等待leaseTime后自动释放
     */
    boolean autoUnlock() default true;
}

2.3 定义切面

接下来,我们定义一个环绕增强的切面,实现加锁、释放锁:

package com.tianji.promotion.utils;

import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

@Component
@Aspect
@RequiredArgsConstructor
public class MyLockAspect implements Ordered{

    private final RedissonClient redissonClient;

    @Around("@annotation(myLock)")
    public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {
    	if (!myLock.autoUnlock() && myLock.leaseTime() <= 0) {
            // 不手动释放锁时,必须指定leaseTime时间
            throw new BizIllegalException("leaseTime不能为空");
        }
        // 1.创建锁对象
        RLock lock = redissonClient.getLock(myLock.name());
        // 2.尝试获取锁
        boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaseTime(), myLock.unit());
        // 3.判断是否成功
        if(!isLock) {
            // 3.1.失败,快速结束
            throw new BizIllegalException("请求太频繁");
        }
        try {
            // 3.2.成功,执行业务
            return pjp.proceed();
        } finally {
            // 4.释放锁
            if (myLock.autoUnlock()) {
                lock.unlock();
            }
        }
    }
    /**
     * 指定切面注解的优先执行顺序
     * 这里设置锁注解要优先于其他注解执行
     * (先加锁,再执行事务)
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

2.4 使用锁

定义好了锁注解和切面,接下来使用直接加上注解就行了:

在这里插入图片描述

可以看到,业务中无需手动编写加锁、释放锁的逻辑了,没有任何业务侵入,使用起来也非常优雅。

不过呢,现在还存在几个问题:

  • Redisson中锁的种类有很多,目前的代码中把锁的类型写死了
  • Redisson中获取锁的逻辑有多种,比如获取锁失败的重试策略,目前都没有设置
  • 锁的名称目前是写死的,并不能根据方法参数动态变化

所以呢,我们接下来还要对锁的实现进行优化,注意解决上述问题。

2.5.工厂模式切换锁类型

Redisson中锁的类型有多种,例如:

在这里插入图片描述

因此,我们不能在切面中把锁的类型写死,而是交给用户自己选择锁类型。

那么问题来了,如何让用户选择锁类型呢?
锁的类型虽然有多种,但类型是有限的几种,完全可以通过枚举定义出来。然后把这个枚举作为MyLock注解的参数,交给用户去选择自己要用的类型。

而在切面中,我们则需要根据用户选择的锁类型,创建对应的锁对象即可。但是这个逻辑不能通过if-else来实现,太low了。
这里我们的需求是根据用户选择的锁类型,创建不同的锁对象。有一种设计模式刚好可以解决这个问题:简单工厂模式

2.5.1 锁类型枚举

我们首先定义一个锁类型枚举:

public enum MyLockType {
    RE_ENTRANT_LOCK, // 可重入锁
    FAIR_LOCK, // 公平锁
    READ_LOCK, // 读锁
    WRITE_LOCK, // 写锁
    ;
}

然后在自定义注解中添加锁类型这个参数:

	/**
     * 使用的锁类型,默认可重入锁
     * @return
     */
    MyLockType lockType() default MyLockType.RE_ENTRANT_LOCK;

在这里插入图片描述

2.5.2 锁对象工厂

然后定义一个锁工厂,用于根据锁类型创建锁对象:

import com.xxx.enums.MyLockType;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import java.util.EnumMap;
import java.util.Map;
import java.util.function.Function;

import static com.tianji.promotion.enums.MyLockType.*;


@Component
public class MyLockFactory {

    //封装的是方法引用
    private final Map<MyLockType, Function<String, RLock>> lockHandlers;

    public MyLockFactory(RedissonClient redissonClient) {
        this.lockHandlers = new EnumMap<>(MyLockType.class);
        this.lockHandlers.put(RE_ENTRANT_LOCK, redissonClient::getLock);
        this.lockHandlers.put(FAIR_LOCK, redissonClient::getFairLock);
        this.lockHandlers.put(READ_LOCK, name -> redissonClient.getReadWriteLock(name).readLock());
        this.lockHandlers.put(WRITE_LOCK, name -> redissonClient.getReadWriteLock(name).writeLock());
    }

    public RLock getLock(MyLockType lockType, String name){
        //.apply调用方法引用封装的方法
        return lockHandlers.get(lockType).apply(name);
    }
}

说明:

  • MyLockFactory内部持有了一个Map,key是锁类型枚举,值是创建锁对象的Function。注意这里不是存锁对象,因为锁对象必须是多例的,不同业务用不同锁对象;同一个业务用相同锁对象。
  • MyLockFactory内部的Map采用了EnumMap。只有当Key是枚举类型时可以使用EnumMap,其底层不是hash表,而是简单的数组。由于枚举项数量固定,因此这个数组长度就等于枚举项个数,然后按照枚举项序号作为角标依次存入数组。这样就能根据枚举项序号作为角标快速定位到数组中的数据。

2.5.3 改造切面代码

我们将锁对象工厂注入MyLockAspect,然后就可以利用工厂来获取锁对象了:

private final MyLockFactory myLockFactory;

RLock lock = myLockFactory.getLock(myLock.lockType(),myLock.name());

在这里插入图片描述

此时,在业务中,就能通过注解来指定自己要用的锁类型了:

在这里插入图片描述

2.6 锁失败策略

多线程争抢锁,大部分线程会获取锁失败,而失败后的处理方案和策略是多种多样的。目前,我们获取锁失败后就是直接抛出异常,没有其它策略,这与实际需求不一定相符。

2.6.1 策略分析

接下来,我们就分析一下锁失败的处理策略有哪些。

大的方面来说,获取锁失败要从两方面来考虑:

  • 获取锁失败是否要重试?有三种策略:
    • 不重试,对应API:lock.tryLock(0, 10, SECONDS),也就是waitTime小于等于0
    • 有限次数重试:对应API:lock.tryLock(5, 10, SECONDS),也就是waitTime大于0,重试一定waitTime时间后结束
    • 无限重试:对应API lock.lock(10, SECONDS) , lock就是无限重试
  • 重试失败后怎么处理?有两种策略:
    • 直接结束
    • 抛出异常

对应的API和策略名如下:

在这里插入图片描述

重试策略 + 失败策略组合,总共以下几种情况:

在这里插入图片描述

那么该如何用代码来表示这些失败策略,并让用户自由选择呢?

相信大家应该能想到一种设计模式:策略模式。同时,我们还需要定义一个失败策略的枚举。在MyLock注解中定义这个枚举类型的参数,供用户选择。

注意:

一般的策略模式大概是这样:

  • 定义策略接口
  • 定义不同策略实现类
  • 提供策略工厂,便于根据策略枚举获取不同策略实现

而在策略比较简单的情况下,我们完全可以用枚举代替策略工厂,简化策略模式。

综上,我们可以定义一个基于枚举的策略模式,简化开发。

2.6.2 策略实现

我们定义一个失败策略枚举,直接将失败策略定义到枚举中:

package com.xxx.utils;

import com.xxx.common.exceptions.BizIllegalException;//自定义业务异常
import org.redisson.api.RLock;

public enum MyLockStrategy {
    SKIP_FAST(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            return lock.tryLock(0, prop.leaseTime(), prop.unit());
        }
    },
    FAIL_FAST(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            boolean isLock = lock.tryLock(0, prop.leaseTime(), prop.unit());
            if (!isLock) {
                throw new BizIllegalException("请求太频繁");
            }
            return true;
        }
    },
    KEEP_TRYING(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            lock.lock( prop.leaseTime(), prop.unit());
            return true;
        }
    },
    SKIP_AFTER_RETRY_TIMEOUT(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            return lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());
        }
    },
    FAIL_AFTER_RETRY_TIMEOUT(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            boolean isLock = lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());
            if (!isLock) {
                throw new BizIllegalException("请求太频繁");
            }
            return true;
        }
    },
    ;

    public abstract boolean tryLock(RLock lock, MyLock prop) throws InterruptedException;
}

然后,在MyLock注解中添加枚举参数:

/**
* 定义锁失败后的策略
* @return
*/
MyLockStrategy lockStrategy() default MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT;

在这里插入图片描述

最后,修改切面代码,基于用户选择的策略来处理:

boolean isLock = myLock.lockStrategy().tryLock(lock, myLock);

在这里插入图片描述

最后,修改切面代码,基于用户选择的策略来处理:

在这里插入图片描述

这个时候,我们就可以在使用锁的时候自由选择锁类型、锁策略了:

在这里插入图片描述

2.7 基于SPEL的动态锁名

现在还剩下最后一个问题,就是锁名称的问题。
在当前业务中,我们的锁对象本来应该是当前登录用户,是动态获取的。而加锁是基于注解参数添加的,在编码时就需要指定。怎么办?

Spring中提供了一种表达式语法,称为SPEL表达式,可以执行java代码,获取任意参数。

思路:
我们可以让用户指定锁名称参数时不要写死,而是基于SPEL表达式。在创建锁对象时,解析SPEL表达式,动态获取锁名称。

思路很简单,不过SPEL表达式的解析还是比较复杂的。不推荐自己编写。

2.7.1 SPEL表达式

SPEL的表达式语法可以参考官网文档:https://docs.spring.io/spring-framework/docs/3.0.x/reference/expressions.html

中文文档:https://itmyhome.com/spring/expressions.html

首先,在使用锁注解时,锁名称可以利用SPEL表达式,例如我们指定锁名称中要包含参数中的用户id,则可以这样写:

在这里插入图片描述

而如果是通过UserContext.getUser()获取,则可以利用下面的语法:

@MyLock(name="lock:coupon:#{T(com.common.util.UserContext).getUser()}")

这里T(类名).方法名()就是调用静态方法。

2.7.2 解析SPEL

在切面中,我们需要基于注解中的锁名称做动态解析,而不是直接使用名称:

在这里插入图片描述

其中获取锁名称用的是getLockName()这个方法:


/**
 * SPEL的正则规则
 */
private static final Pattern pattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
/**
 * 方法参数解析器
 */
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

/**
 * 解析锁名称
 * @param name 原始锁名称
 * @param pjp 切入点
 * @return 解析后的锁名称
 */
private String getLockName(String name, ProceedingJoinPoint pjp) {
    // 1.判断是否存在spel表达式
    if (StringUtils.isBlank(name) || !name.contains("#")) {
        // 不存在,直接返回
        return name;
    }
    // 2.构建context,也就是SPEL表达式获取参数的上下文环境,这里上下文就是切入点的参数列表
    EvaluationContext context = new MethodBasedEvaluationContext(
            TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
    // 3.构建SPEL解析器
    ExpressionParser parser = new SpelExpressionParser();
    // 4.循环处理,因为表达式中可以包含多个表达式
    Matcher matcher = pattern.matcher(name);
    while (matcher.find()) {
        // 4.1.获取表达式
        String tmp = matcher.group();
        String group = matcher.group(1);
        // 4.2.这里要判断表达式是否以 T字符开头,这种属于解析静态方法,不走上下文
        Expression expression = parser.parseExpression(group.charAt(0) == 'T' ? group : "#" + group);
        // 4.3.解析出表达式对应的值
        Object value = expression.getValue(context);
        // 4.4.用值替换锁名称中的SPEL表达式
        name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
    }
    return name;
}

private Method resolveMethod(ProceedingJoinPoint pjp) {
    // 1.获取方法签名
    MethodSignature signature = (MethodSignature)pjp.getSignature();
    // 2.获取字节码
    Class<?> clazz = pjp.getTarget().getClass();
    // 3.方法名称
    String name = signature.getName();
    // 4.方法参数列表
    Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
    return tryGetDeclaredMethod(clazz, name, parameterTypes);
}

private Method tryGetDeclaredMethod(Class<?> clazz, String name, Class<?> ... parameterTypes){
    try {
        // 5.反射获取方法
        return clazz.getDeclaredMethod(name, parameterTypes);
    } catch (NoSuchMethodException e) {
        Class<?> superClass = clazz.getSuperclass();
        if (superClass != null) {
            // 尝试从父类寻找
            return tryGetDeclaredMethod(superClass, name, parameterTypes);
        }
    }
    return null;
}

2.8 完整代码

MyLockAspect 经过一步步修改与最开始在文章中出现有差异这里给出完整版。

import com.common.utils.StringUtils;
import com.promotion.anno.MyLock;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
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.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;

import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Component
@Aspect
@RequiredArgsConstructor
public class MyLockAspect implements Ordered {

    //    private final RedissonClient redissonClient;
    private final MyLockFactory myLockFactory;

    @Around("@annotation(myLock)")
    public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {
    	if (!myLock.autoUnlock() && myLock.leaseTime() <= 0) {
            // 不手动释放锁时,必须指定leaseTime时间
            throw new BizIllegalException("leaseTime不能为空");
        }
        // 1.创建锁对象
        //RLock lock = redissonClient.getLock(myLock.name());//获取可重入锁
        String lockName = getLockName(myLock.name(), pjp);
        RLock lock = myLockFactory.getLock(myLock.lockType(),lockName);
        // 2.尝试获取锁
//        boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaseTime(), myLock.unit());
        //使用策略模式获取锁
        boolean isLock = myLock.lockStrategy().tryLock(lock, myLock);
        // 3.判断是否成功
        if (!isLock) {
            // 3.1.失败,快速结束(使用策略模式后内部会自己抛异常)
            return null;
        }
        try {
            // 3.2.成功,执行业务
            return pjp.proceed();
        } finally {
            // 4.释放锁
            if (myLock.autoUnlock()) {
                lock.unlock();
            }
        }
    }

    /**
     * 指定切面注解的优先执行顺序
     * 这里设置要高于其他注解
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }



    /**
     * SPEL的正则规则
     */
    private static final Pattern pattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
    /**
     * 方法参数解析器
     */
    private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    /**
     * 解析锁名称
     * @param name 原始锁名称
     * @param pjp 切入点
     * @return 解析后的锁名称
     */
    private String getLockName(String name, ProceedingJoinPoint pjp) {
        // 1.判断是否存在spel表达式
        if (StringUtils.isBlank(name) || !name.contains("#")) {
            // 不存在,直接返回
            return name;
        }
        // 2.构建context,也就是SPEL表达式获取参数的上下文环境,这里上下文就是切入点的参数列表
        EvaluationContext context = new MethodBasedEvaluationContext(
                TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
        // 3.构建SPEL解析器
        ExpressionParser parser = new SpelExpressionParser();
        // 4.循环处理,因为表达式中可以包含多个表达式
        Matcher matcher = pattern.matcher(name);
        while (matcher.find()) {
            // 4.1.获取表达式
            String tmp = matcher.group();
            String group = matcher.group(1);
            // 4.2.这里要判断表达式是否以 T字符开头,这种属于解析静态方法,不走上下文
            Expression expression = parser.parseExpression(group.charAt(0) == 'T' ? group : "#" + group);
            // 4.3.解析出表达式对应的值
            Object value = expression.getValue(context);
            // 4.4.用值替换锁名称中的SPEL表达式
            name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
        }
        return name;
    }

    private Method resolveMethod(ProceedingJoinPoint pjp) {
        // 1.获取方法签名
        MethodSignature signature = (MethodSignature)pjp.getSignature();
        // 2.获取字节码
        Class<?> clazz = pjp.getTarget().getClass();
        // 3.方法名称
        String name = signature.getName();
        // 4.方法参数列表
        Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
        return tryGetDeclaredMethod(clazz, name, parameterTypes);
    }

    private Method tryGetDeclaredMethod(Class<?> clazz, String name, Class<?> ... parameterTypes){
        try {
            // 5.反射获取方法
            return clazz.getDeclaredMethod(name, parameterTypes);
        } catch (NoSuchMethodException e) {
            Class<?> superClass = clazz.getSuperclass();
            if (superClass != null) {
                // 尝试从父类寻找
                return tryGetDeclaredMethod(superClass, name, parameterTypes);
            }
        }
        return null;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1567921.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

程序员们应注意的行业特有的法律问题

大家好&#xff0c;我是不会魔法的兔子&#xff0c;是一枚执业律师&#xff0c;持续分享技术类行业项目风险及预防的问题。 一直以来兔子都在以大家做项目时候会遇到的风险问题做分享&#xff0c;最近有个念头一直挥之不去&#xff0c;就是要不要给我们广大的程序员们也分享一…

从头开发一个RISC-V的操作系统(二)RISC-V 指令集架构介绍

文章目录 前提ISA的基本介绍ISA是什么CISC vs RISCISA的宽度 RISC-V指令集RISC-V ISA的命名规范模块化的ISA通用寄存器Hart特权级别内存管理与保护异常和中断 目标&#xff1a;通过这一个系列课程的学习&#xff0c;开发出一个简易的在RISC-V指令集架构上运行的操作系统。 前提…

探索设计模式的魅力:揭秘B/S模式在AI大模型时代的蜕变与进化

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》《MYSQL应用》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 &#x1f680; 转载自热榜文章&#xff1a;探索设计模式的魅力&#xff1a;揭秘B/S…

Gateway是什么?(SpringCloudAlibaba组件)

1、网关介绍 **网关(Gateway)又称网间连接器、协议转换器。网关在传输层上以实现网络互连&#xff0c;是最复杂的网络互连设备&#xff0c;仅用于两个高层协议不同的网络互连。**网关的结构也和路由器类似&#xff0c;不同的是互连层。网关既可以用于广域网互连&#xff0c;也可…

Springboot的日期类格式化

JsonFormat(pattern "yyyy-MM-dd HH:mm:ss", timezone "UTC") 这里是将UTC的时间进行格式化&#xff0c;效果如下&#xff1a; 可以发现DetectDate是detectDate的变种&#xff0c;因为detectDate是UTC时间&#xff0c;而DetectDate是被格式化得到的时间…

Redis 全景图(3)--- Redis 应用于缓存

前言 这是关于 Redis 全景图的最后一篇文章。因为一次写太多会限流&#xff0c;我也是没办法&#xff0c;才分成三篇文章来写。这篇文章是关于 Redis 应用于缓存的。 其实为什么要讲这个话题呢&#xff1f; Redis 应用在很多地方呀&#xff0c;为什么一定要挑着这个话题来讲呢…

【YOLOV5 入门】——构建自己的数据集模型训练模型检验

一、准备工作 1、数据收集 图片类型数据不用多说&#xff1b;视频类型数据利用opencv进行抽帧保存为一张张图片&#xff0c;这里选取30s的名侦探柯南片段进行试验&#xff0c;确保环境解释器下安装了opencv&#xff08;我使用的是另一个虚拟环境&#xff09;&#xff1a; im…

银行数字化转型导师坚鹏:银行数字化转型给支行带来的8大价值

银行数字化转型给支行带来的8大价值 银行数字化转型对不仅对总行、分行产生了深远影响&#xff0c;给总行、分行带来了新质生产力&#xff0c;对银行支行&#xff08;包括网点&#xff09;也会产生重要价值&#xff0c;银行数字化转型导师坚鹏从以下8个方面进行详细分析&#…

CANoe自带的TCP/IP协议栈中TCP的keep alive机制是如何工作的

TCP keep alive机制我们已经讲过太多次,车内很多控制器的TCP keep alive机制相信很多开发和测试的人也配置或者测试过。我们今天想知道CANoe软件自带的TCP/IP协议栈中TCP keep alive机制是如何工作的。 首先大家需要知道TCP keep alive的参数有哪些?其实就三个参数:CP_KEEP…

Android Studio学习9——使用Logcat打印日志

在Android开发中&#xff0c;Logcat是一个工具&#xff0c;它允许开发者查看设备或模拟器的日志信息。开发者可以使用Log类来打印日志信息&#xff0c;这对于调试和错误排查非常有帮助。 v 或 verbose: 最低等级&#xff0c;显示所有消息。d 或 debug: 用于调试消息。i 或 info…

2012年认证杯SPSSPRO杯数学建模B题(第二阶段)节能减排全过程文档及程序

2012年认证杯SPSSPRO杯数学建模 节能减排、抑制全球气候变暖 B题 白屋顶计划 原题再现&#xff1a; 第二阶段问题   虽然环境学家对地球环境温度的改变有许多种不同观点&#xff0c;但大多数科学家可以达成一个基本的共识&#xff1a;近年来人类的活动&#xff0c;尤指二氧…

upload-labs训练平台

GitHub&#xff1a;GitHub - Tj1ngwe1/upload-labs: 一个帮你总结所有类型的上传漏洞的靶场 把下好的文件夹之间拖入到小皮的WWW目录下就可以之间访问网址使用了 目录 Pass-01(前端JS的绕过) (1)抓包绕过 (2)在前端绕过 Pass-02&#xff08;content-type绕过&#xff09;…

指针变量的类型

1.为什么指针变量要要求类型 当指针变量的类型跟存放的地址变量的类型不匹配时&#xff0c;当值变化时&#xff0c;地址增量也不同 #include <stdio.h> int main() {int a 0x1234;int *p&a;char *c&a;printf("p %p\n",p);printf("c %p\n"…

微软邮箱被锁住,该如何解除限制?

ChatGPT账号是微软邮箱注册的&#xff0c;我们在登陆微软邮箱时&#xff0c;http://www.outlook.com&#xff0c;不需要开魔法工具&#xff0c;直接就可以登陆&#xff0c;否则会出现安全验证&#xff0c;限制登陆。 那么如果账号被锁&#xff0c;我们该如何解除限制呢&#x…

云容器引擎CCE弹性伸缩

CCE弹性伸缩介绍 CCE的弹性伸缩能力分为如下两个维度&#xff1a; 工作负载弹性伸缩&#xff1a;即调度层弹性&#xff0c;主要是负责修改负载的调度容量变化。例如&#xff0c;HPA是典型的调度层弹性组件&#xff0c;通过HPA可以调整应用的副本数&#xff0c;调整的副本数会…

【mmDet3D】conda 环境中更新gxx

在安装mmdetection3d 相关依赖时&#xff0c;遇到如下报错&#xff1a; 之前在用openpcdet框架时也有过类似的问题&#xff0c;当时也记载过更新gc版本在conda里面的使用教程&#xff0c;但是今天发现其实有更简单的方法&#xff0c;如下图所示&#xff0c;直接使用conda来安装…

Flask Python:数据库多条件查询,flask中模型关联

前言 在上一篇Flask Python:模糊查询filter和filter_by&#xff0c;数据库多条件查询中&#xff0c;已经分享了几种常用的数据库操作&#xff0c;这次就来看看模型的关联关系是怎么定义的&#xff0c;先说基础的关联哈。在分享之前&#xff0c;先分享官方文档,点击查看 从文档…

【C++学习】哈希表的底层实现及其在unordered_set与unordered_map中的封装

文章目录 1. unordered系列关联式容器1.1 unordered_map1.2 unordered_set1.3.底层结构 2.哈希2.1哈希概念2.2哈希冲突2.3 哈希函数2.4 哈希冲突解决2.4.1闭散列2.4.1开散列2.5开散列与闭散列比较 3.哈希的模拟实现1. 模板参数列表2. 迭代器的实现3. 增加通过key获取value操作4…

【数据结构与算法】归并排序(详解:递归与非递归的归并排序 | 赠:冒泡排序和选择排序)

前言 本篇博客会对排序做一个收尾&#xff0c;将最经典的七大排序介绍完毕。 这次的重点正如标题&#xff0c;主要讲的是归并排序&#xff0c;还会带过相对简单很多的冒泡排序和选择排序。在最后还会给这七大排序做出一个时间复杂度和稳定性展示的总结收尾。同时&#xff0c;这…

基于FPGA的HDMI视频接口设计

HDMI介绍 HDMI(High-DefinitionMultimedia Interface)又被称为高清晰度多媒体接口,是首个支持在单线缆上传输,不经过压缩的全数字高清晰度、多声道音频和智能格式与控制命令数据的数字接口。HDMI接口由Silicon Image美国晶像公司倡导,联合索尼、日立、松下、飞利浦、汤姆逊、东…