Spring:如何在配置文件中实现AND操作?

51

Spring的Profile注解允许您选择配置文件。然而,如果您阅读文档,它只允许您使用OR操作选择多个配置文件。如果您指定了@Profile("A", "B"),那么您的bean将在激活配置文件A或配置文件B时启动。

我们的用例不同,我们想要支持多个配置的TEST和PROD版本。因此,有时我们只想在TEST和CONFIG1配置文件都激活时才自动装配bean。

有没有办法在Spring中实现这个功能?最简单的方法是什么?


1
文档中提到了@Profile("a","b")and/or行为,这不是你要找的吗?文档中写道:如果一个@Component@Configuration类被标记为@Profile({"p1", "p2"}),那么只有在激活了'p1'和/或'p2'配置文件时,该类才会被注册/处理。 - Bond - Java Bond
@JavaBond 表示它是“或”操作符而不是“与”操作符。他们只是想明确指出这不是异或 (xor)。 - Artem
1
我为Spring Source打开了一张工单,以支持Profile注释的“AND”运算符:https://jira.spring.io/browse/SPR-12458 - Artem
好的,让我们看看Spring团队有什么话要说。 - Bond - Java Bond
2
他们接受了该票据,并显然计划在某个时候执行。 - Artem
7个回答

43

自Spring 5.1起(包括在Spring Boot 2.1中),可以在概要文件字符串注释内使用概要文件表达式。

因此,在Spring 5.1(Spring Boot 2.1)及以上版本中,只需:

<p>In <em>Spring 5.1 (Spring Boot 2.1) and above</em> it is as easy as:</strong>

@Component
@Profile("TEST & CONFIG1")
public class MyComponent {}

Spring 4.x 和 5.0.x:

  • 方法1: 由@Mithun回答,它完美地覆盖了你在注释Spring Bean时将OR转换为AND的情况,同时还使用了他的Condition类实现。但我想提供另一种方法,没有人提出过,它有其优点和缺点。

  • 方法2: 只需使用@Conditional并创建尽可能多的Condition实现,以满足所需的所有组合。 它的缺点是必须创建与组合相同数量的实现,但如果您没有太多组合,则我认为这是一个更简洁的解决方案,并且提供了更多的灵活性和实现更复杂逻辑解决方案的机会。

方法2的实现如下:

你的 Spring Bean:

@Component
@Conditional(value = { TestAndConfig1Profiles.class })
public class MyComponent {}

TestAndConfig1Profiles 的实现:

public class TestAndConfig1Profiles implements Condition {
    @Override
    public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().acceptsProfiles("TEST")
                    && context.getEnvironment().acceptsProfiles("CONFIG1");
    }
}

采用这种方法,您可以轻松处理更复杂的逻辑情况,例如:

(TEST & CONFIG1) | (TEST & CONFIG3)

我只是想更新一下您的问题并补充其他答案。


在Spring 5.1中,似乎表达式@Profile("TEST & CONFIG1")在bean(方法)级别上尚未生效。 - agodinhost
@agodinhost 这必须在类级别上。 - f-CJ
是的,我在方法级别使用了@ConditionalOnExpression注解,表达式为"#{environment.acceptsProfiles('spring') && environment.acceptsProfiles('oracle')}",它百分之百地起作用了。如果在方法级别上也有这个Profile Expression,那将非常好。在我看来,在方法级别上与其他级别不同没有意义。 - agodinhost
1
仅有微小的更新,现在过期了,请使用acceptsProfiles(Profiles.of("TEST"))作为String []的重载。 - David Bradley

27

由于Spring框架默认没有提供“AND”特性,我建议采用以下策略:

目前@Profile注解有一个条件注解@Conditional(ProfileCondition.class)。在ProfileCondition.class中,它会遍历所有的profile并检查是否激活。同样地,你可以创建自己的条件实现并限制注册bean。例如:

public class MyProfileCondition implements Condition {

    @Override
    public boolean matches(final ConditionContext context,
            final AnnotatedTypeMetadata metadata) {
        if (context.getEnvironment() != null) {
            final MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
            if (attrs != null) {
                for (final Object value : attrs.get("value")) {
                    final String activeProfiles = context.getEnvironment().getProperty("spring.profiles.active");

                    for (final String profile : (String[]) value) {
                        if (!activeProfiles.contains(profile)) {
                            return false;
                        }
                    }
                }
                return true;
            }
        }
        return true;
    }

}

在你的班级中:

@Component
@Profile("dev")
@Conditional(value = { MyProfileCondition.class })
public class DevDatasourceConfig

注意:我没有检查所有的边缘情况(如null、长度检查等)。但是,这个方向可能会有所帮助。


如何使用XML配置实现相同的功能? - ravshansbox
1
谢谢!我在我的回答中稍微改进了一下您的代码。 - Vova Rozhkov

9

这是@Mithun答案的稍微改进版本:

