@Documented
@NormalScope
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface CallScoped {}
11 September 2020
Quarkus provides no full CDI implementation and as such, no support for CDI extension. This is because CDI extensions are inherently runtime-based and thus do not fit into Quarkus' model of doing as much as possible during build-time. No support for CDI extensions also means no standard support for registration of custom CDI scopes.
Well, it sounds like quiet a limitation, but actually Arc (Quarkus' CDI implementation) provides an API to register custom scopes. And as you will see, implementing a custom scope is 99% the same as you know it from standard CDI.
In this post, I will show the code for a simple custom scope that that is local to the current thread; i.e. the context keeps track of thread-local state.
The scope is called CallScoped
and that is also the the name of the annotation:
@Documented
@NormalScope
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface CallScoped {}
The context-class, which contains the main login of any custom scope, I will not put here in it’s entirety but only describe what is different to a standard CDI context. You can find the CallScopeContext
here.
public class CallScopeContext implements InjectableContext {
static final ThreadLocal<Map<Contextual<?>, ContextInstanceHandle<?>>> ACTIVE_SCOPE_ON_THREAD = new ThreadLocal<>();
//...
}
The context-class needs to implement InjectableContext
which is Quarkus specific, but extends from the standard AlterableContext
. So, there are only two additional methods to implement: destroy
and getState
. The first is to destroy the active scope entirely; and the second allows to capture and browse the state of the context. E.g. it enables this dev-mode feature.
@Override
public void destroy() {
Map<Contextual<?>, ContextInstanceHandle<?>> context = ACTIVE_SCOPE_ON_THREAD.get();
if (context == null) {
throw new ContextNotActiveException();
}
context.values().forEach(ContextInstanceHandle::destroy);
}
@Override
public ContextState getState() {
return new ContextState() {
@Override
public Map<InjectableBean<?>, Object> getContextualInstances() {
Map<Contextual<?>, ContextInstanceHandle<?>> activeScope = ACTIVE_SCOPE_ON_THREAD.get();
if (activeScope != null) {
return activeScope.values().stream()
.collect(Collectors.toMap(ContextInstanceHandle::getBean, ContextInstanceHandle::get));
}
return Collections.emptyMap();
}
};
}
The registration of the custom scope and context happens during built-time in a @BuildStep
.
public class ApplicationExtensionProcessor {
@BuildStep
public void transactionContext(
BuildProducer<ContextRegistrarBuildItem> contextRegistry) {
contextRegistry.produce(new ContextRegistrarBuildItem(new ContextRegistrar() {
@Override
public void register(RegistrationContext registrationContext) {
registrationContext.configure(CallScoped.class).normal().contextClass(CallScopeContext.class) // it needs to be of type InjectableContext...
.done();
}
}, CallScoped.class));
}
}
There is one slight difference to a standard CDI context. As you see, the context-class is registered during build-time by just giving the type. With CDI and a CDI extension, you would provide an instance to CDI. This way, you can create and share a single reference to your context with the CDI implementation and the application-side. I.e. for our CallScoped
, the CallScopeContext
offers an API to the application to start a scope on the current thread via enter
and exit
methods (see here).
Currently, this is a limitation of Quarkus as there is no possibility to share a single instance or access the runtime instance. But because state is usually stored in statics or thread-local, there is no problem in having actually two instances of the context-class; one used by Quarkus internally, one by the application-side. But support for this is already under consideration.
You can find the full code example here. It’s on a branch of my quarkus-sandbox repo which is a good starting point if you want to experiment with Quarkus + Quarkus Extensions (using Gradle).