[spring-projects/spring-boot]@ConfigurationProperties 创建可变集合,即使在不可变类上也是如此

2024-06-26 512 views
6

考虑简单的例子:

@ConfigurationProperties("some.prefix")
@ConstructorBinding
class Props {
  final List<String> strings;
  public Props(List<String> strings) {
    this.strings = strings;
  }
}

尽管我试图在这里创建不可变的类,但 Spring 不会毫无帮助地注入可变的类ArrayList

现在,在一个非常简单的例子中,你当然可以自己包装它,Collections.unmodifiableList()但是这样做很快就会变得乏味,特别是当你嵌套集合类型时。

想象一下深度冻结的代码,例如Map<String, List<String>>:(

当您只想使用lombok.Value或者也许 Kotlin(没有检查)来完全避免编写样板时,情况会更糟。

不确定为什么这些属性类应该是可变的,但如果需要,可以添加属性来@ConfigurationProperties控制它。使用时应该自动启用它,@ConstructorBinding因为这是一个明确的信号,有人正在尝试创建不可变的属性。

回答

8

事实证明,使用鲜为人知的ConfigurationPropertiesBindHandlerAdvisor

@Configuration
public class ImmutableConfigurationSupport {

    @Bean
    ConfigurationPropertiesBindHandlerAdvisor configurationPropertiesBindHandlerAdvisor() {
        return bindHandler -> new AbstractBindHandler(bindHandler) {
            int immutable = 0;

            private boolean isImmutableTarget(Bindable<?> target) {
                var klass = target.getType().resolve();
                return klass != null && klass.isAnnotationPresent(ConstructorBinding.class);
            }

            @Override
            public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
                if (isImmutableTarget(target))
                    immutable++;
                return super.onStart(name, target, context);
            }

            @Override
            public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
                var object = super.onSuccess(name, target, context, result);
                var targetClass = target.getType().resolve();
                if (immutable > 0 && targetClass != null) {
                    if (object instanceof List && targetClass.isAssignableFrom(List.class))
                        return Collections.unmodifiableList((List<?>) object);
                    if (object instanceof Set && targetClass.isAssignableFrom(Set.class))
                        return Collections.unmodifiableSet((Set<?>) object);
                    if (object instanceof Map && targetClass.isAssignableFrom(Map.class))
                        return Collections.unmodifiableMap((Map<?, ?>) object);
                }
                return object;
            }

            @Override
            public void onFinish(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) throws Exception {
                super.onFinish(name, target, context, result);
                if (isImmutableTarget(target))
                    immutable--;
            }
        };
    }

}

这是项目会考虑添加的东西吗?或者我应该创建一个库?

6

我很高兴ConfigurationPropertiesBindHandlerAdvisor可以运行,但我认为我们应该考虑直接在 中做一些事情CollectionBinder。我非常喜欢在任何地方使用不可修改集合的想法,但我有点担心向后兼容性。

在做出任何最终决定之前,我们需要作为一个团队对此进行一些讨论。

8

我们今天讨论了这个问题,并决定不可修改是一个很好的默认设置,但我们需要为那些需要可变绑定的人提供一个出口。

0

这难道不是ConfigurationPropertiesBindHandlerAdvisor逃生通道吗?:)

3

虽然它可以实现相同的最终结果,但它太低级和冗长,不建议作为依赖它的用户恢复当前行为的方式。

2

我对这个问题很感兴趣。我已经使用基于 Spring Mvc 和 Spring Boot 的企业应用程序五年了,时不时地我会窥视一下框架代码。

到目前为止,我已经尝试克隆代码、设置环境、ide。现在我正在尝试构建它。作为初学者,我可能需要一些反馈和熟悉该过程才能开始。

8

谢谢,@rakibmail22。Wiki 中有一些关于如何使用代码的文档。

对于这个特定问题,我认为我们需要对和进行更改,CollectionBinder以便MapBinder它们创建的集合和映射默认是不可变的。还需要对相应的测试(CollectionBinderTests和)进行一些更新,以检查创建的集合和映射现在是否是不可变的。MapBinderTests

我们还需要提供一种方式让某些人选择退出此行为,以便事情按照目前的方式运行。我目前还不确定这应该是什么样子。也许是 上的一个属性@ConfigurationProperties

5

