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.
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.