Dependency injection (DI) is a popular method of wiring together components, and KOS implements this pattern internally using a class named BeanContext. It is a method of adding a collection of beans to a container, generally referred to as a context, and then allowing the context to find all the dependencies between the beans and connect them together. This process is facilitated by annotations that describe how beans depend on each other.
It is common for beans added to a context to be referred to as managed beans. In some frameworks, the context actually creates the beans based on either configuration files or annotated config classes, but within KOS, a bean is created outside of the context and added to the context as a separate step.
It is also common for a context to process all managed beans in a single operation and any missing dependencies results in an error. KOS allows for phased initialization, where not all dependencies need to be resolved. It is also possible to process all the beans in a context and then add additional beans and process the beans again. This provides a great deal of flexibility in wiring together infrastructure.
BeanContext is not so much a general purpose dependency injection engine so much as a highly optimized implementation to support the needs of KOS. This means that many features that might be available in general-purpose dependency injection frameworks, such as Spring, may not be available. In practice, this is rarely a limitation, as KOS is responsible for booting the vast majority of the operating system, so user components generally have fairly simple autowiring requirements.
BeanContext is based entirely on field annotations and interfaces. Unlike some frameworks which allow methods to be annotated to receive callbacks, BeanContext will only examine fields for annotations and perform callbacks to beans that implement specific lifecycle interfaces. This drastically speeds up context processing, which reduces boot time, particularly on lower-end hardware.
There are only two field annotations processed by BeanContext:
*TODO : Requires the bean referenced by the field to become ready before the annotated bean becomes ready. This allows the context to construct a dependency graph and trigger beans in the order required by the graph. This results in automatically sequencing the startup of all beans in the context, even if they require performing some asynchronous work between their dependencies becoming ready and the bean itself becoming ready. *TODO : This indicates that the context should fill in the value of the field using some other bean in the context. There is support for named beans as well as phased initialization. If the type of the autowired field implements Ready, then this will also act as if the field was annotated with @WhenReady.
BeanContext treats certain field types in special ways as a way to eliminate common boilerplate code. For example, any bean that wants to be configurable using the standard KOS configuration system must implement ConfigAware. This interface requires the creation of a subclass of ConfigBean that holds the configuration values for the ConfigAware bean. Any bean added to the context that implements ConfigAware that hasn’t already created the necessary ConfigBean instance will have that instance automatically created and assigned by the context.
Another common boilerplate pattern is the use of listener interfaces. Consider TroubleService, which allows instances of TroubleListener to be registered to receive notifications when troubles are added or removed. One way to connect listeners is to have each listener bean autowire TroubleMgr and call <b>troubleMgr.addListener(this)</b>. Another way is to autowire a list of all beans that implement the interface to a field of TroubleMgr. The first requires a great deal of boilerplate code and the second doesn’t support multiple evaluations of a context where more listeners were added later. BeanContext makes this easy by looking for any TODO field of type ListenerList. The generic type of the list will be used to look for any beans added to the context and automatically add them to the ListenerList object, even if the list has previously been updated. This can also be used in conjunction with child contexts, including the ability to automatically remove listeners when child contexts are destroyed.
One challenge when using dependency injection is the mix of managed and non-managed beans. Managed beans support autowiring, whereas non-managed beans need to access the same resources using a service locator pattern, such as querying the context for each bean it wants access to. Unlike a typical web application, much of the data in KOS is dynamically generated hardware or control state which needs to perform actions. This puts non-managed beans at a distinct disadvantage.
To eliminate this problem, BeanContext supports the idea of a connected bean. A connected bean supports autowiring and Ready processing like a managed bean, but without adding the bean to the context. Any bean that needs access to context resources can simply annotate fields like a managed bean and then use <b>BeanContext.connect()</b> to autowire the dependencies. The connected bean remains outside the context and can be garbage collected like any regular bean.
For more detailed information about BeanContext, see TODO.
KOS BeanContext
|