The container and beans
The container is the part of spring_core_dart that creates the objects of your application, passes each one the other objects it needs, and shuts them down at the end.
An object the container manages is called a bean. You mark a class with an annotation such as @Service(). A build step (spring_core_dart_generator) reads the annotations and writes plain Dart code that creates the beans. At run time an ApplicationContext loads that code, creates every bean once and connects them.
Warning
Checked at startup: a missing or ambiguous bean, a cycle or an unknown placeholder stops the start with a message naming the bean and the parameter.
Why use a container
In a program of some size, main ends up building every service by hand, in the right order, and passing each one its dependencies. Every new constructor parameter means editing main and every test that builds the class. Choosing a fake implementation in development means an if somewhere.
With the container:
- a class declares what it needs in its constructor, and the container passes it in;
- the order of creation follows from the dependencies;
- settings come from configuration files and environment variables (
@Value); - implementations are switched by profile or condition, not by
ifinmain; - setup and cleanup (
@PostConstruct,@PreDestroy) run at the right time.
You do not need it for a script with two classes. It pays off once several services depend on each other, or when tests and environments need different implementations.
Dart in Flutter and AOT executables has no runtime reflection. That is why the annotations are read at build time: the generator writes the wiring code, and the container runs it. There is no scanning or reflection at run time.
A complete example
Add the packages:
dependencies:
spring_core_dart: ^0.7.1
dev_dependencies:
build_runner: ^2.17.0
spring_core_dart_generator: ^0.7.1
Declare where the beans are. Every class in the directory of this file and below is scanned:
// lib/src/beans.dart
import 'package:spring_core_dart/spring_core_dart.dart';
// Written by the generator; it exists after the first build.
import 'package:my_app/spring_gen/src/beans.spring.dart';
@ComponentScan()
final BeanModule appBeans = $appBeans;
Write the beans. Each one asks for its dependencies in the constructor:
// lib/src/orders.dart
import 'package:spring_core_dart/spring_core_dart.dart';
class Order {
Order(this.id, this.total);
final int id;
final double total;
}
// Stores orders. A real application would talk to a database here.
@Repository()
class OrderRepository {
final _orders = <int, Order>{};
void save(Order order) => _orders[order.id] = order;
Order? findById(int id) => _orders[id];
}
// Business logic. The container passes the OrderRepository bean and
// the `orders.limit` setting (100 when it is not configured).
@Service()
class OrderService {
OrderService(this.repository, @Value(r'${orders.limit:100}') this.limit);
final OrderRepository repository;
final int limit;
Order place(int id, double total) {
if (total > limit) {
throw ArgumentError('Order $id is over the limit of $limit');
}
final order = Order(id, total);
repository.save(order);
return order;
}
}
Generate the wiring code, again after every change to the annotations:
dart run build_runner build # or: dart run build_runner watch
Start the container, use a bean, close it:
// bin/main.dart
import 'package:my_app/src/beans.dart';
import 'package:my_app/src/orders.dart';
import 'package:spring_core_dart/spring_core_dart.dart';
const log = SpringLog('my_app');
void main() {
// Creates every bean and injects the dependencies.
final context = ApplicationContext.run([appBeans]);
final orders = context.getBean<OrderService>();
final order = orders.place(1, 42);
log.info('placed order ${order.id}');
// Runs the @PreDestroy methods and releases the beans.
context.close();
}
main never calls OrderRepository() or OrderService(...). Adding a parameter to OrderService means changing only OrderService.
With spring_boot_dart, the configuration files are read for you and main becomes one line. This section describes the container that both use.
Pages in this section
Declaring beans:
- @Component: marks a class as a bean.
- @Service: a bean with business logic.
- @Repository: a bean that stores and loads data.
- @Controller: a bean of the presentation layer.
- @Configuration: a class whose
@Beanmethods declare beans. - @Bean: a method whose return value is a bean.
- @ComponentScan: tells the generator which directory to scan, with filters.
- @Import: adds configuration classes and registrars that the scan does not find.
Shaping a bean:
- @Scope: one instance, one per use, or a custom scope.
- @Lazy: create a bean, or an injected value, on first use.
- @Primary: the bean chosen when several have the same type.
- @Fallback: the bean chosen only when no other one exists.
- @Qualifier: selects one bean among several by a label or a name.
- @DependsOn: creates other beans first.
- @Description: a text describing the bean, for tools.
- @Profile: registers a bean only in some profiles.
- @Conditional: registers a bean only when conditions match.
Injection:
- @Autowired: injects fields, setters, methods, or chooses a constructor.
- @Lookup: a method that returns a fresh bean on every call.
- @Configurable: injects objects the container did not create.
- Jakarta annotations:
@Inject,@Named,@Resource,@PriorityandProvider. - ObjectProvider: a handle that looks beans up later, optionally, or all of them.
Lifecycle:
- @PostConstruct: a method called once the bean is injected.
- @PreDestroy: a method called when the bean is destroyed.
- Lifecycle and SmartLifecycle: background work that starts and stops with the context.
- Aware interfaces: callbacks that hand a bean its name, the factory, the context and more.
The container itself:
- ApplicationContext: the running container; start, look up, close.
- BeanModule: the bean definitions of one scanned directory.
- BeanRegistrar: registers beans in code, decided at startup.
- FactoryBean: a bean that makes another object.
- BeanPostProcessor: sees or wraps every bean as it is created.
- BeanFactoryPostProcessor: changes bean definitions before any bean exists.
- ConfigurableListableBeanFactory: full access to the definitions and the container's setup.
- @BeanMethodAnnotation: lets a library find annotated bean methods without reflection.