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
Since spring-cloud-context 5.0.2 (resetBeanToDefaults, #1680), a rebind empties the live @ConfigurationProperties singleton before repopulating it. For a bean with nested property
objects, the observable intermediate state is not merely a stale or new scalar — it is null, so a
concurrent reader does not read a wrong value, it throws NullPointerException.
This is the same window PR #1709 reported and #1721 partially addressed (writer/writer only). #1709
was closed with the observation acknowledged; this report adds the failure mode it did not cover — a
hard failure rather than a wrong value — and evidence of how often it is hit in a framework where
every service has nested properties read on request threads.
Environment
spring-cloud-context 5.0.2 and 5.0.3 (trains 2025.1.2, 2025.1.3); Spring Boot 4.0.8. Not present in
4.3.x/4.2.x.
resetProperties walks the BeanWrapper descriptors and, for every writable property, does target.setPropertyValue(name, defaultsWrapper.getPropertyValue(name)). For timeout that is setTimeout(null). Between resetBeanToDefaults(bean) and the binding inside initializeBean(bean, name), every reader of myProperties.getTimeout().getX() throws NPE.
spring.cloud.refresh.never-reset-nested-types does not help: it controls recursion into a non-null
nested value, not the top level setTimeout(null).
mvn test # 5.0.3 (default) - both tests fail
mvn test -Dspring-cloud-context.version=5.0.2 # both tests fail
mvn test -Dspring-cloud-context.version=5.0.1 # both tests pass
spring-cloud-context
nestedPropertyIsReadableThroughoutARebind
concurrentReadersSurviveRepeatedRebinds
5.0.1
passes
passes, 0 failed reads
5.0.2
fails, NPE
fails, 10,942,734 of 19,659,911 reads
5.0.3
fails, NPE
fails, 11,072,765 of 20,753,865 reads
The first test is deterministic rather than a race: a PriorityOrderedBeanPostProcessor reads the bean
from inside the re-bind, ahead of ConfigurationPropertiesBindingPostProcessor, which is where a request
thread lands.
NullPointerException: Cannot invoke "...MyProperties$Timeout.getConnect()"
because the return value of "...MyProperties.getTimeout()" is null
The second is one reader thread against 200 re-binds. A tight reader loop overstates the proportion
compared with real traffic, but it shows the window is wide rather than a few instructions.
Impact and frequency
Measured in a framework used across a fleet of services, where a Consul config watch fires ContextRefresher.refresh() on a running service:
In a framework integration test, 5,363 of 2,647,116 reads failed with NPE during a single ContextRefresher.refresh() - 4 reader threads resolving an HTTP connect timeout through the
properties bean, exactly as request threads do for every outbound call.
In production this is a burst of failed outbound calls or 5xx per config change, not a sustained
outage — but it is one burst per config change, per service, unattended.
It is aggravated by ContextRefresher.refresh() also calling scope.refreshAll(): @RefreshScope
beans that read the properties bean are rebuilt at exactly the moment the properties they read are
reset. An application with an async ApplicationEventMulticaster makes it near deterministic,
because /refresh returns before the rebind has finished.
Why @RefreshScope is only a partial answer
@RefreshScope on the properties bean does work — ConfigurationPropertiesBeans skips refresh scoped
beans, so they are replaced rather than rebound — and that is the mitigation adopted here. But it has
to be applied bean by bean, by whoever owns each bean. A framework can scope its own properties beans;
it cannot scope the ones its consuming applications declare. Every application that upgrades to a
2025.1.2+ train silently acquires this exposure in its own configuration classes, with no warning at
build or start time, and the symptom surfaces as an unexplained NPE burst minutes after a config change.
Consider making the reset opt-in — or at least opt-out per bean without losing rebinding — on 5.0.x,
rather than only in the next major. The reset restores removed properties to defaults, which is
valuable, but it turns a previously non-failing operation into a failing one for any bean with
nested properties, and Reset bean to defaults before rebinding values #1680 changed the behavior for non-proxied beans that Recreate proxied @ConfigurationProperties beans on rebind #1662 had deliberately
left alone for backwards compatibility.
Summary
Since spring-cloud-context 5.0.2 (
resetBeanToDefaults, #1680), a rebind empties the live@ConfigurationPropertiessingleton before repopulating it. For a bean with nested propertyobjects, the observable intermediate state is not merely a stale or new scalar — it is
null, so aconcurrent reader does not read a wrong value, it throws
NullPointerException.This is the same window PR #1709 reported and #1721 partially addressed (writer/writer only). #1709
was closed with the observation acknowledged; this report adds the failure mode it did not cover — a
hard failure rather than a wrong value — and evidence of how often it is hit in a framework where
every service has nested properties read on request threads.
Environment
4.3.x/4.2.x.
What happens
resetPropertieswalks theBeanWrapperdescriptors and, for every writable property, doestarget.setPropertyValue(name, defaultsWrapper.getPropertyValue(name)). Fortimeoutthat issetTimeout(null). BetweenresetBeanToDefaults(bean)and the binding insideinitializeBean(bean, name), every reader ofmyProperties.getTimeout().getX()throws NPE.spring.cloud.refresh.never-reset-nested-typesdoes not help: it controls recursion into a non-nullnested value, not the top level
setTimeout(null).Reproduction
rebind-reset-repro.zip is a self-contained project — Spring Boot 4.0.8 with
spring-cloud-contextas the only other dependency, no Spring Cloud Config, no Consul, no@RefreshScope. The test is also attached on its own as RebindEmptiesNestedPropertiesTest.java.txt,and the three runs below as test-results.txt.
spring-cloud-contextnestedPropertyIsReadableThroughoutARebindconcurrentReadersSurviveRepeatedRebindsThe first test is deterministic rather than a race: a
PriorityOrderedBeanPostProcessorreads the beanfrom inside the re-bind, ahead of
ConfigurationPropertiesBindingPostProcessor, which is where a requestthread lands.
The second is one reader thread against 200 re-binds. A tight reader loop overstates the proportion
compared with real traffic, but it shows the window is wide rather than a few instructions.
Impact and frequency
Measured in a framework used across a fleet of services, where a Consul config watch fires
ContextRefresher.refresh()on a running service:ContextRefresher.refresh()- 4 reader threads resolving an HTTP connect timeout through theproperties bean, exactly as request threads do for every outbound call.
outage — but it is one burst per config change, per service, unattended.
ContextRefresher.refresh()also callingscope.refreshAll():@RefreshScopebeans that read the properties bean are rebuilt at exactly the moment the properties they read are
reset. An application with an async
ApplicationEventMulticastermakes it near deterministic,because
/refreshreturns before the rebind has finished.Why
@RefreshScopeis only a partial answer@RefreshScopeon the properties bean does work —ConfigurationPropertiesBeansskips refresh scopedbeans, so they are replaced rather than rebound — and that is the mitigation adopted here. But it has
to be applied bean by bean, by whoever owns each bean. A framework can scope its own properties beans;
it cannot scope the ones its consuming applications declare. Every application that upgrades to a
2025.1.2+ train silently acquires this exposure in its own configuration classes, with no warning at
build or start time, and the symptom surfaces as an unexplained NPE burst minutes after a config change.
Request
is about a different symptom), so the state of it is discoverable.
rather than only in the next major. The reset restores removed properties to defaults, which is
valuable, but it turns a previously non-failing operation into a failing one for any bean with
nested properties, and Reset bean to defaults before rebinding values #1680 changed the behavior for non-proxied beans that Recreate proxied @ConfigurationProperties beans on rebind #1662 had deliberately
left alone for backwards compatibility.
References
that Add a per-bean-name lock around destroy/reset/re-populate/re-initialize steps in rebind #1721 does not fix it.
@Value-injected fields on@ConfigurationPropertiesbeans on the firstEnvironmentChangeEvent. #1716 the permanent@Valuenulling variant, fixed by Autowire beans when rebinding #1720 in 5.0.3.@ConfigurationProperties".