CDI Custom Scope

In this post i will show how to create a custom CDI scope by performing the following steps:

  1. register the scope in a CDI Extension
  2. manage the scope through a CDI context
  3. show how to store and keep custom scoped beans ‘alive’
  4. ‘kill’ custom beans through CDI events

Check the source code of this entry at: https://github.com/rmpestano/cdi-custom-scope.
also you can see a video produced by this blog entry code here

First of all lets talk about CDI scopes, which are responsible by the lifecycle of CDI beans. Every scope is bound to a CDI context[1] which in turn manages the scope and states when the scope is active and passive. The context also holds all instances of the beans associated to the scope it manages.

For example the built in RequestScope is bound to a http request, the RequestContext must ensure that all beans in this scope will be alive during a http request and be passivated at the end of it, you can see weld http based contexts in [4].

Creating the Custom Scope

We will create a CDI Custom Scope which will be activated when a bean in this scope is referenced through EL and or injected in another bean and it will ‘die’ when a specific CDI event is fired.

First thing to do is create an annotation which will represent our scope:

@Scope
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})
public @interface MyScope {}

Now we add our scope and register the context that will manage it  in a CDI extension:

public class CustomScopeExtension implements Extension, Serializable {
    public void addScope(@Observes final BeforeBeanDiscovery event) {
        event.addScope(MyScope.class, true, false);
    }
    public void registerContext(@Observes final AfterBeanDiscovery event) {
        event.addContext(new CustomScopeContext());
    }
}

All the logic of our scope is in the CustomScopeContext class:

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();
    }

    @Override
    public <T> T get(final Contextual<T> contextual) {
        Bean bean = (Bean) contextual;
        if (customScopeContextHolder.getBeans().containsKey(bean.getBeanClass())) {
            return (T) customScopeContextHolder.getBean(bean.getBeanClass()).instance;
        } else {
            return null;
        }
    }

    @Override
    public <T> T get(final Contextual<T> contextual, final CreationalContext<T> creationalContext) {
        Bean bean = (Bean) contextual;
        if (customScopeContextHolder.getBeans().containsKey(bean.getBeanClass())) {
            return (T) customScopeContextHolder.getBean(bean.getBeanClass()).instance;
        } else {
            T t = (T) bean.create(creationalContext);
            CustomScopeInstance customInstance = new CustomScopeInstance();
            customInstance.bean = bean;
            customInstance.ctx = creationalContext;
            customInstance.instance = t;
            customScopeContextHolder.putBean(customInstance);
            return t;
        }
    }

    @Override
    public Class<? extends Annotation> getScope() {
        return MyScope.class;
    }

    public boolean isActive() {
        return true;
    }

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

The Mandatory methods you must implement are getScope() which must return the qualifier bound to this scope, two get() implementaions that i will talk later and isActive() which states when the scope should be taken into account, for example Seam3 viewScope[5] is active only when viewRoot is rendered.

The idea behind the context is that its GET methods will be called every time a bean associated with the context is injected in or is referenced though expression language in a page.  The GET with CreationalContext  parammeter will be called only the first time the bean is referenced so it can be created.

Every time a bean is referenced the context will look in customScopeContextHolder to verify if the bean is already created, in positive case  it will return the instance otherwise it will create one and add into the list of beans managed by customScopeContextHolder which is a singleton class that holds the list of CDI beans associated with our custom scope, note that you might create your own logic to hold your context’s beans. Also note that our context is observing a CDI event to remove beans from the context which is another important thing you must take care when creating your own scope, see destroy method.

Below is the bean ContextHolder code:


public class CustomScopeContextHolder implements Serializable {

    private static CustomScopeContextHolder INSTANCE;
    private Map<Class, CustomScopeInstance> beans;//we will have only one instance of a type so the key is a class

    private CustomScopeContextHolder() {
        beans = Collections.synchronizedMap(new HashMap<Class, CustomScopeInstance>());
    }

