If you never implemented a CDI context/scope it is a simple as implementing this interface:
public interface Context { Class<? extends Annotation> getScope(); <T> T get(Contextual<T> component, CreationalContext<T> creationalContext); <T> T get(Contextual<T> component); boolean isActive(); }
Note: in CDI 1.1 there is AlterableContext too which just adds a destroy(Contextual) method which is not important for this post so I will ignore it but I would recommand you to use it instead of Context if you can rely on CDI 1.1.
The Context implementation is quite simple:
- isActive() returns true if the context is usable by the CDI container
- getScope() returns the associated annotation (often @XXXScoped)
- get(Contextual) returns the instance of the Contextual (~= Bean) for “current” context
- get(Contextual, CreationalContext) creates or returns current instance
Creating a scope “annotation” is as easy as creating a runtime annotation:
// @NormalScope(passivating=false) @Target({ METHOD, TYPE, FIELD }) @Retention(RUNTIME) public @interface WrappingMethodScoped { }
Note: the @NormalScope is optional since it can be done by an extension – this is what we’ll do.
Now we know what is a CDI scope let see how to activate it programmatically.
The technical implementation
We’ll take the example of a context activated either in a custom length scope (ie delimited programmatically) or delimited by a method (ie decorating a method by an annotation will start/stop the scope around the method).
First to activate and make our scope usable we need a small CDI extension, we will call our extension com.github.rmannibucau.cdi.scopes.api.method.MethodScopeExtension – don’t forget to register it in META-INF/services/javax.enterprise.inject.spi.Extension (just enter the fully qualified name in the file).
Here is the simpler implementation to register our scope annotation and context:
package com.github.rmannibucau.cdi.scopes.api.method; import com.github.rmannibucau.cdi.scopes.internal.method.MethodContext; import javax.enterprise.event.Observes; import javax.enterprise.inject.spi.AfterBeanDiscovery; import javax.enterprise.inject.spi.BeforeBeanDiscovery; import javax.enterprise.inject.spi.Extension; public class MethodScopeExtension implements Extension { private final MethodContext methodContext = new MethodContext(); void addScope(@Observes final BeforeBeanDiscovery beforeBeanDiscovery) { beforeBeanDiscovery.addScope(WrappingMethodScoped.class, true, false); } void addContext(@Observes final AfterBeanDiscovery afterBeanDiscovery) { afterBeanDiscovery.addContext(methodContext); } }
Quite simple since CDI events have the addXXX methods we need for that purpose.
Now you probably guessed we need to implement our scope “MethodContext”. The idea is to rely on a Map in a ThreadLocal we set to activate the scope and store the instances. Our scope annotation will be called @WrappingMethodScoped as in the previous sample annotation.
A bit like DeltaSpike ContextControl this context would be controlled in the code either declaratively or programmatically. The advantage is to make it lighter and fluent with java 8 but it is also a very good example of the strength of CDI and how several features can be used together to build new features. You can fake this kind of scope with DeltaSpike ContextControl and @RequestScoped but this is a workaround where here we will provide a scope really designed to be used when the application decides!
package com.github.rmannibucau.cdi.scopes.internal.method; import com.github.rmannibucau.cdi.scopes.api.method.WrappingMethodScoped; import javax.enterprise.context.ContextNotActiveException; import javax.enterprise.context.spi.AlterableContext; import javax.enterprise.context.spi.Contextual; import javax.enterprise.context.spi.CreationalContext; import java.lang.annotation.Annotation; import java.util.HashMap; import java.util.Map; import static java.util.Optional.ofNullable; public class MethodContext implements AlterableContext { private static final RuntimeException NOT_ACTIVE_EXCEPTION = new ContextNotActiveException("@" + WrappingMethodScoped.class.getName() + " is not active."); private final ThreadLocal<Map<Contextual<?>, Instance>> instances = new ThreadLocal<>(); public Class<? extends Annotation> getScope() { return WrappingMethodScoped.class; } public Object execute(final SupplierWithException task) throws Exception { // inheritance is the default behavior for this scope Map<Contextual<?>, Instance> map = instances.get(); final boolean clean = map == null; if (map == null) { map = new HashMap<>(); instances.set(map); } try { return task.run(); } finally { if (clean) { map.values().stream().forEach(Instance::destroy); map.clear(); instances.remove(); } } } @Override public boolean isActive() { final boolean initialized = instances.get() != null; if (!initialized) { instances.remove(); } return initialized; } @Override public <T> T get(final Contextual<T> contextual, final CreationalContext<T> creationalContext) { checkActive(); final Map<Contextual<?>, Instance> localMap = instances.get(); return (T) ofNullable(localMap.get(contextual)) .orElseGet(() -> localMap.computeIfAbsent(contextual, c -> new Instance(contextual, creationalContext))) .create(); } @Override public <T> T get(final Contextual<T> contextual) { checkActive(); return (T) ofNullable(instances.get().get(contextual)).map(i -> i.instance).orElse(null); } @Override public void destroy(final Contextual<?> contextual) { ofNullable(instances.get().get(contextual)).filter(c -> c.instance != null) .ifPresent(Instance::destroy); } private void checkActive() { if (!isActive()) { throw NOT_ACTIVE_EXCEPTION; } } // wrapper for an instance keeping track of the creational context and bean private static class Instance<T> { private final CreationalContext<T> context; private final Contextual<T> bean; private T instance; public Instance(final Contextual<T> contextual, final CreationalContext<T> context) { this.bean = contextual; this.context = context; } // no lock needed cause of the nature of the scope public T create() { if (instance == null) { instance = bean.create(context); } return instance; } public void destroy() { if (instance == null) { return; } bean.destroy(instance, context); instance = null; } } }
The only important points of this implementation are:
- execute method: execute a task in a “method scope”. Note we used a custom SupplierWithException functional interface which is just a function returning a value and able to throw an exception (you can use any @FunctionalInterface you desire/need there.).
- isActive(): the only trick here is to ensure to clean up the thread local is it was not initialized (remove()).
- Instance: this is just a wrapper keeping the association of the final instance to its creational context for destruction.
- execute finally block: ensures to destroy the instances (will trigger @PreDestroy, @Disposes etc…)
Make the technical layer usable by end users
We have everything to make it working but we can’t access the context easily.
Programmatic control
Our context has an execute() method which is almost enough. The only missing thing is to expose it. There are several ways but the easiest is to expose a wrapper in our extension. We can add for instance the methods:
// for task without return value public void executeInScope(final Runnable runnable) { executeInScope(() -> { runnable.run(); return null; }); } // for tasks with a return value public <T> T executeInScope(final Supplier<T> task) { try { return (T) methodContext.execute(() -> task.get()); } catch (final Exception e) { throw new IllegalStateException(e); } }
then in your business code just inject the extension and use these two methods:
extension.executeInScope(() -> { /* use @WrappingMethodScoped beans */ });
Note: a cleaner implementation would be to add a custom bean created in our extension which is @ApplicationScoped and which takes the context as constructor parameter but it would make this post a bit too long ;).
@WithScope activation
The idea is to automate the execute() invocation around a method decorated with @WithScope. What would be the best way to wrap a method with CDI in a generic manner? An interceptor of course. Our interceptor binding will be our @WithScope annotation and the implementation will invoke the method as a task of executeInScope().
Here our binding (registered programmatically):
@Target({ METHOD, TYPE }) @Retention(RUNTIME) public @interface WithScope { }
Here is our interceptor:
public class ScopeActivatorInterceptor implements Interceptor<Object> { private final MethodContext methodContext; private final Set<Type> types = singleton(Object.class); private final Set<Annotation> bindings = singleton(new WithScope() { @Override public Class<? extends Annotation> annotationType() { return WithScope.class; } }); public ScopeActivatorInterceptor(final MethodContext methodContext) { this.methodContext = methodContext; } @Override public Set<Annotation> getInterceptorBindings() { return bindings; } @Override public boolean intercepts(final InterceptionType type) { return type == InterceptionType.AROUND_INVOKE; } @Override public Object intercept(final InterceptionType type, final Object instance, final InvocationContext ctx) throws Exception { return methodContext.execute(ctx::proceed); } @Override public Set<InjectionPoint> getInjectionPoints() { return emptySet(); } @Override public Class<?> getBeanClass() { return Object.class; } @Override public boolean isNullable() { return false; } @Override public Set<Type> getTypes() { return types; } @Override public Set<Annotation> getQualifiers() { return emptySet(); } @Override public Class<? extends Annotation> getScope() { return Dependent.class; } @Override public String getName() { return null; } @Override public Set<Class<? extends Annotation>> getStereotypes() { return emptySet(); } @Override public boolean isAlternative() { return false; } @Override public Object create(final CreationalContext<Object> context) { return this; // ignored anyway } @Override public void destroy(final Object instance, final CreationalContext<Object> context) { // no-op } }
As you can note this is not a “business” interceptor but a programmatic interceptor. We 100% rely on intercept() method in this case.
Now we have the implementation we just need to register it. For that we just add a bit as for the context one addInterceptorBinding() and one addBean():
public class MethodScopeExtension implements Extension { private final MethodContext methodContext = new MethodContext(); void addScope(@Observes final BeforeBeanDiscovery beforeBeanDiscovery) { beforeBeanDiscovery.addScope(WrappingMethodScoped.class, true, false); beforeBeanDiscovery.addInterceptorBinding(WithScope.class); } void addContext(@Observes final AfterBeanDiscovery afterBeanDiscovery) { afterBeanDiscovery.addContext(methodContext); afterBeanDiscovery.addBean(new ScopeActivatorInterceptor(methodContext)); } }
Now we can use our scope “automatically” on a method:
@Inject private AMethodScopedBean bean; @WithScope public void playWithScope() { bean.doSomething(); }
Conclusion
We often hesitate to enter in CDI internals but creating custom scopes is easy and mixing it with other CDI features is very powerful.
Note: if you are tempted don’t hesitate to have a look to deltaspike core modules which provide some helpers to build Bean in very few linse of code and without having to implement all these methods (you only need to implement the create()/destroy() methods).
The code of this post is available on https://github.com/rmannibucau/scopes.
Going further
This extension breaks CDI loose coupling since you will activate the scope “when you know you need it” so the client code knows the scope of the injection…but nothing prevents you to inspect in an extension injection points of beans and to add @WithScope through the extension too so @WithScope becomes optional.
Want more content ? Keep up-to-date on my new blog
Or stay in touch on twitter @rmannibucau
Pingback: Java Weekly 45/15: JavaOne, Microservices, real world Jigsaw