最近在开发一个热部署平台,应用接入平台需要依赖我们提供一个代理包,为应用提供,订阅热补命令、往注册中心写应用地址信息,解析命令进行热部署的能力。

应用需要在平台配置该应用的发布订阅的组件信息。然后应用在启动的时候取注册这个监听。当平台发布热补命令的时候,所有监听到的应用就能接收到命令,进而进行热补处理。

发布订阅

作为平台开发,必须兼容更多组件,才能够吸收更多的项目应用接入平台。那就意味着该代理包必须尽可能多的支持具有发布订阅功能的组件。常见的具有发布订阅的组件有

  • zookeeper
  • nacos
  • redis
  • mq

等等,一般的mq都具有发布订阅的功能,这里就不展开细分的mq。

考虑到公司现有项目的组件使用情况, 当前支持了zookeeper、nacos、redis这三种。

有之则取

因为应用是无法决定或感知接入的应用用了什么发布订阅组件的,因此应用接入平台,除了依赖代理包提供热部署能力外,还需要到平台配置自己的组件信息。代理包通过调用平台接口而获取到配置。

spring redis注册监听

redis注册监听,是在项目启动时候完成。需要创建一个RedisMessageListenerContainer。而RedisMessageListenerContainer的创建,依赖一个RedisConnectionFactory实例,当项目本是就有使用spring redis的情况下,会自动配置一个RedisConnectionFactory bean

org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration

@AutoConfiguration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }
    
    ...
}

其中的

@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})

扫描了两个配置类LettuceConnectionConfiguration和JedisConnectionConfiguration

通过查看他们的源码:

LettuceConnectionConfiguration.java

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({RedisClient.class})
@ConditionalOnProperty(
    name = {"spring.redis.client-type"},
    havingValue = "lettuce",
    matchIfMissing = true
)
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
    LettuceConnectionConfiguration(RedisProperties properties, ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider, ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider, ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
        super(properties, standaloneConfigurationProvider, sentinelConfigurationProvider, clusterConfigurationProvider);
    }

    @Bean(
        destroyMethod = "shutdown"
    )
    @ConditionalOnMissingBean({ClientResources.class})
    DefaultClientResources lettuceClientResources(ObjectProvider<ClientResourcesBuilderCustomizer> customizers) {
        Builder builder = DefaultClientResources.builder();
        customizers.orderedStream().forEach((customizer) -> {
            customizer.customize(builder);
        });
        return builder.build();
    }

    @Bean
    @ConditionalOnMissingBean({RedisConnectionFactory.class})
    LettuceConnectionFactory redisConnectionFactory(ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers, ClientResources clientResources) {
        LettuceClientConfiguration clientConfig = this.getLettuceClientConfiguration(builderCustomizers, clientResources, this.getProperties().getLettuce().getPool());
        return this.createLettuceConnectionFactory(clientConfig);
    }
    
    ...
    
}

注册了一个LettuceConnectionFactory bean,其是RedisConnectionFactory 的实现类。

JedisConnectionConfiguration.java

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({GenericObjectPool.class, JedisConnection.class, Jedis.class})
@ConditionalOnMissingBean({RedisConnectionFactory.class})
@ConditionalOnProperty(
    name = {"spring.redis.client-type"},
    havingValue = "jedis",
    matchIfMissing = true
)
class JedisConnectionConfiguration extends RedisConnectionConfiguration {
    JedisConnectionConfiguration(RedisProperties properties, ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider, ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration, ObjectProvider<RedisClusterConfiguration> clusterConfiguration) {
        super(properties, standaloneConfigurationProvider, sentinelConfiguration, clusterConfiguration);
    }

    @Bean
    JedisConnectionFactory redisConnectionFactory(ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) {
        return this.createJedisConnectionFactory(builderCustomizers);
    }
    
    ...
    
}

注册了一个JedisConnectionFactory bean,其也是RedisConnectionFactory 的实现类。

这里先了解一下应用本身如果用了spring redis,必定会有一个RedisConnectionFactory 的spring bean,RedisConnectionFactory Redis连接的线程安全工厂,维护了客户与服务器的连接。至于那个才是生效配置,我们下面【无之禁用】再介绍,因为其息息相关。

项目本身RedisConnectionFactory有了,我们就尽可能去复用,而不是自己去创建多一个连接工程,这样再微服务场景下,如果项目的实例部署很多,会给redis带来极大的负担。

如何判断拿项目侧是否激活了spring redis自动配置呢?

    private static final String REDIS_STANDALONE = "spring.redis.host";
    private static final String REDIS_SENTINEL = "spring.redis.sentinel.nodes";
    private static final String REDIS_CLUSTER = "spring.redis.cluster.nodes";


/**
     * 在的环境加载末期判断当前应用是否使用了redis自动配置
     * @param environment
     * @return
     */
    private boolean hasRedisAutoConfigure(ConfigurableEnvironment environment) {
        boolean standalone = !Objects.isNull(environment.getProperty(REDIS_STANDALONE));
        boolean sentinel = !Objects.isNull(environment.getProperty(REDIS_SENTINEL));
        boolean cluster = !Objects.isNull(environment.getProperty(REDIS_CLUSTER));
        return standalone || sentinel || cluster;
    }

当springboot项目启用了spring redis的自动配置,环境中肯定会有关于spring redis的相关配置。在判断的时候,要注意redis常见的三种模式进行判断:Standalone\ sentinel\cluster

有些小伙伴可能会想,单单判断节点信息,可以确认是否启用了吗?如果的我只配置了节点信息,其他没有怎么办?

首先,如果只配置了节点,的确会让我们该处理器认为他启用了redis的自动配置,尽管他会因为缺了其他配置导致redis没有被自动配置。但可以被确定的一点是:当你缺了配置,导致redis没有被自动配置,甚至项目根本就启动失败,这种情况其实不是我们要去适配的情况,我们的处理都是默认认为你项目被正确启动,正常运行的情况。

无之则禁

当能够判断redis没有被自动配置后,我们就可以处理由于的接入我们的jar包导致spring redis 被无意触发的情况,进而将自动配置禁用掉。

springboot提供了禁用自动配置的口子,那就是在配置文件中,在spring.autoconfigure.exclude中配置自动配置类的类路径。在项目启动时,读取环境变量的时候,会读取改配置,然后在处理自动配置阶段,将这些类排除不处理。

那有人就会说,那在项目的配置文件中,在spring.autoconfigure.exclude中添加spring redis 自动配置类不就好了吗?正常情况下的确是的。但不要忘了,我们这个是jar包的处理,要尽可能做到对应用的无侵入,能给做的要自己做了,让用户开箱即用。

因此我们需要自己处理。

从上面的阐述也可以看的出来。排除自动配置的配置,本质上是在项目的prepareEnvironment,也就是准备环境阶段中,读取了配置用于后续处理。你可以简单的理解,springboot需要你配置的东西,都将作为项目的一部分“环境”,这不过绝大部分的环境配置都是springboot 约定好了,你按他的要求配置,他到约定的地方去读取。

spring boot提供了一个的用于自定义环境变量的钩子。那就是EnvironmentPostProcessor

@FunctionalInterface
public interface EnvironmentPostProcessor {

   /**
    * Post-process the given {@code environment}.
    * @param environment the environment to post-process
    * @param application the application to which the environment belongs
    */
   void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application);

}

EnvironmentPostProcessor是一个FunctionalInterface,允许在刷新应用程序上下文之前自定义应用程序的环境。 EnvironmentPostProcessor的实现必须在META-INF/spring中注册。使用该类的完全限定名作为键。如果实现希望以特定顺序调用,它们可以实现Ordered接口或使用@Order注释。

使用上来说,只需要实现这个接口,然后将实现类注册给spring容器即可。

有了这个钩子,不意外着万事大吉了。还有很多事情需要考虑:

  • 确定好自定义配置失效的优先级
  • 不影响应用原有的配置。

springboot 的配置来自于很多地方,也就是说它会总各个地方搜集到应用环境的配置。称之为PropertiesSource。这么多个PropertiesSource,springboot只会为每一个key值处理一次,后续再有相关配置就不处理,因此配置的优先级至关重要。