@wilkinsona 我查看了CollectionBinder初始 Collection 的创建位置以及值的绑定位置。但在返回过程中,几乎没有进行转换。方法bind返回AggregateBinder一个Object,并Object在后续步骤中借助转换器将其转换为所需列表。在此转换过程中,如果我Collection直接在内部创建不可变对象,则会失败CollectionBinder。不过,我仍在尝试更好地理解整个绑定过程。

2

感谢您的关注。如果Collection绑定器创建的是所需类型,则无需进行任何转换。您如何使Collection不可修改?我认为Collections.unmodifiableCollection在这种情况下不够,因为结果是Collection而不是更具体的东西,例如List。相反,我认为有必要做这样的事情:

@SuppressWarnings({ "unchecked", "rawtypes" })
private Collection<Object> unmodifiable(Collection<Object> collection) {
    if (collection instanceof SortedSet) {
        return Collections.unmodifiableSortedSet((SortedSet) collection);
    }
    if (collection instanceof Set) {
        return Collections.unmodifiableSet((Set) collection);
    }
    if (collection instanceof List) {
        return Collections.unmodifiableList((List) collection);
    }
    return Collections.unmodifiableCollection(collection);
}
6

我正在尝试类似下面的事情。

@Override
protected Collection<Object> merge(Supplier<Collection<Object>> existing, Collection<Object> additional) {
    Collection<Object> existingCollection = getExistingIfPossible(existing);
    if (existingCollection == null) {
        return unmodifiable(additional);
    }
    try {
        existingCollection.clear();
        existingCollection.addAll(additional);
        return unmodifiable(copyIfPossible(existingCollection));
    }
    catch (UnsupportedOperationException ex) {
        return unmodifiable(createNewCollection(additional));
    }
}

private Collection<Object> unmodifiable(Collection<Object> collection) {
    if (collection instanceof SortedSet) {
        SortedSet<Object> result = (SortedSet<Object>) collection;
        return Collections.unmodifiableSortedSet(result);
    }

    if (collection instanceof Set) {
        Set<Object> result = (Set<Object>) collection;
        return Collections.unmodifiableSet(result);
    }

    if (collection instanceof List) {
        List<Object> result = (List<Object>) collection;
        return Collections.unmodifiableList(result);
    }

    throw new IllegalArgumentException("Unsupported Collection interface: ");
}

两个测试用例失败CollectionBinderTests

  1. bindToCollectionWhenHasExistingCollectionShouldReplaceAllContents()
  2. bindToCollectionWithNoDefaultConstructor()

第一个失败了,因为它正在检查LinkedList 第二个失败,并出现以下潜在错误 Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.util.Collections$UnmodifiableRandomAccessList<?>] to type [org.springframework.boot.context.properties.bind.CollectionBinderTests$MyCustomNoDefaultConstructorList] for value '[a, b, c, c]'; nested exception is java.lang.IllegalArgumentException: Could not instantiate Collection type: org.springframework.boot.context.properties.bind.CollectionBinderTests$MyCustomNoDefaultConstructorList at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)

7

Collection我目前不确定为什么merge会创建一个新的additionaladditional应该是调用的结果bindAggregate,所以我认为它可以按原样使用。换句话说,只要bindAggregate返回一个不可修改的集合,我认为catch中的块merge就可以返回additional。如果需要additional将变成一个新的集合,@mbhave和@philwebb可能会回忆起细节。

在某些情况下,我认为我们不应该绑定不可修改的集合(或 Map)。属性就是LinkedList这种情况,因为我认为我们不应该费尽心思创建LinkedList不可修改的自定义子类。

为了获得不可变的集合或映射,我认为应该将属性声明为SetSortedSetListMap等。属性的实际类型用于影响任何新集合或映射的创建。例如,如果属性Set实际上是 ,TreeSet那么TreeSet在使其成为不可修改的 之前,应该先创建并填充Set

绑定是 Boot 代码库中最复杂的区域之一,因此如果它开始占用您比想象中的更多的时间,请随时离开。

3

恐怕我记不清细节了。看起来#9290 已经做了更改。我认为我们可以additional从该 catch 块返回。

9

是的,我认为在 catch 块中创建新集合是没有必要的。这似乎是一个疏忽。

5

这应该是一个单独的问题吗?或者这个变化应该是当前问题的一部分?

9

@rakibmail22 将其作为此问题的一部分进行更改是可以的。如果我们认为值得,我们可以随时将相同的更改单独应用于其他分支。