public class AndProfilesCondition implements Condition {

public static final String VALUE = "value";
public static final String DEFAULT_PROFILE = "default";

@Override
public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
    if (context.getEnvironment() == null) {
        return true;
    }
    MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
    if (attrs == null) {
        return true;
    }
    String[] activeProfiles = context.getEnvironment().getActiveProfiles();
    String[] definedProfiles = (String[]) attrs.getFirst(VALUE);
    Set<String> allowedProfiles = new HashSet<>(1);
    Set<String> restrictedProfiles = new HashSet<>(1);
    for (String nextDefinedProfile : definedProfiles) {
        if (!nextDefinedProfile.isEmpty() && nextDefinedProfile.charAt(0) == '!') {
            restrictedProfiles.add(nextDefinedProfile.substring(1, nextDefinedProfile.length()));
            continue;
        }
        allowedProfiles.add(nextDefinedProfile);
    }
    int activeAllowedCount = 0;
    for (String nextActiveProfile : activeProfiles) {
        // quick exit when default profile is active and allowed profiles is empty
        if (DEFAULT_PROFILE.equals(nextActiveProfile) && allowedProfiles.isEmpty()) {
            continue;
        }
        // quick exit when one of active profiles is restricted
        if (restrictedProfiles.contains(nextActiveProfile)) {
            return false;
        }
        // just go ahead when there is no allowed profiles (just need to check that there is no active restricted profiles)
        if (allowedProfiles.isEmpty()) {
            continue;
        }
        if (allowedProfiles.contains(nextActiveProfile)) {
            activeAllowedCount++;
        }
    }
    return activeAllowedCount == allowedProfiles.size();
}

}

无法在评论中发布。


7

另一种技巧是在@Configuration中添加@Profile注解,同时在@Bean上添加另一个@Profile注解 - 这将在基于Java的Spring配置中创建2个配置文件之间的逻辑AND运算,适用于许多场景。

@Configuration
@Profile("Profile1")
public class TomcatLogbackAccessConfiguration {

   @Bean
   @Profile("Profile2")
   public EmbeddedServletContainerCustomizer containerCustomizer() {

我觉得这个比上面的更简洁。 - agabor

7
另一个选择是通过@Profile注解允许在类/方法级别上进行游戏。如果适用于您的情况,这种方法不如实现MyProfileCondition灵活,但是快速而且简洁。

例如,当FAST和DEV都处于活动状态时,此选项不会启动,但仅当DEV处于活动状态时,它才会启动:
@Configuration
@Profile("!" + SPRING_PROFILE_FAST)
public class TomcatLogbackAccessConfiguration {

    @Bean
    @Profile({SPRING_PROFILE_DEVELOPMENT, SPRING_PROFILE_STAGING})
    public EmbeddedServletContainerCustomizer containerCustomizer() {

6
如果您已经使用@Profile注解标记了配置类或bean方法,那么使用Environment.acceptsProfiles()可以轻松检查其他的配置文件(例如AND条件)。
@Autowired Environment env;

@Profile("profile1")
@Bean
public MyBean myBean() {
    if( env.acceptsProfiles("profile2") ) {
        return new MyBean();
    }
    else {
        return null;
    }
}

6

我改进了@rozhoc的答案,因为该答案没有考虑到当使用@Profile时,没有配置文件等同于“default”。此外,我想要的条件是!default&& !a,而@rozhoc的代码没有正确处理。最后,出于简洁起见,我使用了一些Java8,并仅显示matches方法。

@Override
public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
    if (context.getEnvironment() == null) {
        return true;
    }
    MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
    if (attrs == null) {
        return true;
    }

    Set<String> activeProfilesSet = Arrays.stream(context.getEnvironment().getActiveProfiles()).collect(Collectors.toSet());
    String[] definedProfiles = (String[]) attrs.getFirst(VALUE);
    Set<String> allowedProfiles = new HashSet<>(1);
    Set<String> restrictedProfiles = new HashSet<>(1);
    if (activeProfilesSet.size() == 0) {
        activeProfilesSet.add(DEFAULT_PROFILE);  // no profile is equivalent in @Profile terms to "default"
    }
    for (String nextDefinedProfile : definedProfiles) {
        if (!nextDefinedProfile.isEmpty() && nextDefinedProfile.charAt(0) == '!') {
            restrictedProfiles.add(nextDefinedProfile.substring(1, nextDefinedProfile.length()));
            continue;
        }
        allowedProfiles.add(nextDefinedProfile);
    }
    boolean allowed = true;
    for (String allowedProfile : allowedProfiles) {
        allowed = allowed && activeProfilesSet.contains(allowedProfile);
    }
    boolean restricted = true;
    for (String restrictedProfile : restrictedProfiles) {
        restricted = restricted && !activeProfilesSet.contains(restrictedProfile);
    }
    return allowed && restricted;
}

以下是如何实际使用它,以防您感到困惑:

@Profile({"!default", "!a"})
@Conditional(value={AndProfilesCondition.class})

rozhok, not rozhoc pls. - Vova Rozhkov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接