You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Regression: spring-cloud-context 5.0.2 permanently nulls @Value-injected fields on @ConfigurationProperties beans on the first EnvironmentChangeEvent. #1716
Full disclosure: I used Claude to assist in diagnosing the issue, generating the text here, and setting up the repro scenario. However, I have carefully edited the LLM-provided text and implementation myself, and manually validated the reproduction scenario and workaround.
Describe the bug
spring-cloud-context 5.0.2 (Spring Cloud 2025.1.2) permanently nulls @Value-injected fields on @ConfigurationProperties beans on the first EnvironmentChangeEvent. The values are never restored.
Introduced by #1680 (Reset bean to defaults before rebinding values). Distinct from the known fallout in #1698 and #1700 (where BeanUtils.instantiateClass fails and the reset is skipped, milestoned for 5.0.3) and from #1709 (the transient race during reset). This is the reset succeeding, and causing permanent silent state loss.
Root cause
The two branches of ConfigurationPropertiesRebinder.rebind(String, ApplicationContext) are not equivalent:
// proxied branch - createBean() runs populateBean(), so @Value IS re-injectedObjectfreshBean = appContext.getAutowireCapableBeanFactory().createBean(targetClass);
// non-proxied branch - no populateBean(), so @Value is NOT re-injectedappContext.getAutowireCapableBeanFactory().destroyBean(bean);
resetBeanToDefaults(bean);
appContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
resetProperties() copies every writable property from a fresh new T() onto the live bean. initializeBean() then runs BeanPostProcessors, so ConfigurationPropertiesBindingPostProcessor re-binds whatever is reachable under the bean's own prefix - but @Value injection happens in AutowiredAnnotationBeanPostProcessor.postProcessProperties(), which runs in populateBean(), and rebind() never calls it.
Net effect, for any non-proxied @ConfigurationProperties bean: a field survives if and only if its value is reachable from that bean's own prefix. @Value state is silently discarded.
Impact: CONSUL_TOKEN silently stops working
ConsulDiscoveryProperties.aclToken is populated by @Value:
None of those keys sit under spring.cloud.consul.discovery, so the binder cannot restore them. On any deployment supplying the token via CONSUL_TOKEN:
Startup succeeds, aclToken is populated, discovery works.
The Consul config watch fires an EnvironmentChangeEvent, typically within a minute.
aclToken becomes null and is never re-injected.
Every subsequent Consul catalog/health query goes out unauthenticated.
With ACLs enabled and default_policy: deny, Consul answers an unauthenticated catalog query with an empty result set, not a 403. So discovery silently returns zero instances for every service, load balancing fails with No servers available for service: <name>, and clients fall through to the literal hostname with UnknownHostException. Nothing anywhere points at the ACL token.
An authenticated, working service mesh becomes a silently empty one, minutes after startup, with no diagnostic.
CONSUL_TOKEN is a supported contract, not a shortcut**
This is the second time this contract has been broken. In spring-cloud/spring-cloud-consul#738, the spring.config.import migration stopped CONSUL_TOKEN being honoured. While the initial response was that it should give way to the prefixed property, after discussion, maintainers accepted the fact that CONSUL_TOKEN is the HashiCorp-conventional environment variable, used by the Consul CLI, Terraform and envconsul, and shared across non-Spring services in a mixed estate, and the fix shipped in 3.1.3.
That @Value chain exists because of spring-cloud/spring-cloud-consul#738. #1680 has now silently undone it through a different mechanism, so please don't treat "use spring.cloud.consul.discovery.acl-token instead" as the resolution here 😄
(ConsulConfigProperties is unaffected: under spring.config.import it is promoted via beanFactory.registerSingleton(), which bypasses BeanPostProcessors, so the rebinder never touches it. Only discovery breaks.)
git clone https://github.com/oliverlockwood/spring-cloud-consul-example
cd spring-cloud-consul-example
mvn test# 2 failures
mvn test -Dspring-boot.version=4.0.6 -Dspring-cloud.version=2025.1.1 # all pass
It covers both a plain @ConfigurationProperties bean with a @Value field and ConsulDiscoveryProperties.aclToken itself, plus a control field under the bean's own prefix that passes on both versions. The README also walks through a live ACL-enabled Consul, showing the token disappearing after a refresh.
Although technically it's an supported pairing, holding Spring Boot at 4.0.6 and moving only Spring Cloud to 2025.1.2 flips the result, so this is not a Boot regression. resetBeanToDefaults / resetProperties exist only in spring-cloud-context 5.0.2 - absent from 5.0.1 and from all of 4.1.x–4.3.3.
Versions
Spring Cloud
2025.1.2 (spring-cloud-context5.0.2)
Last known good
2025.1.1 (spring-cloud-context5.0.1)
Also unaffected
all of 4.1.x – 4.3.3
Spring Boot
4.1.0; also reproduces on 4.0.6
Java
25
Suggested fix
Use createBean(targetClass) in the non-proxied branch too, mirroring the proxied branch, so populateBean() re-applies @Value and @Autowired.
Or call applyBeanPropertyValues(bean, name) after resetBeanToDefaults() and before initializeBean().
More broadly, #1680's premise — that a bean's post-construction state can be rebuilt from new T() plus a prefix rebind — does not hold for any bean whose state has a second source. Restricting the reset to properties actually owned by the bean's prefix, rather than every writable property on the class, would avoid that.
Workaround
SPRING_CLOUD_CONSUL_DISCOVERY_ACL_TOKEN=<token>
Reachable by the binder, so it is restored on every rebind. On 5.0.2 neither CONSUL_TOKEN nor spring.cloud.consul.token is sufficient, including as a real property rather than an environment variable.
Or opt the bean out of rebinding — a replace, not an append, so the Hikari default must be re-listed:
Full disclosure: I used Claude to assist in diagnosing the issue, generating the text here, and setting up the repro scenario. However, I have carefully edited the LLM-provided text and implementation myself, and manually validated the reproduction scenario and workaround.
Describe the bug
spring-cloud-context5.0.2 (Spring Cloud2025.1.2) permanently nulls@Value-injected fields on@ConfigurationPropertiesbeans on the firstEnvironmentChangeEvent. The values are never restored.Introduced by #1680 (
Reset bean to defaults before rebinding values). Distinct from the known fallout in #1698 and #1700 (whereBeanUtils.instantiateClassfails and the reset is skipped, milestoned for 5.0.3) and from #1709 (the transient race during reset). This is the reset succeeding, and causing permanent silent state loss.Root cause
The two branches of
ConfigurationPropertiesRebinder.rebind(String, ApplicationContext)are not equivalent:resetProperties()copies every writable property from a freshnew T()onto the live bean.initializeBean()then runsBeanPostProcessors, soConfigurationPropertiesBindingPostProcessorre-binds whatever is reachable under the bean's own prefix - but@Valueinjection happens inAutowiredAnnotationBeanPostProcessor.postProcessProperties(), which runs inpopulateBean(), andrebind()never calls it.Net effect, for any non-proxied
@ConfigurationPropertiesbean: a field survives if and only if its value is reachable from that bean's own prefix.@Valuestate is silently discarded.Impact:
CONSUL_TOKENsilently stops workingConsulDiscoveryProperties.aclTokenis populated by@Value:None of those keys sit under
spring.cloud.consul.discovery, so the binder cannot restore them. On any deployment supplying the token viaCONSUL_TOKEN:aclTokenis populated, discovery works.EnvironmentChangeEvent, typically within a minute.aclTokenbecomesnulland is never re-injected.With ACLs enabled and
default_policy: deny, Consul answers an unauthenticated catalog query with an empty result set, not a 403. So discovery silently returns zero instances for every service, load balancing fails withNo servers available for service: <name>, and clients fall through to the literal hostname withUnknownHostException. Nothing anywhere points at the ACL token.An authenticated, working service mesh becomes a silently empty one, minutes after startup, with no diagnostic.
CONSUL_TOKENis a supported contract, not a shortcut**This is the second time this contract has been broken. In spring-cloud/spring-cloud-consul#738, the
spring.config.importmigration stoppedCONSUL_TOKENbeing honoured. While the initial response was that it should give way to the prefixed property, after discussion, maintainers accepted the fact thatCONSUL_TOKENis the HashiCorp-conventional environment variable, used by the Consul CLI, Terraform andenvconsul, and shared across non-Spring services in a mixed estate, and the fix shipped in 3.1.3.That
@Valuechain exists because of spring-cloud/spring-cloud-consul#738. #1680 has now silently undone it through a different mechanism, so please don't treat "usespring.cloud.consul.discovery.acl-tokeninstead" as the resolution here 😄(
ConsulConfigPropertiesis unaffected: underspring.config.importit is promoted viabeanFactory.registerSingleton(), which bypassesBeanPostProcessors, so the rebinder never touches it. Only discovery breaks.)Reproduction
Runnable, no infrastructure required — https://github.com/oliverlockwood/spring-cloud-consul-example
It covers both a plain
@ConfigurationPropertiesbean with a@Valuefield andConsulDiscoveryProperties.aclTokenitself, plus a control field under the bean's own prefix that passes on both versions. The README also walks through a live ACL-enabled Consul, showing the token disappearing after a refresh.Although technically it's an supported pairing, holding Spring Boot at 4.0.6 and moving only Spring Cloud to 2025.1.2 flips the result, so this is not a Boot regression.
resetBeanToDefaults/resetPropertiesexist only inspring-cloud-context5.0.2 - absent from 5.0.1 and from all of 4.1.x–4.3.3.Versions
2025.1.2(spring-cloud-context5.0.2)2025.1.1(spring-cloud-context5.0.1)4.1.x–4.3.3Suggested fix
createBean(targetClass)in the non-proxied branch too, mirroring the proxied branch, sopopulateBean()re-applies@Valueand@Autowired.applyBeanPropertyValues(bean, name)afterresetBeanToDefaults()and beforeinitializeBean().resetProperties()skip properties whose backing field carries@Valueor@Autowired— weakest, and leaves ConfigurationPropertiesRebinder exposes invalid state racily #1709 intact.More broadly, #1680's premise — that a bean's post-construction state can be rebuilt from
new T()plus a prefix rebind — does not hold for any bean whose state has a second source. Restricting the reset to properties actually owned by the bean's prefix, rather than every writable property on the class, would avoid that.Workaround
Reachable by the binder, so it is restored on every rebind. On 5.0.2 neither
CONSUL_TOKENnorspring.cloud.consul.tokenis sufficient, including as a real property rather than an environment variable.Or opt the bean out of rebinding — a replace, not an append, so the Hikari default must be re-listed:
spring.cloud.refresh.never-refreshable=com.zaxxer.hikari.HikariDataSource,org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties