【问题标题】:@ConditionalOnProperty for lists or arrays?@ConditionalOnProperty 用于列表或数组?
【发布时间】:2017-06-14 10:30:22
【问题描述】:

我正在使用 Spring Boot 1.4.3 @AutoConfiguration 根据用户指定的属性自动创建 bean。用户可以指定一组服务,其中 nameversion 是必填字段:

service[0].name=myServiceA
service[0].version=1.0

service[1].name=myServiceB
service[1].version=1.2

...

如果用户忘记在一项服务上指定必填字段,我想回退而不创建任何 bean。我可以用@ConditionalOnProperty 完成这个吗?我想要类似的东西:

@Configuration
@ConditionalOnProperty({"service[i].name", "service[i].version"})
class AutoConfigureServices {
....
} 

【问题讨论】:

  • 我不认为这本身是有效的——但是如果你在构造函数中设置这些值,你可能会在那里抛出一个异常(在构造函数,或者如果 Spring 检查存在则隐含)。
  • 这是个好主意。如果在实例化任何bean之前在自动配置类上运行构造函数,也许我可以以某种方式阻止创建bean?不幸的是,我不能抛出异常,因为忘记道具不应该是致命的。
  • 为什么不呢?你有一个半配置的 bean。
  • 是的,如果 bean 的一半在构造时配置,那么构造函数将无法工作。 @Autoconfiguration@Conditionals 阻止创建 bean。这个想法是用户依赖于一个带有自动配置的 jar,但它不应该通过抛出致命错误来阻止他们的应用程序加载。

标签: java spring spring-boot properties


【解决方案1】:

您可以利用org.springframework.boot.autoconfigure.condition.OnPropertyListCondition 类。例如,假设您要检查 service 属性是否具有至少一个值:

class MyListCondition extends OnPropertyListCondition {
    MyListCondition() {
        super("service", () -> ConditionMessage.forCondition("service"));
    }
}

@Configuration
@Condition(MyListCondition.class)
class AutoConfigureServices {

}

请参阅 org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration#wsdlDefinitionBeanFactoryPostProcessor 上使用的 org.springframework.boot.autoconfigure.webservices.OnWsdlLocationsCondition 以了解 Spring 本身的示例。

