SpringBoot模块化开发入门——注解驱动实现自定义包装Redis分布式锁

本文以注解驱动实现Redis分布式锁的方式,介绍Spring Boot模块化开发。先进行技术选型,确定用Redisson框架;从配置文件、注解、API三方面设计方案。接着阐述实现过程,包括锁接口、注解与切面、配置文件读取解析等,最终完成自动化配置和委托封装。

一、引言

不知道你是否有过这样的经历:在引入其他的包和库后,通常什么都不用加,直接启动Spring Boot程序,就能够使用该包或库提供的功能,或者也仅仅需要添加一个 @Enablexxx的注解,在配置文件中提供参数就能够使用相应的功能。我想可能和多人都被注解“迷惑”了,然后就去搜寻和注解相关的知识:AOP、AspectJ、切面等等。实际上,这些操作仍然需要我们通过程序去主动的寻找类、方法等,获取上面的注解,然后进行逻辑处理。实际上,我们真正寻找的并不是【AOP】,而是【自动化】或者叫【模块化】,指的就是 SpringBoot 启动后就能自动扫描类完成配置,在使用时SpringBoot能够识别出注解并作出拦截,这个自动化的过程,才是真正的追求。本文以注解驱动实现Redis分布式锁的方式,来介绍SpringBoot模块化开发,并给出最终实践。

二、准备

技术选型

由Redis实现分布式锁的方案特别多,往下说由通过操作Redis本身来直接实现分布式锁的,往上说由通过对Redis包装的各种框架来实现分布式锁的,如Jedis,Lettuce,Redisson等框架。本文选择对Redisson框架进行包装,从而实现分布式锁。Redisson是一款十分优秀的框架,它封装Redis操作的大部分细节,并采用Java原生API实现了分布式锁,异步调用等功能。本文的分布式锁方案只是用了Redisson分布式锁的API,感兴趣的朋友可以考虑用Redisson取代RedisTemplate来操作Redis

期望方案

1. 配置文件驱动

本文主要设计一款注解驱动的Redis分布式锁方案,通过配置文件就可以轻松配置多个数据源和模式。配置文件参考如下:

lock:
  redisson:
    list:
      # 单实例模式
      - schema: redis-single-1
        settings:
          threads: 16
          nettyThreads: 32
          # codec:
          transportMode: "NIO"
          singleServerConfig:
            idleConnectionTimeout: 10000
            connectTimeout: 10000
            timeout: 3000
            retryAttempts: 3
            retryInterval: 1500
            # password: null
            subscriptionsPerConnection: 5
            clientName: null
            address: redis://127.0.0.1:6379
            subscriptionConnectionMinimumIdleSize: 1
            subscriptionConnectionPoolSize: 50
            connectionMinimumIdleSize: 24
            connectionPoolSize: 64
            database: 0
            dnsMonitoringInterval: 5000
      # 哨兵模式
      - schema: redis-sentinel-1
        settings:
          threads: 16
          nettyThreads: 32
          # codec:
          transportMode: "NIO"
          sentinelServersConfig:
            idleConnectionTimeout: 10000
            connectTimeout: 10000
            timeout: 3000
            retryAttempts: 3
            retryInterval: 1500
            # password: null
            subscriptionsPerConnection: 5
            clientName: null
            # loadbalancer: 
            subscriptionConnectionMinimumIdleSize: 1
            subscriptionConnectionPoolSize: 50
            connectionMinimumIdleSize: 24
            connectionPoolSize: 64
            database: 0
            failedSlaveReconnectionInterval: 3000
            failedSlaveCheckInterval: 60000
            slaveConnectionMinimumIdleSize: 24
            slaveConnectionPoolSize: 64
            readMode: "SLAVE"
            sentinelAddresses:
              - "redis://127.0.0.1:6379"
              - "redis://127.0.0.1:6479"
            masterName: "redis-master"
      # 集群模式
      - schema: redis-cluster-1
        settings:
          threads: 16
          nettyThreads: 32
          # codec:
          transportMode: "NIO"
          lockWatchdogTimeout: 30000
          clusterServersConfig:
            idleConnectionTimeout: 10000
            connectTimeout: 10000
            timeout: 3000
            retryAttempts: 3
            retryInterval: 1500
            # password: null
            subscriptionsPerConnection: 5
            clientName: null
            # loadbalancer:
            subscriptionConnectionMinimumIdleSize: 1
            subscriptionConnectionPoolSize: 50
            connectionMinimumIdleSize: 24
            connectionPoolSize: 64
            failedSlaveReconnectionInterval: 3000
            failedSlaveCheckInterval: 60000
            slaveConnectionMinimumIdleSize: 24
            slaveConnectionPoolSize: 64
            readMode: "SLAVE"
            subscriptionMode: "SLAVE"
            nodeAddresses:
              - "redis://127.0.0.1:16379"
              - "redis://127.0.0.1:16479"
              - "redis://127.0.0.1:16579"
            scanInterval: 1000
            pingConnectionInterval: 30000
            keepAlive: false
            tcpNoDelay: true


如图可见,通过配置文件,可以配置多个数据源(即lock.redisson.list),在每个数据源中都可以为当前数据源选择不同的模式(single, sentinel, cluster),这最大程度满足了配置驱动的需求。事实上,正是Redisson提供了此类配置文件,才可以轻松使用。至于怎么解析,我们放在后面再说

2. 注解驱动

通过在启动类或配置类上标记注解,就能够进行自动配置, 可以启用分布式锁功能

@SpringBootApplication
@EnableRedisMutexLock
public class SpringRedisMutexLockApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringRedisMutexLockApplication.class, args);
    }
}

