前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >为了控制Bean的加载我使出了这些杀手锏

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

作者头像
Bug开发工程师
发布2020-02-24 17:27:07
1.2K0
发布2020-02-24 17:27:07
举报
文章被收录于专栏:码农沉思录码农沉思录

以下文章来源于猿天地 ,作者尹吉欢

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

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

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

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

MQ 的配置代码如下:

代码语言:javascript
复制
@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。代码如下:

代码语言:javascript
复制
@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。

代码语言:javascript
复制
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。

代码语言:javascript
复制
@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。这个就相当灵活了,可以将需要满足的条件都写进来。

代码语言:javascript
复制
@ConditionalOnExpression("#{'test'.equals(environment['env']) || 'prod'.equals(environment['env'])}")
代码语言:javascript
复制


上面的表达式定义了 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,让使用者自己去引入依赖。

代码语言:javascript
复制
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
  <optional>true</optional>
</dependency>

问题二

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

下面是默认的配置:

代码语言:javascript
复制
@Bean
public FeignRequestInterceptor feignRequestInterceptor() {
  return new FeignRequestInterceptor();
}
@Bean
public RestTemplateRequestInterceptor restTemplateRequestInterceptor() {
  return new RestTemplateRequestInterceptor();
}

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

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

代码语言:javascript
复制
@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();
    }

}
代码语言:javascript
复制

故事三:自己去学习

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

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-02-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农沉思录 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 故事一:绝代有佳人,幽居在空谷
    • 方案一:@ConditionalOnProperty
      • 方案二:继承 SpringBootCondition 自定义条件
        • 方案三:继承 AnyNestedCondition 自定义条件
          • 方案四:@ConditionalOnExpression
          • 故事二:北方有佳人,绝世而独立
            • 问题一
              • 问题二
              • 故事三:自己去学习
              相关产品与服务
              容器服务
              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档