Use Spring @RefreshScope, @Conditional annotations

2020-07-29 17:47发布

问题:

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.

回答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.