IoC – a pretty simple example
Before exploring the CDI, let's use a very simple example (I would say, a handmade example) to illustrate what a bean container is.
We will use an application that has TimeService, which simply provides a now() method returning the current LocalDateTime.
Here is what it can look like in terms of code:
public interface TimeService {
LocalDateTime now();
}
A trivial implementation will rely on the native now() implementation:
public class TimeServiceImpl implements TimeService {
@Override
public LocalDateTime now() {
return LocalDateTime.now();
}
}
But you may also need to be able to switch to a mock (for tests or another customer, for instance):
public class MockTimeService implements TimeService {
@Override
public LocalDateTime now() {
return LocalDateTime.of(2017, Month.SEPTEMBER, 4, 19, 0);
}
}
In terms of code, you will likely implement the switch with a plain old factory:
public static class TimeServiceFactory {
public TimeService create() {
if (useDefault()) {
return new TimeServiceImpl();
}
return new MockTimeService();
}
}
Then, you need to use the factory everywhere in the callers, which is quite impacting, especially when you need to add a parameter to the create() method. To solve this issue, you can put all your application instances in a single place, which we will call Container:
public class Container {
private final Map<Class<?>, Class<?>> instances = new HashMap<>();
public <A, I extends A> Container register(final Class<A> api,
final Class<I> implementation) {
instances.put(api, implementation);
return this;
}
public <T> T get(final Class<T> api) {
try {
return api.cast(
ofNullable(instances.get(api))
.orElseThrow(() -> new
IllegalArgumentException("No bean for api
<" + api.getName() + ">"))
.getConstructor()
.newInstance());
} catch (final Exception e) {
throw new IllegalArgumentException(e);
}
}
}
This is a very minimal and trivial implementation. But once it is done, you can just register all your application beans in your bootstrap class, and all the code will rely on Container to retrieve the instance. In other words, the lookup of the classes is centralized. This also means that the updates are simpler:
public class Main {
public static void main(final String[] args) {
final Container container = new Container()
.register(TimeService.class, TimeServiceImpl.class)
/*other registers if needed*/;
final TimeService timeService =
container.get(TimeService.class);
System.out.println(timeService.now());
}
}
As the last thing before starting to deal with the CDI itself, you can add services on top of the container, since the instances are created by Container. For instance, if you want to log any call to the method of a registered API, you can change the get(Class<?>) method in the following way:
public <T> T get(final Class<T> api) {
try {
final Object serviceInstance = ofNullable(instances.get(api))
.orElseThrow(() -> new IllegalArgumentException("No
bean registered for api <" + api.getName() + ">"))
.getConstructor()
.newInstance();
return api.cast(Proxy.newProxyInstance(api.getClassLoader(),
new Class<?>[]{api}, new LoggingHandler(serviceInstance,
api)));
} catch (final Exception e) {
throw new IllegalArgumentException(e);
}
}
The entire logic will be implemented in LoggingHandler, which will fully decorate the registered instance logic with logging invocations. In other words, each method invocation on the proxy instance will be forwarded to the handler:
public class LoggingHandler implements InvocationHandler {
private final Object delegate;
private final Logger logger;
public LoggingHandler(final Object delegate, final Class<?> api) {
this.delegate = delegate;
this.logger = Logger.getLogger(api.getName());
}
@Override
public Object invoke(final Object proxy, final Method method, final
Object[] args) throws Throwable {
logger.info(() -> "Calling " + method.getName());
try {
return method.invoke(delegate, args);
} catch (final InvocationTargetException ite) {
throw ite.getTargetException();
} finally {
logger.info(() -> "Called " + method.getName());
}
}
}
Now, if you call TimeService.now(), you will be able to observe the corresponding output. With the default logging setup, it looks something like this:
sept. 03, 2017 4:29:27 PM com.github.rmannibucau.container.LoggingHandler invoke
INFOS: Calling now
sept. 03, 2017 4:29:27 PM com.github.rmannibucau.container.LoggingHandler invoke
INFOS: Called now
By itself, it is not that useful, but if you add some metrics (timing), parameter logging, and so on, it can become really neat. Also, keep in mind that you can chain the handlers you add on top of the proxy.
What does this mean, regarding the performance? Well, it means that a simple call to a method we fully control (user method) can do really different things from the user code; it will be slow due to the Container class and not due to the user code. If you doubt it, take a case where the user method implementation is empty and the handler pauses for some minutes. Of course, the EE implementation doesn't do it, but it adds some complexity on top of the end user code.