01 June 2011

Using CDI from Spring

Spring and Java EE are converging in many areas, mainly due to CDI. In fact, by confining yourself to a suitable subset of annotations and other configuration mechanisms, you can design your persistence and service layers to run either on Java EE 6 or on Spring 3, simply by selecting the appropriate set of libraries.

The web layer is a different story: if your web framework is tightly coupled to either Java EE 6 or Spring, it will be hard or even impossible to simply change the container, but even in the web layer, there are solutions like Apache Wicket which work well both with Spring and Java EE, all you need is some glue code to access the dependency injection container.

I'm currently migrating a web application from Tomcat/Spring to Glassfish, and since this application is based on Spring MVC, I cannot completely replace Spring in this step. The goal of the migration is to drop all Spring dependencies from all components except the web frontend and to somehow make the service beans (stateless session beans and CDI managed beans) visible to the Spring web application context.


Looking for a solution, I found Rick Hightower's post on CDI and Spring living in harmony and the related CDISource project. The basic idea is to use a Spring BeanFactoryPostProcessor to populate Spring's application context with a factory bean for each CDI bean obtained from the CDI BeanManager, thus making all CDI beans accessible as Spring beans.

It was very easy to integrate the CDISource Spring Bridge into my project, I just cloned their Git repository, ran the Maven build, added

    <dependency>
      <groupId>org.cdisource.springbridge</groupId>
      <artifactId>springbridge</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>

to my own POM and included
<bean class="org.cdisource.springintegration.CdiBeanFactoryPostProcessor"/>
in my Spring configuration.

This was enough to get going, but I soon found out that some standard but not-so-basic use cases are not yet supported, e.g.
  • @Stateless EJBs @Injected into CDI beans.
  • Multiple beans of the same type with @Named or other qualifiers.
  • Producer methods
So I started hacking the CdiBeanFactoryPostProcessor to make it work for my application. Here is the solution:

package org.cdisource.springintegration;

import java.lang.reflect.Type;
import java.util.Set;

import javax.enterprise.inject.spi.Bean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;

public class CdiBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    private static Logger logger = LoggerFactory.getLogger(CdiBeanFactoryPostProcessor.class);

    private boolean useLongName;

    private BeanManagerLocationUtil beanManagerLocationUtil = new BeanManagerLocationUtil();

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {

        DefaultListableBeanFactory factory = (DefaultListableBeanFactory) beanFactory;

        Set<Bean<?>> beans = beanManagerLocationUtil.beanManager().getBeans(Object.class);
        for (Bean<?> bean : beans) {
            if (bean instanceof SpringIntegrationExtention.SpringBean) {
                continue;
            }

            if (bean.getName() != null && bean.getName().equals("Spring Injection")) {
                continue;
            }
            logger.debug("bean types = {}", bean.getTypes());
            Class<?> beanClass = getBeanClass(bean);
            BeanDefinitionBuilder definition = BeanDefinitionBuilder
                    .rootBeanDefinition(CdiFactoryBean.class)
                    .addPropertyValue("beanClass", beanClass)
                    .addPropertyValue("beanManager", beanManagerLocationUtil.beanManager())
                    .addPropertyValue("qualifiers", bean.getQualifiers()).setLazyInit(true);
            String name = generateName(bean);
            factory.registerBeanDefinition(name, definition.getBeanDefinition());
            logger.debug("bean name = {}, bean class = {}", bean.getName(), beanClass.getName());
        }
    }

    private Class<?> getBeanClass(Bean<?> bean) {
        Class<?> klass = Object.class;
        for (Type type : bean.getTypes()) {
            if (type instanceof Class) {
                Class<?> currentClass = (Class<?>) type;
                if (klass.isAssignableFrom(currentClass)) {
                    klass = currentClass;
                }
            }
        }
        return klass;
    }

    private String generateName(Bean<?> bean) {
        String name = bean.getName() != null ? bean.getName() : generateNameBasedOnClassName(bean);
        return name;
    }

    private String generateNameBasedOnClassName(Bean<?> bean) {
        Class<?> beanClass = getBeanClass(bean);
        return !useLongName ? beanClass.getSimpleName() + "FactoryBean" : beanClass.getName()
                .replace(".", "_") + "FactoryBean";
    }

    public void setUseLongName(boolean useLongName) {
        this.useLongName = useLongName;
    }

}