@EnableRedisMutexLock标注的情况下,可以通过在方法上标记注解,来表明这个方法需要获取到分布式锁后执行。

	@RedisMutexLock(schema = "redis-single-1", name = "test-lock-annotation", tryTimeout = 1000)
	public void testAnnotation() {
	   doSomeThing();
	}
3. API 驱动

即使已经实现了注解驱动的分布式锁,但是在某些场景下我们不能够添加注解,因此还需要手动通过API方式来获取分布式锁并进行上锁和解锁的功能

	public void testApi() {
	    final RMLock lock = RedisMutexLockSupport.createLock("redis-single-1", "lock-1");
	    lock.lock(1000, TimeUnit.MILLISECONDS);
	    doSomeThing();
	    lock.unlock();
	}
4. 总结

本文要从 配置文件、注解、API 三个方面实现Redis分布式锁,方便对Redis分布式锁进行配置和使用

Maven pom 依赖

本文主要以SpringBoot进行开发,严格上并不涉及Web等组件。因此在Pom文件上,只给出项目必要的依赖,其他依赖不做限制

        <!-- SpringBoot依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        
        <!-- Redisson的自动配置包, 内部去除了jedis和lettuce -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.18.0</version>
        </dependency>
        
        <!-- SpringBoot Aop依赖, 编写切面 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

		<!-- SpringBoot 的注解配置处理器, 可以优化配置文件中的警告 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>

三、实现

1. 锁接口实现

我们要操作分布式锁,自然要明确到底要操作什么功能。锁接口就是我们能操作功能的集合,事实上,通过AOP方式本质上也是获取了锁对象并调用锁的接口,只不过锁的持有者从用户变为了切面类。这里参考Redisson的RLock接口,设计了该分布式锁系统的锁接口RMLock

/**
 * 实现了 {@link java.util.concurrent.locks.Lock} 和可重入锁的分布式锁
 * @see RLock
 */
public interface RMLock extends RLock {

    /**
     * 获取锁对象名称
     *
     * @return name - 对象名称
     */
    String getName();

    /**
     * 获取定义了 <code>leaseTime</code> 参数的锁.
     * 一直等待锁直至获取, 并在获取锁后的 <code>leaseTime</code> 后自动释放锁
     *
     * @param leaseTime 获取锁后,在锁未通过 {@code unlock} 释放前的最大持有时间.
     *                  如果该值为 -1, 则锁会被一直持有直到显式地通过 {@code unlock} 释放
     * @param unit 时间单位
     * @throws InterruptedException - 线程发生中断
     */
    void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException;

    /**
     * 尝试获取定义了 <code>waitTime</code> 参数的锁.
     * 等待最多 <code>waitTime</code> 时间直到获取锁.
     * 在定义了 <code>leaseTime</code> 参数后, 锁会在不超过该时间前释放
     *
     * @param waitTime 获取锁的最大等待时间
     * @param leaseTime 最大持有时间
     * @param unit 时间单位
     * @return <code>true</code> 成功获取锁,
     *         <code>false</code> 获取锁失败
     * @throws InterruptedException - 线程发生中断
     */
    boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

    /**
     * 获取定义了 <code>leaseTime</code> 参数的锁.
     * 一直等待锁直至获取, 并在获取锁后的 <code>leaseTime</code> 后自动释放锁
     *
     * @param leaseTime 获取锁后,在锁未通过 {@code unlock} 释放前的最大持有时间.
     *                  如果该值为 -1, 则锁会被一直持有直到显式地通过 {@code unlock} 释放
     * @param unit 时间单位
     *
     */
    void lock(long leaseTime, TimeUnit unit);

    /**
     * 忽略锁的状态 <code>state</code> 强制释放锁
     *
     * @return <code>true</code> 锁存在并且成功释放, 否则 <code>false</code>
     */
    boolean forceUnlock();

    /**
     * 检查该锁是否被任意线程持有
     *
     * @return <code>true</code> 锁被持有, 否则 <code>false</code>
     */
    boolean isLocked();

    /**
     * 检查该锁是否被线程ID为 <code>threadId</code> 的线程持有
     *
     * @param threadId 要检验的线程ID
     * @return <code>true</code> 该锁确实被该线程持有, 否则 <code>false</code>
     */
    boolean isHeldByThread(long threadId);

    /**
     * 检查该锁是否被当前线程持有
     *
     * @return <code>true</code> 当前线程持有该锁, 否则 <code>false</code>
     */
    boolean isHeldByCurrentThread();

    /**
     * 当前线程持有该锁的数量 (可重入锁)
     *
     * @return 如果当前线程持有锁, 则为持有数量, 否则为 0
     */
    int getHoldCount();

    /**
     * 锁的剩余持有时间
     *
     * @return 锁的剩余持有时间(毫秒).
     *          -2 如果锁不存在.
     *          -1 如果锁存在但是没有过期时间.
     */
    long remainTimeToLive();
}

有的朋友可能好奇为什么上面接口中没有 lock(), unlock(), tryLock() 这种经典的无参方法呢?这里剧透一下,实际上是 RMLock 继承的 RLock 接口继承自 java.util.concurrent.locks.Lock,这些经典的方法都在 Lock 中定义了,因此不用重复定义。如果需要,也可以显式地定义方法


2. 注解与切面的实现

在上面我们提到的各个方案中,只有注解是实现起来最简单的,我们首先按照需求分别创建两个注解

@EnableRedisMutexLock 启动配置注解

public @interface EnableRedisMutexLock { }

@RedisMutexLock 分布式锁注解

