@ConditionalOnProperty

@ConditionalOnProperty源码解析

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {
    // 数组,获取对应property名称的值,与name不可同时使用
    String[] value() default {};

    // 配置属性名称的前缀,比如spring.http.encoding
    String prefix() default "";

    // 数组,配置属性完整名称或部分名称
    // 可与prefix组合使用,组成完整的配置属性名称,与value不可同时使用
    String[] name() default {};

    // 可与name组合使用,比较获取到的属性值与havingValue给定的值是否相同,相同才加载配置
    String havingValue() default "";

    // 缺少该配置属性时是否可以加载。如果为true,没有该配置属性时也会正常加载;反之则不会生效
    boolean matchIfMissing() default false;
}

最近在写一个“启动时导入数据”的小功能。实现很简单, CommandLineRunner会在SpringBoot启动时运行,第一版长这样:

@Order(1)
@Component
public class DictionaryInitializer implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
          //do import dictionary
    }
}

自然的,我们希望这个可配置化,只在需要的时候运行。我们使用

@ConditionalOnProperty
@Order(1)
@Component
@ConditionalOnProperty(name="app.initialize.dictionary", havingValue="true")
public class DictionaryInitializer implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
          //do import dictionary
    }
}

然后在application.properties中加入

app.initialize.dictionary=true

随着功能的迭代,我们又更多类似的导入功能,例如导入Product和Parameter。依样画葫芦,application.properties中多了相似的配置

app.initialize.dictionary=true
app.initialize.product=true
app.initialize.parameter=true

进一步优化,随着导入功能的增多。我们可能需要一个一键导入的功能,同时也要保留原来的功能。

app.initialize.all=true

这个有点麻烦,涉及到多个条件的组合。ConditionalOnProperty是支持 “多个条件逻辑与”的

@ConditionalOnProperty(name={"app.initialize.dictionary","app.initialize.all"}, havingValue="true")

当然这个不符合我们的要求,我们需要“逻辑或”
首先一个类是不能标注多个相同annotation的,编译通不过。

@Order(1)
@Component
✖️@ConditionalOnProperty(name="app.initialize.dictionary", havingValue="true")
✖️@ConditionalOnProperty(name="app.initialize.all", havingValue="true")
public class DictionaryInitializer implements CommandLineRunner

@ConditionalOnProperty 本身也并没有这样的功能
一种繁琐的做法,是自定义条件,继承AnyNestedCondition

class DicOrAllCondition extends AnyNestedCondition {

    public DicOrAllCondition() {
        super(ConfigurationPhase.PARSE_CONFIGURATION);
    }

    @ConditionalOnProperty(name = "app.initialize.dictionary", value = "true")
    static class DicCondition {
    }

    @ConditionalOnProperty(name = "app.initialize.all", value = "true")
    static class AllCondition {
    }
}


@Order(1)
@Component
@Conditional(DicOrAllCondition.class)
public class DictionaryInitializer implements CommandLineRunner{}

还有一种比较灵活的方式是使用@ConditionalOnExpression写一个表达式

@Order(1)
@Component
@ConditionalOnExpression("${app.initialize.dictionary:false} || ${app.initialize.all:false}")
public class DictionaryInitializer implements CommandLineRunner{}

精益无止境,其实还有更灵活的配置方式:除了dictionary其他都导入:

app.initialize.all=true
app.initialize.dictionary=false

换种说法就是:如果局部有配置,则按局部配置处理。如果局部没配置,则按全局配置处理。如果全局、局部都没配置,则默认不导入。这里可以利用“默认值”嵌套表达式实现。

@ConditionalOnExpression("${app.initialize.dictionary:${app.initialize.all:false}}")