messaging icon indicating copy to clipboard operation
messaging copied to clipboard

Annotation-Based API for Consuming Messages

Open dblevins opened this issue 6 years ago • 6 comments

The only means to declare message consumers via configuration or annotation is currently via JMS Message-Driven Beans. A new expressive annotation-based approach modeled after JAX-RS is desired to bring JMS into the future.

A follow-up to an older issue: https://github.com/jakartaee/messaging/issues/134.

Current MDB API Example:

import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.ObjectMessage;

@MessageDriven(activationConfig = {
        @ActivationConfigProperty(propertyName = "maxSessions", propertyValue = "3"),
        @ActivationConfigProperty(propertyName = "maxMessagesPerSessions", propertyValue = "1"),
        @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"),
        @ActivationConfigProperty(propertyName = "destination", propertyValue = "TASK.QUEUE")
})
public class BuildTasksMessageListener implements MessageListener {

    @Override
    public void onMessage(Message message) {
        try {

            if (!(message instanceof ObjectMessage)) {
                throw new JMSException("Expected ObjectMessage, received " + message.getJMSType());
            }

            final ObjectMessage objectMessage = (ObjectMessage) message;

            final BuildTask buildTask = (BuildTask) objectMessage.getObject();

            doSomethingUseful(buildTask);

        } catch (JMSException e) {
            // Why can't I throw a JMS Exception
            throw new RuntimeException(e);
        }
    }

    // This is the only "useful" code in the class
    private void doSomethingUseful(BuildTask buildTask) {
        System.out.println(buildTask);
    }
}

Issues with this API involve:

  • User manual required to know what names can be used in @ActivationConfigProperty.
  • Loosely typed. All values are string, but have an implied type which is not compile-time checked.
  • Poor targeting. The metadata for the onMessage method is on the class, not the method, making additional methods impossible.
  • Too Course-grained. The MessageListener.onMessage(Message msg) method is similar to the HttpServlet.service(ServletRequest req, ServletResponse res) in being too course-grained and requires boilerplate; casting, message property checking, string parsing.
  • EJB-specific. The above API is only available to EJB Message-Driven beans.

Some form of annotation-based approach styled after JAX-RS could solve all of the above issues. For example:

import io.breezmq.MaxMessagesPerSession;
import io.breezmq.MaxSessions;

import javax.ejb.MessageDriven;
import javax.jms.JMSException;
import javax.jms.JMSMessageDrivenBean;
import javax.jms.ObjectMessage;
import javax.jms.QueueListener;
import javax.jms.TopicListener;

@MessageDriven
@MaxSessions(3)
@MaxMessagesPerSession(1)
public class BuildTasksMessageListener implements JMSMessageDrivenBean {

    @QueueListener("TASK.QUEUE")
    public void processBuildTask(final ObjectMessage objectMessage) throws JMSException {

        final BuildTask buildTask = (BuildTask) objectMessage.getObject();

        doSomethingUseful(buildTask);

    }

    @TopicListener("BUILD.TOPIC")
    public void processBuildNotification(final ObjectMessage objectMessage) throws JMSException {

        final BuildNotification notification = (BuildNotification) objectMessage.getObject();

        System.out.println("Something happened " + notification);
    }


    // This is the only "useful" code in the class
    private void doSomethingUseful(BuildTask buildTask) {
        System.out.println(buildTask);
    }
}

Benefits:

  • Multiple user-defined message consuming methods allowed.
  • Method signature implies message type, avoiding casting.
  • Message Properties can be passed as annotated method arguments. (not shown above)
  • Strongly-typed annotations replace string properties so names and values are compile-time checked.
  • Opens the door for use outside EJB

The above API should be considered a straw-man just to get conversations started.

Proposals

Actual proposals from the community are welcome, but should achieve or not-conflict with the same 5 benefits. Partial proposals are welcome, such as #241 which focuses on one aspect required to make an annotation-based API work.

  • @MessageConsumer for discovery #241

File your proposals and mention in the comments below and we'll add it to the list, regardless of state.

TODO: find JMS 2.1 proposal and link it

Related

dblevins avatar Sep 29 '19 21:09 dblevins

Spring can process jms via simple @JmsListener, how hard we still need the interface?


@ApplicationScoped 
@Slf4j
public class Receiver {

    @JmsListener(destination = "hello")
    public void onMessage(String message) {
        log.debug("receving message: {}", message);
    }

}

Provides programmatic APIs to set the JSM global properties, and also allow to set properties attribute(a Map or a string of properties joint by ",") on the JmsListener annotation.

hantsy avatar Apr 28 '23 09:04 hantsy

I agree with @hantsy, no interface is needed. And I also like that any CDI bean can be turned into a JMS observer. However, I'm not sure whether a single @JmsListener is enough to distinguish between a queue and a topic. An implementation might need to know whether the listener wants to connect to a queue or a topic.

Another idea is to turn this into a native CDI event observer, with a @JmsListener qualifier. Then all the CDI event API could be used, including asynchronous API. Using @JmsListener on the method could be also allowed but would work as a shorthand, e.g.:

@JmsListener(destination = "hello")
    public void onMessage(String message) {
        log.debug("receving message: {}", message);
    }

would be equivalent to:

    public void onMessage(@Observes @JmsListener(destination = "hello") String message) {
        log.debug("receving message: {}", message);
    }

The latter syntax also allows injection per method call, e.g. the following would inject a myProcessor CDI bean:

    public void onMessage(@Observes @JmsListener(destination = "hello") String message, MyProcessor myProcessor) {
        myProcessor.process(message);
    }

We could allow injecting some contextual information, if it's technically possible. E.g. inject a CDI bean of type of Topic/Queue for the current queue/topic. But we would need to explore that separately, I'm not sure if CDI allows this dynamically for each message.

OndroMih avatar May 28 '23 17:05 OndroMih

This is a very deep topic with a number of discussions that took place over a long time but never implemented. If the idea is to finally do this work, I suggest starting with a very simple straw man and detailed follow up discussions on the mailing list. I would be delighted to participate and share incrementally all the things from the past. It’s too much for this one issue I think. The interim outcome may be creating several smaller issues based on some initial progress that hopefully can go into Jakarta EE 11.

m-reza-rahman avatar May 28 '23 17:05 m-reza-rahman

@m-reza-rahman I remember JBoss Seam2/3(Seam 3 is based CDI) has very simple approach to bridge the JMS to CDI event observer, this JMS handling should be simple as possible.

hantsy avatar May 29 '23 02:05 hantsy