最新要闻

广告

手机

光庭信息跌4.57% 2021上市超募11亿2022扣非降74% 时快讯

光庭信息跌4.57% 2021上市超募11亿2022扣非降74% 时快讯

搜狐汽车全球快讯 | 大众汽车最新专利曝光:仪表支持拆卸 可用手机、平板替代-环球关注

搜狐汽车全球快讯 | 大众汽车最新专利曝光:仪表支持拆卸 可用手机、平板替代-环球关注

家电

Jar包开发之【有之则用,无之则禁】|世界新要闻

来源:博客园

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

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

发布订阅

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


(资料图片仅供参考)

  • 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 standaloneConfigurationProvider, ObjectProvider sentinelConfigurationProvider, ObjectProvider clusterConfigurationProvider) {        super(properties, standaloneConfigurationProvider, sentinelConfigurationProvider, clusterConfigurationProvider);    }    @Bean(        destroyMethod = "shutdown"    )    @ConditionalOnMissingBean({ClientResources.class})    DefaultClientResources lettuceClientResources(ObjectProvider customizers) {        Builder builder = DefaultClientResources.builder();        customizers.orderedStream().forEach((customizer) -> {            customizer.customize(builder);        });        return builder.build();    }    @Bean    @ConditionalOnMissingBean({RedisConnectionFactory.class})    LettuceConnectionFactory redisConnectionFactory(ObjectProvider 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 standaloneConfigurationProvider, ObjectProvider sentinelConfiguration, ObjectProvider clusterConfiguration) {        super(properties, standaloneConfigurationProvider, sentinelConfiguration, clusterConfiguration);    }    @Bean    JedisConnectionFactory redisConnectionFactory(ObjectProvider 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

@FunctionalInterfacepublic 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 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;    }}

关键词: