Chefling is a minimal dependency injection container written in pure Java. It does not rely on annotations, only does constructor injection and has limited (but powerful) configuration options.
Chefling requires at minimum Java 7.
The distribution is hosted on Bintray. To include the package in your projects, you can add the jCenter repository.
Add jCenter to your repositories
block (not necessary for Android - jCenter is the default
repository):
repositories {
jcenter()
}
and add the project to the dependencies
block in your build.gradle
:
dependencies {
compile 'com.cookingfox:chefling-di-java:7.1.1'
}
Add jCenter to your repositories in pom.xml
or settings.xml
:
<repositories>
<repository>
<id>jcenter</id>
<url>http://jcenter.bintray.com</url>
</repository>
</repositories>
and add the project declaration to your pom.xml
:
<dependency>
<groupId>com.cookingfox</groupId>
<artifactId>chefling-di-java</artifactId>
<version>7.1.1</version>
</dependency>
- Dependency injection without annotations: keeps your code clean.
- Automatic resolving of dependencies using reflection and constructor injection.
- Lifecycle hooks for the instance creation and destruction phases.
- Builder to control configuration and initialization order.
- Modular container configurations support through composite containers.
You can find the Javadocs on javadoc.io.
The easiest way to create a
CheflingContainer
is by doing:
CheflingContainer container = Chefling.createContainer();
This provides you with an instance of the default container implementation.
It is also possible to use the designated Builder class:
CheflingContainer container = Chefling.createBuilder().buildContainer();
See Builder for more information. Also see Modular container configurations for instructions on how to create and add container child configurations.
There are two ways to have Chefling provide you with an instance of a type (class / interface):
-
CheflingContainer#getInstance(type)
: returns a stored instance of the type, or creates and stores a new instance, using: -
CheflingContainer#createInstance(type)
: always creates a new instance that is NOT stored. This method should only be called directly when you are absolutely sure you need a new instance, which is usually not the case.
When CheflingContainer#createInstance(type)
is called, Chefling attempts to resolve all
dependencies (constructor arguments) of the provided type. See the
F.A.Q. for information on which
types can and can not be resolved by Chefling. If the type implements the CheflingLifecycle
interface, its initialize()
method will be called by the createInstance(type)
method. See
Lifecycle for more information.
You can configure the container, so that when you ask for a type, the container provides you with a specific instance or implementation.
Mapping a specific instance of a type is useful when it has dependencies on unresolvable types, such
as String
or Boolean
:
// provide the specific instance
container.mapInstance(MyClass.class, new MyClass("some value", true));
// `resolved` is the provided instance
MyClass resolved = container.getInstance(MyClass.class);
Chefling can not create an instance of an interface or abstract class, so you will need to define which implementation you want to use:
// map the interface to a specific implementation
container.mapType(MyInterface.class, MyImplementation.class);
// `resolved` is an instance of MyImplementation
MyInterface resolved = container.getInstance(MyInterface.class);
If a type has dependencies that are both resolvable and unresolvable, you can map a
CheflingFactory
implementation:
// map the type to a factory
container.mapFactory(MyInterface.class, new CheflingFactory<MyInterface>() {
@Override
public MyInterface createInstance(CheflingContainer container) {
return new MyImplementation("some value", container.getInstance(OtherType.class));
}
});
// `resolved` is the result of the Factory method
MyInterface resolved = container.getInstance(MyInterface.class);
If the Factory
returns null or something that is not an instance of the expected type, an
exception will be thrown.
It is important to clean up your object references at the end of your application (segment) to avoid
memory leaks. Use CheflingContainer#disposeContainer()
to remove all mappings, created instances
and other references. Please note that after this call, the container will be in an unusable state,
so you should re-create it.
The CheflingLifecycle
interface
allows implementing classes to hook into the lifecycle processes of the container:
-
When
CheflingContainer#createInstance(type)
is called and an instance of the requested type is created, it will call theCheflingLifecycle#initialize()
method. This will also happen for types that have been mapped using themap...
methods, evenmapInstance()
. For example, if a typeFoo
is mapped to a specific instance of the class, and it implements theCheflingLifecycle
interface, then itsinitialize()
method will be called. -
The
CheflingContainer#removeInstanceAndMapping()
andCheflingContainer#disposeContainer()
methods will call theCheflingLifecycle#dispose()
method of instances that implement theCheflingLifecycle
interface.
As your application grows, the Chefling container configuration grows as well. You'll start
noticing different types of configuration, such as libraries, your application domain and the
initialization of the application. The
CheflingBuilder
allows you to
modularize your Chefling configuration into CheflingConfig
instances:
CheflingConfig libraryConfig = new CheflingConfig() {
@Override
public void apply(CheflingContainer container) {
// configure container with library dependencies
container.mapType(IMyLib.class, MyLibImpl.class);
}
};
CheflingConfig initAppConfig = new CheflingConfig() {
@Override
public void apply(CheflingContainer container) {
// initialize application components
container.getInstance(MyViewController.class);
}
};
CheflingContainer container = Chefling.createBuilder()
.addConfig(libraryConfig)
.addConfig(initAppConfig)
.buildContainer();
The CheflingContainer
applies the CheflingConfig
instances in the order they were added. Of
course, you can define your own classes that implement this interface for the desired level of
modularity.
The CheflingBuilder
also
contains a removeConfig()
method which can be used to override a CheflingConfig
(for example
for testing) before it is built.
The last part of the code example above could also be rewritten using a CheflingConfigSet
:
CheflingContainer container = Chefling.createBuilder()
.addConfig(new CheflingConfigSet(libraryConfig, initAppConfig))
.buildContainer();
Apart from the "instance lifecycle", the container has its own lifecycle too: the
CheflingBuilder
creates a container, configures it, and later the container can be disposed using
CheflingContainer#disposeContainer()
. The CheflingContainerListener
provides the following hooks
to which you can respond:
preBuilderApply
: Triggered byCheflingBuilder#buildContainer()
. Called before all addedCheflingConfig
instances are applied. At this point the config mappings are not yet available.postBuilderApply
: Called after theCheflingBuilder
applied all addedCheflingConfig
instances. At this point all config mappings are available.preContainerDispose
: Triggered byCheflingContainer#disposeContainer()
. Called before the container disposes all stored instances and clears all mappings. At this point all instances and mappings are still available.postContainerDispose
: Called after the container disposed all stored instances and cleared all mappings. At this point the container is in a completely disposed state, which means it should not be accessed anymore.
If you do not want to implement all listener methods, you can also extend
DefaultCheflingContainerListener
and only override the methods you are interested in.
Chefling supports modularizing container configurations through a composite pattern:
// example module configuration
CheflingContainer moduleContainer = Chefling.createContainer();
moduleContainer.mapType(IModule.class, ModuleImplementation.class);
// other container configuration
CheflingContainer appContainer = Chefling.createContainer();
appContainer.mapType(IApp.class, AppImpl.class);
// add module configuration to app container
appContainer.addChildContainer(moduleContainer);
This means that when the appContainer
asks for IModule
, it will receive a ModuleImplementation
instance.
Note that when a child container is added which contains a mapping or instance for a type that is already present in the container it is added to, an exception will be thrown:
CheflingContainer moduleContainer = Chefling.createContainer();
moduleContainer.mapType(IModule.class, ModuleImplementation.class);
// container configuration with same mapping
CheflingContainer appContainer = Chefling.createContainer();
appContainer.mapType(IModule.class, OtherModuleImplementation.class);
// exception: mapping for "IModule" already exists
appContainer.addChildContainer(moduleContainer);
It is also possible to do the inverse: set the parent of a container, using
setParentContainer(CheflingContainer)
.
There's a helper method available for creating a child container and adding it immediately:
createChildContainer()
.
Since dependencies are resolved at runtime, it can be useful to make sure your configuration is
correct during the development phase. To have Chefling resolve all mappings, use the
CheflingContainer#validateContainer()
method. This will bring any configuration issues to light.
Note that resolving the full object graph is an expensive operation, so it should only be used
during development as a test.
To validate the full container initialization and destruction flow, you can use
Chefling#validateBuilderAndContainer(CheflingBuilder)
which builds the container, validates it
and then disposes it.
WARNING: Make sure to remove this call for production builds!
No, the following types are not allowed:
- Classes in the
java.*
andjavax.*
packages: these are considered Java language constructs. Examples:java.lang.String
,java.lang.Exception
,java.util.LinkedList
. - Classes that are not public.
- Primitive types (e.g.
boolean
,int
). - Exceptions (or anything which extends
Throwable
). enum
types.- Annotations.
- Non-static member ("inner") classes.
- Anonymous classes.
These types are not allowed because the container would not know how to resolve them automatically.
Either because there is no logical default (e.g. boolean
, String
), or because no instance can
be created of the type (e.g. enum
, annotation).
The createInstance()
method walks through all constructors and picks one when either:
- The constructor has no parameters. It makes it the most reasonable default.
- All constructor parameters are resolvable by the container. (see question above)
If no 'default' constructor can be picked, an exception will be thrown. This will also be the case
if the class has no public
constructors.
Yes, an exception will be thrown. The only proper way of handling circular dependencies is to change one of the classes, for example by introducing a setter method.
The robot legs problem describes how two classes can depend on the same type, but expect a different instance of that type to be injected:
interface IFoot {}
class LeftFoot implements IFoot {}
class RightFoot implements IFoot {}
class LeftLeg {
IFoot leftFoot;
LeftLeg (IFoot leftFoot) {
// expects an instance of LeftFoot
this.leftFoot = leftFoot;
}
}
class RightLeg {
IFoot rightFoot;
RightLeg (IFoot rightFoot) {
// expects an instance of RightFoot
this.rightFoot = rightFoot;
}
}
To have a dependency injection container resolve the expected dependencies, it would need to know which specific instance to inject, per class. This would be possible by:
- Having the ability to configure the class dependencies per constructor parameter.
- Using metadata (e.g. an
@Inject
annotation) to define which type should be injected.
Chefling does not and will not have these features. Its philosophy is to be concise and apply convention over configuration.
We used cooking as an analogy for dependency injection (we're Cooking Fox after all): when asking a chef to prepare a meal, he/she prepares all the necessary ingredients and uses these to cook the dish. This is basically what a DI container does: ask for an instance of a class and it will create one, resolving its dependencies. The word "Chefling" suggests a 'small' chef, which corresponds to the limited functionality and scope of this library.
Code and documentation copyright 2016 Cooking Fox. Code released under the Apache 2.0 license.