/**
 * 注解在方法上, 获取分布式锁后执行, 否则为null
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisMutexLock {
    /**
     * 锁采用的配置模式
     */
    String schema();

    /**
     * 锁的名称
     */
    String name();

    /**
     * 是否使用公平锁
     */
    boolean fair() default false;

    /**
     * 锁的最大超时时间(单位毫秒, 为0时为永久等待)
     */
    long tryTimeout() default 0;
}

可以注意到 @EnableRedisMutexLock 注解实现细节很少,甚至没有 @Retention这样的元注解。请别着急,这涉及到了本次自动化的核心注解,我们一会再说,这里着重实现 注解及切面的AOP功能。
让我们创建@RedisMutexLock实现逻辑的Aspect类

/**
 * 分布式锁切面
 */
@Aspect
public class RedisMutexLockAspect {

    private static final Logger log = LoggerFactory.getLogger(RedisMutexLockAspect.class);

    // 这个切面的意思即为寻找所有携带 @RedisMutexLock的方法, 进行拦截
    @Pointcut("@annotation(com.example.springredismutexlock.annotation.RedisMutexLock)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) {
        final MethodSignature signature = (MethodSignature) pjp.getSignature();
        final RedisMutexLock annotation = signature.getMethod().getAnnotation(RedisMutexLock.class);

        final String schema = annotation.schema();
        final String name = annotation.name();
        final boolean fair = annotation.fair();
        final long tryTimeout = annotation.tryTimeout();

		// 这里通过显式调用API获取锁对象, 具体实现稍后再说
        final RMLock lock = RedisMutexLockSupport.createLock(schema, name, fair);
        if (lock == null)
            return null;

        Object result = null;
        boolean lockAcquire = false;
        try {
            if (lock != null) {
                if (tryTimeout <= 0) {
                    lock.lock();
                    lockAcquire = true;
                } else {
                    lockAcquire = lock.tryLock(tryTimeout, timeUnit);
                }
            }
            // 获取锁则执行方法
            if (lockAcquire) {
                result = pjp.proceed();
            }
        } catch (InterruptedException e) {
            log.info("Occurs an interrupt when wait lock '{}'", name);
        } catch (Throwable e) {
            log.error("System running exception during business, distribute lock will be release, more details: ", e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                // 执行完毕后解锁
                lock.unlock();
            }
        }
        return result;
    }
}

OK, 这样我们就完成了大部分注解驱动的实现,毕竟注解驱动本质上是通过AOP的方式减少用户操作,把锁的持有者由用户转为切面类。接下来,如何实现获取锁才是代码能够执行的关键


3. 配置文件读取与解析实现

无论是API还是注解,我们获取和操作的都是锁对象,也就是说我们在使用时不关心锁到底是怎么生成的,只需要知道提供参数就能获取锁并使用就行了。但对于进行模块开发的我们来说,锁的生成才是最重要的一环,它涉及到了配置文件读取与解析,Bean的注册与管理等等。

提起配置文件读取,很多人第一时间想到的是 @ConfigurationProperties 或者 @Value 注解,这两个注解能够将配置文件中的属性和配置类中的字段进行绑定,使得在加载过程中将配置自动注入到配置类中,因此对于上面复杂的配置,很多人第一时间想到的是这样:

@ConfiguratonProperties("lock.redisson")
public class ConfigTop {
	private List<ConfigInner1> list;
	
	public static class ConfigInner1 {
		private String schema;
		private ConfigInner2 settings;
		
		public static class ConfigInner2 {
			private Integer threads;
			private Integer nettyThreads;
			private String transportMode;
			private ConfigInner3 singleServerConfig;
			
			public static class ConfigInner3 {
				private Long idleConnectionTimeout;
				private Long connectTimeout;
				......
			}
		}
	}
}

但如果我告诉你 ConfigInner3 实际上就是 org.redisson.config.SingleServerConfig 呢, ConfigInner2 实际上就是 org.redisson.config.Config 呢,你还会这么配置吗?

你可能想那就把参数换一下,然后解析时也能创建Config对象和SingleServerConfig对象不久可以了?
想法很好,但实际上在redissson中,SingleServerConfig是通过单独的文件解析的,在redisson官方文档的配置文件中,需要指定一个file作为详细的配置文件,在该文件中就可以选择为 SingleServerConfig 或 SentinelServersConfig 或其他。

此外,最重要的一个问题就是多数据源问题,默认的redisson也好还是其他类型数据源也好,都是单点的,这意味着最多配置一个数据源,对于多出来的配置,默认的解析类也不会处理,甚至都无法识别多数据源的情况。因此我们必须手动实现多数据源注册的问题,这就意味着不能够通过 @Bean等方式注入Bean来生成Bean了,毕竟注入的可不止一个,我们生成的也不是随便生成,而是一对一生成。因此,自定义Bean注册器就显得很重要

3.1 定义属性配置类
// @ConditionalOnProperty(RedisMutexLockProperties.REDIS_PROPERTY_LIST_PREFIX)
@ConfigurationProperties(RedisMutexLockProperties.REDIS_PROPERTY_PREFIX)
public class RedisMutexLockProperties {

    public static final String REDIS_PROPERTY_PREFIX = "lock.redisson";
    public static final String REDIS_PROPERTY_LIST_PREFIX = "lock.redisson.list";

    private List<RedisMutexLockNodeProperties> list;

	// 省略构造, get/set方法

    public static class RedisMutexLockNodeProperties {
        public static final String REDIS_MODE_PROPERTY_PREFIX = "settings";

        private String schema;
        private String type;
        private Config settings;
		
		// 省略构造, get/set方法
    }
}

