欢迎访问Spring Cloud中国社区

《重新定义Spring Cloud实战》由Spring Cloud中国社区倾力打造,基于Spring Cloud的Finchley.RELEASE版本,本书内容宽度足够广、深度足够深,而且立足于生产实践,直接从生产实践出发,包含大量生产实践的配置。欢迎加微信Software_King进群答疑,国内谁在使用Spring Cloud?欢迎登记

Dynamic Configuration Properties in Spring Boot and Spring Cloud

admin · 5月前 · 906 ·

Dynamic Configuration Properties in Spring Boot and Spring Cloud

TL;DR

Typical Scenarios

  • Feature flags and canaries
  • Timeouts
  • Pool sizes
  • Polling or export frequency
  • Temporary log level changes

Features

Spring Environment

The Spring Environment is the canonical source of property values for configuration of Spring Boot applications. Spring Boot also goes the extra mile and includes properties files included by @PropertySource declarations. When we talk iof the Environment below, mostly we mean “the combined property sources managed by Spring Boot”. But there is a distinction in practice and implementations sometimes need to be ware of them. The @PropertySources are not dynamic property sources (they come only from the classpath), so for the purposes of discussing dynamic configuration, we are talking about the PropertySources instance inside the Environment. It is always a ConfigurableEnvironment in a Spring Boot application (unless the user explicitly changes it), and it is always mutable, but adding and removing a PropertySource is atomic. Changes are available at runtime through the Environment itself, and also through the various public APIs provided by Spring Boot and Spring Cloud. Spring Boot in particular puts the PropertySources in a clever wrapper that detects changes and audits the access to the various property sources, so that it can report on binding errors with precise details.

ConfigurationPropertiesBinder

Spring Boot 2.0 exposes ConfigurationPropertiesBinder as the API underneath @ConfigurationProperties binding. It uses it on start up (or more precisely whenever a bean is initialized) but makes it available to Spring Cloud to use if the Environment changes.

The /refresh Endpoint

Spring Cloud has an Actuator endpoint which computes changes in the Environment and sends an EnvironmentChangeEvent with the keys that changed. It does not detect deleted keys, or changes in array lengths. It actually delegates the to a ContextRefresher, which is a public API that can be used by other interested components. There is another ApplicationEvent (RefreshEvent) that can be used to trigger a context refresh in exactly the same way (i.e. a RefreshEvent can trigger an EnvironmentChangeEvent).

ConfigurationPropertiesRebinder

Spring Cloud responds to EnvironmentChangeEvent by locating and rebinding the @ConfigurationProperties. It doesn’t currently optimize this in any way (like only refreshing the beans whose keys it kows has changed) - they just all get rebound using the public API provided by Spring Boot. The rebinding actually happens just by applying the bean lifecycle callbacks using AutowireCapableBeanFactory.initializeBean(), so users can put validation and initialization logic in a @PostConstruct for example, and rely on BeanFactoryPostProcessors all being applied to the new state.

There is one exception. Any @ConfigurationProperties bean that is also in @RefreshScope is not rebound when the event is consumed. They could be rebound, but in the light of what happens in @RefreshScope, it would be redundant. Instead, they follow the usual path of @RefreshScope beans.

RefreshScope

A bean that is declared in @RefreshScope is created as a proxy. The actual target bean is also created on startup and stored in a cache with a key equal to its bean name. When a method call arrives at the proxy, it is passed down to the target. When the EnvironmentChangeEvent is consumed the cache is cleared and the BeanFactory callbacks on bean disposal are called by Spring, so the next method call on the proxy results in the target being re-created (the full Spring lifecycle, just as with any Scope).

Other Scopes

Refresh Scope is a little bit special, because it has some public APIs and events associated with it (and some other stuff). But a bean in any scope other than singleton can also result in the bean factory initializing the bean during the lifetime of the application context. For example, with @Scope("prototype") the bean is initialized every time there is a call to BeanFactory.getBean() (for that bean definition). In @RequestScope the bean is initialized on first usage per HTTP request. These beans will also pick up changes to the Environment (this has been a source of quite a lot of confusion and a few bugs in Spring Boot and Spring Cloud 2.0).

EnvironmentManager and /env Endpoint

Spring Cloud has a POSTable /env endpoint backed by an EnvironmentManager. It creates and updates a high priority PropertySource in the Environment. Changes persist in memory only, so it is useful for temporary changes, and experiments, but not for permanent

Encryption and Decryption

Spring Cloud supports decryption of Environment properties through an ApplicationContextInitializer (EnvironmentDecryptApplicationInitializer). It adds a high priority PropertySource to the Environment with decrypted values. It uses SystemEnvironmentPropertySource even if the decrypted properties do not come from the System environment variables (as a convenience so that foo.bar and FOO_BAR can both be used to bind to foo.bar, the logic for which is contained in the PropertySource in Spring Framework).

Log Levels

Since 1.5.x Spring Boot supports changing log levels dynamically via a special Actuator endpoint. Spring Cloud continues to support log level changes via the Environment.

