I've a BeanDefinitionRegistryPostProcessor class that registers beans dynamically. Sometimes, the beans being registered have the Spring Cloud annotation @RefreshScope. However, when the cloud configuration Environment is changed, such beans are not being refreshed. Upon debugging, the appropriate application events are triggered, however, the dynamic beans don't get reinstantiated. Need some help around this. Below is my code:

TestDynaProps:

public class TestDynaProps {

    private String prop;

    private String value;

    public String getProp() {
        return prop;
    }

    public void setProp(String prop) {
        this.prop = prop;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("TestDynaProps [prop=").append(prop).append(", value=").append(value).append("]");
        return builder.toString();
    }

}

TestDynaPropConsumer:

@RefreshScope
public class TestDynaPropConsumer {

    private TestDynaProps props;

    public void setProps(TestDynaProps props) {
        this.props = props;
    }

    @PostConstruct
    public void init() {
        System.out.println("Init props : " + props);
    }

    public String getVal() {
        return props.getValue();
    }

}

BeanDefinitionRegistryPostProcessor:

public class PropertyBasedDynamicBeanDefinitionRegistrar implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {

    private ConfigurableEnvironment environment;

    private final Class<?> propertyConfigurationClass;

    private final String propertyBeanNamePrefix;

    private final String propertyKeysPropertyName;

    private Class<?> propertyConsumerBean;

    private String consumerBeanNamePrefix;

    private List<String> dynaBeans;

    public PropertyBasedDynamicBeanDefinitionRegistrar(Class<?> propertyConfigurationClass,
        String propertyBeanNamePrefix, String propertyKeysPropertyName) {
        this.propertyConfigurationClass = propertyConfigurationClass;
        this.propertyBeanNamePrefix = propertyBeanNamePrefix;
        this.propertyKeysPropertyName = propertyKeysPropertyName;
        dynaBeans = new ArrayList<>();
    }

    public void setPropertyConsumerBean(Class<?> propertyConsumerBean, String consumerBeanNamePrefix) {
        this.propertyConsumerBean = propertyConsumerBean;
        this.consumerBeanNamePrefix = consumerBeanNamePrefix;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = (ConfigurableEnvironment) environment;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory arg0) throws BeansException {

    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefRegistry) throws BeansException {
        if (environment == null) {
            throw new BeanCreationException("Environment must be set to initialize dyna bean");
        }
        String[] keys = getPropertyKeys();
        Map<String, String> propertyKeyBeanNameMapping = new HashMap<>();
        for (String k : keys) {
            String trimmedKey = k.trim();
            String propBeanName = getPropertyBeanName(trimmedKey);
            registerPropertyBean(beanDefRegistry, trimmedKey, propBeanName);
            propertyKeyBeanNameMapping.put(trimmedKey, propBeanName);
        }
        if (propertyConsumerBean != null) {
            String beanPropertyFieldName = getConsumerBeanPropertyVariable();
            for (Map.Entry<String, String> prop : propertyKeyBeanNameMapping.entrySet()) {
                registerConsumerBean(beanDefRegistry, prop.getKey(), prop.getValue(), beanPropertyFieldName);
            }
        }
    }

    private void registerConsumerBean(BeanDefinitionRegistry beanDefRegistry, String trimmedKey, String propBeanName, String beanPropertyFieldName) {
        String consumerBeanName = getConsumerBeanName(trimmedKey);
        AbstractBeanDefinition consumerDefinition = preparePropertyConsumerBeanDefinition(propBeanName, beanPropertyFieldName);
        beanDefRegistry.registerBeanDefinition(consumerBeanName, consumerDefinition);
        dynaBeans.add(consumerBeanName);
    }

    private void registerPropertyBean(BeanDefinitionRegistry beanDefRegistry, String trimmedKey, String propBeanName) {
        AbstractBeanDefinition propertyBeanDefinition = preparePropertyBeanDefinition(trimmedKey);
        beanDefRegistry.registerBeanDefinition(propBeanName, propertyBeanDefinition);
        dynaBeans.add(propBeanName);
    }

