springejbcdiejb-3.1

Define and inject a map in EJB 3.1 or CDI


After several years developing in Spring, I switched to EJB and I am not happy that I have no solution for this use-case. Let's say it is the strategy pattern implemented by a map. In Spring, it could look like this.

<bean id="myBean" class="MyBeanImpl">
    <property name="handlers">
        <map>
            <entry key="foo" value-ref="fooHandler"/>
            <entry key="bar" value-ref="barHandler"/>
        </map>
    </property>
</bean>

In EJB/CDI, I have this.

@Stateless
public class MyBeanImpl implements MyBean {

    private Map<String, Class<? extends Handler>> handlers = new HashMap<>();

    @PostConstruct
    public void init() {
        handlers.put("foo", FooHandlerImpl.class);
        handlers.put("bar", BarHandlerImpl.class);
    }

    //jndi lookup handlerClass.getSimpleName()

}

Mind that jndi lookup works with implementations, not interfaces. Isn't there any better solution? And no, I do not want to have separate fields (foo, bar), inject them and create the map afterwards (It can be huge list and changed often). Ideally, in case of any configuration change, I would not touch the MyBeanImpl class at all.


Solution

  • The more CDI like way would look something like:

    @Qualifier
    @Target({ TYPE, METHOD, PARAMETER, FIELD })
    @Retention(RUNTIME)
    @Documented
    public @interface Handles {
        String value();
    }
    
    
    public class HandlerLiteral extends AnnotationLiteral<Handles> implements Handles{
        private final String value;
        public HandlerLiteral(String value) {
            this.value = value;
        }
        @Override
        public String value() {
            return value;
        }
    }
    

    You would then annotate each of your Handler implementations with @Handles("someName"), e.g. the class name as you're using it here. The use of a qualifier here is more in line with how CDI works, and we use the internal Instance object to resolve appropriate beans. Then in your service code (or wherever) you would simply do:

    @Inject @Any
    private Instance<HandlerService> handlerInstance;
    
    ...
    handlerInstance.select(new HandlerLiteral("whateverName")).get().handle(context);
    

    If you're really constrained to using a map, this wouldn't work for you. But this should allow for more dynamic registration and essentially looks at every handler in your context.