Vanilla Java is a series of posts advocating for pure Java first, then purpose built utilities with JDK, and only adding third-party dependencies as a last resort.
What is a Dependency Injection (DI) container?
DI in its simplest form is a design pattern describing application's object graph lifetime. A DI container is an opinionated implementation of this design pattern, i.e. a framework. Throughout my Java software engineering career I gained hands-on experience with Guice, Spring, various J2EE implementations since v2.x up, Grails and many others. After initial euforia which laster for nearly a decade I realized they basically all become a maintenance nightmare as the project grows.
After Java 8 became wide-spread, which is where I draw a line for "modern" Java, there came a realization that none of the DI containers are necessary at all. The language has evolved to the point vanilla Java is good enough.
SRDI pronounced "SERDI", hard "R": Sender - Receiver Dependency Injection
For networking enthusiasts the terms SEnder and Receiver immediately ring the bell. These terms are borrowed from the RFC 9293 and there is definitely prior art.
Am I re-inventing the wheel? Not at all. Any of the aforementioned frameworks are an abstract, where your own application is an instance. I aim to remove the abstract away and go straight to the instance. The DI design pattern servers as a reference.
Minimal application
A short refresher on com.sun.net.httpserver.HttpServer in my older post.
Using the JDK's built in HTTP server a trivial main(String[] args):
package eu.freshmen.srdi;
import com.sun.net.httpserver.HttpServer;
import eu.freshmen.srdi.sender.Logger;
import eu.freshmen.srdi.sender.Server;
public class Application {
interface Dependencies extends Server {
// a Receiver: declares all dependencies via extension
}
private final Logger log;
private final HttpServer server;
Application(Dependencies deps) {
log = deps; // downcast
server = deps.server(); // instantiation
}
void start() {
final var addr = server.getAddress();
log.debug("Starting on " + addr.getHostName() + ":" + addr.getPort() + ", SIGKILL to stop.");
server.start();
}
public static void main(String[] args) {
final var app = new Application(new Dependencies() {
// trivial Sender: all dependencies provide default implementations
});
app.start();
}
}
- Why vanilla JDK?
- With pure interfaces and language's backwards compatibility, there is near zero chance SRDI stops working during upgrades and zero chance of forced framework upgrades.
- Where does SRDI shine?
- SRDI is a compile time DI. Meaning nothing builds unless all the dependencies are resolved. There is no runtime classpath scan, annotations, name resolution, unexpected surprises, etc.
- Startup time only depends on how quickly instantiation of dependencies works, near zero overhead.
- Dependency overrides or profiles are clear from the interface hierarchy and any IDE can visualize it.
- Where does SRDI fall short?
- SRDI does not have a concept of scopes, meaning everything lives as long as the process does.
- SRDI needs an explicit memoize component in case a singleton is desired.
- request scope needs to be hand rolled and is generally application specific.
Sources
- Spring Reference: IoC and DI
- Guice Dependency Injection
- Learn .NET Dependency Injection
- Manning: Writing Maintainable, Loosely-Coupled Code
- Java Illegal Reflective Access (
module opens)