3
\$\begingroup\$

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.

asked Jul 14, 2022 at 19:47
\$\endgroup\$
3
  • 4
    \$\begingroup\$ Why reinvent the wheel? There are already robust dependency injection frameworks, like Dagger (compile time injection) and Spring (runtime injection) \$\endgroup\$ Commented Jul 18, 2022 at 7:11
  • 2
    \$\begingroup\$ Drop the unchecked annotation: instead (T) ... do abstraction.cast(...). \$\endgroup\$ Commented Jul 22, 2022 at 8:37
  • 2
    \$\begingroup\$ Seems like a fun exercise. My only thought was instead of exposing the Lifetime enum, encapsulate that and instead expose addSingleton and addTransient instead of addService. \$\endgroup\$ Commented Jul 22, 2022 at 14:57

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.