这里需要注意一点,就是上面的 @ConditionalOnProperty 注解被注释掉了,知道的朋友明白这是一个条件加载注解,但是 SpringBoot 中的配置条件加载是 配置本身是否有值来判断的,比如下面两个配置

lock:
  redisson:
    list: 'xxxx'

-------------------
lock:
  redisson:
    list:
      a: 'xxx'
      b: 'xxx'
      c: 'xxx'    
 

在上面的配置中, lock.redisson.list是一个实际存在的配置,它的值是 xxxx
而下面的配置中 lock.redisson.list 本身不存在,它只是一个前缀,真正存在的配置是 lock.redisson.list.a, lock.redisson.list.b, lock.redisson.list.c 这三个配置。因此当使用 @ConditionalOnProperties(“lock.redisson.list”) 时一定不会成功, 因为就不存在这个配置

3.2 定义配置解析器
public class RedisMutexLockPropertiesParser implements EnvironmentAware {
    private Environment environment;
    private Binder binder;

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
        this.binder = Binder.get(this.environment);
        parseProperties();
    }
}

解析器实现 EnvironmentAware接口可以将Environment注入到当前实例中,这个事件会在Spring应用启动不久,读取配置文件生成环境对象后,对容器中所有实现了该接口的Bean进行回调。上文提到过,无论是 Conditional 还是 Environment, 都是根据属性是否存在而不是层级是否存在来挑选配置的,因此就无法直接通过 environment 获取 lock.redisson.list 下的配置。这里可以使用另一个工具 Binder, Binder 允许我们使用集合或Map来接收这些层级属性配置。

    /**
     * 解析生成 Properties
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    public void parseProperties() {
        List<Map> mapList;
        try {
            mapList = binder.bind(RedisMutexLockProperties.REDIS_PROPERTY_LIST_PREFIX, Bindable.listOf(Map.class)).get();
        } catch (NoSuchElementException e) {
            log.error("Failed To configure RedisMutexLock: '{}' attribute is not specified", RedisMutexLockProperties.REDIS_PROPERTY_LIST_PREFIX);
            return;
        }
        List<RedisMutexLockProperties.RedisMutexLockNodeProperties> propertiesList = new ArrayList<>();
        for (Map map : mapList) {
            // 这里本质上解析出来的map是 Map<String, String> 类型
            final Map<String, Object> propertyMap = (Map<String, Object>) map;
            final String schema = (String) propertyMap.get("schema");
            final String type = (String) propertyMap.get("type");
            final Config config = configParse(propertyMap);
            if (config == null)
                continue;

            RedisMutexLockProperties.RedisMutexLockNodeProperties node = new RedisMutexLockProperties.
                    RedisMutexLockNodeProperties(schema, type, config);
            propertiesList.add(node);
        }
        properties = new RedisMutexLockProperties(Collections.unmodifiableList(propertiesList));
    }

解析方法也很简单,就是通过 Binder 绑定一个键,然后选择合适的对象接收。这里因为不存在 “lock.redissson.list” 这个键,所以我使用了 List<Map> 来接收这个键的内容,表示这个键是集合,集合内部使用 Map 接收

重点放在怎么解析出Config上面,其实也很简单,只要翻阅Config源码,自然就能找到它解析的方式,这里直接将代码copy到类中


	/**
     * 解析生成 Config
     */
    private Config configParse(Map<String, Object> parseMap) {
        final Map<String, Object> settings = (Map<String, Object>) parseMap.get("settings");
        final ConfigSupport support = new ConfigSupport();
        Config config = null;
        try {
            config = support.fromYAML(yamlMapper.writeValueAsString(settings), Config.class);
        } catch (IOException e) {
            log.error("Failed to initialize redis mode configuration properties, reasons are ", e);
        }
        return config;
    }
	

	private final ObjectMapper yamlMapper = createMapper(new YAMLFactory(), null);
    
    /**
     * @see ConfigSupport#createMapper(JsonFactory, ClassLoader)
     */
    private ObjectMapper createMapper(JsonFactory mapping, ClassLoader classLoader) {
        ObjectMapper mapper = new ObjectMapper(mapping);

        mapper.addMixIn(Config.class, ConfigSupport.ConfigMixIn.class);
        mapper.addMixIn(ReferenceCodecProvider.class, ConfigSupport.ClassMixIn.class);
        mapper.addMixIn(AddressResolverGroupFactory.class, ConfigSupport.ClassMixIn.class);
        mapper.addMixIn(Codec.class, ConfigSupport.ClassMixIn.class);
        mapper.addMixIn(RedissonNodeInitializer.class, ConfigSupport.ClassMixIn.class);
        mapper.addMixIn(LoadBalancer.class, ConfigSupport.ClassMixIn.class);
        mapper.addMixIn(NatMapper.class, ConfigSupport.ClassMixIn.class);
        mapper.addMixIn(NameMapper.class, ConfigSupport.ClassMixIn.class);
        mapper.addMixIn(NettyHook.class, ConfigSupport.ClassMixIn.class);

        FilterProvider filterProvider = new SimpleFilterProvider()
                .addFilter("classFilter", SimpleBeanPropertyFilter.filterOutAllExcept());
        mapper.setFilterProvider(filterProvider);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

        if (classLoader != null) {
            TypeFactory tf = TypeFactory.defaultInstance()
                    .withClassLoader(classLoader);
            mapper.setTypeFactory(tf);
        }

        return mapper;
    }

OK,这样整个类的部分就完成了。在上面的方法中,我们把解析好的数据重新封装成了 Properties,方便其他类直接使用

3.3 自定义 Bean 注册器

