Introduction
Please be aware that this article was aimed at JBoss 7.0 and is now old and obsolete. The JMS section has been migrated to the getting started article and expanded, use the link below.
If you have read my article on getting started with JBoss 7, you'll know I find JBoss 7 a very exciting piece of tech. Yet three services present in JBoss 4/5/6 that I have used and abused a lot are no longer available or not available by default: a scheduler, my beloved jmx-console and messaging support.
This can be explained away like this: JEE6 has vastly improved timer support with full scheduling services. Unfortunately this may not solve all your needs, so you may need to reach out to a fully fledged scheduler such as Quartz. Messaging is not enabled by default, but you can activate it yourself by using the standalone-full configuration in stead of the default standalone configuration, or copy over the relevant bits as I will show you later. The missing jmx-console is because of the greatly improved yet still evolving JBoss management interface. But I liked it a lot, so I want something like that anyway. Lets see what we can do.
Quartz: the replacement scheduler
We'll roll our own of course. Quartz has been around for some time now and provides all the scheduling support you might ever need. So what we'll do is plug Quartz into our application framework.First of all we need Quartz deployed as part of our application. To do that, use the following maven dependency:
<dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.1.0</version> </dependency>
Now we need to bootstrap the scheduler. How depends on what type of application you are building.
EJB: we'll use a singleton EJB
WAR: you can use either a singleton EJB or a @WebListener annotated class
I'll demonstrate how to do it with a singleton EJB. JEE6 supports an annotation called @Startup, which forces the bean to be initialized upon application startup. We can use this fact in combination with a singleton EJB to get what we want.
@Startup @Singleton public class SchedulerBean { @PostConstruct void init() { try { Scheduler sched = new StdSchedulerFactory().getScheduler(); // schedule jobs here sched.start(); } catch (Throwable t) { // log exception } } }
That will do it. The fact that the EJB is a singleton adds thread safety to the mix by the way.
Of course you need to be able to schedule jobs. The Quartz manual and example programs are your best example, but here is a job that prints a message every 10 seconds.
public class TestJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("Quartz test job executed!"); } }
And here is how you schedule it:
JobDetail testjob = JobBuilder.newJob(TestJob.class) .withIdentity("testjob", "testgroup") .build(); SimpleTrigger trigger = TriggerBuilder.newTrigger() .withIdentity("testtrigger", "testgroup") .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(10) .repeatForever()) .build(); sched.scheduleJob(testjob, trigger);
The SimpleTrigger/SimpleScheduleBuilder is what you'll probably use in most cases, it contains scheduling intervals of every X seconds, every X minutes, every X hours, every day at X.X hour, etc.
Of course a job is not a container managed resource, so you'll need to do manual JNDI lookups to use EJBs unfortunately. If you need to know how, see my article on creating a prototype webapp which demonstrates it.
Building your own jmx-console
The old jmx-console was a wonderfully powerful tool because it provided among other services the possibility to invoke services manually. With only a few annotations you could expose EJB calls through a simple auto-generated web interface.
That we no longer have, so we'll have to create something ourselves. Recreating the jmx-console is madness, what you want is a simple way to expose service methods through the web. Using JSF 2.1 for this task is just the ticket, especially when you have something you can copy/paste and adapt between applications.
So what do we need?
- a page listing all invocable service calls
- a page that displays the results of said service calls
- a backing bean which will actually invoke the service call for us
I'll define three dummy service calls I want to expose through the web.
- createReportingData; this is used to generate dummy data to test/demonstrate a reporting function in a webapp.
- optimizeDatabase; something which is normally invoked scheduled every night at 3AM and will invoke some wicked magic that optimizes the database.
- generateJobSummary; a special call that generates a report of running jobs (wherever they may come from).
The administration page will look something like this. I'm adapting this from an existing admin module I created, please excuse copy/paste/modify errors.
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:a4j="http://richfaces.org/a4j" xmlns:rich="http://richfaces.org/rich" template="template.xhtml"> <ui:define name="content"> <h:form> <h3>Services</h3> <table> <tr><th>Operation</th><th>actions</th></tr> <tr><td>optimize database</td><td><h:commandButton action="#{adminBean.optimizeDatabase}" value="Invoke"/></td></tr> <tr><td>Get job summary</td><td><h:commandButton action="#{adminBean.generateJobSummary}" value="Invoke"/></td></tr> </table> <h3>Mock Database management</h3> <table> <tr><th>Operation</th><th>actions</th></tr> <tr><td>create report data</td><td><h:commandButton action="#{adminBean.createReportData}" value="Invoke"/></td></tr> </table> </h:form> </ui:define> </ui:composition>
Facelets page, I assume you have some facelets template that you can hook this in. I'm not going to pre-chew everything :)
We also need a page to show results. I'm making that as basic as you can possibly make it:
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:a4j="http://richfaces.org/a4j" xmlns:rich="http://richfaces.org/rich" template="template.xhtml"> <ui:define name="content"> <p> <h:outputText escape="false" rendered="#{not empty adminBean.output}" value="#{adminBean.output}"/> <h:outputText rendered="#{empty adminBean.output}" value="The operation completed with no results."/> </p> <p> <h:form> <h:commandButton action="admin" value="Back"/> </h:form> </p> </ui:define> </ui:composition>
Notice the escape="false". The idea is that the services can generate output as if you could do in the original jmx-console, returning a String which would then be output in the result page. By not escaping you can even return HTML content if you see fit.
Now what remains is the actual backing bean.
@ManagedBean @RequestScoped public class AdminBean { @EJB private MyEjbBean myEjb; private String output; public String optimizeDatabase() { try{ output = null; myEjb.optimizeDatabase(); } catch(Throwable t){ output = exceptionToString(t); } return "adminresult"; } public String generateJobSummary { output = myEjb.generateJobSummary(); return "adminresult"; } public String createReportData() { output = "Report data in italic."; return "adminresult"; } public String getOutput(){ return output; } public void setOutput(String op){ output = op; } private String exceptionToString(Throwable t){ ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos); t.printStackTrace(ps); String ret = String(baos.toByteArray()); return "" + ret + ""; } }
The action method simply invoke the EJB, collect any output if necessary and then kick the user to the 'adminresult' page.
The exceptionToString() method is to mimic the jmx-console a bit. The exception stacktrace is transformed into a String and then will be generated as output in a <pre> so formatting is preserved (remember: the output is printed with escape="false").
Of course without the EJB this is not a working example, but I'm pretty sure you can whip something up yourself in a minute.
Adding messaging to the default configuration
I already altered standalone.xml and now I've figured out that I cannot use JMS this way because the required subsystem is not there. At this point you have two options.- rename standalone-full.xml to standalone.xml and copy over all the configuration changes you made in the old file
- copy over the relevant parts from standalone-full.xml to standalone.xml
I decided to do the last. The following will activate HornetQ AND MDB support:
STEP 1: under extensions, copy over the org.jboss.as.messaging module
STEP 2: under the urn:jboss:domain:ejb3:1.1 subsystem, remove the "lite=true" attribute
STEP 3: copy over the <mdb> block under the same subsystem.
STEP 4: under the same subsystem, copy over the mdb-strict-max-pool instance pool.
STEP 5: copy over the entire urn:jboss:domain:messaging subsystem (under profile)
STEP 6: under socket-binding-group, copy over the messaging and messaging-throughput socket-binding elements.
That's it, your default standalone server now has HornetQ enabled. All configuration options you would normally look for in the hornetq-configuration.xml file can now be found in the messaging subsystem you just copied over (any missing configuration can be added here as if you would add them to the hornetq-configuration.xml file too).
Queues and topics can be defined inside the messaging subsystem, under jms-destinations. This is also the place where you define your own queues/topics, you do not deploy *-hornetq-jms.xml files like you would in JBoss 6. Example:
<subsystem xmlns="urn:jboss:domain:messaging:1.3"> <hornetq-server> ... <jms-destinations> <jms-queue name="testQueue"> <entry name="queue/test"/> <entry name="java:jboss/exported/jms/queue/test"/> </jms-queue> <jms-topic name="testTopic"> <entry name="topic/test"/> <entry name="java:jboss/exported/jms/topic/test"/> </jms-topic> <jms-queue name="YourQueue"> <entry name="queue/yourqueue"/> <entry name="java:jboss/exported/jms/queue/yourqueue"/> </jms-queue> </jms-destinations> </hornetq-server> </subsystem>
If you need more information about HornetQ, I discovered this very useful blog that is dedicated to it. The linked article is what tipped me off to the fact that messaging was enabled in standalone-preview.xml. My Jboss 6 migration guide also contains some entries on HornetQ which were specific to JBoss 6; if you run into issues you may just find an answer there.
To test out that it works, you could use this simplistic code. To compile this code you need the JMS API on your classpath, using Maven:
<dependency> <groupId>javax.jms</groupId> <artifactId>jms</artifactId> <version>1.1</version> <scope>provided</scope> </dependency>
First of all a test MDB listening on the test queue:
@MessageDriven(activationConfig = { @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"), @ActivationConfigProperty(propertyName = "destination", propertyValue = "queue/test") }) public class MyTestMDB implements MessageListener { protected Logger logger = LoggerFactory.getLogger(getClass()); public void onMessage(Message msg) { ObjectMessage om = (ObjectMessage) msg; try{ String text = (String) om.getObject(); logger.info("Message received! : " + text); } catch(Throwable t){ logger.error("Exception receiving message", t); } } }
This simple MDB will consume messages on the test queue and print out a log each time it receives one.
Now an EJB that can send messages:
@Stateless public class MyTestEjb { protected Logger logger = LoggerFactory.getLogger(getClass()); @Resource(name = "QueueConnectionFactory", mappedName = "java:/ConnectionFactory") protected QueueConnectionFactory connectionFactory; public void sendMessage(){ sendObjectMessage("java:/queue/test", "Test"); } private void sendObjectMessage(String queueName, Serializable msgBody){ QueueConnection connection = null; QueueSession session = null; MessageProducer producer; try { InitialContext context = new InitialContext(); javax.jms.Queue queue = (javax.jms.Queue) context.lookup(queueName); connection = connectionFactory.createQueueConnection(); session = connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE); producer = session.createProducer(queue); producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); ObjectMessage omsg = session.createObjectMessage(); omsg.setObject(msgBody); omsg.setJMSCorrelationID("1"); omsg.setJMSDeliveryMode(DeliveryMode.NON_PERSISTENT); producer.send(omsg); } catch (Throwable e) { logger.error("Exception sending message", e); throw new IllegalStateException("Exception while trying to send JMS queue message!", e); } finally { try { if (session != null) { session.close(); } if (connection != null) { connection.close(); } } catch (JMSException jmse) { logger.error("Exception sending message: ", jmse); } } } }
This code has been slightly simplified from what I normally use, for example the correlation ID is normally uniquely generated. Note the lookup of the connection factory by using the JNDI name "java:/ConnectionFactory". You can find this name configured in the messaging subsystem. You'll find that this is bound to the "in-vm" connector, which is enough for most use cases. If you want to send persistent messages that are part of a two-phase transaction you would use the connector configured under "java:/JmsXA".
For a discussion on connectors and transports I refer you to the HornetQ manual.
Finally we need a way to actually send the message. Our home-grown jmx-console can do the trick there. Simply add a new button the the admin.xhtml:
<tr><td>Test message</td><td><h:commandButton action="#{adminBean.sendTestMessage}" value="Invoke"/></td></tr>
And the action method to the AdminBean:
@EJB private MyTestEjb testEjb; public String sendTestMessage() { output = null; testEjb.sendMessage(); return "adminresult?faces-redirect=true"; }
And voila, you can check out if you can send and receive messages through HornetQ. JMS is killer technology, if you know how and when to use it properly of course.
In closing
There you go; home grown replacements for the JBoss scheduler and the jmx-console. Of course our own versions are not as flexible, but they get the job done until you find a better alternative.
On top of that you have now activated messaging support in your JBoss 7 installation!