Spring Cloud Bus

Users can fire a RefreshEvent by sending a message on the Spring Cloud Bus. It can be a “broadcast” (targetting all applications and instances) or it can target individual apps or instances via pattern matching.

Third Party Tools

There are a couple of libraries in the ecosystem that deal with Feature Flags, notably Tooglz and FF4J. Neither was designed for, nor is in use by, Spring users, but both are friendly to Spring Boot, and happily accept contributions. Most of the code is both libraries is about providing back ends for storing configuration properties, and front ends for managing the flags at runtime. Neither of these features is very interesting for Spring Boot: for configuration properties we prefer the Environment as an abstraction, and for runtime management of that there are plenty of options. The GUIs provided by the libraries are nice for demos, but probably not practical in a production system with multiple scaled up applications.

In summary: both Tooglz and FF4J can be used idiomatically by Spring Boot users as long as they stick to the Environment for configuration, and the state is updated at runtime if there is a RefreshEvent.

Concurrency

Because the Environment can be updated at runtime, there are clearly implications for components that operate concurrently. For a Spring Cloud app the trigger is nearly always the RefreshEvent. If a bean is @ConfigurationProperties it gets rebound, or if it is in @RefreshScope it gets destroyed, all in the same thread. For a vanilla Spring Boot app, only scoped proxies (e.g. @RequestScope) receive additional callbacks at runtime, by virtue of the normal Spring lifecycle. Other components can access the Environment directly, if they choose, but it isn’t idiomatic and isn’t really encouraged by the Spring Boot programming model.

Users of those components (other components usually) might need to be aware of the changes, or their implications at least. At a minimum they should access configuration properties that they expect to change via an injected @ConfigurationProperties bean. But callers can not rely on the state being the same from one moment to another. This means, for example, that state changes might occur in between method executions, or even during a single method execution. If things do change while a component is in use, callers might be surprised, but there is nothing we can do at present to help them generically at the framework level.

Environment

The Environment implementations that Spring uses under the hood are all backed by a MutablePropertySource that has a CopyOnWriteArrayList of PropertySource. The copy-on-write features mean that the a new PropertySource can usually be safely appended or removed at runtime. This happens, for example, when consuming a RefreshEvent, where the PropertySources are updated by replacing them with new ones that are built from the same sources. Other changes can come from mutating the PropertySource instances themselves - most of them have Map or Properties as source data (less common, but happens when the user POSTs to /env for example).

@ConfigurationProperties

Most @ConfigurationProperties beans are pure data holders and do not care or need to care about their internal state and its consistency or lack thereof, but it may be important to be aware of the implications of the state changing at runtime.

No special treatment is given to accommodate concurrent access by the ConfigurationPropertiesRebinder. On application startup all the property binding happens in a single thread which is part of the ApplicationContext lifecycle, and callers can rely on the internal state being fixed as soon as they have a reference to the bean instance. After startup things are different. Without @RefreshScope, any @ConfigurationProperties bean with a @PostConstruct that modifies its state can rely on the callback being made, but not on when it happens, so the internal state may be inconsistent when a property is accessed. Callers may need to be aware of that, or else the author of the @ConfigurationProperties could protect access to critical parts of the state of the bean using locks and/or synchronized blocks behind its public API.

@RefreshScope

Remember a bean in @RefreshScope is a proxy, wrapping a target instance of the desired type. Two consecutive method executions on the same bean in the same thread may be applied to different targets, if things get really busy. There’s nothing that the scope can do itself to protect against that.

That’s the bad news. The good news is that each method execution is applied to a target that is fully initialized, and therefore has consistent state (to the extent that this is required by the target). If your method execution is the one that triggers the initialization, then it all even happens in the same thread. There’s not much that can go wrong with a single method execution, but the framework does have to do some work to protect callers from state changes.

The bean initialization and removal (on refresh) inside @RefreshScope is protected by a synchronized block. In addition, a disposable bean (one with a destruction callback) is detected by RefreshScope and the proxy is created in a special way, so that the call to the destruction callback takes place within a WriteLock, whereas all other method access is protected with a ReadLock from the same ReadWriteLock. There is a single instance of the lock per bean (per proxy in other words). The implication is that callers of a method in a @RefreshScope bean can be sure that they have a single, stable instance of the target bean for the duration of the method execution, and it will not be destroyed, and its state probably changed, by the BeanFactory until after the method has finished execution.

NOTE: The interceptor that applies the lock has to be inserted into the proxy before the interceptor that handles the target method calls, so that there is no window where the scope has started the destruction but the lock has not yet been acquired (there is a bug in older versions of Spring Cloud that has that flaw, but actually no-one noticed).

@ConfigurationProperties beans in @RefreshScope get special treatment by Spring Cloud (as mentioned already), so they behave just as any other @RefreshScope bean, including the callback to bind to the Environment, which comes from the Spring Boot ConfigurationPropertiesBindingPostProcessor not from the ConfigurationPropertiesRebinder.