2

I'm running a PoC around replacing bean injection at runtime after a ConfigurationProperties has changed. This is based on spring boot dynamic configuration properties support as well summarised here by Dave Syer from Pivotal.

In my application I have a simple interface implemented by two different concrete classes:

@Component @RefreshScope @ConditionalOnExpression(value = "'${config.dynamic.context.country}' == 'it'") public class HelloIT implements HelloService { @Override public String sayHello() { return "Ciao dall'italia"; } } 

and

@Component @RefreshScope @ConditionalOnExpression(value = "'${config.dynamic.context.country}' == 'us'") public class HelloUS implements HelloService { @Override public String sayHello() { return "Hi from US"; } } 

application.yaml served by spring cloud config server is:

config: name: Default App dynamic: context: country: us 

and the related ConfigurationProperties class:

@Configuration @ConfigurationProperties (prefix = "config.dynamic") public class ContextHolder { private Map<String, String> context; Map<String, String> getContext() { return context; } public void setContext(Map<String, String> context) { this.context = context; } 

My client app entrypoint is:

@SpringBootApplication @RestController @RefreshScope public class App1Application { @Autowired private HelloService helloService; @RequestMapping("/hello") public String hello() { return helloService.sayHello(); } 

First time I browse http://locahost:8080/hello endpoint it returns "Hi from US"

After that I change country: us in country: it in application.yaml in spring config server, and then hit the actuator/refresh endpoint ( on the client app).

Second time I browse http://locahost:8080/hello it stills returns "Hi from US" instead of "ciao dall'italia" as I would expect.

Is this use case supported in spring boot 2 when using @RefreshScope? In particular I'm referring to the fact of using it along with @Conditional annotations.

5
  • Relevant open GitHub issue: github.com/spring-cloud/spring-cloud-config/issues/1089 and SO question: stackoverflow.com/questions/46271052/… Commented Aug 24, 2018 at 16:24
  • 1
    @ConditionalOn* annotations are meant to be used in @Configuration classes, not on beans directly. Configuration aren't re-run at refresh scope, just the individual beans are recreated. I would inject the @ConfigurationProperties into a single bean to get the desired effect. @RefreshScope also behaves badly on @Configuration classes, it is meant only for individual beans. Commented Aug 24, 2018 at 16:35
  • @spencergibb I'm not sure I understood how you would get the same effect injecting the @ConfigurationProperties. Looking at my example, are you saying to inject it into App1Application bean ( which has @RefreshScope annotation) and then using it to select the desired HelloService implementation programatically ( using for example applicationContext.getBean() ) ? Commented Aug 24, 2018 at 20:50
  • Refresh scope can't change which bean is loaded, it can only re initialize a bean Commented Aug 25, 2018 at 3:34
  • @spencergibb ok, so it's clear that this is the wrong path for replacing spring bean implementation at runtime based on configuration changes. The purpose of this PoC was to select business logic at runtime based on context changes. I think I will investigate about creating a custom Annotation which serve as pointcut for a custom Advice which, in turn, will use ConfigurationProperties (the context) for selecting at runtime different bean methods. Something similar to the @Flip annotation used by ff4j feature flag framework Commented Aug 25, 2018 at 10:39

1 Answer 1

1

This implementation worked for me:

@Component @RefreshScope public class HelloDelegate implements HelloService { @Delegate // lombok delegate (for the sake of brevity) private final HelloService delegate; public HelloDelegate( // just inject value from Spring configuration @Value("${country}") String country ) { HelloService impl = null; switch (country) { case "it": this.delegate = new HelloIT(); break; default: this.delegate = new HelloUS(); break; } } } 

It works the following way:

  1. When first invocation of service method happens Spring creates bean HelloDelegate with configuration effective at that moment; bean is put into refresh scope cache
  2. Because of @RefreshScope whenever configuration is changed (country property particularly in this case) HelloDelegate bean gets cleared from refresh scope cache
  3. When next invocation happens, Spring has to create bean again because it does not exist in cache, so step 1 is repeated with new country property

As far as I watched the behavior of this implementation, Spring will try to avoid recreating RefreshScope bean if it's configuration was untouched.


I was looking for more generic solution of doing such "runtime" implementation replacement when found this question. This implementation has one significant disadvantage: if delegated beans have complex non-homogeneous configuration (e.g. each bean has it's own properties) code becomes lousy and therefore unsafe.

I use this approach to provide additional testability for artifacts. So that QA would be able to switch between stub and real integration without significant efforts. I would strongly recommend to avoid using such approach for business functionality.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.