0
votes

I have a Java EE 6 web application and use the WebSocket protocol to communicate with browsers. The browser can send various types of messages and in the servers onMessage method I would like to route (or dispatch) the message to a specific message handler class depending on the message type. I would like to configure or register these message handlers via annotations, similar to the mechanism of servlets (@WebServlet("/there")). And like in servlets, I would like to be able to use CDI injection in the message handlers.

For now I have a MessageType annotation, a MessageHandler interface and 3 implementations.

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MessageType
{
    String value();
}


public interface MessageHandler
{
    public void processMessage(String inputMesssage);
}


@MessageType("first")
public class FirstMessageHandler implements MessageHandler
{
    @Inject
    ResourceBundleProvider resourceBundleProvider;

    @Override
    public void processMessage(String inputMesssage)
    {
        System.out.println("FirstMessageHandler#processMessage: " + inputMesssage);
        System.out.println("InjectionTest: " + resourceBundleProvider.getValue("label.language"));
    }
}


@MessageType("second")
public class SecondMessageHandler implements MessageHandler
{
    @Override
    public void processMessage(String inputMesssage)
    {
        System.out.println("SecondMessageHandler#processMessage: " + inputMesssage);
    }
}


public class DefaultMessageHandler implements MessageHandler
{
    @Override
    public void processMessage(String inputMesssage)
    {
        System.out.println("DefaultMessageHandler#processMessage: " + inputMesssage);
    }
}

I also have a class MessageDispatcher which uses reflections to scan the classpath for the annotated message handlers, instantiates them and puts them into a map:

@ApplicationScoped
public class MessageDispatcher
{
    private Map<String, MessageHandler> messageHandlerMap = new HashMap<String, MessageHandler>();

    @Inject
    DefaultMessageHandler defaultMessageHandler;

    public MessageDispatcher()
    {
        registerAnnotatedHandlers();
    }

    private void registerAnnotatedHandlers()
    {
        Reflections reflections = new Reflections("namespace");

        try
        {
            for (Class<?> annotatedClass : reflections.getTypesAnnotatedWith(MessageType.class))
            {
                String annotationValue = annotatedClass.getAnnotation(MessageType.class).value();

                for (Class<?> interfaceClass : annotatedClass.getInterfaces())
                    if (!annotationValue.isEmpty() && interfaceClass.equals(MessageHandler.class))
                        messageHandlerMap.put(annotationValue, (MessageHandler) annotatedClass.newInstance());
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }       
    }

    public MessageHandler getMessageHandler(String key)
    {
        MessageHandler messageHandler = messageHandlerMap.get(key);

        return messageHandler != null ? messageHandler : defaultMessageHandler;
    }
}

And finally in my websocket servlet's onMessage method I extract the key from the inbound message and use it for the message routing:

public synchronized void onMessage(String data)
{
    String[] message = data.split(":");

    // Choose the message handler from the message
    MessageHandler messageHandler = messageDispatcher.getMessageHandler(message[0]);

    // Process the message by the message handler
    messageHandler.processMessage(message[1]);
}

My 3 incoming sample messages are:

"first:Message to handle with FirstMessageHandler"
"second:Message to handle with SecondMessageHandler"
"third:Message to handle with DefaultMessageHandler"

This works fine, The first and second messages are processed by FirstMessageHandler and SecondMessageHandler respectively. The third message is processed by the default message handler since there is no other handler registered for handling the key "third".

My Problem: I cannot use injection in the message handlers because they are created using Java reflection. Does anybody know how to get annotation processing and CDI injection 'married'? Or does anybody think this approach is bullshit and has another solution for that?

Best Regards
Sebastian

3

3 Answers

1
votes

This is my final approach:

I spend a PostConstruct method to my MessageDispachter where I look for all message handler beans. For each of these beans I get their annotation value and a reference to the bean (which also includes creation of the bean). Then I store both, the annotation value and the bean reference into my messageHandlerMap. There is a lot of CDI delegating and interception involved, but it works:

public class MessageDispatcher
{
    private Map<String, MessageHandler> messageHandlerMap = new HashMap<String, MessageHandler>();

    @Inject
    DefaultMessageHandler defaultMessageHandler;

    @Inject
    BeanManager beanManager;

    @PostConstruct
    public void registerHandlers()
    {
        Set<Bean<?>> messageHandlerBeans = beanManager.getBeans(MessageHandler.class, new MessageTypeLiteral());
        for (Bean<?> bean : messageHandlerBeans)
        {
            String key = bean.getBeanClass().getAnnotation(MessageType.class).value();

            if (!key.isEmpty())
            {
                CreationalContext<?> creationalContext = beanManager.createCreationalContext(bean);
                MessageHandler messageHandler = (MessageHandler) beanManager.getReference(bean, MessageHandler.class, creationalContext);
                messageHandlerMap.put(key, messageHandler);
            }
        }
    }

    public MessageHandler getMessageHandler(String key)
    {
        MessageHandler messageHandler = (MessageHandler) messageHandlerMap.get(key);
        return messageHandler != null ? messageHandler : defaultMessageHandler;
    }
}


@Documented
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface MessageType
{
    @Nonbinding
    String value();
}


@SuppressWarnings("all")
public class MessageTypeLiteral extends AnnotationLiteral<MessageType> implements MessageType
{
    private static final long serialVersionUID = 1L;

    @Override
    public String value()
    {
        return "";
    }
}


public class DefaultMessageHandler implements MessageHandler
{
    @Inject
    ResourceBundleProvider resourceBundleProvider;

    @Override
    public void processMessage(String inputMesssage)
    {
...


@MessageType("first")
public class FirstMessageHandler implements MessageHandler
{
    @Inject
    ResourceBundleProvider resourceBundleProvider;

    @Override
    public void processMessage(String inputMesssage)
    {
...

The @NonBinding annotation in the @MessageType annotation seems to be important to find all beans annotated with @MessageType("xxx") independent of the actual annotation value (here: xxx).

I hope this explains the important things. For further details please ask me

Sebastian

0
votes

I think your simplest solution to this would be to keep what you have, strip out the scanning because you don't need it, change your annotation to be a qualifier and fire a CDI event with the qualifier (you'll need to create an AnnotationLiteral for each of three different qualifiers because the value is binding) and the message as the payload.

I can explain more if you need it.

0
votes

See and adjust Dynamically fire CDI event with qualifier with members It is a CDI way for dynamic runtime selecting services by runtime decision. The TypeEnum can also be a String.