Skip to content
Chapter 13 · Microservices

@RefreshScope: Reloading Configuration at Runtime ​

Chapter 12 left one problem open. order-service reads its properties from the config server at startup. Change a value in the configuration repository while it runs, and it keeps the old one. Do we have to restart every service after every configuration change? No.

Step 1: reproduce the problem ​

The setup from chapter 12: the configuration repository, the config server on 8888, and order-service as a config client with profile dev. In the repository, order-service-dev.properties contains:

properties
custom.message=Hello from order dev

This time order-service reads the value through a @ConfigurationProperties class rather than @Value (the reason comes at the end of the chapter):

java
@Component
@ConfigurationProperties(prefix = "custom")
public class OrderProperties {

    private String message;                 // bound from custom.message

    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
}
java
@RestController
@RequestMapping("/orders")
public class OrderController {

    private int counter = 0;

    @Autowired
    OrderProperties orderProperties;

    @GetMapping("/fetch")
    public String getOrders() {
        counter++;
        return "fetched orders and message: " + orderProperties.getMessage()
                + " and counter value is: " + counter;
    }
}

The counter shows whether the controller object survives; ignore it for now.

text
GET /orders/fetch  ->  ... Hello from order dev and counter value is: 1

Now commit a change to the repository: custom.message=Hello from order dev UPDATED.

text
GET /orders/fetch  ->  ... Hello from order dev and counter value is: 2

Still the old value. The config server already serves the new one (GET localhost:8888/order-service-dev.properties shows UPDATED), but order-service fetched its properties at startup and never asked again.

Step 2: the refresh endpoint ​

Something has to tell order-service to fetch again. That is a refresh event, and the usual trigger is an Actuator endpoint. (Actuator, including custom endpoints, is covered in the Spring Boot book.)

Spring Cloud defines a custom Actuator endpoint, /actuator/refresh:

java
@Endpoint(id = "refresh")                       // URL: /actuator/refresh
public class RefreshEndpoint {

    private final ContextRefresher contextRefresher;

    public RefreshEndpoint(ContextRefresher contextRefresher) {
        this.contextRefresher = contextRefresher;
    }

    @WriteOperation                             // invoked with POST
    public Collection<String> refresh() {
        Set<String> keys = this.contextRefresher.refresh();
        return keys;                            // the property keys that changed
    }
}

In a config client, contextRefresher.refresh():

  1. fetches the properties from the config server again and updates the environment;
  2. rebinds every @ConfigurationProperties bean to the new values;
  3. destroys every bean in the refresh scope, so it is recreated with the new values on next use.

To use it, add Actuator to order-service:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

and expose the endpoint (Actuator exposes only health over HTTP by default):

properties
management.endpoints.web.exposure.include=health,refresh

Step 3: refresh ​

Call it with POST (a @WriteOperation; a GET returns 405):

text
POST localhost:8081/actuator/refresh   ->  ["config.client.version","custom.message"]

The response lists the keys that changed: the message, and the config version (the new Git commit). Now:

text
GET /orders/fetch  ->  ... Hello from order dev UPDATED and counter value is: 3

The new value, without a restart. The counter kept counting (3), so the controller is the same object as before.

The config server itself needs no refresh for this. With a Git repository, it checks the repository for new commits whenever a client asks, so it already served the new value. Calling /actuator/refresh on the config server (with Actuator added there too) only refreshes the server's own configuration.

What gets updated, and what does not ​

Three ways of reading the same property, observed after one refresh:

BeanAfter POST /actuator/refresh
@ConfigurationProperties classupdated (rebound in place)
@Value field in a normal beanold value
@Value field in a @RefreshScope beanupdated (bean recreated)

@ConfigurationProperties beans are rebound on every refresh, with or without @RefreshScope. That is why the example worked without the annotation.

A @Value field is injected once, when the bean is created. To change it, the bean must be recreated, and that is what @RefreshScope is for.

@RefreshScope ​

@RefreshScope marks a bean whose instance is destroyed on each refresh and recreated, with the current values, the next time it is used:

java
@Component
@RefreshScope
public class MessageHolder {

    @Value("${custom.message}")
    private String message;

    public String getMessage() { return message; }
}

Other beans do not hold the real instance. They hold a proxy, which forwards each call to the current instance and lets the scope swap that instance on refresh. That has one consequence: always go through methods, never fields. messageHolder.getMessage() reaches the current instance; reading messageHolder.message directly reads the proxy's own empty field and gives null.

Not on controllers or services ​

Recreating a bean throws away its state. Put @RefreshScope on the controller from Step 1, using @Value directly:

java
@RestController
@RequestMapping("/orders")
@RefreshScope
public class OrderController {

    private int counter = 0;

    @Value("${custom.message}")
    String message;

    @GetMapping("/fetch")
    public String getOrders() {
        counter++;
        return "fetched orders and message: " + message + " and counter value is: " + counter;
    }
}
text
GET /orders/fetch            ->  ... counter value is: 1
GET /orders/fetch            ->  ... counter value is: 2
POST /actuator/refresh
GET /orders/fetch            ->  ... counter value is: 1      <- a new controller object

Even when no value changed, the refresh replaced the controller and its state was lost. In a real controller or service, whatever the bean held (caches, counters, connections) is gone, and requests in flight while it is torn down can fail.

So keep @RefreshScope (and changing configuration in general) on stateless beans that only hold configuration. A @ConfigurationProperties class is exactly that, and it is refreshed without any annotation, which is why this chapter reads configuration through one.

One refresh per service? ​

Every instance of every service must receive its own POST /actuator/refresh. With hundreds of instances, that is impractical. It would be better to trigger one refresh and have every service hear about it.

Next: Chapter 14 does that with Spring Cloud Bus: one refresh call, broadcast to every service over a message broker.

Check yourself ​

  1. Why does a running service not see a value changed in the configuration repository?
  2. What does /actuator/refresh do in a config client?
  3. Which HTTP method does it need, and why?
  4. What must you add and configure to use it?
  5. Does the config server need its own refresh before clients refresh? Why?
  6. After a refresh, which are updated: a @ConfigurationProperties bean, a plain @Value field, a @Value field in a @RefreshScope bean?
  7. What does @RefreshScope do to a bean on refresh?
  8. Why can reading a field of a @RefreshScope bean directly give null?
  9. Why should controllers and services not be @RefreshScope?
  10. What is the drawback of refreshing through Actuator in a large system?
Answers
  1. It fetched its properties once at startup and does not ask the config server again on its own.
  2. It fetches the properties from the config server again, rebinds @ConfigurationProperties beans, destroys refresh-scoped beans so they are recreated with new values, and returns the changed keys.
  3. POST: it is an Actuator @WriteOperation; a GET returns 405.
  4. spring-boot-starter-actuator, and management.endpoints.web.exposure.include including refresh.
  5. No. With a Git backend the server checks the repository for new commits on each client request; refreshing it only refreshes the server's own configuration.
  6. The @ConfigurationProperties bean (rebound) and the @RefreshScope bean (recreated); the plain @Value field keeps its old value.
  7. It destroys the instance; a new one is created with current values on next use. Callers hold a proxy that forwards to the current instance.
  8. Callers hold a proxy; its fields are not the real instance's fields. Only method calls are forwarded.
  9. Recreating them discards their state, and requests being handled during the swap can fail.
  10. Every instance of every service must be called separately.