The main change is in getBeanClass(): It is important to note that the CDI API Bean.getBeanClass() does not return the class of the given bean: the result is the class in which the bean is defined. In particular, for producer methods, the result is the class containing the method and not the method return type.

Bean.getTypes() returns all classes and interfaces of potential injection points that the bean will match. This set of types does not always include the bean's implementation class, e.g. for a stateless session bean with a local business interface, the bean types will only be java.lang.Object and the business interfaces (and any superinterfaces of the business interface).

getBeanClass() currently selects the most specialized CDI bean type and uses this type to create the corresponding Spring factory bean.

The next point to note are qualifiers: I added a qualifiers property to the CdiFactoryBean and changed its getObject() method to use the qualifiers for looking up the bean in the CDI bean container:

package org.cdisource.springintegration;

import java.lang.annotation.Annotation;
import java.util.Set;

import javax.enterprise.inject.spi.BeanManager;

import org.cdisource.beancontainer.BeanContainer;
import org.cdisource.beancontainer.BeanContainerImpl;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;

public class CdiFactoryBean implements FactoryBean<Object>, InitializingBean {

    private Class<?> beanClass;
    private boolean singleton = true;
    private BeanContainer beanContainer;
    private BeanManager beanManager;
    private Set<Annotation> qualifiers;

    @Override
    public void afterPropertiesSet() throws Exception {
        if (beanManager == null)
            throw new IllegalStateException("BeanManager must be set");
        beanContainer = new BeanContainerImpl(beanManager);
    }

    public void setBeanClass(Class<?> beanClass) {
        this.beanClass = beanClass;
    }

    @Override
    public Object getObject() throws Exception {
        return beanContainer.getBeanByType(beanClass, qualifiers.toArray(new Annotation[] {}));
    }

    @Override
    public Class<?> getObjectType() {
        return beanClass;
    }

    @Override
    public boolean isSingleton() {
        return singleton;
    }

    public void setSingleton(boolean singleton) {
        this.singleton = singleton;
    }

    public void setBeanManager(BeanManager beanManager) {
        this.beanManager = beanManager;
    }

    public void setQualifiers(Set<Annotation> qualifiers) {
        this.qualifiers = qualifiers;
    }
}


I'm sure this is not the end of the story, both Spring and CDI are complex enough to allow use cases where this bridge is likely to break, but it now works perfectly for me in my real-life application.

5 comments:

RickHigh said...

Thanks for using the bridge. Let me know if you ever try it the other direction. I wrote some CDI extensions so you can inject Spring beans into CDI as well. :)

I have been a bad monkey.

I need to add some documents. I got busy configuring several banks for VM instance running Ubuntu for a private cloud. Then the weeks got away from me.....

--Rick Hightower

RickHigh said...

BTW, You asked about sending some patched. You are invited to join CDISource and work on the project. For now, create a patch and create a pull request.

Your blog motivated me to knock the dust off an article I was working on for this and go ahead and post it:

http://cdisource.org/site/2011/06/spring-and-java-ee-6-cdi-get-it-done/

Harald Wellmann said...

Thanks, I'll create a patch then. Just need to clean up my code a little and get more familiar with Git.

What's the best place to discuss CDISource? There's Github and Google Code, at least two blogs and the CDI advocate Google group...

Jason said...

Nice work so far. Has this project been improved on since then?

Harald Wellmann said...

@Jason: I forked the CDISource repository, committed my changes and created a pull request, which is still pending.

There has been no activity in the CDISource repo since April.

You can check out my fork at
https://github.com/hwellmann/cdisource