前面提到过,针对多数据源一对一的情况,需要自己手动注册Bean,本文通过Bean注册器来为每个数据源注册相应的Beans。这个类的实现大家直接看代码吧,里面定义了通过 Config 生成了 RedissonClient 对象,并以此生成了 RedisTemplate 等对象。

在上面一节中我们介绍了解析器,解析器解析出来的属性需要暴露给外部使用,而在Bean注册阶段又不能使用注入Bean 的方式引用其他类型Bean的方法(原因下面说),因此选择将解析器作为注册器的内部类来进行声明,作为代价,EnvironmentAware接口从解析器冒泡给了注册器

public class RedisMutexLockBeanRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
    private static final Logger log = LoggerFactory.getLogger(RedisMutexLockBeanRegister.class);

    private Environment environment;
    private Binder binder;
    private RedisMutexLockPropertiesParser parser;
    
    private final Map<String, Object> registerMap = new ConcurrentHashMap<>();

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
        this.binder = Binder.get(this.environment);
        this.parser = new RedisMutexLockPropertiesParser(this.binder);
    }


    /**
     * 注册多个 {@link RedissonClient}, 并为每个 RedissonClient 生成对应的 <br>
     * {@link RedissonConnectionFactory}, <br>
     * {@link StringRedisTemplate}, <br>
     * {@link RedisTemplate}, <br>
     * {@link RedissonReactiveClient}, <br>
     * {@link RedissonRxClient} <br>
     * Bean命名规则为 schema 命名空间和类名结合的驼峰式命名
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        final RedisMutexLockProperties properties = parser.getProperties();
        boolean onPrimary = true;
        for (RedisMutexLockProperties.RedisMutexLockNodeProperties node : properties.getList()) {
            final String schema = node.getSchema();
            final String type = node.getType();
            final Config settings = node.getSettings();
            if (settings == null) {
                log.error("Missing create beans for schema and type [{}-{}], caused by org.redisson.config.Config is null", schema, type);
                continue;
            }
            final String camelSchema = StringUtil.convertToCamel(schema);

            final String clientName = camelSchema + RedissonClient.class.getSimpleName();
            final Supplier<RedissonClient> clientSupplier = () -> {
                RedissonClient client = (RedissonClient) registerMap.get(clientName);
                if (client == null) {
                    client = Redisson.create(settings);
                    registerMap.put(clientName, client);
                }
                return client;
            };
            final RedissonClient client = clientSupplier.get();
            final BeanDefinition clientBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(RedissonClient.class, clientSupplier).getBeanDefinition();
            clientBeanDefinition.setPrimary(onPrimary);
            registry.registerBeanDefinition(clientName, clientBeanDefinition);

            final String factoryName = camelSchema + RedissonConnectionFactory.class.getSimpleName();
            final Supplier<RedissonConnectionFactory> factorySupplier = () -> {
                RedissonConnectionFactory fac = (RedissonConnectionFactory) registerMap.get(factoryName);
                if (fac == null) {
                    fac = new RedissonConnectionFactory(client);
                    registerMap.put(factoryName, fac);
                }
                return fac;
            };
            final RedissonConnectionFactory factory = factorySupplier.get();
            final BeanDefinition factoryBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(RedissonConnectionFactory.class, factorySupplier).getBeanDefinition();
            factoryBeanDefinition.setPrimary(onPrimary);
            registry.registerBeanDefinition(factoryName, factoryBeanDefinition);

            final String stringRedisTemplateName = camelSchema + StringRedisTemplate.class.getSimpleName();
            final BeanDefinition stringRedisTemplateBeanDefinition = stringRedisTemplateBeanDefinition(factory, onPrimary);
            registry.registerBeanDefinition(stringRedisTemplateName, stringRedisTemplateBeanDefinition);

            final String redisTemplateName = camelSchema + RedisTemplate.class.getSimpleName();
            final BeanDefinition redisTemplateBeanDefinition = redisTemplateBeanDefinition(factory, onPrimary);
            registry.registerBeanDefinition(redisTemplateName, redisTemplateBeanDefinition);

            final String redissonReactiveName = camelSchema + RedissonReactiveClient.class.getSimpleName();
            final BeanDefinition redissonReactiveBeanDefinition = redissonReactiveBeanDefinition(client, onPrimary);
            registry.registerBeanDefinition(redissonReactiveName, redissonReactiveBeanDefinition);

            final String redissonRxName = camelSchema + RedissonRxClient.class.getSimpleName();
            final BeanDefinition redissonRxBeanDefinition = redissonRxBeanDefinition(client, onPrimary);
            registry.registerBeanDefinition(redissonRxName, redissonRxBeanDefinition);

            if (onPrimary) onPrimary = false;

			// 下一小节——Bean的信息保存
            RedisMutexLockInfoHolder.registerName(schema, RedissonClient.class, clientName);
            RedisMutexLockInfoHolder.registerName(schema, RedisConnectionFactory.class, factoryName);
            RedisMutexLockInfoHolder.registerName(schema, StringRedisTemplate.class, stringRedisTemplateName);
            RedisMutexLockInfoHolder.registerName(schema, RedisTemplate.class, redisTemplateName);
            RedisMutexLockInfoHolder.registerName(schema, RedissonReactiveClient.class, redissonReactiveName);
            RedisMutexLockInfoHolder.registerName(schema, RedissonRxClient.class, redissonRxName);
        }
    }

    /**
     * 生成 {@link StringRedisTemplate}
     */
    private BeanDefinition stringRedisTemplateBeanDefinition(RedisConnectionFactory factory, boolean onPrimary) {
        final GenericBeanDefinition definition = new GenericBeanDefinition();
        definition.setBeanClass(StringRedisTemplate.class);
        final ConstructorArgumentValues values = new ConstructorArgumentValues();
        values.addIndexedArgumentValue(0, factory);
        definition.setConstructorArgumentValues(values);
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);
        definition.setPrimary(onPrimary);
        return definition;
    }

    /**
     * 生成 {@link RedisTemplate}
     */
    private BeanDefinition redisTemplateBeanDefinition(RedisConnectionFactory factory, boolean onPrimary) {
        final GenericBeanDefinition definition = new GenericBeanDefinition();
        definition.setBeanClass(RedisTemplate.class);
        definition.getPropertyValues().add("connectionFactory", factory);
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);
        definition.setPrimary(onPrimary);
        return definition;
    }

    /**
     * 懒加载生成 {@link RedissonReactiveClient}
     */
    private BeanDefinition redissonReactiveBeanDefinition(RedissonClient client, boolean onPrimary) {
        final GenericBeanDefinition definition = new GenericBeanDefinition();
        definition.setBeanClass(RedissonReactiveClient.class);
        definition.setInstanceSupplier(client::reactive);
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);
        definition.setPrimary(onPrimary);
        definition.setLazyInit(true);
        return definition;
    }

    /**
     * 懒加载生成 {@link RedissonRxClient}
     */
    private BeanDefinition redissonRxBeanDefinition(RedissonClient client, boolean onPrimary) {
        final GenericBeanDefinition definition = new GenericBeanDefinition();
        definition.setBeanClass(RedissonRxClient.class);
        definition.setInstanceSupplier(client::rxJava);
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);
        definition.setPrimary(onPrimary);
        definition.setLazyInit(true);
        return definition;
    }

    static class RedisMutexLockPropertiesParser {
        private Binder binder;
        private RedisMutexLockProperties properties;
        private final ObjectMapper yamlMapper = createMapper(new YAMLFactory(), null);

        public RedisMutexLockPropertiesParser(Binder binder) {
            this.binder = binder;
            parseProperties();
        }

        public RedisMutexLockProperties getProperties() {
            return properties;
        }

        /**
         * 解析生成 Properties
         */
        private void parseProperties() {
            List<Map> mapList;
            try {
                mapList = binder.bind(RedisMutexLockProperties.REDIS_PROPERTY_LIST_PREFIX, Bindable.listOf(Map.class)).get();
            } catch (NoSuchElementException e) {
                log.error("Failed To configure RedisMutexLock: '{}' attribute is not specified", RedisMutexLockProperties.REDIS_PROPERTY_LIST_PREFIX);
                return;
            }
            List<RedisMutexLockProperties.RedisMutexLockNodeProperties> propertiesList = new ArrayList<>();
            for (Map map : mapList) {
                final Map<String, Object> propertyMap = (Map<String, Object>) map;
                final String schema = (String) propertyMap.get("schema");
                final String type = (String) propertyMap.get("type");
                final Config config = configParse(propertyMap);
                if (config == null)
                    continue;

                RedisMutexLockProperties.RedisMutexLockNodeProperties node = new RedisMutexLockProperties.
                        RedisMutexLockNodeProperties(schema, type, config);
                propertiesList.add(node);
            }
            properties = new RedisMutexLockProperties(Collections.unmodifiableList(propertiesList));
        }

        /**
         * 解析生成 Config
         */
        private Config configParse(Map<String, Object> parseMap) {
            final Map<String, Object> settings = (Map<String, Object>) parseMap.get("settings");
            final ConfigSupport support = new ConfigSupport();
            Config config = null;
            try {
                config = support.fromYAML(yamlMapper.writeValueAsString(settings), Config.class);
            } catch (IOException e) {
                log.error("Failed to initialize redis mode configuration properties, reasons are ", e);
            }
            return config;
        }

        /**
         * @see ConfigSupport#createMapper(JsonFactory, ClassLoader)
         */
        private ObjectMapper createMapper(JsonFactory mapping, ClassLoader classLoader) {
            ObjectMapper mapper = new ObjectMapper(mapping);

            mapper.addMixIn(Config.class, ConfigSupport.ConfigMixIn.class);
            mapper.addMixIn(ReferenceCodecProvider.class, ConfigSupport.ClassMixIn.class);
            mapper.addMixIn(AddressResolverGroupFactory.class, ConfigSupport.ClassMixIn.class);
            mapper.addMixIn(Codec.class, ConfigSupport.ClassMixIn.class);
            mapper.addMixIn(RedissonNodeInitializer.class, ConfigSupport.ClassMixIn.class);
            mapper.addMixIn(LoadBalancer.class, ConfigSupport.ClassMixIn.class);
            mapper.addMixIn(NatMapper.class, ConfigSupport.ClassMixIn.class);
            mapper.addMixIn(NameMapper.class, ConfigSupport.ClassMixIn.class);
            mapper.addMixIn(NettyHook.class, ConfigSupport.ClassMixIn.class);

            FilterProvider filterProvider = new SimpleFilterProvider()
                    .addFilter("classFilter", SimpleBeanPropertyFilter.filterOutAllExcept());
            mapper.setFilterProvider(filterProvider);
            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
            mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

            if (classLoader != null) {
                TypeFactory tf = TypeFactory.defaultInstance()
                        .withClassLoader(classLoader);
                mapper.setTypeFactory(tf);
            }

            return mapper;
        }
    }
}

