Magic-less Dependency Injection with JayWire
This article is a short tutorial on how to get started using it in projects of any size from small single- to large multi-module ones.
Join the DZone community and get the full member experience.
Join For FreeJayWire is a small, easy to use magic-less Dependency Injection library for Java 8. This article is a short tutorial on how to get started using it in projects of any size from small single- to large multi-module ones.
Why Another Dependency Injection Library?
Interestingly enough, the key feature of JayWire is not that it can supply objects as dependencies to other objects, nor that objects may be defined to have a scope (such as singleton scope, request scope, etc.). These are all supported by every other DI framework out there.
The distinguishing characteristic of this library is that it doesn’t use “Magic” to achieve these features, and is therefore easier to understand, less restrictive about objects it can work with, and the architectures it can support.
Specifically, following technical solutions all count as “Magic”, and are avoided in JayWire:
- Classpath-scanning
- Reflection
- Annotations
- Bytecode enhancement / weaving, AOP
- Transparent proxy objects
- Code generation or special compiler plugins
- Hidden static state
JayWire is therefore 100% Java code, does not force its programming model on the objects in manages, is statically safe, can be debugged or logged if needed, and can be used multiple times in a single JVM.
Problem Statement: Dependency Injection
The problem is surprisingly simple actually: some objects need other objects to work. These “other” objects are called Dependencies, and the act of supplying these Dependencies to the objects that need them is called Injection. It looks like this:
public interface Database {
}
public class PeopleRepository {
public PeopleRepository(Database database) {
...
}
}
In this snippet, PeopleRepository depends on a Database to work, therefore Database is a dependency of PeopleRepository. This is how injection then looks like:
new PeopleRepository(new SomeDatabaseImpl(...));
Please note that neither the definitions of these classes nor the injection code requires any “framework” or “library” to work. Also note that the class definitions do not need to know at all how the injection will happen, nor do they need to know what scopes they will be in inside a specific application.
The Application Startup
At some point during the startup of an application, all objects that are needed to “start” the application need to be instantiated, with all of their dependencies, and the dependencies of the dependencies recursively.
In container-based environments, such as in CDI or Spring, this process is implicit and hidden from the developer. It is however relatively easy to make this important part of the application explicit, and with it, put it under the control of the developer. Let’s try to write the application’s “wiring” class, still without any frameworks, to instantiate all objects for startup. Assuming we are trying to start a Web Server with two published Services, this is how it would look:
public class MyApplication {
public PeopleRepository getPeopleRepository() {
...
}
public GroupRepository getGroupRepository() {
...
}
public WebServer getWebServer() {
return new WebServer(getPeopleRepository(), getGroupRepository());
}
}
Please note, that the WebServer has two dependencies and both are “injected” using plain old method calls. No magic needed here. MyApplication can be started then with the following code:
new MyApplication().getWebServer().start();
Unfortunately both repository objects still depend on the Database, so that needs to be added:
public class MyApplication {
public Database getDatabase() {
...
}
public PeopleRepository getPeopleRepository() {
return new PeopleRepository(getDatabase());
}
public GroupRepository getGroupRepository() {
return new GroupRepository(getDatabase());
}
...
}
“Singletons” with JayWire
The challenge here is to implement the getDatabase() in a way, that will instantiate the database only once, and return the same instance for all subsequent calls, because normally the instance would pool the connections to the database, maybe synchronize calls, etc. This can still be done without any frameworks:
public class MyApplication {
private Database database;
public synchronized Database getDatabase() {
if (database != null) {
database = new SomeDatabaseImpl(...);
}
return database;
}
...
}
This is arguably the point when the boilerplate becomes uncomfortably big and error prone. Instead of throwing out our otherwise perfectly good initialization code, JayWire can step in here, and solve this single problem without forcing any other change:
public class MyApplication extends StandaloneModule {
public Database getDatabase() {
return singleton( () -> new SomeDatabaseImpl(...) );
}
...
}
Through extending the StandaloneModule, JayWire offers the method singleton(), which takes a “factory” that can instantiate the specific object. This factory can be easily implemented using lambda expressions as seen above. JayWire will use the factory to instantiate the database on the first call, and use the factory as key to return the same instance on all subsequent calls.
Please note, that the information whether the database is a singleton does not reside in the database implementation. Also note, that the database implementation was not changed, not even with an annotation, and therefore does not depend in any way on JayWire.
Dynamic Objects
Most traditional “scopes” are supported:
- Singleton
- ThreadLocal
- Request
- Session
The “singleton” scope will not produce a static singleton (one instance per JVM), but a singleton relative to the Application instance. The request and session scopes are only available if bound to a web context (see JayWire Integration).
Sometimes, when scope boundaries are crossed between an object and its dependency, a statically injected object is not enough. Consider the following code:
public class OrganizationService {
public User currentUser;
public OrganizationService(User currentUser) {
this.currentUser = currentUser;
}
public List<Employee> getSubordinates() {
return currentUser.getSubordinates();
}
}
The problem with this piece of code is, that OrganizationService is a singleton, and the current user is a session scoped object. Therefore injecting a static User object does not work. Other DI frameworks usually solve this problem by not really injecting a User object, but a transparent proxy, which again, hides important information from the developer.
To make this underlying difference between the service and user visible, the code can be changed to this:
import java.util.function.Supplier;
public class OrganizationService {
public Supplier<User> currentUserSupplier;
public OrganizationService(Supplier<User> currentUserSupplier) {
this.currentUserSupplier = currentUserSupplier;
}
public List<People> getSubordinates() {
return currentUserSupplier.get().getSubordinates();
}
}
With the indirection through a Supplier, the code represents the existing indirection through the Session, without actually depending on the Session. The code can/must therefore explicitly prepare for getting different instances of users, as it should anyway.
The standard scopes in JayWire all support this kind of explicit indirection through a Supplier the following way:
public class MyApplication extends SparkModule {
public Supplier<User> getCurrentUserSupplier() {
return sessionScope( () -> new User(...) );
}
public OrganizationService getOrganizationService() {
return new singleton( () -> new OrganizationService(getCurrentUserSupplier()) );
}
}
Going Multi-Module
To be able to scale up the object graph of the application, it needs to be possible to split the wiring code into different fragments or Modules. To do this, there needs to be a mechanism for defining module-level dependencies, objects which the module needs but does not manage.
Some frameworks, like Spring, have a solution to this (called “external beans” in this case), others, like CDI, just sidestep the issue with implicit “automatic” wiring based on interfaces.
JayWire, of course, does not only explicitly define inter-module dependencies, but makes them compile-time safe. If some module dependency is not fulfilled, the application will not compile!
The mechanism for this is quite simply abstract methods. Let’s assume a Module contains the previous two repository objects, but does not know what the database implementation should be or how it is instantiated. In this case the Module can be defined as:
public abstract class RepositoryModule extends StandaloneModule {
public abstract Database getDatabase();
public PeopleRepository getPeopleRepository() {
return singleton( () -> new PeopleRepository(getDatabase()) );
}
public GroupRepository getGroupRepository() {
return singleton( () -> new GroupRepository(getDatabase()) );
}
...
}
Obviously this Module can not be instantiated as long as the Database dependency is not defined explicitly somewhere higher up the module dependency tree.
Dependency on Scopes
One problem with the above Module is that it extends the StandaloneModule directly to be able to access the standard JayWire scopes. This dependency is wrong most of the time, since a Module does not need to know whether the application it will be included in will be a standalone application, a JEE application, or a Spark application.
To avoid directly depending on a specific integration class, JayWire offers interfaces for all standard scopes, which can be used the following way:
public abstract class RepositoryModule implements SingletonScopeSupport {
public abstract Database getDatabase();
public PeopleRepository getPeopleRepository() {
return singleton( () -> new PeopleRepository(getDatabase()) );
}
public GroupRepository getGroupRepository() {
return singleton( () -> new GroupRepository(getDatabase()) );
}
...
}
This Module still defines the same objects, but now has a “dependency” on the singleton scope (there is an abstract getSingletonScope() method through the SingletonScopeSupport interface), which somebody higher up has to provide, quite similarly to how a Database needs to be provided. So basically Scopes can be thought of as ordinary dependencies themselves.
Combining Modules
To start an application, all the relevant Modules need to be used, with all the missing dependencies filled/implemented at this point, including the Scope objects themselves.
Combining multiple Modules through inheritance is not possible, since that would be multiple inheritance. Combining multiple Modules through composition, while possible, would be quite complicated and redundant. Java 8, however, offers another (though still admittedly controversial) approach: Mixins.
Since the Modules themselves do not define any instance variables, only methods to create objects, it is possible to convert them to interfaces with default methods:
public interface RepositoryModule extends SingletonScopeSupport {
Database getDatabase();
default PeopleRepository getPeopleRepository() {
return singleton( () -> new PeopleRepository(getDatabase()) );
}
default GroupRepository getGroupRepository() {
return singleton( () -> new GroupRepository(getDatabase()) );
}
...
}
This Module still contains the same knowledge, but now as a pure interface, and therefore can be combined freely.
At the top of the dependency graph, all the Modules can be combined in the following way:
public class MyApplication extends SparkModule implements
RepositoryModule, DatabaseModule, WebPagesModule {
@Override
public Database getDatabase() {
return getSomeSpecificDatabase();
}
}
The Scopes are provided by extending a specific integration implementation of JayWire, and the application Modules are “mixed-in” through the interfaces implemented.
At this point, the decision can be made what database to use for the repository objects, and with that all dependencies are available to instantiate MyApplication. Please note, that if any dependencies are missing the compiler would indicate an error immediately.
Summary
This tutorial shows how JayWire can be used to explicitly and safely wire objects graphs of arbitrary size together. The resulting code is not only compile-time safe, but avoids magical tools that would make the process less transparent and less readable, and also does not impose any programming models or life-cycle restrictions on the objects used.
JayWire is available on GitHub: https://github.com/vanillasource/jaywire
Published at DZone with permission of Robert Brautigam. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Authorization: Get It Done Right, Get It Done Early
-
Writing a Vector Database in a Week in Rust
-
Tomorrow’s Cloud Today: Unpacking the Future of Cloud Computing
-
Transactional Outbox Patterns Step by Step With Spring and Kotlin
Comments