【讨论】:

    【解决方案2】:

    这是我对在 Spring 自动配置中使用自定义条件的问题的看法。有点类似于 @Strumbels 提出的建议,但更可重用。

    @Conditional 注释在应用程序启动的早期执行。属性源已加载,但尚未创建 ConfgurationProperties bean。但是,我们可以通过自己将属性绑定到 Java POJO 来解决这个问题。

    首先我介绍一个函数式接口,它使我们能够定义任何自定义逻辑来检查属性是否确实存在。在您的情况下,此方法将负责检查属性 List 是否为空/null,以及其中的所有项目是否有效。

    public interface OptionalProperties {
      boolean isPresent();
    }
    

    现在让我们创建一个注释,它将使用 Spring @Conditional 进行元注释,并允许我们定义自定义参数。 prefix 代表属性命名空间,targetClass 代表属性应该映射到的配置属性模型类。

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(OnConfigurationPropertiesCondition.class)
    public @interface ConditionalOnConfigurationProperties {
    
      String prefix();
    
      Class<? extends OptionalProperties> targetClass();
    
    }
    

    现在是主要部分。自定义条件实现。

    public class OnConfigurationPropertiesCondition extends SpringBootCondition {
    
      @Override
      public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MergedAnnotation<ConditionalOnConfigurationProperties> mergedAnnotation = metadata.getAnnotations().get(ConditionalOnConfigurationProperties.class);
        String prefix = mergedAnnotation.getString("prefix");
        Class<?> targetClass = mergedAnnotation.getClass("targetClass");
        // type precondition
        if (!OptionalProperties.class.isAssignableFrom(targetClass)) {
          return ConditionOutcome.noMatch("Target type does not implement the OptionalProperties interface.");
        }
        // the crux of this solution, binding properties to Java POJO
        Object bean = Binder.get(context.getEnvironment()).bind(prefix, targetClass).orElse(null);
        // if properties are not present at all return no match
        if (bean == null) {
          return ConditionOutcome.noMatch("Binding properties to target type resulted in null value.");
        }
        OptionalProperties props = (OptionalProperties) bean;
    
        // execute method from OptionalProperties interface 
        // to check if condition should be matched or not
        // can include any custom logic using property values in a type safe manner
        if (props.isPresent()) {
          return ConditionOutcome.match();
        } else {
          return ConditionOutcome.noMatch("Properties are not present.");
        }
      }
    
    }
    

    现在您应该创建自己的配置属性类来实现OptionalProperties 接口。

    @ConfigurationProperties("your.property.prefix")
    @ConstructorBinding
    public class YourConfigurationProperties implements OptionalProperties {
    
      // Service is your POJO representing the name and version subproperties
      private final List<Service> services;
    
      @Override
      public boolean isPresent() {
        return services != null && services.stream().all(Service::isValid);
      }
    
    }
    

    然后在 Spring @Configuration 上课。

    @Configuration
    @ConditionalOnConfigurationProperties(prefix = "", targetClass = YourConfigurationProperties.class)
    class AutoConfigureServices {
    ....
    } 
    

    此解决方案有两个缺点:

    • 必须在两个位置指定属性前缀:@ConfigurationProperties 注释和@ConditionalOnConfigurationProperties 注释。这可以通过在配置属性 POJO 中定义 public static final String PREFIX = "namespace" 来部分缓解。
    • 属性绑定过程在每次使用我们的自定义条件注释时单独执行,然后再次创建配置属性 bean 本身。它仅在应用启动期间发生,因此应该不是问题,但仍然是一种低效率。

    【讨论】:

      【解决方案3】:

      老问题,但我希望我的回答对 Spring2.x 有所帮助: 感谢@Brian,我检查了迁移指南,我受到了示例代码的启发。这段代码对我有用:

      final List<String> services = Binder.get(context.getEnvironment()).bind("my.services", List.class).orElse(null);
      

      我确实尝试获取 POJO 列表(作为 AutoConfigureService),但我的课程与 AutoConfigureServices 不同。为此,我使用了:

      final Services services = Binder.get(context.getEnvironment()).bind("my.services", Services.class).orElse(null);
      

      好吧,继续玩吧:-D

      【讨论】:

        【解决方案4】:

        这是我创建的自定义Condition。它需要一些修饰才能更通用(即不是硬编码字符串),但对我来说效果很好。

        为了使用,我用@Conditional(RequiredRepeatablePropertiesCondition.class)注释了我的配置类

        public class RequiredRepeatablePropertiesCondition extends SpringBootCondition {
        
            private static final Logger LOGGER = LoggerFactory.getLogger(RequiredRepeatablePropertiesCondition.class.getName());
        
            public static final String[] REQUIRED_KEYS = {
                    "my.services[i].version",
                    "my.services[i].name"
            };
        
            @Override
            public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
                List<String> missingProperties = new ArrayList<>();
                RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(context.getEnvironment());
                Map<String, Object> services = resolver.getSubProperties("my.services");
                if (services.size() == 0) {
                    missingProperties.addAll(Arrays.asList(REQUIRED_KEYS));
                    return getConditionOutcome(missingProperties);
                }
                //gather indexes to check: [0], [1], [3], etc
                Pattern p = Pattern.compile("\\[(\\d+)\\]");
                Set<String> uniqueIndexes = new HashSet<String>();
                for (String key : services.keySet()) {
                    Matcher m = p.matcher(key);
                    if (m.find()) {
                        uniqueIndexes.add(m.group(1));
                    }
                }
                //loop each index and check required props
                uniqueIndexes.forEach(index -> {
                    for (String genericKey : REQUIRED_KEYS) {
                        String multiServiceKey = genericKey.replace("[i]", "[" + index + "]");
                        if (!resolver.containsProperty(multiServiceKey)) {
                            missingProperties.add(multiServiceKey);
                        }
                    }
                });
                return getConditionOutcome(missingProperties);
            }
        
            private ConditionOutcome getConditionOutcome(List<String> missingProperties) {
                if (missingProperties.isEmpty()) {
                    return ConditionOutcome.match(ConditionMessage.forCondition(RequiredRepeatablePropertiesCondition.class.getCanonicalName())
                            .found("property", "properties")
                            .items(Arrays.asList(REQUIRED_KEYS)));
                }
                return ConditionOutcome.noMatch(
                        ConditionMessage.forCondition(RequiredRepeatablePropertiesCondition.class.getCanonicalName())
                    .didNotFind("property", "properties")
                    .items(missingProperties)
                );
            }
        }
        

        【讨论】:

        • 请注意,此代码仅适用于 Spring Boot 1.x,Spring Boot 2 用不同的 API 替换了此代码,请参阅他们的 migration guideRelaxedPropertyResolver 特别是不再存在,也没有直接替代 RelaxedPropertyResolver.getSubProperties(...)PropertySourceUtils.getSubProperties(...)
        • @Brice 是正确的,感谢您提供迁移指南的链接。我尝试了他们示例中的代码,这对我有用:```Binder.get(context.getEnvironment()).bind("my.services", List.class).orElse(null); ``` 返回 List。我没有尝试解析成 POJO(作为 AutoConfigureService),但有助于简单的字符串
        猜你喜欢
        • 2023-03-23
        • 1970-01-01
        • 2018-07-20
        • 2021-09-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-08-09
        相关资源
        最近更新 更多