An easy to configure IoC dependency injector for Java
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.circleci
.github
src/main/java/com/opsbears/webcomponents/dic
.gitignore
CHANGELOG.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
LICENSE
README.md
pom.xml
settings.xml

README.md

A simple IoC dependency injector

CircleCI

This IoC dependency injection container aims to simplify the integration of dependency injection into Java projects. It does not utilize any annotations and it has greatly simplified the definition and creation of objects.

It is inspired by Auryn IoC for PHP.

Why dependency injection?

When building applications, we often have a large number of services, such as UserCreateService, etc. Often times these services depend on each other, such as the UserCreateService may depend on the UserStorageService. Without going into too much detail, the probably best way to do this is to request dependencies in the constructor:

class UserCreateService {
    public UserCreateService(
        UserStorageService storage
    ) {
        //...
    }
}

Now, a large dependency tree would be hard to create this way, so that's where a dependency injector like this comes into play. The injector automatically discovers dependencies and creates an instance of UserCreateService with all its dependencies.

Use cases

This injector does not offer the best possible performance, it puts emphasis on readability and easy configuration.

Therefore, it is not recommended where fast start up times are required, such as Android applications. Its main use case is web applications with a standalone web server or batch processing utilities.

License

Copyright 2018 Janos Pasztor

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Installation

This injector is available on Maven Central as com.opsbears.webcomponents:dic:1.0.0-alpha4. You are also going to need the actual injector implementation which can be found under com.opsbears.webcomponents.dic:jit:1.0.0-alpha4. To use it, copy this into your pom.xml:

<dependencies>
    <dependency>
        <groupId>com.opsbears.webcomponents</groupId>
        <artifactId>dic</artifactId>
        <version>1.0.0-alpha4</version>
    </dependency>
    <dependency>
        <groupId>com.opsbears.webcomponents.dic</groupId>
        <artifactId>jit</artifactId>
        <version>1.0.0-alpha4</version>
    </dependency>
</dependencies>

Note: the versions 1.0.0-PR1 and 1.0.0-PR2 have been mistakenly pushed and are from a legacy version of this injector.

Basic usage

At its core the injector requires you to pass it a configuration. The simplest use case would be this:

class MyApplication {
    public static void main(String[] argv) {
        InjectorConfiguration config = new InjectorConfiguration();
        
        //Define all classes you want to be injectable here
        config = config.withDefined(MyOtherClass.class);
        
        Injector injector = new JITInjector(
            config
        );
        
        //The injector will resolve constructor dependencies automatically.
        MyOtherClass myOtherClass = injector.make(MyOtherClass.class);
        myOtherClass.doSomething();
    }
}

How configuration works

Note that the configuration does NOT implement a builder pattern, instead it is an immutable class. This is done so that the configuration can be split of for multiple different use cases from a base configuration without fear of accidentally modifying global application state.

So this WILL NOT WORK:

InjectorConfiguration config = new InjectorConfiguration();
config.withDefined(MyApplication.class);

Instead you must do this:

InjectorConfiguration config = new InjectorConfiguration();
config = config.withDefined(MyApplication.class);

Defining classes for injection

The dependency injector is written in such a way that it cowardly refuses to inject any classes that have not been "defined". This is done so that classes that have side effects in their constructors are not accidentally used.

So you need to define your classes like this:

config = config.withDefined(MyApplication.class);

That's it, your class is now marked as injectable and all public constructors will be tried for injection.

If you want, you can also pass a single constructor and avoid other constructors:

config = config.withDefined(MyApplication.class.getConstructor());

This is especially useful if you are using annotations. (See below for details.)

Defining interface and abstract class implementations

A dependency injector is a nice tool to have, but in order to implement Inversion of Control, it needs some way to define what class to pass if an interface or abstract class is requested. This is called an alias. Defining it is simple:

config = config.withAlias(MyInterface.class, MyImplementation.class);

Note: the implementing class must be defined. The abstraction does not need to be defined since it will not be instantiated anyway.

Alias scoping

Sometimes you may want to define an alias for a single class or constructor only. Say you want to define a different database connection for a certain implementation:

class CachingBlogRepository implements BlogRepository {
    public CachingBlogRepository(BlogRepository backingRepository) {
        
    }
}

class MySQLBlogRepository implements BlogRepository {
    
}

In this case you need two aliases, one for the CachingBlogRepository and one on a global scope. This would be done like so:

config = config.withAlias(CachingBlogRepository.class, BlogRepository.class, MySQLBlogRepository.class);
config = config.withAlias(BlogRepository.class, CachingBlogRepository.class);

It does not matter which order you define the aliases in, the most specific rule will be used, falling back to generic rules.

Defining implementation (alias) lists

Sometimes it is useful to have more than one implementation for an abstraction. For example when building a plugin architecture. Say a class has a dependency like this:

class MyPluginContainer {
    private final List<Plugin> plugins;
    
    public MyPluginContainer(List<Plugin> plugins) {
        this.plugins = plugins;
    }
}

In this case it would be nice if the dependency injector could "collect" the possible implementations and pass it on as a list. This can be done with a separate call:

config = config.withCollectedAlias(Plugin.class, PluginImplementation1.class);
config = config.withCollectedAlias(Plugin.class, PluginImplementation2.class);

Your plugin container will now be passed a list of instantiated plugin implementations.

Note: using withAlias does not automatically add the implementation to the collected alias list. Collected aliases must be defined separately.

Defining named parameters

Sometimes, despite our best intentions, some parameters must be passed by name rather than by type. This can, of course, be done, but your either have to compile the target code with the -parameters flag so the bytecode contains the parameter name. Alternatively, you can also use the @Named annotation on the parameter in question. If an annotation is present, it will override the parameter name in the bytecode.

Defining a named parameter is easy:

config = config.withNamedParameter(MyClass.class, "parameterName", "Hello world!");

Defining shared instances

By default the injector always creates a new instance when requested. This helps with avoiding unintended global state. However, sometimes it is useful to have a shared instance. The easiest way to do that is to define an instance as shared:

config = config.withShared(new MySharedInstance());

Alternatively, you could also ask the injector to cache the first instance it creates as shared. Note, however, that this can have some unintended side effects. (See below for details.)

config = config.withShared(MySharedInstance.class)

About the side effects: When not using a pre-instantiated shared instances there are possible side effects. If you, for example, create multiple injectors, these injectors do not share their instance cache, so you may end up with having multiple copies of the same object.

Using factories

Sometimes classes are not that easy to instantiate. This is when a factory comes in handy. All a factory needs to do is implement the javax.inject.Provider interface and it can be used with the dependency injector. One such factory could look like this:

class MyClassFactory implements Provider<MyClass> {
    public MyClass get() {
        return new MyClass(
            "custom-parameter"
        );
    }
}

You could then create an instance of the factory and pass it to the injector configuration:

config = config.withFactory(MyClass.class, new MyClassFactory());

Alternatively, you can ask the injector to also make the factory when needed. Instead of passing an instance, you can pass the class itself. The injector will try to resolve this and instantiate the factory itself:

config = config.withFactory(MyClass.class, MyClassFactory.class);
config = config.withDefined(MyClassFactory.class);

Keep in mind that in this case MyClassFactory will be instantiated every time it is needed, unless you define it as shared.

Tip: like aliases, factories can also be scoped.

Using annotations

Instead of explicitly defining classes for injection, annotations can also be used if you have an annotation parser in your toolchain. The usage is simple:

InjectorConfiguration config = new InjectorConfiguration();
YourAnnotationParser annotationParser = new YourAnnotationParser();

config = new AnnotationProcessor().process(annotationParser.getClassList(), config);

This parser does thw following

  1. Read all constructors with the @Inject annotation defined, and mark them as injectable
  2. Take all implementations of the Provider interface, and based on the type annotation set them up as factories.
  3. If a method or field, non-public constructor, or non-public class with the @Inject annotation are defined, it will throw an exception, since these are not supported.

If you have classes with the annotation, but you do not want them to be used, you must filter those out before passing them to the AnnotationProcessor.

Note: We have an annotation processor that you can install. See this package for detailed instructions.

I need the injector injected! How do I do that?

First of all, a big, big warning! You should NEVER inject the injector into your business logic, controllers, etc and then use it to ask for certain services. This is called a Service Locator pattern and violates the Law of Demeter and leads to horrible code. In general, you should always ask for things in the constructor and not look for things using the service container itself.

If you need more information on this topic, please watch The Clean Code Talks - Don't Look For Things! by Misko Hevery.

In fact, the injector configuration prevents you from shooting yourself in the foot by defining a global alias or factory for the injector. So this will NOT work:

MyInjectorContainer injectorContainer = new MyInjectorContainer(); 
config.withFactory(Injector.class, injectorContainer);
Injector injector = new JITInjector(config);
injectorContainer.setInjector(injector);

The configuration step will throw an exception because you are not supposed to do that. However, sometimes you are in need of the injector itself, for example in the routing part of a web application. To avoid ugly hacks, the JITInjector internally defines a factory that you can use in a scoped manner, such as this:

config = config.withFactory(YourRouter.class, Injector.class, InjectorFactory.class);

This will lead to the injector being available to YourRouter only, but not to other classes.

Recommended usage / example

In order to ensure that your code is clean and maintainable, I would recommend creating a module structure, such as this:

interface Module {
    InjectorConfiguration configure(InjectorConfiguration configuration);
}

Each module in your code now deals with configuring its own classes. The main application class then simply calls the modules in order, and then uses the injector to initialize the app, such as this:

class MyApplication implements Runnable {
    public static void main(String[] argv) {
        Module[] modules = new Module[] {
            new ModuleX(),
            new ModuleY()
        };
        
        InjectorConfiguration injectorConfiguration = new InjectorConfiguration();
        
        for (Module module : modules) {
            injectorConfiguration = module.configure(injectorConfiguration);
        }
        
        Injector injector = new JITInjector(injectorConfiguration);
        
        injector.make(MyApplication).run();
    }
    
    public void run() {
        //Do something useful here
    }
}

You can then extend your module structure to include, for example, configuration loading.

Need help?

If you are stuck, feel free to drop in on my Discord and ask for help! I'm usually online during the week from 10 AM to 8 PM CEST.