4. 信息保存与API操作

终于,在我们的努力下,我们成功将Bean注册到了容器中,现在,我们可以使用了!嗯?我怎么不知道我保存的bean呢…

是的,在上面的流程中,我们成功的将Bean都注册到了容器中,也成功为锁的创建提供了充足的条件,但是当我们想要创建锁的时候,一个巨大的问题出现了:我的Client呢?

我们创建锁的前提条件 RedissonClient 被注入到了容器中,但是我们没有任何记录,这就导致我们根本无法使用我们创建的Bean,我们不可能知道每个Bean的名字,然后在代码中显式地指定,这样的代码耦合太深,侵入性太大,所以我们需要一个信息持有者RedisMutexLockInfoHolder

正如名字所言,RedisMutexLockInfoHolder专门记录配置过程中的Bean信息,我可以直接将代码贴出来,方便理解这个类究竟要干什么

public class RedisMutexLockInfoHolder implements ApplicationContextAware {
    private static final Logger log = LoggerFactory.getLogger(RedisMutexLockSupport.class);
    private static ApplicationContext ctx;
    private static final Map<String, Map<Class<?>, Object>> schemaClassBeanMap = new HashMap<>();
    private static final Map<Class<?>, Map<String, Object>> classNameBeanMap = new HashMap<>();
    private static final Map<String, String> nameSchemaMap = new HashMap<>();
    private static final Map<String, Object> nameBeanMap = new HashMap<>();

