I have come up with a quick and simple IoC container to enable minimal dependency injection support in one of my Java projects.
The container supports services with:
- A transient lifetime, meaning, a new instance of the service will be created each time they are requested from the container.
- A singleton lifetime, which only creates an instance only once and returns this same instance upon subsequent requests.
Services are registered using the addService()
method which takes in the types representing the abstraction and the corresponding implementation and the service lifetime.
A service can be requested using the getService()
method which returns the corresponding service implementation for an abstraction.
Finally, I have an injectAndConstruct()
method that takes in a client that depends on one or more of these services. It extracts the public constructor of the client and resolves all the dependencies and creates a new instance using this constructor through reflection.
Code
public final class Container {
private final Map<Class<?>, ServiceDescriptor<?, ?>> services = new HashMap<>();
// holds singleton instances after initial creation
private final Map<Class<?>, Object> singletonInstances = new HashMap<>();
public <T> void addService(
Class<T> abstraction,
Class<? extends T> implementation,
Lifetime lifetime) {
validateAbstraction(abstraction);
validateService(implementation);
services.put(abstraction, new ServiceDescriptor<>(abstraction, implementation, lifetime));
}
@SuppressWarnings("unchecked")
public <T> T getService(Class<T> abstraction) {
var descriptor = (ServiceDescriptor<T, ? extends T>)services.get(abstraction);
if (descriptor == null) {
throw new ServiceNotFoundException(abstraction); // custom exception, definition omitted for brevity
}
Class<? extends T> implType = descriptor.implementationType();
Constructor<? extends T> publicConstructor = (Constructor<? extends T>) implType.getConstructors()[0];
return switch(descriptor.lifetime()) {
case TRANSIENT -> newInstanceOf(publicConstructor);
case SINGLETON -> {
// retrieve any existing instance
T instance = (T) singletonInstances.getOrDefault(abstraction, null);
// first request: create singleton instance and save to map
if (instance == null) {
instance = newInstanceOf(publicConstructor);
singletonInstances.put(abstraction, instance);
}
yield instance;
}
};
}
@SuppressWarnings("unchecked")
public <T> T injectAndConstruct(Class<T> clazz) {
validateService(clazz);
Constructor<? extends T> publicConstructor = (Constructor<? extends T>) clazz.getConstructors()[0];
return newInstanceOf(publicConstructor);
}
private <T> T newInstanceOf(Constructor<T> constructor) {
var parameterTypes = constructor.getParameterTypes();
Object[] parameters = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
parameters[i] = getService(parameterTypes[i]);
}
try {
return constructor.newInstance(parameters);
} catch (ReflectiveOperationException exception) {
throw new RuntimeException(exception);
}
}
private void validateService(Class<?> implementation) {
if (implementation.getConstructors().length != 1) {
throw new IllegalArgumentException("The service implementation must have exactly one public constructor");
}
}
private void validateAbstraction(Class<?> abstraction) {
if (!Modifier.isAbstract(abstraction.getModifiers()) && !abstraction.isInterface()) {
throw new IllegalArgumentException("The parameter \"abstraction\" must represent an abstract class or an interface");
}
}
}
The ServiceDescriptor
holds information about a service:
record ServiceDescriptor<A, I extends A>(
Class<A> abstractionType,
Class<I> implementationType,
Lifetime lifetime) {
}
The Lifetime
enum is to specify the service lifetime:
enum Lifetime {
SINGLETON,
TRANSIENT
}
Usage
Container container = new Container();
container.addService(Service1.class, Service1Impl.class, Lifetime.TRANSIENT);
container.addService(Service2.class, Service2Impl.class, Lifetime.SINGLETON);
Client client = container.injectAndConstruct(Client.class);
class Client {
public Client(Service1, s1, Service2 s2) { .. }
}
interface Service1 { ... }
interface Service2 { ... }
class Service1Impl implements Service1 { ... }
class Service2Impl implements Service2 { ... }
This seems more of a service locator than injection.
I plan to use this in my JavaFX projects for dependency injection in my FXML controllers.
The JavaFX FXML loader allows setting a controller factory that helps control creation of controller instances. This factory is essentially a javafx.util.Callback<Class<?>, Object>
.
The above container allows me to do this:
Container container = new Container();
// add services
FXMLLoader fxmlLoader = new FXMLLoader(...);
fxmlLoader.setControllerFactory(x -> container.injectAndConstruct(MyController.class));
And my controller can declare dependencies in the constructor:
public class MyController {
// Injected by factory
public MyController(Service1 s1, Service2 s2) {..}
}
Let me know if there is something I can refactor or improve or if I'm missing something.
(T) ...
doabstraction.cast(...)
. \$\endgroup\$