Lifecycle and SmartLifecycle
The lifecycle interfaces let a bean run background work between a start and a stop, and run setup and cleanup at fixed points of the container's life.
Why
There are two kinds of callbacks.
Per bean, around its creation and destruction:
InitializingBean.afterPropertiesSet()runs once the bean is injected. @PostConstruct does the same without an interface.DisposableBean.destroy()runs when the bean is destroyed. @PreDestroy does the same without an interface.SmartInitializingSingleton.afterSingletonsInstantiated()runs once every non-lazy singleton exists. Use it for work that needs the other beans, such as registering every handler in a registry.
In application code the annotations are usually simpler. The interfaces suit framework classes and hand-written definitions.
Background work, between a start and a stop:
SmartLifecycleis the usual choice: a client that connects at startup, a scheduler, a server. The context starts it at the end ofrefresh()and stops it at shutdown.Lifecycleis the plain form. It is not started at refresh, only by an explicitcontext.start(), or when an auto-startup bean depends on it. It is still stopped bystop(),pause()andclose().
Only singletons take part. A lazy plain Lifecycle bean takes part only once it was created.
Phases
SmartLifecycle.phase orders the beans. Lower phases start first and stop last, so a bean that others need gets a lower phase. The default, SmartLifecycle.defaultPhase, is the highest value: the bean starts after all others and stops before them. Plain Lifecycle beans count as phase 0. A bean's dependencies start before it and its dependents stop before it, whatever their phase. Within one phase, beans start in registration order and stop in reverse order.
Stopping asynchronously
stopWithCallback(callback) stops the bean and calls callback once it has really stopped, for example when a socket is closed. closeAsync() and pauseAsync() wait for the callbacks of one phase before stopping the next, at most 10 seconds per phase by default. The synchronous close() and pause() do not wait.
Pausing
context.pause() stops the running lifecycle beans but keeps every bean; context.restart() starts the auto-startup beans again. A Flutter app calls them when it goes to the background and comes back. A SmartLifecycle bean whose isPauseable is false keeps running during a pause.
The whole order
At startup (refresh()):
- each singleton is created in dependency order: constructor, injection, aware callbacks,
@PostConstruct,afterPropertiesSet, init method; afterSingletonsInstantiatedon everySmartInitializingSingleton, in registration order;start()on the auto-startupSmartLifecyclebeans, lowest phase first;ContextRefreshedEvent.
At shutdown (close() or closeAsync()):
ContextClosedEvent;stop()orstopWithCallback()on the running lifecycle beans, highest phase first;- each singleton is destroyed after the beans that depend on it:
@PreDestroy,destroy(), destroy method.
afterPropertiesSet and destroy may return a Future. refreshAsync() and closeAsync() wait for it. The synchronous refresh() fails for an asynchronous init callback; the synchronous close() logs an asynchronous destroy callback and goes on.
Example
import 'dart:async';
import 'dart:io';
import 'package:spring_core_dart/spring_core_dart.dart';
abstract interface class Handler {
String get topic;
}
// Registers every handler bean once all of them exist.
@Service()
class HandlerRegistry implements SmartInitializingSingleton {
HandlerRegistry(this._handlers);
final ObjectProvider<Handler> _handlers;
final byTopic = <String, Handler>{};
@override
void afterSingletonsInstantiated() {
for (final handler in _handlers) {
byTopic[handler.topic] = handler;
}
}
}
// Connects when the application starts and disconnects at shutdown,
// before the beans in lower phases stop.
@Component()
class FeedClient with SmartLifecycle {
WebSocket? _socket;
@override
int get phase => 10;
@override
void start() {
unawaited(
WebSocket.connect('wss://feed.example.com').then((s) => _socket = s),
);
}
@override
void stop() => _socket?.close();
// closeAsync() waits until the socket is closed.
@override
void stopWithCallback(void Function() callback) {
final socket = _socket;
_socket = null;
if (socket == null) {
callback();
} else {
socket.close().whenComplete(callback);
}
}
@override
bool get isRunning => _socket != null;
}
// Polls every 5 seconds, only after an explicit context.start().
@Component()
class Poller implements Lifecycle {
Timer? _timer;
@override
void start() =>
_timer = Timer.periodic(const Duration(seconds: 5), (_) => poll());
@override
void stop() => _timer?.cancel();
@override
bool get isRunning => _timer?.isActive ?? false;
void poll() {}
}
// A file closed when the application shuts down.
@Repository()
class Journal implements InitializingBean, DisposableBean {
late final IOSink _sink;
@override
void afterPropertiesSet() =>
_sink = File('journal.log').openWrite(mode: FileMode.append);
@override
Future<void> destroy() => _sink.close(); // awaited by closeAsync()
}
To change the shutdown timeouts, declare a lifecycleProcessor bean:
@Configuration()
class LifecycleConfig {
@Bean()
LifecycleProcessor lifecycleProcessor(
ConfigurableListableBeanFactory factory,
) => DefaultLifecycleProcessor(
factory,
timeoutPerShutdownPhase: const Duration(seconds: 30),
);
}
Members
| Interface | Member | Meaning |
|---|---|---|
Lifecycle |
void start() |
Starts the work. Not called when isRunning is true. An error fails the start as an ApplicationContextException. |
void stop() |
Stops the work. Called only when isRunning is true. An error is logged; the other beans still stop. |
|
bool get isRunning |
Whether the work is running. | |
SmartLifecycle |
bool get isAutoStartup |
true: started at the end of refresh(). |
int get phase |
SmartLifecycle.defaultPhase. Lower starts earlier and stops later. |
|
bool get isPauseable |
true: stopped by pause(). |
|
void stopWithCallback(void Function() callback) |
Calls stop(), then callback. Override for an asynchronous stop; call callback exactly once. |
|
InitializingBean |
FutureOr<void> afterPropertiesSet() |
Runs after @PostConstruct, before the init method. |
DisposableBean |
FutureOr<void> destroy() |
Runs after @PreDestroy, before the destroy method. |
SmartInitializingSingleton |
void afterSingletonsInstantiated() |
Runs once at the end of preInstantiateSingletons. Not called for a lazy singleton created later. |
SmartLifecycle is a mixin class: with SmartLifecycle gives its defaults. With implements, write every member.
DefaultLifecycleProcessor(beanFactory, {timeoutPerShutdownPhase, timeoutsForShutdownPhases}) takes a timeout per phase (10 seconds by default) and overrides for single phases.
In Spring (Java)
@Component
class FeedClient implements SmartLifecycle {
public void start() { ... }
public void stop() { ... }
public void stop(Runnable callback) { ... }
public boolean isRunning() { ... }
public int getPhase() { return 10; }
}
Differences that matter:
stop(Runnable)isstopWithCallback. Dart has no overloads.- Dart cannot block. The synchronous
close()does not wait for stop callbacks;closeAsync()does. afterPropertiesSetanddestroymay return aFuture, awaited byrefreshAsync()andcloseAsync().pause(),restart()andisPauseablefollow Spring 7.