Injection Container - coolsamson7/inject GitHub Wiki
- Overview
- BeanDescriptor
- Environment
- Defining Beans
- XML Configuration
- Configuration
- Reflection Requirements
- Abstract Beans
- Bean Scopes
- Bean Post Processors
- Lifecycle
- Factory Beans
- Environment Startup
- Retrieving Beans
Overview
The basic idea of this dependency injection container is to have a central instance that acts as a registry for different objects by storing recipes, that describe how to build them. Every recipe on the other hand may define dependencies to other objects ( by id or by type ) that will be resolved by the container. In order to do that the container will sort the dependency graph topologically, and start building objects starting with objects without dependencies. Resolving dependencies by type will regard the class hierarchy and will as well assure that only one candidate is available.
As soon, as the container is up and running, a simple api is provided that is used to retrieve the stored objects.
Different factors determine the lifecycle of objects
- object scopes determine how often objects and when they are created: a singleton scope will create one instance of any type ( and do that when the container starts, unless it is marked as lazy ). A prototype scope will create new objects on every request. Other custom scopes may be added that are based on a session concept, etc.
- lazy objects are created only on demand.
Before we continue with the container itself, we need to explain the concept how structural information of beans are analyzed and collected, since all other mechanisms rely on this concept
BeanDescriptor
BeanDescriptor maintains bean structures by analyzing classes with respect to properties and their corresponding types as well as the superclass hierarchy. In addition it offers reflection features.
A BeanDescriptor is created by calling class functions
BeanDescriptor.forType(type : Any.Type) throws -> BeanDescriptor
BeanDescriptor.forClass(clazz : AnyClass) throws -> BeanDescriptor
BeanDescriptor.forClass(className : String) throws -> BeanDescriptor
or by calling the constructor with an instance.
Once constructed, it offers a number of functions
getBeanClass() -> AnyClassreturns the referenced classgetSuperBean() -> BeanDescriptor?the descriptor of the direct superclass, if anygetSubBeans() -> [BeanDescriptor]list of direct inherited classesgetProperties(local : Bool = false) -> [PropertyDescriptor]returns a list of properties, either local to the specific class or overall including all superclasses.getProperty(name : String) -> PropertyDescriptorlookup and return a property by name. It will throw an error, if not found. A subscript operator is provided that calls this method!findProperty(name : String) -> PropertyDescriptor?will returnnilif the property is not found
Warning: the func getSubBeans() returns only instances with respect to already analyzed classes, since Swift does not offer any functions to retrieve subclasses ( Only superclasses work ). This may cause some strange effects!
Example: Assuming derived classes Base and Derived
// create base descriptor
var descriptor = BeanDescriptor.forClass(Base.self)
var subBeans = descriptor.getSubBeans() // will be empty!
BeanDescriptor.forClass(Derived.self)
subBeans = descriptor.getSubBeans() // will contain one member!
In order to analyze the class structure, a bean descriptor requires an instance in order to have a mirror parameter. This is either the provided instance or an instance which is cerated dynamically. Since creating instances based on an AnyClass arguments also does not work ( oh boy... ) there is a special protocol
public protocol Initializable : class {
init()
}
that the corresponding classes need to implement! NSObject is already extended with this protocol, btw.!
The mirror is then used to analyze the class structure and the complete class hierarchy. What is not possible to analyze automatically are implemented protocols. For this purpose the descriptor class must be modified manually with the function implements(protocol : Any.Type).
This can be done manually or integrated in the class by implementing the protocol
/// this protocol defines a callback function that will be invoked by the bean descriptor whenever it analyzes an object of this type.
public protocol BeanDescriptorInitializer {
/// implement any code to add information to the bean descriptor
func initializeBeanDescriptor(descriptor : BeanDescriptor) -> Void
}
Example:
class Foo : Initializable, BeanDescriptorInitializer {
// Initializable
required init() {
}
// implement BeanDescriptorInitializer
func initializeBeanDescriptor(beanDescriptor : BeanDescriptor) {
try! beanDescriptor.implements(Initializable.self, BeanDescriptorInitializer.self)
}
}
A PropertyDescriptor offers the following interface
getName() -> Stringreturn the namegetPropertyType() -> Any.Typereturn the property type. If the original type is anOptional, the delegate is stored here, while an optional flag stores the information.isOptional() -> Boolreturntrue, if the original type was optionalget(object: AnyObject!) throws -> Any?returns the property value given an objectset(object: AnyObject, value: Any?) throws -> Voidset a property value given an object
The current limitation for the reflection is the it only works for NSObject's!
Example:
class Foo : NSObject {
// instance data
var string : String = ""
var int : Int = 0
var float : Float = 0.0
var double : Double = 0.0
var strings : [String] = []
}
...
let foo = Foo()
let bean = try BeanDescriptor.forClass(Foo.self)
try! bean["string"].set(foo, value: "hello")
Environment
Environment is the central container. It is created with the constructor
init(name: String, parent : Environment? = nil, traceOrigin : Bool = false) throws
name: a suitable nameparent: an optional parent environmenttraceOrigin: iftrueall bean definitions will remember where - file, line, column - they where created
Whenever a parent is specified all of its definitions and other internals - configuration manager, etc. - are inherited
The function
report() -> String
may be called to get a report of all bean definitions.
Defining Beans
The basic - fluent - function to define a bean is
func define(declaration : Environment.BeanDeclaration) throws -> Self
This inner class is created by different bean(...) functions and in turn offers an api, that can be used to add additional configuration options such as property declarations.
Example:
let environment = try Environment(name: "environment")
try environment
.define(environment.bean(Foo.self, id: "foo")
.property("id", value: "foo")
.property("number", value: 1))
.define(environment.bean(Bar.self)
.property("id", value: "bar")
.property("age", value: 4711))
Let's look at the possible parameters of every bean
| Name | Description |
|---|---|
| class | the class of the bean. |
| id | an optional id that may be used to reference specific instances |
| lazy | boolean value. if true, the instance will be created on demand |
| abstract | the bean declaration is only a template and will not be instantiated |
| scope | the scope will determine the lifecycle of beans. Builtin scopes are "singleton" and "prototype". Others may be added |
| factory | the factory will create instances. Depending on the configuration, different flavors exist. ( e.g. call init, call closure, return value, etc. ) |
The following functions are provided
func bean(instance : AnyObject, id : String? = nil) -> Environment.BeanDeclarationSpecify a singleton - non lazy - bean by passing a constructed object and an optional idfunc bean(className : String, id : String? = nil, lazy : Bool = false, abstract : Bool = false) throws -> Environment.BeanDeclarationSpecify a bean given the - fully qualified - class name, an optional id, and parameters for the lazy and abstract attributesfunc bean<T>(clazz : T.Type, id : String? = nil, lazy : Bool = false, abstract : Bool = false, factory : (() throws -> T)? = nil)Specify a bean with the appropriate type by passing a factory closure function.
Let's look at some examples
Example: init call
let environment = ...
environment.define(environment.bean(Foo.self, factory: Foo.init) // a factory for type T is a function () -> T
Example: closure
let environment = ...
environment.define(environment.bean(Foo.self, factory: { Foo() })
The Environment.BeanDeclaration offers a number of additional fluent functions
func id(id : String) -> Selfset the idfunc lazy(lazy : Bool = true) -> Selfset the lazy attributefunc abstract(abstract : Bool = true) -> Selfset the abstract attributefunc parent(parent : String) -> Selfset the id of an abstract bean definition that acts as a templatefunc scope(scope : String) -> Selfset the bean scope.func requires(id id: String) -> Selfspecifies that this bean needs to be constructed after specified bean given the idfunc requires(class clazz: AnyClass) -> Selfspecifies that this bean needs to be constructed after a bean of the specified typefunc target(clazz : AnyClass) throws -> Selfthis will determine the target class forFactoryBean's.func property(name: String, value : Any? = nil, ref : String? = nil, resolve : String? = nil, bean : BeanDeclaration? = nil, inject : InjectBean? = nil)this function will define a property injection. Given a property name, different parameters determine the injection type. Only one of the following optional parameters may be specified:valuea valuerefthe id of a referenced beanresolvea string that contains configuration placeholder that will be resolved with the internal configuration manager and possibly converted to the expected typebeana recursive bean definitioninjectanInjectBeaninstance - that optionally contains an id -. The result is that a the container will find a matching candidate ( and will throw an error if multiple candidates exist ) and inject it
Let's look at an example for a closer factory with external dependencies:
Example: init call
let environment = ...
environment.define(
environment.bean(Foo.self, factory: {
return Foo(bar: environment.getBean(Bar.self))
})
.requires(Bar.self) // we beed to make sure that bar is constructed first!
In addition to injections that are expressed by the fluent interface it is also possible to directly stick it to the class properties.
Injections
Since Swift does not allow for user defined attributes which could be used to define injections ( such as the @Inject in Java ) we have to do it differently.
Since BeanDescriptor is a class that collects structural information about classes, the concept has been extended.
First of all, all injections are expressed by subclasses of the base class
Inject
This specific subclass will identify the required injection and possibly add additional parameters for the injection process.
Attaching the instance to a property is done by calling the property function
inject(inject : Inject)
Implemented injections are currently:
InjectBeanwithinit(id : String? = nil)InjectConfigurationValuewithinit(namespace : String = "", key : String, defaultValue : Any? = nil)
InjectBean without parameters can also be expressed by calling the property function
autowire().
Let's look at a real sample as used in the framework:
public class AbstractConfigurationSource : NSObject, ConfigurationSource, Bean, BeanDescriptorInitializer {
// MARK: instance data
...
var configurationManager : ConfigurationManager? = nil // injected
// MARK: implement BeanDescriptorInitializer
public func initializeBeanDescriptor(beanDescriptor : BeanDescriptor) {
beanDescriptor["configurationManager"].autowire()
}
// MARK: implement Bean
public func postConstruct() throws -> Void {
try configurationManager!.addSource(self)
}
...
}
Internally all injections are executed by corresponding subclasses of the class Injection which contains the code to compute the necessary value. All Injection's on the other hand are maintained by a central class Injector that acts a a registry for the different injections. Every environment manages an Injector instance which is part of the post processing of beans. More injections can be added by simply defining the corresponding Injection class within the environment!
XML Configuration
The environment function
loadXML(data : NSData) throws -> Self
loads a xml file according to the basic spring syntax. Not supported are
- constructor injections
- method injections
- all property injections other than literal values ( e.g. dictionary... )
Configuration
Every environment stores a configuration manager that keeps track of different configuration sources and will return configuration values ( via api or within xml ).
The environment function
addConfigurationSource(source : ConfigurationSource)
may be used to add different sources. The other possibility is to add manual configuration values which is done like this:
environment.define(environment.define.settings()
.setValue(key: "key", value: 4711))
On the other hand the public function
getConfigurationValue<T>(type : T.Type, namespace : String = "", key : String, defaultValue: T? = nil, scope : Scope? = nil) throws -> T
is used to retrieve values.
Reflection Requirements
Depending on the exact configuration, reflection may be used or not.
Any property definition or injections expressed in the class itself will make use of a reflection feature in order to set the property value. In the current Swift version ( 2.x ) this requires the objects to derive from NSObject!
In all other cases at least the bean structure needed to be determined - see the chapter on BeanDescriptor's -. Deepening on the configuration the technical framework may need to - unless instances are provided by value or via a factory - construct a sample object in order to analyze ist structure. The default is to look for a Initializable protocol that simply defines a init()constructor ( NSObject already is extended ).
Abstract Beans
An abstract bean defines a construction template which may be completed or altered by child beans.
Example:
let environment = Environment()
environment
.define(environment.bean(Bar.self, id: "bar-template", abstract: true)
.property("flavor", value: "vanilla")
.define(environment.bean(Bar.self, id: "bar-1", parent: "bar-template")
.property("id", value: "1")
.define(environment.bean(Bar.self, id: "bar-1", parent: "bar-template")
.property("id", value: "2")
)
let bar = environmet.getBean(Bar.self, byId: "bar-1")
The property "flavor" is inherited in the example, while "id" is separately set.
Bean Scopes
A bean scope defines the lifecycle of beans and ist define be the following protocol.
public protocol BeanScope {
/// the name of the scope
var name : String {
get
}
/// prepare a bean declaration of an environment after validation
/// specific implementing classes may use this callback to create instacnes on demand
/// - Parameter bean: the `BeanDeclaration`
/// - Parameter factory: the factory that cerates an instance
func prepare(bean : Environment.BeanDeclaration, factory : BeanFactory) throws
/// return a new or possibly cached instance given the bean declaration
/// - Parameter bean: the `BeanDeclaration`
/// - Parameter factory: the factory that cerates an instance
func get(bean : Environment.BeanDeclaration, factory : BeanFactory) throws -> AnyObject
/// execute any cleanup code after a scope has ended. ( e.g. after session removal )
func finish()
}
An abstract class AbstractBeanScope is implemented that can serve as a base class.
Builtin scopes are
- "singleton" whenever a bean is created that instance is cached as a singleton
- "prototype" whenever a bean is requested a new instance will be created.
If new scopes need to be referenced the new class can registered manually with the environment function
registerScope(scope : beanScope) -> Self
or simply defined in the current environment.
environment.define(environment.bean(SampleBeanScope.self, factory: SampleBeanScope.init))
Bean Post Processors
All beans of type
protocol BeanPostProcessor {
// post process the given object
// - Parameter bean: the bean
// - Returns: the possibly modified object
// - Throws any error
func process(bean : AnyObject) throws -> AnyObject
}
will be called by the container giving it the possibility to modify or completely replace an instance ( e.g. proxy ). Different post processors are possible. The order is defined by the definition order!
Lifecycle Callbacks
All classes that implement the following protocol
public protocol Bean {
/// called after the instance is constructed including all injections
func postConstruct() throws -> Void ;
}
will get called after all injections and prost processors have been executed.
All classes that implement the following protocol
protocol EnvironmentAware {
/// the setter will be called by the environment
var environment: Environment? { get set }
}
will be notified by the container about the current environment.
Factory Beans
The following protocol
public protocol FactoryBean {
/// create the corresponding bean
/// - Returns: the bean instance
/// - Throws: any possible error
func create() throws -> AnyObject
}
may be implemented and is used to create instances of the target type ( which needs to be part of the definition ).
Example:
class BarFactoryBean: NSObject, FactoryBean {
// MARK: instance data
var flavor : String = ""
// MARK: implement FactoryBean
func create() throws -> AnyObject {
let result = Bar()
result.flavor = flavor
return result
}
}
...
let environment = Environment()
environment.define(environment.bean(BarFactoryBean.self)
.property("flavor", value: "vanilla")
.target(Bar.self)
)
let bar = environment.getBean(Bar.self) // this is created by the factory
Environment Startup
An environment will startup, if any one of the functions that retrieve beans is called or if the function
startup()
is called manually.
The following steps occur:
- all definitions are analyzed with respect to their dependencies. A dependency is established be one of the following constructs
- injection as expressed in the bean descriptor
- explicit dependency expressed by
requires(...)ordependsOn(),ref()orbean()
- the resulting graph is sorted topologically and traversed starting with beans without dependencies. Every bean that with singleton scope which are not lazy are resolved - e.g. constructed - directly.
- the construction process involves
- the creation of the instance
- execution of all property injections
- execution of all prost processors. The environment self is a prost processor which includes the steps
- execution of injections
- callback execution for the protocols
BeanandEnvironmentAware
Retrieving Beans
The public api is pretty simple:
func getBean<T>(type : T.Type, byId id : String? = nil) throws -> Treturn a suitable object instance given a type and an optional id.func getBeansByType<t>(type : T.Type) throws -> [T]return a list of beans that are assignable to the class.
If getBean() is called without an id, it will assume that there is only one match. If not, it will throw an error.
The getBean() function will respect class hierarchies as well as protocols. Requesting an object with no result will try to check all inherited classes or implementing classes in case of a protocol parameter.
Some remarks on this feature:
Protocols
Correlations between classes and protocols have to be specified manually. ( Swift does not help here ) This can be done by adding specific code to the class itself:
class Swift : BeanDescriptorInitializer {
// Initializable
required init() {
}
// implement BeanDescriptorInitializer
func initializeBeanDescriptor(beanDescriptor : BeanDescriptor) {
try! beanDescriptor.implements(BeanDescriptorInitializer.self)
}
}
or by adding it to a bean declaration
environment
.define(environment.bean(Swift.self, factory: Swift.init)
.implements(BeanDescriptorInitializer.self))
Timing
The container - or rather the BeanDescriptor - will know about subclasses and implementing classes as soon as the corresponding descriptor of a subclass is created. When he container is starting up it will create all non lazy singletons and will create the appropriate bean descriptors on the fly.
If a subclass relationship is not known - "no candidate found for " - this may be due to the fact that the requested bean is lazy and has not been created yet. The solution is to force the class BeanDescriptor to create the appropriate instance. ( e.g. manual constructor call )