    private RedisMutexLockInfoHolder() {
    }

    @SuppressWarnings("unchecked")
    public static <T> T getBeanBySchemaAndType(String schema, Class<T> type) {
        final Map<Class<?>, Object> classObjectMap = schemaClassBeanMap.get(schema);
        if (classObjectMap == null)
            return null;
        return (T) classObjectMap.get(type);
    }

    public static Map<Class<?>, Object> getClassBeanMapBySchema(String schema) {
        return schemaClassBeanMap.get(schema);
    }

    public static Object getBeanByName(String name) {
        return nameBeanMap.get(name);
    }

    public static Map<String, Object> getNameBeanMapByClass(Class<?> clazz) {
        return classNameBeanMap.get(clazz);
    }

    static void registerName(String schema, Class<?> clazz, String beanName) {
        if (!schemaClassBeanMap.containsKey(schema)) {
            schemaClassBeanMap.put(schema, new HashMap<>());
        }
        if (!classNameBeanMap.containsKey(clazz)) {
            classNameBeanMap.put(clazz, new HashMap<>());
        }
        final Map<Class<?>, Object> classMap = schemaClassBeanMap.get(schema);
        if (classMap.containsKey(clazz)) {
            log.warn("Failed to register for bean [{} {}]: class in schema has already existed", clazz, beanName);
            return;
        }
        final Map<String, Object> nameMap = classNameBeanMap.get(clazz);
        if (nameMap.containsKey(beanName)) {
            log.warn("Failed to register for bean [{} {}]: name in class has already existed", clazz, beanName);
            return;
        }
        if (nameSchemaMap.containsKey(beanName) || nameBeanMap.containsKey(beanName)) {
            log.warn("Failed to register for bean [{} {}]: schema for name has already existed", clazz, beanName);
        }
        classMap.put(clazz, null);
        nameMap.put(beanName, null);
        nameSchemaMap.put(beanName, schema);
        nameBeanMap.put(beanName, null);
    }

    static boolean disabled() {
        return nameSchemaMap.isEmpty() && nameBeanMap.isEmpty();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RedisMutexLockInfoHolder.ctx = applicationContext;
        for (Class<?> aClass : classNameBeanMap.keySet()) {
            for (Map.Entry<String, ?> entry : ctx.getBeansOfType(aClass).entrySet()) {
                final String beanName = entry.getKey();
                final Object beanObject = entry.getValue();

                final Map<String, Object> nameBeanMap = classNameBeanMap.get(aClass);
                final String schema = nameSchemaMap.get(beanName);
                if (null == nameBeanMap || null == schema) {
                    log.warn("Unregister bean for [{} {}]", aClass, beanName);
                    continue;
                }
                final Map<Class<?>, Object> classBeanMap = schemaClassBeanMap.get(schema);
                if (null == classBeanMap) {
                    log.warn("Failed to store bean for [{} {}]: System Running Exception, schemaClassBeanMap not initialize", aClass, beanName);
                    continue;
                }
                nameBeanMap.put(beanName, beanObject);
                classBeanMap.put(aClass, beanObject);
                RedisMutexLockInfoHolder.nameBeanMap.put(beanName, beanObject);
            }
        }
    }
}

很简单!它仅仅内部使用了几个Map来维护 schema, name, bean 之间的关系,比较显眼的是它实现了 ApplicationContextAware 接口,这是因为在上面的 注册阶段,我们注册的并不是Bean,而是 BeanDefinition。它只是提供了这个 Bean 究竟要怎样去定义,还没有完全生成(当然有的直接就生成了),而 ApplicationContextAware 在容器构建后就会触发回调,这时候容器的Bean已经全部生成,并注入了依赖。我们就可以通过名称来获取Bean,并添加到Map中,方便API调用。

这里的API指的是供内部使用的API,而用户API还是不建议直接使用

RedisMutexLockInfoHolder类定义完毕后,我们终于集齐了最后一块拼图,可以定义 RedisMutexLockSupport 这个 API 操作类了,其实真的很简单

public class RedisMutexLockSupport {

    private static final Logger log = LoggerFactory.getLogger(RedisMutexLockSupport.class);

    @SuppressWarnings("all")
    public static boolean isLockEnabled() {
        return !RedisMutexLockInfoHolder.disabled();
    }

