Skip to content

Binding configuration properties dynamically is lot harder than it needs to be #12664

@asarkar

Description

@asarkar

I've a use case where I need to bind configuration properties based on a prefix determined at runtime.

interface Condition {
    companion object {
        const val PREFIX = "touchstone.condition"
    }

    enum class Phase {
        PRE, POST
    }

    fun run(chunkContext: ChunkContext): ExitStatus

    fun phase(): Phase = Phase.PRE

    fun order(): Int = 1

    fun shouldRun(): Boolean = true

    val qualifiedName: String
        get() = listOf(PREFIX, phase().name, javaClass.simpleName)
                .joinToString(separator = ".") { it.toLowerCase(Locale.ENGLISH) }
}

Each implementation of the above interface can override the values orderand shouldRun using usual Boot configuration overrides. This is not unlike how Hystrix Configurations work; in other words, not unprecedented. However, doing this in practice is a lot harder than it needs to be, due to unnecessary package-level access of some classes in the framework, and in general lack of support for the Open/closed principal. Let's break it down:

  1. ConfigurationPropertiesBinder.bind(Bindable<?> target) method is public and looks promising since creating a Bindable is not hard, but then it does
Assert.state(annotation != null, "Missing @ConfigurationProperties on " + target);

which is a problem when dynamically creating a target. The annotation can be synthesized (more on that later), so it's not technically necessary for it to be specified at compile time.

  1. Methods getValidators and getBindHandler called from bind are private scoped, so I had to copy-paste them with minor modifications.

  2. bind creates a Binder, which uses quite a few classes with package-scoped constructor. Thus, in order to create a Binder, I'd to resort to the hack of putting the following class in org.springframework.boot.context.properties package. This is completely unnecessary.

class BinderFactory {
    companion object {
        private fun propertyEditorInitializer(ctx: ApplicationContext): Consumer<PropertyEditorRegistry>? {
            return if (ctx is ConfigurableApplicationContext) {
                Consumer {
                    ctx.beanFactory.copyRegisteredEditorsTo(it)
                }
            } else null
        }

        fun newBinder(ctx: ApplicationContext): Binder {
            val propertySources = PropertySourcesDeducer(ctx)
                    .propertySources
            return Binder(
                    ConfigurationPropertySources.from(propertySources),
                    PropertySourcesPlaceholdersResolver(propertySources),
                    ConversionServiceDeducer(ctx).conversionService,
                    propertyEditorInitializer(ctx)
            )
        }
    }
}
  1. Having done all that, now I can write the following, apparently simple, code:
val annotation = synthesizeAnnotation(qn)
val target = bindable(annotation)
val handler = bindHandler(annotation)

BinderFactory.newBinder(ctx).bind(qn, target, handler)
qn to target.value.get()

where synthesizeAnnotation uses AnnotationUtils.synthesizeAnnotation to do what it claims. The rest of the private methods calls should be obvious from the context.

Possible fix:

  1. An overloaded version of bind accepting a ConfigurationProperties annotation should solve this problem while maintaining backward compatibility.
  2. Creating a BinderFactory class (or a static factory method in Binder) will not require changing the package scope of the various constructors used by Binder for those who want to use it directly without going through ConfigurationPropertiesBinder.
  3. I see no reason why bindHandler as shown below can't be a public static method.
private fun bindHandler(annotation: ConfigurationProperties): BindHandler {
    var handler = BindHandler.DEFAULT
    if (annotation.ignoreInvalidFields) {
        handler = IgnoreErrorsBindHandler(handler)
    }
    if (!annotation.ignoreUnknownFields) {
        val filter = UnboundElementsSourceFilter()
        handler = NoUnboundElementsBindHandler(handler, filter)
    }
    return handler
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: declinedA suggestion or change that we don't feel we should currently apply

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions