我希望能够在我的Spring Boot@ConfigurationProperties类中注入ImmutableList个实例(来自Guava)(而不必 for each 值执行ImmutableList.copyOf(...)操作).

我try 添加了各种转换器,但看起来属性绑定试图直接构建集合,所以像public class ListToImmutableListConverter implements Converter<List<Any>, ImmutableList<Any>>这样的转换器永远不会被调用.

根据堆栈跟踪,在CollectionFactory.createCollection(...)中创建属性似乎失败,但我没有看到任何用于在其中添加新集合类型的扩展点.有没有一种简单的方法来向Spring注册新的集合类型?

编辑:作为参考,这是一个Kotlin/Java混合环境,所以我们不能仅仅依靠Kotlin的"MuableList vs List",因为Java Bean可以获得List并在其上调用.add(...).

代码:

fun main(args: Array<String>) {
    val context = SpringApplication.run(Microservice::class.java, *args)
    println("--------------------------------------------------------------------------------------------------------")
    context.getBean(SomeBean::class.java).doStuff()
    println("--------------------------------------------------------------------------------------------------------")
    SpringApplication.exit(context)
}

@SpringBootApplication
@ConfigurationPropertiesScan
class Microservice

@ConfigurationProperties(prefix = "test")
@Validated
class Properties(
    val someList: ImmutableList<String>
)

@Component
@ConfigurationPropertiesBinding
class ListToImmutableListConverter : Converter<List<Any>, ImmutableList<Any>> {
    override fun convert(source: List<Any>): ImmutableList<Any> = source.toImmutableList()
}

@Component
class SomeBean(
    private val properties: Properties
) {
    fun doStuff() {
        println(properties.someList)
    }
}

示例application.yaml文件:

spring.application.name: test-service
test:
  some-list:
    - foo
    - bar
    - baz

try 了其他转换方法(使用TODO,只是为了看看Spring是否会调用转换器):

@Component
@ConfigurationPropertiesBinding
class ListToImmutableListConverter : GenericConverter {
    override fun getConvertibleTypes(): Set<GenericConverter.ConvertiblePair> = immutableSetOf(
        GenericConverter.ConvertiblePair(List::class.java, ImmutableList::class.java)
    )


    override fun convert(source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor): Any? {
        TODO("Not yet implemented")
    }
}


@Component
@ConfigurationPropertiesBinding
class ListToImmutableListConverter : ConverterFactory<List<*>, ImmutableList<*>> {
    override fun <T : ImmutableList<*>?> getConverter(targetType: Class<T>): Converter<List<*>, T> {
        TODO("Not yet implemented")
    }
}

故障日志(log)示例:

18:56:05.069 [main] WARN  o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'someBean' defined in file [****]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'test-test.spring.Properties': Could not bind properties to 'Properties' : prefix=test, ignoreInvalidFields=false, ignoreUnknownFields=true
18:56:05.073 [main] INFO  org.apache.catalina.core.StandardService - Stopping service [Tomcat]
18:56:05.103 [main] INFO  o.s.b.a.l.ConditionEvaluationReportLogger - 

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
18:56:05.130 [main] ERROR o.s.b.d.LoggingFailureAnalysisReporter - 

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to bind properties under 'test.some-list' to com.google.common.collect.ImmutableList<java.lang.String>:

    Property: test.some-list[0]
    Value: "foo"
    Origin: class path resource [application.yaml] - 6:7
    Reason: org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'test.some-list' to com.google.common.collect.ImmutableList<java.lang.String>

Action:

Update your application's configuration


Process finished with exit code 1

堆栈跟踪:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'someBean' defined in file [****]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'test-test.spring.Properties': Could not bind properties to 'Properties' : prefix=test, ignoreInvalidFields=false, ignoreUnknownFields=true
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:245)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1352)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1189)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:973)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:942)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:608)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:436)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:312)
    at shared.bootstarters.microservice.SpringMicroservice.startAndInitialize(SpringMicroservice.kt:41)
    at shared.bootstarters.microservice.SpringMicroservice.startAndInitialize$default(SpringMicroservice.kt:30)
    at test.spring.MicroserviceKt.main(Microservice.kt:22)
Caused by: org.springframework.boot.context.properties.ConfigurationPropertiesBindException: Error creating bean with name 'test-test.spring.Properties': Could not bind properties to 'Properties' : prefix=test, ignoreInvalidFields=false, ignoreUnknownFields=true
    at org.springframework.boot.context.properties.ConstructorBound.from(ConstructorBound.java:47)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.lambda$createBeanDefinition$1(ConfigurationPropertiesBeanRegistrar.java:97)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainInstanceFromSupplier(AbstractAutowireCapableBeanFactory.java:1254)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.obtainInstanceFromSupplier(DefaultListableBeanFactory.java:949)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1214)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1158)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1417)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:888)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
    ... 19 common frames omitted