    private String getConsumerBeanPropertyVariable() throws IllegalArgumentException {
        Field[] beanFields = propertyConsumerBean.getDeclaredFields();
        for (Field bField : beanFields) {
            if (bField.getType().equals(propertyConfigurationClass)) {
                return bField.getName();
            }
        }
        throw new BeanCreationException(String.format("Could not find property of type %s in bean class %s",
            propertyConfigurationClass.getName(), propertyConsumerBean.getName()));
    }

    private AbstractBeanDefinition preparePropertyBeanDefinition(String trimmedKey) {
        BeanDefinitionBuilder bdb = BeanDefinitionBuilder.genericBeanDefinition(PropertiesConfigurationFactory.class);
        bdb.addConstructorArgValue(propertyConfigurationClass);
        bdb.addPropertyValue("propertySources", environment.getPropertySources());
        bdb.addPropertyValue("conversionService", environment.getConversionService());
        bdb.addPropertyValue("targetName", trimmedKey);
        return bdb.getBeanDefinition();
    }

    private AbstractBeanDefinition preparePropertyConsumerBeanDefinition(String propBeanName, String beanPropertyFieldName) {
        BeanDefinitionBuilder bdb = BeanDefinitionBuilder.genericBeanDefinition(propertyConsumerBean);
        bdb.addPropertyReference(beanPropertyFieldName, propBeanName);
        return bdb.getBeanDefinition();
    }

    private String getPropertyBeanName(String trimmedKey) {
        return propertyBeanNamePrefix + trimmedKey.substring(0, 1).toUpperCase() + trimmedKey.substring(1);
    }

    private String getConsumerBeanName(String trimmedKey) {
        return consumerBeanNamePrefix + trimmedKey.substring(0, 1).toUpperCase() + trimmedKey.substring(1);
    }

    private String[] getPropertyKeys() {
        String keysProp = environment.getProperty(propertyKeysPropertyName);
        return keysProp.split(",");
    }

The Config class:

@Configuration
public class DynaPropsConfig {

    @Bean
    public PropertyBasedDynamicBeanDefinitionRegistrar dynaRegistrar() {
        PropertyBasedDynamicBeanDefinitionRegistrar registrar = new PropertyBasedDynamicBeanDefinitionRegistrar(TestDynaProps.class, "testDynaProp", "dyna.props");
        registrar.setPropertyConsumerBean(TestDynaPropConsumer.class, "testDynaPropsConsumer");
        return registrar;
    }
}

Application.java

@SpringBootApplication
@EnableDiscoveryClient
@EnableScheduling
public class Application extends SpringBootServletInitializer {

    private static Class<Application> applicationClass = Application.class;

    public static void main(String[] args) {
        SpringApplication sa = new SpringApplication(applicationClass);             
        sa.run(args);
    }
}

And, my bootstrap.properties:

spring.cloud.consul.enabled=true
spring.cloud.consul.config.enabled=true
spring.cloud.consul.config.format=PROPERTIES
spring.cloud.consul.config.watch.delay=15000
spring.cloud.discovery.client.health-indicator.enabled=false
spring.cloud.discovery.client.composite-indicator.enabled=false

application.properties

dyna.props=d1,d2

d1.prop=d1prop
d1.value=d1value
d2.prop=d2prop
d2.value=d2value
2

There are 2 answers

1
Shailesh On BEST ANSWER

We finally resolved this by appending the @RefreshScope annotation on the proposed dynamic bean classes using ByteBuddy and then, adding them to Spring Context using Bean Definition Post Processor. The Post Processor is added to spring.factories so that it loads before any other dynamic bean dependent beans.

0
Ken Krueger On

Here are some guesses:

1) Perhaps the @RefreshScope metadata is not being passed to your metadata for the bean definition. Call setScope()?

2) The RefreshScope is actually implemented by https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java, which itself implements BeanDefinitionRegistryPostProcessor. Perhaps the ordering of these two post processors is issue.

Just guesses.