-
Notifications
You must be signed in to change notification settings - Fork 41.7k
Description
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:
ConfigurationPropertiesBinder.bind(Bindable<?> target)method ispublicand looks promising since creating aBindableis 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.
-
Methods
getValidatorsandgetBindHandlercalled frombindareprivatescoped, so I had to copy-paste them with minor modifications. -
bindcreates aBinder, which uses quite a few classes with package-scoped constructor. Thus, in order to create aBinder, I'd to resort to the hack of putting the following class inorg.springframework.boot.context.propertiespackage. 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)
)
}
}
}- 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:
- An overloaded version of
bindaccepting aConfigurationProperties annotationshould solve this problem while maintaining backward compatibility. - Creating a
BinderFactoryclass (or a static factory method inBinder) will not require changing the package scope of the various constructors used byBinderfor those who want to use it directly without going throughConfigurationPropertiesBinder. - I see no reason why
bindHandleras shown below can't be apublic staticmethod.
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
}