ConfigurationPropertiesRebinder.java
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.context.properties;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.target.SingletonTargetSource;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration.RefreshProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.util.ProxyUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.Environment;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* Listens for {@link EnvironmentChangeEvent} and rebinds beans that were bound to the
* {@link Environment} using {@link ConfigurationProperties
* <code>@ConfigurationProperties</code>}. When these beans are re-bound and
* re-initialized, the changes are available immediately to any component that is using
* the <code>@ConfigurationProperties</code> bean.
*
* @author Dave Syer
* @author Yanming Zhou
* @see RefreshScope for a deeper and optionally more focused refresh of bean components.
*
*/
@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
private static final Log logger = LogFactory.getLog(ConfigurationPropertiesRebinder.class);
private ConfigurationPropertiesBeans beans;
private ApplicationContext applicationContext;
private Map<String, Exception> errors = new ConcurrentHashMap<>();
private final Set<String> neverResetNestedTypes;
public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
this(beans, Collections.emptySet());
}
public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans, RefreshProperties refreshProperties) {
this(beans, refreshProperties.getNeverResetNestedTypes());
}
private ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans, Set<String> neverResetNestedTypes) {
this.beans = beans;
this.neverResetNestedTypes = neverResetNestedTypes;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* A map of bean name to errors when instantiating the bean.
* @return The errors accumulated since the latest destroy.
*/
public Map<String, Exception> getErrors() {
return this.errors;
}
@ManagedOperation
public void rebind() {
this.errors.clear();
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
@ManagedOperation
public boolean rebind(String name) {
if (!this.beans.getBeanNames().contains(name)) {
return false;
}
ApplicationContext appContext = this.applicationContext;
while (appContext != null) {
if (appContext.containsLocalBean(name)) {
return rebind(name, appContext);
}
else {
appContext = appContext.getParent();
}
}
return false;
}
/**
* WARNING: This method rebinds beans from any context in the hierarchy using the main
* application context.
* @param type bean type to rebind.
* @return true, if successful.
*/
public boolean rebind(Class type) {
String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.applicationContext, type);
if (beanNamesForType.length > 0) {
String name = beanNamesForType[0];
if (ScopedProxyUtils.isScopedTarget(name)) {
name = ScopedProxyUtils.getOriginalBeanName(name);
}
return rebind(name, this.applicationContext);
}
return false;
}
private boolean rebind(String name, ApplicationContext appContext) {
try {
Object bean = appContext.getBean(name);
if (bean != null) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
// TODO: determine a more general approach to fix this.
// see
// https://github.com/spring-cloud/spring-cloud-commons/issues/571
if (getNeverRefreshable().contains(targetClass.getName()) || getNeverRefreshable().contains(name)) {
return false; // ignore
}
if (AopUtils.isAopProxy(bean) && bean instanceof Advised advised) {
Object target = ProxyUtils.getTargetObject(bean);
if (target != bean && !targetClass.isInterface()
&& !Modifier.isAbstract(targetClass.getModifiers())) {
Object freshBean = appContext.getAutowireCapableBeanFactory().createBean(targetClass);
Object freshTarget = AopUtils.isAopProxy(freshBean) ? ProxyUtils.getTargetObject(freshBean)
: freshBean;
advised.setTargetSource(new SingletonTargetSource(freshTarget));
appContext.getAutowireCapableBeanFactory().destroyBean(target);
}
else {
appContext.getAutowireCapableBeanFactory().destroyBean(target);
resetBeanToDefaults(target);
appContext.getAutowireCapableBeanFactory().initializeBean(target, name);
}
}
else {
appContext.getAutowireCapableBeanFactory().destroyBean(bean);
resetBeanToDefaults(bean);
appContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
}
return true;
}
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
catch (Exception e) {
this.errors.put(name, e);
throw new IllegalStateException("Cannot rebind to " + name, e);
}
return false;
}
/**
* Reset bean properties to their class-level defaults so that removed properties do
* not retain stale values after rebinding.
*/
private void resetBeanToDefaults(Object bean) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
if (!hasDefaultConstructor(targetClass)) {
// Beans that have no default constructor (for example constructor-bound beans
// or beans with required dependencies) cannot be instantiated to obtain their
// defaults, so the reset is skipped. The bean is still re-bound from the
// Environment afterwards; only reverting removed properties to their defaults
// is skipped.
if (logger.isDebugEnabled()) {
logger.debug("No default constructor for " + targetClass.getName()
+ "; skipping property reset before rebinding");
}
return;
}
Object freshInstance;
try {
freshInstance = BeanUtils.instantiateClass(targetClass);
}
catch (Exception ex) {
logger.warn("Cannot create default instance of " + targetClass.getName()
+ " for reset; skipping property reset", ex);
return;
}
resetProperties(bean, freshInstance, Collections.newSetFromMap(new IdentityHashMap<>()));
}
/**
* Whether the given type declares a no-argument constructor (of any visibility),
* which is what {@link BeanUtils#instantiateClass(Class)} needs to build a defaults
* template.
*/
private boolean hasDefaultConstructor(Class<?> type) {
for (Constructor<?> constructor : type.getDeclaredConstructors()) {
if (constructor.getParameterCount() == 0) {
return true;
}
}
return false;
}
private void resetProperties(Object bean, Object defaults, Set<Object> visited) {
// Guard against cyclic object graphs so that recursion always terminates.
if (bean == null || !visited.add(bean)) {
return;
}
BeanWrapper target = new BeanWrapperImpl(bean);
BeanWrapper defaultsWrapper = new BeanWrapperImpl(defaults);
for (PropertyDescriptor pd : target.getPropertyDescriptors()) {
String propertyName = pd.getName();
if ("class".equals(propertyName)) {
continue;
}
try {
if (target.isWritableProperty(propertyName) && defaultsWrapper.isReadableProperty(propertyName)) {
Object defaultValue = defaultsWrapper.getPropertyValue(propertyName);
target.setPropertyValue(propertyName, defaultValue);
}
else if (target.isReadableProperty(propertyName) && defaultsWrapper.isReadableProperty(propertyName)) {
Object value = target.getPropertyValue(propertyName);
Object defaultValue = defaultsWrapper.getPropertyValue(propertyName);
if (value instanceof Collection collection) {
collection.clear();
if (defaultValue instanceof Collection defaultCollection) {
collection.addAll(defaultCollection);
}
}
else if (value instanceof Map map) {
map.clear();
if (defaultValue instanceof Map defaultMap) {
map.putAll(defaultMap);
}
}
else if (value != null && defaultValue != null && isResettableNestedType(value.getClass())) {
resetProperties(value, defaultValue, visited);
}
}
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to reset property '" + propertyName + "' on "
+ AopUtils.getTargetClass(bean).getName(), ex);
}
}
}
}
/**
* Determine whether a nested property value should be recursively reset. Only
* user-defined types are descended into. Recursing into JDK or standard API types
* (for example {@link javax.net.ssl.SSLContext}) is both unnecessary and unsafe:
* their object graphs may be cyclic, which previously led to a
* {@link StackOverflowError} (see gh-1698), and they may expose internal collections
* or maps that must not be cleared. Additional types can be excluded via the
* {@code spring.cloud.refresh.never-reset-nested-types} property.
*/
boolean isResettableNestedType(Class<?> type) {
if (type.isArray() || BeanUtils.isSimpleValueType(type) || isJdkClass(type) || isStandardApiClass(type)) {
return false;
}
String typeName = type.getName();
for (String excluded : this.neverResetNestedTypes) {
if (typeName.startsWith(excluded)) {
return false;
}
}
return true;
}
/**
* Whether the given type is provided by the JDK itself. JDK types are loaded either
* by the bootstrap class loader (for example everything in {@code java.base}, which
* reports a {@code null} class loader) or by the platform class loader (the other
* platform modules); application and library types are loaded by the application
* class loader.
*/
private boolean isJdkClass(Class<?> type) {
ClassLoader classLoader = type.getClassLoader();
return classLoader == null || classLoader == ClassLoader.getPlatformClassLoader();
}
/**
* Whether the given type belongs to a standard API namespace (Jakarta EE or the
* legacy {@code javax} packages). These namespaces are reserved for specifications
* and, unlike the JDK modules, are loaded by the application class loader, so they
* are not detected by {@link #isJdkClass}.
*/
private boolean isStandardApiClass(Class<?> type) {
String packageName = type.getPackageName();
return packageName.startsWith("jakarta.") || packageName.startsWith("javax.");
}
@ManagedAttribute
public Set<String> getNeverRefreshable() {
String neverRefresh = this.applicationContext.getEnvironment()
.getProperty("spring.cloud.refresh.never-refreshable", "com.zaxxer.hikari.HikariDataSource");
return StringUtils.commaDelimitedListToSet(neverRefresh);
}
@ManagedAttribute
public Set<String> getBeanNames() {
return new HashSet<>(this.beans.getBeanNames());
}
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
rebind();
}
}
}