Inject beans into JBoss 7 Modules

It seems like JBoss 7.0 CDI has an issue with injecting beans that are located in shared libraries, and are not part of an EAR file. Irritating (but solved in the next version). However, I didn’t want to upgrade, so I decided to solve it instead.
Here’s a short step-by-step:

Deltaspike

Deltaspike is an apache library that has several useful CDI extensions. Add it as a module to your jboss-as. You can use the following module.xml file:

<?xml version="1.0" encoding="UTF-8"?>

<module xmlns="urn:jboss:module:1.1" name="org.deltaspike">

    <resources>
        <resource-root path="deltaspike-core-api.jar" />
        <resource-root path="deltaspike-core-impl.jar" />
    </resources>

    <dependencies>
        <module name="com.google.guava" />
        <module name="javax.enterprise.api" />
        <module name="javax.inject.api" />
    </dependencies>
</module>

Reflections

Reflections is a very useful library that allows you to find classes that has an annotation in runtime.
Add it as a module to your jboss-as.

<?xml version="1.0" encoding="UTF-8"?>

<module xmlns="urn:jboss:module:1.1" name="org.reflections">

    <resources>
        <resource-root path="reflections.jar" />
    </resources>

    <dependencies>
        <module name="com.google.guava" />
        <module name="org.javassist" />
        <module name="org.slf4j" />
    </dependencies>
</module>

Module extensions

This is the heart of the solution. This module runs when the CDI container starts, and adds relevant beans to the CDI. Make sure this class is part of a JAR file that is inside your EAR file!

This code is greatly influenced from https://rmannibucau.wordpress.com/2013/08/19/adding-legacy-beans-to-cdi-context-a-cdi-extension-sample/

package com.tona.cdi;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.enterprise.event.Observes;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.BeforeBeanDiscovery;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.util.AnnotationLiteral;
import javax.inject.Named;
import javax.inject.Singleton;

import org.apache.deltaspike.core.util.bean.BeanBuilder;
import org.apache.deltaspike.core.util.metadata.builder.AnnotatedTypeBuilder;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Strings;

public class ModuleConfigurationExtension implements Extension {
    private static final Logger log = LoggerFactory.getLogger(ModuleConfigurationExtension.class);
    private final Map<String, AnnotatedType<Object>> beans = new HashMap<>();

    /**
     * This method is automatically activated by CDI, and loads all classes in the com.tona package that has NAMED or
     * SINGLETON annotations.
     * @param bdd
     */
    void readAllConfigurations(final @Observes BeforeBeanDiscovery bdd, BeanManager bm) {
        log.info("Starting to load beans from modules");
        addBeansFromPackage(bdd, bm, "com.tona");
    }

    private void addBeansFromPackage(final BeforeBeanDiscovery bdd, BeanManager bm, String packageName) {
        Reflections reflections = new Reflections(packageName);
        Set<Class<?>> beanClasses = reflections.getTypesAnnotatedWith(Named.class);
        beanClasses.addAll(reflections.getTypesAnnotatedWith(Singleton.class));

        for (Class<?> bean : beanClasses) {
            @SuppressWarnings({ "unchecked", "rawtypes" })
            AnnotatedType<Object> annotatedType = new AnnotatedTypeBuilder().readFromType(bean).create();
            Set<Bean<?>> foundBeans = bm.getBeans(annotatedType.getBaseType(), new AnnotationLiteral<Any>() {
            });

            if (foundBeans.size() == 0) {
                bdd.addAnnotatedType(annotatedType);
                String name;
                Named named = bean.getAnnotation(Named.class);
                if (named == null || Strings.isNullOrEmpty(named.value())) {
                    name = bean.getSimpleName();
                } else {
                    name = named.value();
                }
                beans.put(name, annotatedType);
            }
        }
    }

    /**
     * This method actually initializes the beans we discovered in <code>readAllConfigurations</code>. Again - this
     * method is automatically activated by CDI
     * @param abd
     * @param bm
     * @throws Exception
     */
    public void addCdiBeans(final @Observes AfterBeanDiscovery abd, final BeanManager bm) throws Exception {
        log.info("Starting to initialize beans from modules");

        for (Map.Entry<String, AnnotatedType<Object>> bean : beans.entrySet()) {
            Set<Bean<?>> foundBeans = bm.getBeans(bean.getValue().getBaseType(), new AnnotationLiteral<Any>() {
            });

            if (foundBeans.size() == 0) {
                final Bean<Object> cdiBean = createBean(bm, bean.getKey(), bean.getValue());
                abd.addBean(cdiBean);
                log.debug("Added bean " + cdiBean.getName());
            }
        }
    }

    private static Bean<Object> createBean(final BeanManager bm,
            final String name,
            final AnnotatedType<Object> annotatedType)
            throws Exception {
        final BeanBuilder<Object> beanBuilder = new BeanBuilder<Object>(bm).
                readFromType(annotatedType).
                name(name);

        return beanBuilder.create();
    }
}

Configuring the extension

Create a file called META-INF/services/javax.enterprise.inject.spi.Extension. It should only have the following line:

com.tona.cdi.ModuleConfigurationExtension

Updating your EAR file

The EAR file should have dependencies on the org.deltaspike and org.reflections module. Add it in the MANIFEST.MF file.