    public synchronized static CustomScopeContextHolder getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new CustomScopeContextHolder();
        }
        return INSTANCE;
    }

    public Map<Class, CustomScopeInstance> getBeans() {
        return beans;
    }

    public CustomScopeInstance getBean(Class type) {
        return getBeans().get(type);
    }

    public void putBean(CustomScopeInstance customInstance) {
        getBeans().put(customInstance.bean.getBeanClass(), customInstance);
    }

    void destroyBean(CustomScopeInstance customScopeInstance) {
        getBeans().remove(customScopeInstance.bean.getBeanClass());
        customScopeInstance.bean.destroy(customScopeInstance.instance, customScopeInstance.ctx);
    }

    /**
* wrap necessary properties so we can destroy the bean later:
*
* @see
* CustomScopeContextHolder#destroyBean(custom.scope.extension.CustomScopeContextHolder.CustomScopeInstance)
*/
    public static class CustomScopeInstance<T> {

        Bean<T> bean;
        CreationalContext<T> ctx;
        T instance;
    }
}

Holding bean references

Note that CustomScopeContext is a normal java class, it is instantiated via new operator, so it is not a good ideia storing our bean references in it because CDI will instantiate it as it needs losing the bean instances stored in it(see Martin’s comment). For example when a bean(in our context) participate on a CDI event the CDI container will create a thread of the context to attend the event so because of this we introduced the CustomScopeContextHolder to manage our bean instances. There are other approaches such as ThreadLocals[6], http session, JSF viewroot[5], static variables etc… I decided to use a singleton something like Deltaspike TransactionContext[7] but faraway simpler.

Conclusion

We saw here a powerful mechanism provided by CDI to manage our beans lifecycle. In 90% of the cases the built in scopes will be sufficient but having the hability to extend the plataform to allow things such as CODI scopes[8] or Jaxb objects handled by a CDI scope[9][10] is priceless.

Now to use our scope we just declare a bean with @MyScope and inject it in another bean, to see the CustomScope in action visit: http://www.youtube.com/watch?v=2JkZFIQqrVo or clone the code at https://github.com/rmpestano/cdi-custom-scope.


References:

  1. http://docs.jboss.org/weld/reference/latest/en-US/html/scopescontexts.html
  2. http://adventuresintechology.blogspot.com.br/2012/04/custom-cdi-scopes.html
  3. http://www.verborgh.be/articles/2010/01/06/porting-the-viewscoped-jsf-annotation-to-cdi/
  4. https://github.com/weld/core/tree/2.0/impl/src/main/java/org/jboss/weld/context/http
  5. https://github.com/seam/faces/blob/develop/impl/src/main/java/org/jboss/seam/faces/context/ViewScopedContext.java
  6. https://github.com/aaronanderson/cdi-scope-test/blob/master/src/main/java/com/github/FooCDIContextImpl.java
  7. https://github.com/apache/deltaspike/blob/master/deltaspike/modules/jpa/impl/src/main/java/org/apache/deltaspike/jpa/impl/transaction/context/TransactionContext.java
  8. https://cwiki.apache.org/confluence/display/EXTCDI/JSF+Usage#JSFUsage-Scopes
  9. https://github.com/aaronanderson/jaxb-cdi
  10. http://adventuresintechology.blogspot.com.br/2013/06/more-on-cdi-scopes.html
Advertisements

6 thoughts on “CDI Custom Scope

  1. Nice post! Just a few notes and corrections…

    CustomScopeExtension:
    “event.addScope(MyScope.class, true, false)” is not necessary. It’s only required if you wish to make an annotation a scope type without adding the scope annotation. See also javax.enterprise.inject.spi.BeforeBeanDiscovery javadoc.

    Passivation:
    I wouldn’t use the term “passivation” for destroying bean instances. Passivation has a completely different meaning in the CDI spec – see also section 6.6 Passivation and passivating scopes.

    Holding bean references:
    CDI does not instantiate the context as it needs – it works with the instance you provide. However in your case CustomScopeContext is also a dependent CDI managed bean which observes KillEvent. And so CDI will create a new instance of CustomScopeContext bean each time a KillEvent is fired.

    Like

  2. Hm, your implementation is not taking the Contextual into account when looking for beans. You just look if you already have an instance for the Bean class.

    This of course will cause strange errors when you have non-trivial injections with classifiers (like @Named)…

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s