skip to content

Walk through the events fired by ContextRefresher during /actuator/refresh: EnvironmentChangeEvent vs RefreshScopeRefreshedEvent — what each carries and their order.

level: seniorimportance: should knowfreq 40%

answer

  1. ContextRefresher orchestrates
  2. EnvironmentChangeEvent(keys) FIRST
  3. RefreshScopeRefreshedEvent(no keys) SECOND
  4. rebinder + LoggingRebinder listen to EnvChange
  5. refreshAll() evicts, lazy re-create

basics

~20 s

First the Environment is reloaded and an EnvironmentChangeEvent is published carrying the set of changed property keys. Then the refresh scope is cleared and a RefreshScopeRefreshedEvent is published to signal that refresh-scoped beans were evicted.

solid answer

~40 s

ContextRefresher.refresh() runs in a fixed order. It snapshots current properties, rebuilds the Environment, and computes the changed keys. It then publishes EnvironmentChangeEvent(context, changedKeys) — this is the event ConfigurationPropertiesRebinder and any custom listeners use to react to specific property changes; it carries the Set<String> of keys that changed. After that it calls refreshScope.refreshAll(), which evicts all refresh-scoped beans and publishes RefreshScopeRefreshedEvent — a signal with no key payload, meaning 'the refresh scope has been cleared, beans will be rebuilt lazily.' So EnvironmentChangeEvent (keys) fires before RefreshScopeRefreshedEvent (no keys). You can listen to either: EnvironmentChangeEvent to react to a specific key, RefreshScopeRefreshedEvent to run logic after the scope reset (e.g., re-warm a cache).

code

java · 27 lines
java
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class RefreshListeners {

    // Fires FIRST — carries the changed property keys
    @EventListener
    public void onEnvChange(EnvironmentChangeEvent event) {
        if (event.getKeys().contains("app.pool.size")) {
            // react only to this specific property change
            reconfigurePool();
        }
    }

    // Fires SECOND — no keys, signals the refresh scope was cleared
    @EventListener
    public void onScopeRefreshed(RefreshScopeRefreshedEvent event) {
        // e.g. re-warm caches now that refresh-scoped beans were evicted
        rewarmCaches();
    }

    private void reconfigurePool() { /* ... */ }
    private void rewarmCaches() { /* ... */ }
}

go deeper

for a junior

Just know two events exist and refresh reloads config then clears scoped beans.

for a middle

State the order and that EnvironmentChangeEvent carries the changed keys.

for a senior

Name ContextRefresher, describe the full sequence and which listeners react to which event.

for a principal

Discuss synchronous listener execution risks and using the events as hooks for programmatic reconfiguration.

**The orchestration.** `RefreshEndpoint` (the `@Endpoint(id = "refresh")`) simply delegates to `ContextRefresher.refresh()`. Its sequence: 1. **Snapshot** — capture the current values of relevant property sources (`before`). 2. **Rebuild the Environment** — `updateEnvironment()` creates a throwaway Spring Boot context to re-read all property sources (files, Config Server, env, etc.) and copies the resulting property sources back into the live `Environment`. 3. **Diff** — compute `changedKeys = changes(before, after)`. 4. **Publish `EnvironmentChangeEvent`** — `this.context.publishEvent(new EnvironmentChangeEvent(this.context, changedKeys))`. This event **carries `getKeys()` : Set<String>** of the property names whose values changed. Listeners: - `ConfigurationPropertiesRebinder` — rebinds `@ConfigurationProperties` beans. - `LoggingRebinder` — applies changed `logging.level.*`. - Any custom `ApplicationListener<EnvironmentChangeEvent>` you write. 5. **Refresh the scope** — `this.scope.refreshAll()` on the `RefreshScope` bean: it **destroys/evicts every bean in the `refresh` scope** (calling their `@PreDestroy`/`DisposableBean`) so they'll be lazily recreated, then **publishes `RefreshScopeRefreshedEvent`** — a plain signal with **no changed-keys payload**. 6. **Return** — the endpoint returns `changedKeys` as the HTTP response body. **So the order is: EnvironmentChangeEvent first, RefreshScopeRefreshedEvent second.** Environment is fully updated before either event fires, so listeners always see new values. **Which to listen to.** - `EnvironmentChangeEvent`: react to a *specific* property change — inspect `event.getKeys()` and act only if your key is present. Good for programmatic reconfiguration (e.g., adjust a thread pool size). - `RefreshScopeRefreshedEvent`: react to the *fact that beans were evicted* — e.g., re-prime a lazily-built cache, log an audit entry, or trigger dependent recomputation. It has no keys, so use it when you don't care which properties changed. **Gotchas / edge cases.** - If **nothing changed**, `EnvironmentChangeEvent` is still published but with an **empty key set**, and `refreshAll()` still evicts refresh-scoped beans — so a refresh is never a strict no-op for refresh-scoped beans (they'll be rebuilt on next use even if no property changed). - Listeners run **synchronously** on the thread handling the POST; a slow or throwing listener slows/breaks the refresh call. - Ordering between listeners of the same event follows normal Spring `@Order`/`Ordered` semantics. - Re-creation of refresh-scoped beans is still **lazy** after `refreshAll()`; the `RefreshScopeRefreshedEvent` fires at eviction time, not at re-creation time. **Out of scope here:** propagating a refresh to *other instances* (via a message broker) is `RefreshRemoteApplicationEvent`/Spring Cloud Bus — a sibling topic.

  • Which event does ConfigurationPropertiesRebinder listen to?
    EnvironmentChangeEvent. It rebinds @ConfigurationProperties beans when the Environment changes; it does not use RefreshScopeRefreshedEvent.
  • If no properties changed, do the events still fire?
    Yes. EnvironmentChangeEvent fires with an empty key set and refreshAll() still evicts refresh-scoped beans, so they're rebuilt on next access regardless.

saying these in an interview costs you the question

  • Reversing the order (claiming RefreshScopeRefreshedEvent fires before EnvironmentChangeEvent)
  • Thinking RefreshScopeRefreshedEvent carries the changed keys
  • Believing events fire before the Environment is updated, so listeners see stale values

context