Walk through the events fired by ContextRefresher during /actuator/refresh: EnvironmentChangeEvent vs RefreshScopeRefreshedEvent — what each carries and their order.
answer
- ContextRefresher orchestrates
- EnvironmentChangeEvent(keys) FIRST
- RefreshScopeRefreshedEvent(no keys) SECOND
- rebinder + LoggingRebinder listen to EnvChange
- refreshAll() evicts, lazy re-create
basics
~20 sFirst 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 sContextRefresher.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 linesimport 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
Just know two events exist and refresh reloads config then clears scoped beans.
State the order and that EnvironmentChangeEvent carries the changed keys.
Name ContextRefresher, describe the full sequence and which listeners react to which event.
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