Dependency Injection - Skullabs/kikaha GitHub Wiki
Kikaha's core is SPI based, and uses it to search for implementations of its internal APIs. This allowed Kikaha to keep its development stack quite lean, and does not forces developers to import specific CDI library.
On other hand, Java's CDI (JSR-330 and JSR-299) is a very popular mechanism of dependency injection and Kikaha also support it. Both approaches of dependency injection is discussed on the following topics.
At many places at this documentation you will be noticed to make an implementation an Injectable Service or a Managed Service. Kikaha will consider something an injectable (managed) service if:
- It is defined as an SPI implementation of an interface (or a superclass)
- Is annotated with
javax.inject.Singleton
Java 6 brought SPI support out-of-box. It loads implementations of an interface (or extensions of superclasses) dynamically. It allows libraries developers to avoid scans the entire ClassPath in order to find interface implementations at the startup time of an application. Kikaha makes intense use of SPI in order to improve its warm up time.
To make a class an injectable service through SPI, as specified the Java's ServiceLoader API, basically you have to create a text file with the canonical name of the intended implementation and include a line with the concrete class' canonical name for each implementation you want to available.
For an example, if you just created an implementation of kikaha.core.security.AuthenticationMechanism
and intend to manually make it available as an injectable service you have to create a file at the resources
folder called META-INF/services/kikaha.core.security.AuthenticationMechanism
and insert one line with the canonical name of any your AuthenticationMechanism's implementation.
#META-INF/services/kikaha.core.security.AuthenticationMechanism
sample.security.UsernameAndPasswordAuthenticationMechanism
sample.security.SSLAuthenticationMechanism
For a more detailed overview about SPI, please take a deep look at the Java's ServiceLoader API documentation.
Once Kikaha was designed as a micro-services middleware, in order to keep things simple and avoid complicated/obfuscated code design, it only offers basic JSR-330 and JSR-299 support. Bellow a comparison table of Annotations that is supported by Kikaha CDI Module.
Functionality | CDI JSR-330 |
---|---|
Singleton services | javax.inject.Singleton |
Inject a service into a field | javax.inject.Inject |
Producer/factory method pattern | javax.enterprise.inject.Produces (JSR-299) |
A method that should be dispatched after a service is instantiated. | javax.annotation.PostConstruct |
Multiple services implementing the same type | javax.inject.Inject + javax.enterprise.inject.Typed (works only on Iterable/Collection fields) |
Create annotations that qualifies candidates to be injected | javax.inject.Qualifier |
In order to activate CDI support you should include the kikaha-injection
module on your dependency list.
# Via command line
kikaha project add_dep "io.skullabs.kikaha:kikaha-injection"
# Via maven
<dependency>
<groupId>io.skullabs.kikaha</groupId>
<version>${kikaha.version}</version>
<artifactId>kikaha-injection</artifactId>
</dependency>
<!-- all dependency is resolved at the compile time.
thus, maven kikaha developers should include the
following library (needed only at compile time)
in order to the Dependency Discovery process works
as expected. If your maven project extends kikaha-parent
then you can skip this dependency. -->
<dependency>
<groupId>io.skullabs.kikaha</groupId>
<version>${kikaha.version}</version>
<artifactId>kikaha-injection-processor</artifactId>
<scope>provided</scope>
</dependency>
Every time you need dependency injection on your source code, you will use annotations available at the javax.inteject
and javax.enterprise.inject
packages. On our examples we shall assume we've use the following imports:
// basic imports
import javax.enterprise.inject.*;
import javax.inject.*;
You will see this term widely used at this documentation. Managed classes are classes that is instantiated by the Kikaha's CDI context. Classes that handled requests is an example of classes that the CDI context instantiate at the boot of your application.
Is a basic service that initiate the entire Kikaha application. It handles all service discovery needed by the Kikaha Lifecycle. It instantiate, to list a few: Modules, Listeners, Http Handlers, the configuration reader, and all services created by developers. To understand how CDI Context is initialized take a look at the Architecture Overview topic of this documentation.
You can inject both Singleton and Non-Singleton dependencies on your source classes, as long as your class (which will have dependencies injected) is managed through the CDI context. The bellow sample code exemplify this behavior.
@Singleton
public class MySingletonService {}
public class MyNonSingletonService {
@Inject MySingletonService singleton;
}
@Singleton
public class AServiceThatInjectDependencies(){
@Inject MyNonSingletonService nonSingleton;
@Inject MySingletonService singleton;
}
Classes annotated with the @Singleton
annotation will always be managed by the CDI context. The CDI Context will always ensure to satisfy all dependencies a managed class may have. If a non-singleton class is required to be inject, it will be also instantiated and will have all its dependencies resolved too.
It is also possible to inject concrete classes that implements a specific Interface (or extends a specific superclass).
public interface Hero {}
@Singleton
public class Batman implements Hero {}
@Singleton
public class Superman implements Hero {}
public class ActionSceneThatRequiresAnHero {
@Inject Hero hero;
}
At the above example, the first implementation of Hero available at the class path will be injected into the ActionSceneThatRequiresAnHero.hero
field. In some cases, this may be not the expected behavior. Sometimes, we need all available implementations of a specific interface or superclass. The follow exemple, though, show how is possible to inject all Hero
implementation on a managed class.
public interface Hero {}
@Singleton
public class Batman implements Hero {}
@Singleton
public class Superman implements Hero {}
public class JusticeLeague {
@Inject @Typed(Hero.class)
Iterable<Hero> heros;
}
Note: in order to have multiple implementations injected into your classes you should annotate your field with both
@Inject
and@Typed
annotations. Also, it should be ofIterable
type.
By default, a service is automatically mapped to be injected as its own class, or its direct implemented interfaces. You can use the @Typed
annotation to specify which types of candidates your implementation should be available for, as shown at the bellow sample code.
public interface Hero {}
public abstract class AbstractHero {}
@Singleton
@Typed(AbstractHero.class)
public class Batman extends AbstractHero implements Hero {}
@Singleton
@Typed(AbstractHero.class)
public class Superman extends AbstractHero implements Hero {}
public class JusticeLeague {
@Inject @Typed(AbstractHero.class)
Iterable<AbstractHero> heros;
}
Have more than one implementation candidates of a specific interface or superclass may become a problem, specially if these implementations may have different needs. Its possible to create a qualifier annotation handle this ambiguation, as shown at the following example.
public interface Hero {}
@Qualifier
@Retention(RUNTIME)
public @interface @WithSuperPowers{}
@Singleton
@WithSuperPowers
public class Superman implements Hero {}
@Qualifier
@Retention(RUNTIME)
public @interface @WithLotOfMoney{}
@Singleton
@WithLotOfMoney
public class Batman implements Hero {}
public class JusticeLeague {
@Inject @WithLotOfMoney Hero heroWithMoney;
@Inject @WithSuperPowers Hero heroWithSuperPowers;
}
You can use the Factory Pattern to manually create an object and make them available on the CDI Context. The following exemple shows the usage of @javax.enterprise.inject.Produces
annotation to define which method will create a specific object and make it available at the CDI.
// Hero definition
public interface Hero {
String getName();
}
// Hero implementation
public class ManuallyCreatedHero implements Hero {
String name;
public String getName(){ return name; }
}
// A service that will create Hero implementations
public class ManuallyCreatedHeroProvider {
@Produces
// the method that will create the implementation
public Hero manuallyCreateHeroes(){
return new ManuallyCreatedHero( "Chuck Norris" );
}
}
public class AServiceThatPrintTheNameOfHero {
@Inject Hero hero;
public void printHeroName(){
System.out.println( hero.getName() );
}
}
It may sounds like a verbose approach to create a simple object, but this brings a lot of benefits when you have a complex application to write. You may use this approach to create low coupled classes and easier to maintain. One common example of its usage is when you have pre-defined values defined on the configuration file, but you don't want it tight coupled to the kikaha.config.Config
class.
// Hero definition
public interface Hero {
String getName();
}
// Hero implementation
public class ManuallyCreatedHero implements Hero {
String name;
public String getName(){ return name; }
}
// A service that will create Hero implementations
public class ManuallyCreatedHeroProvider {
@Inject kikaha.config.Config config;
@Produces
// the method that will create the implementation
public Hero manuallyCreateHeroes(){
return new ManuallyCreatedHero( config.getString( "my-app.hero-name" ) );
}
}
public class AServiceThatPrintTheNameOfHero {
@Inject Hero hero;
public void printHeroName(){
System.out.println( hero.getName() );
}
}
At this exemple, we can see that behaves exactly as in the later example and does not depends on kikaha.config.Config
file, but the default implementation available at the CDI Context will always be populated with the hero name defined at the entry my-app.hero-name
available at your application.conf
file. In other hand, if, for some reason, you need to create it manually it is just a matter of write new ManuallyCreatedHeroProvider( "name here" )
.
Be aware of multiple copies of the same object. Is important to say that, unlike the Singleton ones, objects created through the Factory Pattern may have more than one copy. Every time a class needs an object that is provided through this pattern, the CDI execute the factory method in order to retrieve the expected object. This means that, if you have two services that needs to inject the same type of object - which is provided through this pattern - both will receive new copies of this object.
If you have a Singleton implementation of a specific type and also have a method annotated with the
@Produces
annotation, the CDI will always use the object created by the factory as an injectable candidate - even if the method returnsnull
.