Search code examples
javajakarta-eeejb

Is it possible give an EJB service call a callback


Is it possible to make an EJB service which accepts callbacks and call it on the client which invokes the service? The use case is: Uploading a large byte array to the service which will parse it and transform the result into Objects and persist them. I want to notify the client which of these steps are done.

@Local
public interface MyService {
    Status upload(byte[] content, Callable<Void> onReceived, Calable<Void> onPersisting);
}

@Stateless(name = "MyService")
public class MyServiceImpl extends MyService {
    Status upload(byte[] content, Callable<Void> onReceived, Calable<Void> onPersisting) {
        // Invoke this because all date is transfered to server.
        onReceived.call();
        // Do the parsing stuff ...
        onPersisting.call();
        // Do the persisting stuff ...
        return new Status(...); // Done or failed or such.
    }
}

On the client I pass in the callables:

Context ctx = ...
MyService service = ctx.get(...);

ctx.upload(bytes, new Callable<void() {
    @Override
    public Void call() {
        // Do something
        return null;
    }
}, new Callable<Void>() {
    @Override
    public Void call() {
        // Do something
        return null;
    }
});

Is something like that possible in EJB?

I'm new to the JEE world: I know that the client get some stubs of the EJB interface and the calls are transfered by "background magic" to the servers real EJB implementation.


Solution

  • Case 1: Using a local business interface (or no-interface view)

    Yes it's possible as long as your service is only accessed by an local business interface. Why? A local business interface can be only accessed by local client.

    A local client has these characteristics [LocalClients].

    • It must run in the same application as the enterprise bean it accesses.

    • It can be a web component or another enterprise bean.

    • To the local client, the location of the enterprise bean it accesses is not transparent.

    To summarize the important characteristics. It run in the same application respectively in the same JVM, it's a web or EJB component and that the location of the accessed bean is not transparent for the local client. Please take a look at LocalClients for more details.

    Below a simple Hello World example. My example uses a no-interface view this is equivalent to a local business interface.

    Edit: Example expanded by JNDI lookup.

    /** Service class */
    
    import javax.ejb.Stateless;
    
    @Stateless
    public class Service {
    
        public void upload(final Callback callback) {
            callback.call();
        }
    
    }
    
    /** Callback class */
    
    public class Callback {
    
        public void call() {
            System.out.println(this + " called.");
        }
    
    }
    
    /** Trigger class */
    
    import javax.ejb.EJB;
    import javax.ejb.Schedule;
    import javax.ejb.Singleton;
    
    @Singleton
    public class Trigger {
    
        @EJB
        Service service;
    
        @Schedule(second = "*/5", minute = "*", hour = "*", persistent = false)
        public void triggerService() {
            System.out.println("Trigger Service call");
            service.upload(new Callback());
            //or by JNDI lookup and method overriding
            try {
               Service serviceByLookup = (Service) InitialContext.doLookup("java:module/Service");
               serviceByLookup.upload(new Callback() {
                   @Override
                   public void call() {
                       System.out.println("Overriden: " + super.toString());
                   }
               });
    
           } catch (final NamingException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
        }
    }
    

    It's also possible to implement the Callback class as StatelessBean and inject it in the Service class.

    /** Service class */
    @Stateless
    public class Service {
    
        @EJB
        Callback callback;
    
        public void upload() {
            callback.call();
        }
    
    }
    

    Case 2: Using a remote business interface

    If you are using a remote interface it's not possible to pass a callback object to your EJB. To get status information back to your client you have to use JMS.

    Below a short kick-off example.

    @Remote
    public interface IService {
    
        void upload();
    
    }
    
    @Stateless
    public class Service implements IService {
    
        @EJB
        private AsyncUploadStateSender uploadStateSender;
    
        @Override
        public void upload() {
            for (int i = 0; i <= 100; i += 10) {
                uploadStateSender.sendState(i);
                try {
                    Thread.sleep(1000L);
                } catch (final InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    
    @Stateless
    public class AsyncUploadStateSender {
    
        @Resource(lookup = "jms/myQueue")
        private Queue queue;
    
        @Inject
        private JMSContext jmsContext;
    
        @Asynchronous
        public void sendState(final int state) {
            final JMSProducer producer = jmsContext.createProducer();
            final TextMessage msg = jmsContext.createTextMessage("STATE CHANGED " + state + "%");
            producer.send(queue, msg);
        }
    }
    
    public class Client {
    
        public static void main(final String args[]) throws NamingException, InterruptedException, JMSException {
    
            final InitialContext ctx = ... // create the InitialContext;
            final IService service = (IService) ctx.lookup("<JNDI NAME OF IService>");
            final ConnectionFactory factory = (ConnectionFactory) ctx.lookup("jms/__defaultConnectionFactory");
            final Queue queue = (Queue) ctx.lookup("jms/myQueue");
            // set consumer
            final Connection connection = factory.createConnection();
            final MessageConsumer consumer = connection.createSession().createConsumer(queue);
            consumer.setMessageListener(new MessageListener() {
                @Override
                public void onMessage(final Message msg) {
                    try {
                        System.out.println(((TextMessage) msg).getText());
                    } catch (final JMSException e) {
                        e.printStackTrace();
                    }
                }
            });
            connection.start();
            // start upload
            service.upload();
            Thread.sleep(1000L);
        }
    }
    

    Note: you have to create the queue jms/myQueue and the connection factory jms/__defaultConnectionFactory in your application server to make the example work.