Caused by: org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'test.some-list' to com.google.common.collect.ImmutableList<java.lang.String>
    at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:392)
    at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:352)
    at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$4(Binder.java:478)
    at org.springframework.boot.context.properties.bind.ValueObjectBinder$ConstructorParameter.bind(ValueObjectBinder.java:318)
    at org.springframework.boot.context.properties.bind.ValueObjectBinder.bind(ValueObjectBinder.java:76)
    at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:482)
    at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:596)
    at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:582)
    at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:480)
    at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:419)
    at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:348)
    at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:337)
    at org.springframework.boot.context.properties.bind.Binder.bindOrCreate(Binder.java:329)
    at org.springframework.boot.context.properties.bind.Binder.bindOrCreate(Binder.java:314)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bindOrCreate(ConfigurationPropertiesBinder.java:101)
    at org.springframework.boot.context.properties.ConstructorBound.from(ConstructorBound.java:44)
    ... 35 common frames omitted
Caused by: java.lang.IllegalArgumentException: Could not instantiate Collection type: com.google.common.collect.ImmutableList
    at org.springframework.core.CollectionFactory.createCollection(CollectionFactory.java:212)
    at org.springframework.boot.context.properties.bind.CollectionBinder.lambda$bindAggregate$0(CollectionBinder.java:48)
    at org.springframework.boot.context.properties.bind.AggregateBinder$AggregateSupplier.get(AggregateBinder.java:107)
    at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:111)
    at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:86)
    at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:70)
    at org.springframework.boot.context.properties.bind.CollectionBinder.bindAggregate(CollectionBinder.java:49)
    at org.springframework.boot.context.properties.bind.AggregateBinder.bind(AggregateBinder.java:56)
    at org.springframework.boot.context.properties.bind.Binder.lambda$bindAggregate$3(Binder.java:443)
    at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:596)
    at org.springframework.boot.context.properties.bind.Binder.bindAggregate(Binder.java:443)
    at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:404)
    at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:348)
    ... 49 common frames omitted
Caused by: java.lang.InstantiationException: null
    at java.base/jdk.internal.reflect.InstantiationExceptionConstructorAccessorImpl.newInstance(InstantiationExceptionConstructorAccessorImpl.java:48)
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
    at org.springframework.core.CollectionFactory.createCollection(CollectionFactory.java:208)
    ... 61 common frames omitted

推荐答案

有几个因素阻碍了这一点的奏效:

  1. a bug in Spring Boot,这意味着基于构造函数的对自定义Collection类型的绑定不起作用.
  2. 您的自定义转换器不匹配,因为Kotlin的Any被视为Object而不是?.我认为这是一个Spring框架错误.我已经开了#31370个这样他们就可以调查了.

考虑到这些问题,如果您放弃Properties类中的不变性,并对您的转换器使用更精确的类型,则可以使绑定到Guava的ImmutableList起作用:

package com.example.guavaimmutablelist

import com.google.common.collect.ImmutableList
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.SpringBootConfiguration
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ComponentScan
import org.springframework.core.convert.converter.Converter
import org.springframework.stereotype.Component

@ComponentScan
@SpringBootConfiguration
@EnableConfigurationProperties(Properties::class)
class GuavaImmutableListApplication

fun main(args: Array<String>) {
    runApplication<GuavaImmutableListApplication>("--test.someList[0]=zero", "--test.someList[1]=one", "--debug")
}

@ConfigurationProperties(prefix = "test")
class Properties {
    var someList: ImmutableList<String> = ImmutableList.of()
}

@Component
@ConfigurationPropertiesBinding
class ListToImmutableListConverter : Converter<List<String>, ImmutableList<String>> {
    override fun convert(source: List<String>): ImmutableList<String> = ImmutableList.copyOf(source)
}

@Component
class SomeBean(
    private val properties: Properties
): CommandLineRunner {
    override fun run(vararg args: String) {
        println("someList: ${properties.someList}")
    }
}

Kotlin相关问答推荐

Android Jetpack编写androidx.compose.oundation.lazy.grid.Items

如何让Gradle8+在编译Kotlin代码之前编译Groovy代码?然后把它们混合在一个项目中?

Kotlin:类型不匹配:推断的类型已运行,但应等待

S使用MAP和ElseThrow的习惯用法是什么?

在 Kotlin 中将两个字节转换为 UIn16

Kotlin 获取继承类的默认 hashCode 实现

为什么 trySend 会发出假数据?

如果带注释的成员未被特定块包围,则发出 IDE 警告

如何将 `throw` 放置在辅助函数中但仍然具有空安全性?

如何在 Hibernate Panache 中进行部分搜索

Saripaar formvalidation 在 kotlin 中第二次不起作用

禁用 Android 12 默认启动画面

在 Spring Framework 5.1 中注册具有相同名称的测试 bean

kotlin:扩展方法和空接收器

Kotlin:什么是 kotlin.String!类型

Android插件2.2.0-alpha1无法使用Kotlin编译

TornadoFX 中设置 PrimaryStage 或 Scene 属性的方法

编译错误:-Xcoroutines has no effect: coroutines are enabled anyway in 1.3 and beyond

Gradle:无法连接到 Windows 上的 Kotlin 守护程序

Android Compose 生产准备好了吗?