由于每个PropertiesSource里面的配置其实没有规律的,取决于实际的配置情况,而且每个数组配置的配置项,最终都会解析成key[n]的字符串形式,你无法直接获取到PropertiesSource里面是否的有的spring.autoconfigure.exclude的配置,有的话到底配置了几个。

比如某一个PropertiesSource里面有spring.autoconfigure.exclude配置,配置了3个,那将会被解析成spring.autoconfigure.exclude[0]、spring.autoconfigure.exclude[1]、spring.autoconfigure.exclude[3],这些键值不能重复,重复会报错。因此在拥有spring.autoconfigure.exclude配置的PropertiesSource里面塞入我们的的自定义配置,不是一个很好的办法。

我们只能自己创建一个PropertiesSource。

创建一个新的PropertiesSource放到环境中去,我们要确保两点。

  1. 搜集项目原有的完整的spring.autoconfigure.exclude配置,放入我们新建的PropertiesSource,然后将我们自定义配置放在最后。
  2. 将新建的PropertiesSource放置于PropertiesSource列表的最前面,确保最先处理。

要确保能够搜集到全量的配置,就必须使得我们EnvironmentPostProcessor处理器最后处理。order必须最大,优先级最低。

因此最终的实现如下:

@Configuration(proxyBeanMethods = false)
public class RedisCheckProcessor implements EnvironmentPostProcessor, Ordered {

    private static final String REDIS_STANDALONE = "spring.redis.host";
    private static final String REDIS_SENTINEL = "spring.redis.sentinel.nodes";
    private static final String REDIS_CLUSTER = "spring.redis.cluster.nodes";

    public static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = "bootstrap";


    private static final String SPRING_AUTOCONFIGURE_EXCLUDE_KEY = "spring.autoconfigure.exclude";

    private static final String EXCLUDE_AUTO_CONFIG_SOURCE_NAME = "excludeAutoConfig";

    private static final String EXCLUDE_AUTO_CONFIG = "org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration";

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {

        // 排除springCloud场景
        if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
            return;
        }
        // 禁用redis自动配置
        if (!hasRedisAutoConfigure(environment)) {

            // 覆写应用spring.autoconfigure.exclude配置
            List<String> excludes = new ArrayList<>();

            MutablePropertySources mutablePropertySources = environment.getPropertySources();
            mutablePropertySources.forEach(propertySource -> {
                for (int i = 0; i < Integer.MAX_VALUE; i++) {
                    String key = SPRING_AUTOCONFIGURE_EXCLUDE_KEY + "[" + i + "]";
                    Object value = propertySource.getProperty(key);
                    if (Objects.isNull(value)) {
                        break;
                    }
                    if (value instanceof String) {
                        String exclude = (String) value;
                        if (StringUtils.isNotBlank(exclude)) {
                            excludes.add(exclude);
                        }
                    }
                }
            });

            // 添加新增排除配置,并将PropertySources激活时机置于application.yml激活时机之前以生效设置
            excludes.add(EXCLUDE_AUTO_CONFIG);
            Properties properties = new Properties();
            for (int i = 0; i < excludes.size(); i++) {
                properties.setProperty(
                    SPRING_AUTOCONFIGURE_EXCLUDE_KEY + "[" + i + "]",
                    excludes.get(i));
            }
            PropertiesPropertySource propertiesPropertySource
                = new PropertiesPropertySource(EXCLUDE_AUTO_CONFIG_SOURCE_NAME, properties);
            environment.getPropertySources()
                .addFirst(propertiesPropertySource);
        }

    }

    @Override
    public int getOrder() {
        // 最后加载,确保application.yml配置读取完成
        return Ordered.LOWEST_PRECEDENCE;
    }

    /**
     * 在的环境加载末期判断当前应用是否使用了redis自动配置
     * @param environment
     * @return
     */
    private boolean hasRedisAutoConfigure(ConfigurableEnvironment environment) {
        boolean standalone = !Objects.isNull(environment.getProperty(REDIS_STANDALONE));
        boolean sentinel = !Objects.isNull(environment.getProperty(REDIS_SENTINEL));
        boolean cluster = !Objects.isNull(environment.getProperty(REDIS_CLUSTER));
        return standalone || sentinel || cluster;
    }
}