CAMEL-24501: Report starter configuration options that cannot be bound - #1935
Open
Croway wants to merge 3 commits into
Open
CAMEL-24501: Report starter configuration options that cannot be bound#1935Croway wants to merge 3 commits into
Croway wants to merge 3 commits into
Conversation
The generated starter code discarded configuration in two places, in both cases without a log line, so an option that never took effect looked exactly like one that did. The generated *ComponentConverter, *DataFormatConverter and *LanguageConverter classes resolve the bean reference that an option of a complex (object) type is configured with. They returned null for any value that did not start with #, and for a value naming a bean that does not exist, so a typo in the bean id produced a component with the option unset. The generated convert() body now delegates to the new BeanReferenceHelper, which resolves #bean:id, #id, a plain bean id, #autowired and #type:fqn, and throws IllegalArgumentException naming the value, the target type and the configuration prefix when the value cannot be resolved. This also drops the per-type switch, which returned null for a target type it did not list. The generated customizers copied the whole configuration onto the target with CamelPropertiesHelper.copyProperties, which binds with failIfNotSet=false, so an option with no matching setter on the target was dropped. The same mojo emits failIfNotSet=true for camel.rest.*, so the two disagreed. The customizers now call the new CamelPropertiesHelper.copyConfigurationProperties, which: - removes the options owned by the auto configuration layer itself (enabled and customizer) before binding, as they are not options on the Camel target; - fails with IllegalArgumentException when an option the application configured itself cannot be set; - logs at DEBUG when an option that only carries its catalog default cannot be set, since the target keeps its own default and there is nothing the application can do about it. Telling the two apart uses the Spring ConfigurationPropertySources, so a catalog default that has never been bindable (camel.language.simple.trim, for example, which is an option of the expression model rather than of SimpleLanguage) does not turn into a startup failure for every application. Blanket strict binding needs the catalog defaults to stop being materialised as field initializers on the configuration classes first, which is left for a follow-up. camel.springboot.lenient-configuration-binding=true logs an explicitly configured option at WARN and continues, instead of failing. The language converter template also referenced an applicationContext field it did not declare; no starter currently generates a language converter, so this was latent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Regenerated the starters with the updated generator plugin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oscerd
approved these changes
Sep 2, 2026
…ion binding Review follow-up on the previous commit. The generated converters are registered with @ConfigurationPropertiesBinding, so they take part in every @ConfigurationProperties binding in the application, not only in Camel's own. Failing closed unconditionally therefore turned an unrelated application property of a type a starter registers for, such as javax.net.ssl.HostnameVerifier, into a startup failure that talks about camel.component.*. BeanReferenceHelper now resolves the class being bound from TypeDescriptor.getSource(), which Spring Boot's binder fills with the setter's MethodParameter (or the Field for field access), and only applies the strict behaviour when that class is Camel's own: under org.apache.camel, or annotated with @ConfigurationProperties for a camel. prefix. Any other class keeps the behaviour it had before, and an unrecognised source counts as Camel's own so that Camel's own binding is never weakened. This is decided in convert() rather than in ConditionalGenericConverter.matches: GenericConversionService caches the converter it picked per source/target TypeDescriptor pair and TypeDescriptor.equals ignores the source, so matches is consulted once for the first class bound and the answer reused for every other class with a field of the same type. A conditional converter would therefore be order dependent, and in the bad order it would report a missing converter for a valid Camel property. Also from the review: - #type: now checks that the bean found by type is assignable to the option type, instead of leaving a ClassCastException for the binder to hit later. - camel.springboot.lenient-configuration-binding is read from the Spring Environment rather than from Camel's PropertiesComponent, and is declared in additional-spring-configuration-metadata.json so it shows up in IDE completion. - The mojo fails the build if a catalog option is ever named enabled or customizer, since the customizers strip those before binding. No component, data format or language declares one today. - The error message names the target class and points out that the option may be listed in the starter documentation, which is generated from the catalog rather than from that class, and so may never have taken effect. - isExplicitlyConfigured logs at WARN when it cannot decide, as returning false there downgrades a hard error to an ignored option. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes CAMEL-24501.
SpringBootAutoConfigurationMojogenerates two pieces of binding code into every starter that discardconfiguration without reporting it, so an option that never took effect is indistinguishable from one that
did. Both are addressed here; the third item on the ticket is deferred and explained below.
1. Generated converters no longer turn an unresolvable value into
nullAn option of a complex (object) type is configured with a reference to a bean:
The generated
convert()body returnednullfor any value that did not start with#, and for a valuenaming a bean that does not exist, so a typo in the bean id produced a component with the option unset. It
also returned
nullfor a target type itsswitchdid not list, which happens when the bound type is asubtype of a registered pair.
The body now delegates to a new
org.apache.camel.spring.boot.util.BeanReferenceHelper, which#bean:myBean,#myBean, a plainmyBean,#autowiredand#type:com.foo.MyType;IllegalArgumentExceptionnaming the value, the target type and the configuration prefix when thevalue cannot be resolved to a bean of that type.
The per-type
switchis gone, so the generated converters shrink to a single delegating line and the whole"not in the switch" branch disappears.
These converters are registered with
@ConfigurationPropertiesBinding, so they take part in every@ConfigurationPropertiesbinding in the application.BeanReferenceHelpertherefore only applies the strictbehaviour when the class being bound is Camel's own — under
org.apache.camel, or annotated with@ConfigurationPropertiesfor acamel.prefix. Any other class keeps the behaviour it had before, so putting astarter on the classpath cannot make an unrelated application property fail to bind. See the review follow-up
below for why this is decided in
convert()rather than inConditionalGenericConverter.matches.2. Generated customizers no longer drop an option that cannot be set
createComponentBody/createDataFormatBody/createLanguageBodyemittedCamelPropertiesHelper.copyProperties, which binds withfailIfNotSet=false, while the same mojo emitsfailIfNotSet=truefor thecamel.rest.*path. They now call a newCamelPropertiesHelper.copyConfigurationProperties, whichenabledandcustomizerbefore binding — they belong to the auto-configuration layer, not to theCamel target, and were being offered to it on every startup;
IllegalArgumentExceptionwhen an option the application configured itself cannot be set;DEBUGwhen an option that only carries its catalog default cannot be set.Whether an option was configured by the application is decided with Spring's
ConfigurationPropertySources, so relaxed binding and environment variables are handled.Why not blanket
failIfNotSet=trueThat was the first implementation, and it does not survive contact with the existing generated code. The
configuration classes materialise every catalog default as a field initializer (item 2 on the ticket), so the
customizer copies defaults for options that were never bindable. The clearest case is
camel-core-starter:trim,pretty,trimResultandnestedare options of the expression model, not ofSimpleLanguage, andthey carry catalog defaults, so
failIfNotSet=truewould abort startup for every application that hascamel-core-starteron the classpath. Failing only on options the application actually set keeps the fixnarrow and still closes the reported hole.
Behaviour change and how to opt back
An application that configured an option which never took effect now fails at startup, with a message naming
the option. The remedy is to correct or remove it. To keep starting while doing so:
camel.springboot.lenient-configuration-binding = trueSuch an option is then logged at
WARNwith its name, instead of being dropped silently as before. Thelegacy
CamelPropertiesHelper.copyProperties/setCamelProperties(..., failIfNotSet=false)path, which ispublic API and no longer used by generated code, also logs at
WARNnow instead of staying silent.An upgrade-guide entry for
docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adocinapache/camelhas been written and will be raised separately.Deferred
Item 2 of the ticket — catalog defaults emitted as literal field initializers, so the customizer copies a
default over a value set programmatically on a user-supplied component bean — is not fixed here. Dropping
the initializers is not a local change:
spring-configuration-metadata.jsonderivesdefaultValuefrom the field initializers, so IDE completionand the generated
.adocdocumentation pages (which the readme mojo builds from that metadata) would bothlose every default;
set an option back to its default explicitly.
It needs its own change with the metadata and docs generation adjusted at the same time, and it is the
prerequisite for turning the strictness above into blanket
failIfNotSet=true. The ticket is left open for it.Tests
SpringBootAutoConfigurationMojoTest(new, 5 tests) — the generated converter and customizer bodies, and thereserved option name guard.
BeanReferenceHelperTest(new, 22 tests) — every supported syntax, the failure cases, and which bindings thehelper considers Camel's own.
CamelPropertiesHelperTest(3 new tests) —enabled/customizerare not offered to the target, aconfigured option that cannot be set fails, a catalog default that cannot be set does not.
CamelPropertiesHelperLenientBindingTest(new) — the opt-out.HttpComponentBeanReferenceBindingTest(new, 6 tests) — end-to-end through the Spring Boot binder on a realstarter, including a third-party
@ConfigurationPropertiesclass that is not intercepted.Run green: the generator plugin,
core/camel-spring-boot(full suite),camel-core-starter,camel-http-starter,camel-netty-http-starterandcamel-jackson-starter.Review follow-up
1. Generated converters are global (blocking). Spiked first, and the spike changed the shape of the fix.
targetType.getSource()does carry the bound class: it is aorg.springframework.core.MethodParameterwhosegetContainingClass()/getDeclaringClass()is the@ConfigurationPropertiesclass (Spring Boot 4.1.1).ConditionalGenericConverter.matchesis nevertheless not usable for this:GenericConversionServicecaches theconverter it picked per source/target
TypeDescriptorpair, andTypeDescriptor.equalsignores the source. Inthe spike, with one
camel.*class and one third-party class each having aHostnameVerifierfield,matcheswas called once and
converttwice — so the decision taken for whichever class binds first is reusedfor the other. Implementing
matchesthat way would be order-dependent, and in the bad order it would give aConverterNotFoundExceptionfor a perfectly valid Camel property.The decision is therefore taken in
convert(), which the spike shows is invoked per binding with the correcttargetType.BeanReferenceHelper.isCamelConfigurationTarget(TypeDescriptor)resolves the bound class fromgetSource()(MethodParameterorField) and returns true when it is underorg.apache.camelor carries@ConfigurationPropertieswith acamel.prefix, and true when the source does not identify a class, so Camel'sown binding can never be weakened by a binder shape this does not recognise. For any other class the pre-4.23
behaviour is kept exactly: a value that is not a
#reference converts tonull. This is the substance ofoption (a) with the precision the reviewer asked for — it keys on the class being bound, not on the target
type's package, so
camel.component.http.x509-hostname-verifier = myVerifierstill resolves even thoughjavax.net.ssl.HostnameVerifieris not a Camel type. The generated body stays one line, and all converters wereregenerated. Covered by
BeanReferenceHelperTest(both rules, the unknown-source fallback, and that athird-party binding is not intercepted) and end-to-end by
HttpComponentBeanReferenceBindingTest.2.
#type:. AftergetBean(Class)the result is checked withtype.isInstance(bean)and the sameIllegalArgumentExceptionis thrown otherwise, naming the type actually found. Test:testTypeOfAnUnrelatedBeanFailsClosed.3.
camel.springboot.lenient-configuration-binding. Now read from the SpringEnvironmentthrough theApplicationContextthe helper already receives, instead of Camel'sPropertiesComponent. Declared incore/camel-spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.jsonnext tocamel.vault.ignore-resolution-failures, andsrc/main/docs/spring-boot.jsonregenerated.4.
enabled/customizer. Confirmed by reading all 479 component, data format and language JSON filesunder
catalog/camel-catalog-provider-springboot: no option carries either name today. So that nothing can startcarrying one unnoticed,
SpringBootAutoConfigurationMojonow fails the build when a catalog option is namedenabledorcustomizer, checked for components, data formats and languages.5. Error message. It no longer just says to remove the option. It names the target class and says the option
may be listed in the starter documentation — which is generated from the catalog rather than from the target
class — in which case it has never taken effect.
6. Legacy
copyPropertiespath. TheWARNstays, and the upgrade note now says so explicitly: it is publicAPI used by hand-written customizers outside the generated starters, those keep ignoring an unbindable option,
but each one is now logged at
WARNwhere nothing was logged before.7. Language converter template. No starter generates a language converter today, so the fix to it (it
referenced an
applicationContextfield it did not declare) is covered only by the mojo string assertion inSpringBootAutoConfigurationMojoTest, not by any running starter.8.
BeanReferenceHelperTest.testMessageMentionsTheTargetTypeusesassertTrue.9.
isExplicitlyConfigurednow logs atWARNwith the exception when it cannot decide, since returningfalse there downgrades a hard error to an ignored option.
All ~425 starters were regenerated again after these changes and the result is byte identical to the previous
CAMEL-24501: Regencommit, since the emitted converter and customizer bodies are unchanged one-liners and thereview only touched what they call and a build-time check. So there is no second regeneration commit. That run
also exercised the new reserved-name check against every catalog entry without tripping it.
Claude Code (Opus 5) on behalf of Federico Mariani