quarkuscdi

Quarkus CDI - custom manually toggleable ManagedContext/Scope


I am trying to implement a custom scope/context, similar to @RequestScoped, but I want to be able to manually toggle it, similar to how QuarkusTransaction.begin() and QuarkusTransaction.commit() toggle @TransactionScoped scope.

I looked through the Quarkus's source code, and could not really figure out how to do so. Initially, I wanted to use the approach described here: https://rpestano.wordpress.com/2013/06/30/cdi-custom-scope/, and use events and observables on my scope:

public class CustomScopeContext implements Context, Serializable {
 
    private Logger log = Logger.getLogger(getClass().getSimpleName());
 
    private CustomScopeContextHolder customScopeContextHolder;
 
    public CustomScopeContext() {
        log.info("Init");
        this.customScopeContextHolder = CustomScopeContextHolder.getInstance();
    }

    // ...

    public void destroy(@Observes KillEvent killEvent) {
        if (customScopeContextHolder.getBeans().containsKey(killEvent.getBeanType())) {
            customScopeContextHolder.destroyBean(customScopeContextHolder.getBean(killEvent.getBeanType()));
        }
    }
}

This does not work in Quarkus, because in order for Quarkus to receive events, a class should be defined as a bean, but by doing so via @ApplicationScoped and an appropriate build-item, each time it received my event, it had a completely different empty CustomScopeContextHolder.

From the Quarkus's source code, it seems that I need to use ManagedContext to implement contexts that can be manually controlled, but the problem is that its activate() and deactivate() methods accept no parameters, so there is no way to identify which scope to start/stop. I tried using ThreadLocal<Map<Contextual<?>, ContextualInstance<?>>> instances that seemed to have worked, but I wonder if ThreadLocal behaves appropriately in Quarkus. So far, there were no issues, but I am not sure if it is going to just be a floating bug at some point. (From what I found, precisely because of issues with ThreadLocal, other CDI frameworks, like Weld have their own classes org.jboss.weld.context.BoundContext for classes that rely on ThreadLocal state)

Additionally, I found a CurrentManagedContext that is the base class that RequestContext and SessionContext use under the hood in Quarkus, but it also lacks proper documentations, so I could not figure out how to use it; furthermore, it is a part of the arc.impl package, so I am afraid that the Quarkus is free to break these classes without any warning.

So, how can I implement a scope that can be toggled manually via some functions, for example:

MyBean.java

@MyScoped
public class MyBean{

    private String value;
    // ...
}

MyService.java

@ApplicationScoped
public class MyService{
   
    @Inject
    MyBean myBean;

    public void foo() {
        // myBean unavailable
        MyScope.init();
        // myBean#1 is available
        myBean.setValue("foo");
        System.out.println(myBean.getValue()); // prints "foo"
        MyScope.deinit();
 
        // myBean unavailable 
        // Will throw an exception:
        // System.out.println(myBean.getValue());
 
        MyScope.init();
        // myBean#2 is available
        myBean.setValue("bar");
        System.out.println(myBean.getValue()); // prints "bar"
        MyScope.deinit();
    }
}

Solution

  • This does not work in Quarkus, because in order for Quarkus to receive events, a class should be defined as a bean

    This should be true not only for Quarkus but other implementations as well. A context object, while maintained by CDI container, is not a CDI bean.

    Below code assumes that you know how to register the custom scope/context in CDI/Quarkus and that you want to activate/deactivate based on certain CDI events you are firing/observing.

    One way way I can think of is to activate/deactivate this context from any other helper bean monitoring the events you are looking for - Quarkus/ArC keeps track of the Context instance and you can ask for it. For this to work, your context will need to implement io.quarkus.arc.InjectableContext which is an extended API Quarkus uses for contexts and which isn't going to change without warnings. Here's how you can get context object:

            List<InjectableContext> contextImpls = Arc.container().getContexts(MyScoped.class);
            if (contextImpls.size() != 1) {
                // throw some error
            }
            InjectableContext myContextImpl = contextImpls.iterator().next();
    

    From here you can call any method on your context. Let's say your context has (among other things) following methods:

    public class CustomScopeContext implements InjectableContext {
        
        public void activateContext() {
            // do any work needed for context activation
        }
    
        public void deactivateContext() {
            // do any work needed for context deactivation/destruction
        }
    }
    

    You could then have a CDI bean observing the events you are looking for and manipulating your context. Here's a snippet:

    @ApplicationScoped
    public class ContextManipulatingBean {
    
        public void destroy(@Observes KillEvent killEvent) {
                    ((CustomScopeContext)Arc.container().getContexts(MyScoped.class).iterator().next()).deactivateContext();
            }
        }
    }
    

    For an example of a custom context implementing InjectableContext you can take a look at TransactionContext used by JTA (Narayana).