    public static RMLock createLock(String schema, String lockName) {
        if (!isLockEnabled()) {
            log.warn("RedisMutexLock unable to use, getting lock[{}-{}] failure", schema, lockName);
            return null;
        }
        final RedissonClient client = getClient(schema);
        if (client == null) {
            log.warn("Failed to get lock from client: schema [{}] not exist", schema);
            return null;
        }
        return RedisMutexLockBuilder.fromILock(client.getLock(lockName));
    }

    public static RMLock createLock(String schema, String lockName, boolean fair) {
        if (!isLockEnabled()) {
            log.warn("RedisMutexLock unable to use, getting lock[{}-{}-{}] failure", schema, lockName, "fair");
            return null;
        }
        final RedissonClient client = getClient(schema);
        if (client == null) {
            log.warn("Failed to get lock from client: schema [{}] not exist", schema);
            return null;
        }
        if (fair)
            return RedisMutexLockBuilder.fromILock(client.getLock(lockName));
        else
            return RedisMutexLockBuilder.fromFairLock((RedissonFairLock) client.getFairLock(lockName));
    }

    public static RedissonClient getClient(String schema) {
        if (!isLockEnabled()) {
            log.warn("RedisMutexLock unable to use, getting client[{}] failure", schema);
            return null;
        }
        return RedisMutexLockInfoHolder.getBeanBySchemaAndType(schema, RedissonClient.class);
    }

}

如你所见,它就是调用了 InfoHolder 类来获取Bean,并创建锁对象。代码中的 RedisMutexLockBuilder 则是为了解决兼容性问题,毕竟系统暴露的是 RMLock,而Redisson提供的则是RLock。这个最后再说


5. 自动化

终于到了文章重点中的重点:自动化/模块化

在学习 Spring 的时候,我们就知道将类实例注册为Bean的方式不仅有 @Bean, @Component, @Configuration,还有 @Import 这个注解,@Import注解才是真正的主角,王牌中的王牌

在上面我们定义的类如 BeanRegister, Aspect, InfoHolder 等都需要注册为 Bean 才能享用回调/启用AOP。我们当然可以直接通过在类上面打上 @Component 注解来将这个 Bean 注册到容器中。问题是这样做仅仅也就是将 类实例化注册为Bean了,它无法控制是否将类实例化注册。还记得我们前面提到过的 @EnableRedisMutexLock 吗?我们需要它来控制是否启用当前模块,而不是让它仅仅成为一个花瓶。

因此,我们需要创建 RedisMutexLockAutoConfiguration 类,让 @EnableRedisMutexLock 来 import 上面的类,从而控制整个模块的加载。

@Inherited
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({RedisMutexLockAutoConfiguration.class})
@EnableAspectJAutoProxy
public @interface EnableRedisMutexLock {
}

其中可以看到在注解上打了 @EnableAspectJAutoProxy,因为我们要使用切面功能,所以主动加上这个注解,毕竟我们不知道项目究竟是否有该环境。

@ConditionalOnClass({Redisson.class, RedisOperations.class})
@AutoConfigureAfter({RedisAutoConfiguration.class})
@EnableConfigurationProperties({RedisMutexLockProperties.class})
@Import({RedisMutexLockInfoHolder.class})
public class RedisMutexLockAutoConfiguration {
    @Bean
    public RedisMutexLockBeanRegister redisMutexLockBeanRegister() {
        return new RedisMutexLockBeanRegister();
    }
}

上面就是 RedisMutexLockAutoConfiguration 的代码了,怎么是不是很失望,感觉没有期望中的功能那么多?

可以注意到这个类上面没有 @Configuration 注解,这是因为 @Import 注解可以让被import类按照配置类来加载,所以不用担心不加 @Configuration 会失效。反而加了不能控制是否自动装配了。

写到这里,就是整个自动化/模块化的配置了,一路走下来,其实最重要的就是 @Import,它引入别的类,使你看不到类真正注册到容器的过程,从而无感知自动化了。


6. 委托封装

最后一步,到了这一步其实已经不是很重要了。完全可以去除 RMLock 而直接使用 RLock 来操作 Redisson。不过有些人可能需要这样的功能,那就必须使用委托功能。

public abstract class RedisMutexBaseLock implements RMLock, RExpirable, RObject, RObjectAsync {
    protected final RLock lock;
    protected final RExpirable expirable;

    RedisMutexBaseLock(RLock lock, RExpirable expirable) {
        this.lock = lock;
        this.expirable = expirable;
    }

	// 省略方法

	    @Override
    public RFuture<Void> unlockAsync(long threadId) {
        return lock.unlockAsync(threadId);
    }

    @Override
    public long remainTimeToLive() {
        return expirable.remainTimeToLive();
    }

	// 省略方法
}

上面选择两个有代表性的方法,因为这两个连续的方法刚好委托给了不同的对象。有人可能好奇明明操作的是 RedisssonLock,但是会有两个操作对象呢?这是因为 RedisssonBaseLock 继承自 RedissonExpirable, 实现了 RLock接口。而前者实现了 RExpirable接口,这两个接口没有任何关联,无法统一起来,因此只能构造两个参数进行分别委托。

接下来就简单了,仿照 RedissonBaseLock 实现 RedisMutexBaseLock,并委托实现所有方法。接下来对于每个 Lock 生成一个对应的 Lock 就可以了。如对于 RedissonFairLock,就创建 RedisMutexFairLock,继承 RedisMutexBaseLock即可。这里不再赘述

三、总结

本文使用 SpringBoot 对 Redissson 进行了封装,实现了 注解驱动的Redis分布式锁模块。通过在本文阅读的过程中,您能逐步了解SpringBoot模块化的背后,并对自己开发模块有了一定的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值