美文网首页
为了控制Bean的加载我使出了这些杀手锏

为了控制Bean的加载我使出了这些杀手锏

作者: 猿天地 | 来源:发表于2020-02-26 10:00 被阅读0次

    故事一: 绝代有佳人,幽居在空谷

    美女同学小张,在工作中遇到了烦心事。心情那是破凉破凉的,无法言喻。

    故事背景是最近由于需求变动,小张在项目中加入了MQ的集成,刚开始还没什么问题,后面慢慢问题的显露出来了。

    自己在本地Debug的时候总是能消费到消息,由于历史原因,公司的项目只区分了两套环境,也就是测试和线上。本地启动默认就是测试环境,所以会消费测试环境的消息。

    MQ的配置代码如下:

    @Configuration
    public class MqConfig {
        @Bean(initMethod = "start", destroyMethod = "shutdown")
        public ConsumerBean consumerBean() {
            // ....
        }
    }
    

    想要解决小张的问题,那么就必须得有第三个环境的区分,也就是增加一个本地开发环境,然后通过环境来决定是否需要初始化MQ。

    这个时候就可以用到Spring Boot为我们提供的Conditional家族的注解了,@Conditional注解会根据具体的条件决定是否创建 bean 到容器中, 如下图:

    通过@ConditionalOnProperty来决定MqConfig是否要加载,@ConditionalOnProperty的name就是配置项的名称,havingValue就是匹配的值,也就是在application配置中存在env=dev才会初始化MqConfig。代码如下:

    @Configuration
    @ConditionalOnProperty(name = "env", havingValue = "dev")
    public class MqConfig {
        @Bean(initMethod = "start", destroyMethod = "shutdown")
        public ConsumerBean consumerBean() {
            // ....
        }
    }
    

    但这好像不符合小张同学的需求呀,需求是dev环境不加载才对。还有一个就是历史原因,增加一个环境有风险,因为对应的环境加载的内容什么的,都需要有变动,所以还是保留历史情况,环境不变,看能不能从其他的点解决这个问题。

    现在面临的问题是不能增加新的环境,保留之前的test和prod。只需要在test和prod初始化Mq。

    方案一:@ConditionalOnProperty

    还是坚持使用@ConditionalOnProperty,既然不能通过环境来,我们可以单独增加一个属性来决定是否要启用Mq, 比如定义为:mq.enabled=true表示开启,mq.enabled=false表示不开启。

    然后在test和prod启动的时候增加-Dmq.enabled=true或者在对应的配置文件中增加也可以,本地开发的时候-Dmq.enabled=false就可以了。

    虽然能够解决问题,但是不是最佳的方案,因为已有的环境和开发人员本地都得增加启动参数。

    方案二:继承SpringBootCondition自定义条件

    可以使用@Conditional(MqConditional.class)注解,自定义一个条件类,在类中去判断是否要加载bean。

    public class MqConditional extends SpringBootCondition {
        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
            Environment environment = context.getEnvironment();
            String env = environment.getProperty("env");
            if (StringUtils.isBlank(env)) {
                return ConditionOutcome.noMatch("no match");
            }
            if (env.equals("test") || env.equals("prod")) {
                return ConditionOutcome.match();
            }
            return ConditionOutcome.noMatch("no match");
        }
    }
    

    方案三:继承AnyNestedCondition自定义条件

    可以使用@Conditional(MqAvailableCondition.class)注解,自定义一个条件类,在类中可以使用其他的Conditional注解来进行判断,比如使用@ConditionalOnProperty。

    @Order(Ordered.LOWEST_PRECEDENCE)
    public class MqAvailableCondition extends AnyNestedCondition {
        public MqAvailableCondition() {
            super(ConfigurationPhase.REGISTER_BEAN);
        }
        @ConditionalOnProperty(name = "env", havingValue = "test")
        static class EnvTest {
        }
        @ConditionalOnProperty(name = "env", havingValue = "prod")
        static class EnvProd {
        }
    }
    

    方案四:@ConditionalOnExpression

    支持SpEL进行判断,如果满足SpEL表达式条件则加载这个bean。这个就相当灵活了,可以将需要满足的条件都写进来。

    @ConditionalOnExpression("#{'test'.equals(environment['env']) || 'prod'.equals(environment['env'])}")
    

    上面的表达式定义了Spring Environment中只要有env为test或者prod的时候就会初始化MqConfig。这样一来老的启动命令都不用改变,本地开发的时候也不用增加参数,可以说是最佳的方案,因为改动的点变少了,出错的几率小,使用难度低。

    故事二: 北方有佳人,绝世而独立

    美女小杨同学最近也遇到了烦心事,虽然是女生,但是也工作了几年了。最近受到领导重用,让她搭一套Spring Cloud的框架给同事们分享一下。

    她有个想法是将某些信息可以通过Feign或者RestTemplate进行传递,天然友好的方式就是在拦截器中统一实现。

    如果在每个服务中都写一份一样的代码,就显得很低级了,所以她将这两个拦截器统一写在一个模块中,作为Spring Boot Starter的方式引入。

    问题一

    遇到的第一个问题是这个模块引入了Feign和spring-web两个依赖,想做的通用一点,就是使用者可能会用Feign来调用接口,也可能会用RestTemplate来调用接口,如果使用者不用Feign, 但是引入了这个Starter也会依赖Feign。

    所以需要在依赖的时候设置Feign的Maven依赖optional=true,让使用者自己去引入依赖。

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
        <optional>true</optional>
    </dependency>
    

    问题二

    第二个问题是拦截器的初始化,如果不做任何处理的话两个拦截器都会被初始化,如果使用者没有依赖Feign,那么就会报错,所以我们需要对拦截器的初始化进行处理。

    下面是默认的配置:

    @Bean
    public FeignRequestInterceptor feignRequestInterceptor() {
        return new FeignRequestInterceptor();
    }
    @Bean
    public RestTemplateRequestInterceptor restTemplateRequestInterceptor() {
        return new RestTemplateRequestInterceptor();
    }
    

    两个拦截器都是实现框架自带的接口,所以我们可以在最外层使用@ConditionalOnClass来判断如果项目中存在这个Class再装置配置。

    第二层可以通过@ConditionalOnProperty来决定是否要启用,将控制权交给使用者。

    @Configuration
    @ConditionalOnClass(name = "feign.RequestInterceptor")
    protected static class FeignRequestInterceptorConfiguration {
            @Bean
            @ConditionalOnProperty("feign.requestInterceptor.enabled")
            public FeignRequestInterceptor feignRequestInterceptor() {
                return new FeignRequestInterceptor();
            }
    }
    @Configuration
    @ConditionalOnClass(name = "org.springframework.http.client.ClientHttpRequestInterceptor")
    protected static class RestTemplateRequestInterceptorConfiguration {
            
            @Bean
            @ConditionalOnProperty("restTemplate.requestInterceptor.enabled")
            public RestTemplateRequestInterceptor restTemplateRequestInterceptor() {
                return new RestTemplateRequestInterceptor();
            }
            
    }
    

    故事三:自己去学习

    文章里只根据案例讲了一个使用的方式,当然还有很多没有讲的,大家可以自己去尝试了解一些作用以及在什么场景可以使用,像@ConditionalOnBean,@ConditionalOnMissingBean等注解。

    另一种学习的方式就是鼓励大家去看一些框架的源码,特别在Spring Cloud这些框架中大量的自动配置,都有用到这些注解,我贴几个图给大家看看。

    相关文章

      网友评论

          本文标题:为了控制Bean的加载我使出了这些杀手锏

          本文链接:https://www.haomeiwen.com/subject/wfgwfhtx.html