If I Have J2EE, Why Do I Need Quartz?

Since J2EE first came on the scene in the late 1990s, developers have been perplexed by some of the specification decisions and some of the seemingly obvious missing features. This is not necessarily a criticism of the specification writers, but more indicative of the problem that arises when many separate groups, all with differing opinions and agendas, try to agree on a single set of prioritiessort of like the United Nations, but not as nice. Many of the needed features showed up, but a few of the key ones were left out, to be added later. One of the key features that was deferred from the early specifications was a timer service.

I Need a Timer Service

Many business processes require asynchronous job scheduling. For example, Web sites usually need to send e-mails to registered users to alert them to new features or the latest specials. Medical claimprocessing companies typically need to download medical data at night and do something with that data. A company that sells some type of product might have reports generated each night that show sales and commission information. All of these scenarios could benefit from a timer service that executes asynchronous jobs.

The Java/J2EE community has produced several attempts at solving the timer problem. Early on, vendors added propriety solutions within their J2EE servers. (For this chapter, the terms J2EE server and J2EE container are used interchangeably.) For example, the WebLogic product had some custom extensions, as did the IBM J2EE server. Unfortunately, they were not exactly compatible for moving components from one to another. Later, these vendors and others tried to develop a common set of timer components.

Starting with Java 1.3, Sun added the java.util.Timer and java.util.TimerTask classes to help add basic Timer functionality to the core language. Although the Timer and TimerTask can work for simple requirements, there is much more to true job scheduling than can be solved by two concrete classes. Hopefully, that's a point that you already understand.

Quartz/J2EE Deployment Models

Two basic strategies exist for architecting and deploying Quartz with J2EE. With one strategy, you can design Quartz to work outside the J2EE container as a standard J2SE client. As you'll see shortly, this is the simplest approach. The second strategy is to deploy Quartz as a J2EE client that resides within the J2EE container. In this scenario, the J2EE client is a Web Archive (WAR) file and is deployed like any other Web application. The strategy you choose depends on your exact needs. Each comes with a set of pros and cons.

Running Quartz as a J2SE Client

If you just need to invoke services on Enterprise JavaBeans (EJBs) or put messages inside a JMS queue or topic, the easiest way to configure Quartz with J2EE is to run Quartz outside the J2EE container as a stand-alone J2SE application. It then would function like any other Java application that lives outside the container but needs to call methods on distributed components within the container.

We've essentially been practicing this approach in the previous chapters, minus the part about calling EJBs. You can create a Quartz application that contains the Quartz libraries and job-scheduling information and connects to the J2EE server through the home and remote interfaces. You can then invoke methods on EJBs like any other distributed client. You can also create and insert JMS messages and have them processed by message-driven beans (MDB) running within the container. This approach is shown in Figure 10.1.

Figure 10.1. Quartz can work with J2EE as a stand-alone J2SE client.

This approach works nicely if you have existing J2EE components that you want Quartz to interoperate with, and you don't want to or can't make any changes to the server. To implement this approach, you just need to build a Quartz application, as we've done in previous chapters, and use the EJBInvokerJob that ships with Quartz. We discuss the EJBInvokerJob shortly.

Deploying Quartz Within the J2EE Server

Quartz can also be deployed directly within the container to do away with the external Quartz application. This is commonly referred to as using a J2EE client. You might choose this approach over the previous one for several reasons. One reason is that there's only one application to maintain, compared with two in the other approach. If the external Quartz client shuts down, the job-scheduling information is temporarily lost, and the business owners will not be thrilled. In other words, it's one failure point versus two. Another reason for deploying Quartz within the container is to have Quartz use some of the other resources that the container offers, such as mail sessions, data sources, and other resource adapters. If you are using the J2EE server in a clustered environment, it also makes sense to deploy Quartz within the container to make the clustering easier and more manageable. Figure 10.2 illustrates how Quartz can be deployed with the J2EE application.

Figure 10.2. Quartz can be deployed within a J2EE application.

When Quartz is deployed within a J2EE container, you must understand and deal with some complications. Before we get into those, let's talk about how to deploy Quartz as a J2SE client and access the container from outside the container.

Running Quartz as a J2SE Client

By far the easiest and simplest way to use Quartz with J2EE is to deploy it outside the container. What makes this approach easier is that you don't have to deal with many of the issues that will surface as Quartz attempts to create threads and execute within the container. It's also easier because deploying applications into a J2EE container can be frustrating, even with all of the latest tools and technologies such as XDoclet and administrative consoles.

Using the Quartz EJBInvokerJob to Call an EJB

The Quartz framework includes the org.quartz.jobs.ee.ejb.EJBInvokerJob, which enables you to schedule a job that can invoke an EJB method when triggered. The job is easy to set up and can be used regardless of which deployment scenario you've chosen with Quartz. Suppose, for example, that you have a Stateless Session Bean (SLSB) like the one in Listing 10.1.

Listing 10.1. An Example Stateless Session Bean

import java.rmi.RemoteException; import javax.ejb.EJBException; import javax.ejb.SessionBean; import javax.ejb.SessionContext; public class TestBean implements SessionBean { /** The session context */ private SessionContext context; public TestBean() { super(); } // EJB Lifecycle Methods not shown for brevity public void helloWorld() throws EJBException { System.out.println("Hello World"); } public void helloWorld(String msg) throws EJBException { System.out.println("Hello World - " + msg); } }

With this EJB deployed and ready in your J2EE application server of choice, you can use the EJBInvokerJob to invoke one of the helloWorld() methods available to remote clients.

You set up the EJBInvokerJob just as you would for any other job. Listing 10.2 shows an example of using the EJBInvokerJob to invoke the helloWorld() on the SLSB.

Listing 10.2. A Simple Example Using the EJBInvokerJob

package org.cavaness.quartzbook.chapter10; import java.util.Date; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.Trigger; import org.quartz.TriggerUtils; import org.quartz.impl.StdSchedulerFactory; import org.quartz.jobs.ee.ejb.EJBInvokerJob; public class Listing_10_2 { static Log logger = LogFactory.getLog(Listing_10_2.class); public static void main(String[] args) { Listing_10_2 example = new Listing_10_2(); try { // Create a Scheduler and schedule the Job Scheduler scheduler = example.createScheduler(); example.scheduleJob(scheduler); // Start the Scheduler running scheduler.start(); logger.info("Scheduler started at " + new Date()); } catch (SchedulerException ex) { logger.error(ex); } } // Schedule the EJBInvokerJob private void scheduleJob(Scheduler scheduler) throws SchedulerException { // Create a JobDetail for the Job JobDetail jobDetail = new JobDetail("HelloWorldJob", Scheduler.DEFAULT_GROUP, org.quartz.jobs.ee.ejb.EJBInvokerJob.class); loadJobDataMap(jobDetail); // Create a trigger that fires every 10 seconds, forever Trigger trigger = TriggerUtils.makeSecondlyTrigger(10); trigger.setName("helloWorldTrigger"); // Start the trigger firing from now trigger.setStartTime(new Date()) // Associate the trigger with the job in the scheduler scheduler.scheduleJob(jobDetail, trigger); } /* * Configure the EJB parameters in the JobDataMap */ public JobDetail loadJobDataMap(JobDetail jobDetail) { jobDetail.getJobDataMap().put(EJBInvokerJob.EJB_JNDI_NAME_KEY, "ejb/HelloWorldSession"); jobDetail.getJobDataMap().put(EJBInvokerJob.EJB_METHOD_KEY, "helloWorld"); jobDetail.getJobDataMap().put(EJBInvokerJob.PROVIDER_URL, "t3://localhost:7001"); jobDetail.getJobDataMap().put( EJBInvokerJob.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory"); return jobDetail; } /* * return an instance of the Scheduler from the factory */ public Scheduler createScheduler() throws SchedulerException { return StdSchedulerFactory.getDefaultScheduler(); } }

As you can see from Listing 10.2, the EJBInvokerJob is configured like any other job. A JobDetail and trigger are created and registered with the Scheduler. Several JobDataMap parameters can be used for the job to function properly with various J2EE containers. Table 10.1 lists the JobDataMap parameters that the job supports.

The parameters you add to the JobDataMap depend on which J2EE server you're using and what its requirements are. For example, if you're using BEA WebLogic, you would need to specify at least the ones from Listing 10.1, obviously substituting values for your specific environment. If you were using WebSphere, most of the values would be different.

When we set up and run Listing 10.2 within our external Quartz application, every 10 seconds the helloWorld() method on the EJB is invoked. This approach is nice because we don't have to worry about deploying the Quartz application within the J2EE container. It enforces a separation of job information from business processing logic.

Table 10.1. The EJBInvokerJob Uses Several Parameters, Depending on Your Specific J2EE Server

Static Constant

String Value

EJB_JNDI_NAME_KEY

ejb

Notes: JNDI name of the bean's home interface

PROVIDER_URL

java.naming.provider.url

Notes: Vendor-specific URL that specifies where the server can be found

INITIAL_CONTEXT_FACTORY

java.naming.factory.initial

Notes: Vendor-specific context factory that is used to look up resources

EJB_METHOD_KEY

method

Notes: Name of the method to invoke on the EJB

EJB_ARGS_KEY

args

Notes: Object[] of the args to pass to the method (optional, if left out, there are no arguments)

EJB_ARG_TYPES_KEY

argType

Notes: Class[] of the args to pass to the method (optionalif left out, the types will be derived by calling getClass() on each of the arguments)

PRINCIPAL

java.naming.security.principal

Notes: The principal (user) to be used for the EJB method call

CREDENTIALS

java.naming.security.credentials

Notes: The credentials to be used for the EJB method call

In the example in Listing 10.2, the helloWorld() method that was invoked on the EJB didn't defined any parameters. The EJBInvokedJob class enables you to pass arguments to an EJB method by specifying them using the EJB_ARGS_KEY and EJB_ARG_TYPES_KEY parameters shown in Table 10.1.

Listing 10.3 shows another simple example that passes an argument to a different version of helloWorld() EJB running on the Apache Geronimo J2EE server.

Listing 10.3 is very similar to Listing 10.2, except that it includes the parameters EJB_ARGS_KEY and EJB_ARG_TYPES_KEY. Also, because it's running against the Geronimo J2EE application server, it needed to add the arguments for PRINCIPAL and CREDENTIALS.

Listing 10.3. A Simple Example Using the EJBInvokerJob

package org.cavaness.quartzbook.chapter10; import java.util.Date; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.Trigger; import org.quartz.TriggerUtils; import org.quartz.impl.StdSchedulerFactory; import org.quartz.jobs.ee.ejb.EJBInvokerJob; public class Listing_10_3 { static Log logger = LogFactory.getLog(Listing_10_3.class); public static void main(String[] args) { Listing_10_3 example = new Listing_10_3(); try { // Create a Scheduler and schedule the Job Scheduler scheduler = example.createScheduler(); example.scheduleJob(scheduler); // Start the Scheduler running scheduler.start(); logger.info("Scheduler started at " + new Date()); } catch (SchedulerException ex) { logger.error(ex); } } // Schedule the EJBInvokerJob private void scheduleJob(Scheduler scheduler) throws SchedulerException { // Create a JobDetail for the Job JobDetail jobDetail = new JobDetail("HelloWorldJob", Scheduler.DEFAULT_GROUP, org.quartz.jobs.ee.ejb.EJBInvokerJob.class); // Load all of the necessary EJB parameters loadJobDataMap(jobDetail); // Create a trigger that fires every 10 seconds, forever Trigger trigger = TriggerUtils.makeSecondlyTrigger(10); trigger.setName("helloWorldTrigger"); // Start the trigger firing from now trigger.setStartTime(new Date()); // Associate the trigger with the job in the scheduler scheduler.scheduleJob(jobDetail, trigger); } /* * Configure the EJB parameters in the JobDataMap */ public JobDetail loadJobDataMap(JobDetail jobDetail) { jobDetail.getJobDataMap().put( EJBInvokerJob.EJB_JNDI_NAME_KEY, "ejb/Test"); jobDetail.getJobDataMap().put(EJBInvokerJob.EJB_METHOD_KEY, "helloWorld"); Object[] args = new Object[1]; args[0] = " from Quartz"; jobDetail.getJobDataMap().put( EJBInvokerJob.EJB_ARGS_KEY, args); Class[] argTypes = new Class[1]; argTypes[0] = java.lang.String.class; jobDetail.getJobDataMap().put( EJBInvokerJob.EJB_ARG_TYPES_KEY, argTypes); jobDetail.getJobDataMap().put( EJBInvokerJob.PROVIDER_URL, "127.0.0.1:4201"); jobDetail.getJobDataMap().put( EJBInvokerJob.INITIAL_CONTEXT_FACTORY, "org.openejb.client.RemoteInitialContextFactory"); jobDetail.getJobDataMap().put( EJBInvokerJob.PRINCIPAL, "system"); jobDetail.getJobDataMap().put( EJBInvokerJob.CREDENTIALS, "manager"); return jobDetail; } /* * return an instance of the Scheduler from the factory */ public Scheduler createScheduler() throws SchedulerException { return StdSchedulerFactory.getDefaultScheduler(); } }

EJBInvokerJob Parameters and Serialization

Because of the typical serialization problems that are associated with Java and distributed applications, you should stick to passing Strings and primitives to your EJB methods. If you need to pass more complex types, your code must serialize the objects between client and server properly. For more in-depth information on Java serialization, check out Sun's Serialization specification at http://java.sun.com/j2se/1.5.0/docs/guide/serialization.

Because Quartz needs to get a reference to the home and remote interfaces for the EJB, you need to deploy some J2EE client JARs with your external Quartz application. The JARs you need to add depend on which J2EE container you're using. If you're using WebLogic, for example, you'll probably just put the weblogic.jar with the Quartz application. For Geronimo, several are involved. Check with the server documentation to be sure.

Running Quartz Within the J2EE Application Server

Running Quartz as a J2EE client is a little more involved than running Quartz as an external J2SE application. This is mostly because deploying applications within the container is somewhat more complicated. In addition, the J2EE specification puts some constraints on components within the container. One of the biggest guidelines that the specification gives involves who and what can create Java threads. Because it's the container's responsibility to manage all resources, it can't allow just anything or anyone to create threads. If it did, it would have a harder time managing the environment and keeping things stable. Quartz creates its own worker threads, so you need to follow some steps to make sure things work properly.

Assume that a stateless session bean such as the one from Listing 10.1 is already deployed in the container. The easiest way to deploy Quartz within the container is to build a WAR file that contains all the necessary files and then use the admin tools, or Eclipse, to deploy the Web application within the container.

The Web application directory structure is just like that of any other Web application. You need to add the following files to it:

Because you are building a Web application, you need to add the requisite web.xml deployment descriptor. Listing 10.4 shows the web.xml for our client application that will be installed within the container.

Listing 10.4. The web.xml for the Quartz J2EE Client Application

QuartzServlet org.quartz.ee.servlet.QuartzInitializerServlet 1 QuartzServlet /servlet/QuartzServlet

The Quartz framework includes a Java servlet called QuartzInitializerServlet that, when invoked, initializes the Quartz Scheduler and loads job information. In Listing 10.4, we've set the parameter to have a value of 1 so the servlet will be loaded and initialized when the container is started. By using the servlet to start the Quartz Scheduler, we avoid the issues of thread permission because the container will allow servlets to create user threads.

QuartzInitializerListener Added to Quartz

Recently, a new class called QuartzInitializerListener was added to Quartz that implements the javax.servlet.ServletContextListener interface. This class can be used as an alternative to the QuartzInitializerServlet mentioned earlier.

Next, you need to put the standard quartz.properties file into the WEB-INF/classes directory of the Web application. There's nothing special about this version of the properties file; it's essentially what we did in past chapters. However, here we use the JobInitializationPlugin (this was shown in Chapter 8, "Using Quartz Plug-Ins," and is designed to load job information from an XML file). By default, the plug-in looks for a file called quartz_jobs.xml and loads the jobs found in the file. As Chapter 8 described, using this particular plug-in keeps you from having to write job-loading code and be forced to recompile when changes occur. The quartz_jobs.xml file for this example is shown in Listing 10.5.

Listing 10.5. The quartz_jobs.xml Used Within the J2EE Client

HelloWorldJob DEFAULT org.quartz.jobs.ee.ejb.EJBInvokerJob false false false ejb ejb/Test java.naming.factory.initial org.openejb.client.RemoteInitialContextFactory java.naming.provider.url 127.0.0.1:4201 method helloWorld java.naming.security.principal system java.naming.security.credentials manager helloWorldTrigger DEFAULT HelloWorldJob DEFAULT 2005-06-10 6:10:00 PM -1 10000

You can see from Listing 10.5 that we are still using the EJBInvoker Job by specifying it within the quartz_jobs.xml file.

Specifying the Plug In in quartz properties

Chapter 8 stated that you need to specify the plug-in information for a Quartz plug-in within the quartz.properties file. For the JobInitializationPlugin, you must add the following line in the properties file:

org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin

After all these files have been configured, you can build the WAR file and deploy it within your container. When the container starts up, the servlet is loaded and initialized and starts the Scheduler. The Scheduler uses the JobInitializerPlugin to load job information from the quartz_jobs.xml file. From that point, the EJBInvokerJob invokes the helloWorld() method on the EJB.

Including J2EE Client JARs

When packaging the J2EE Client application, you need to package the J2EE client JARs necessary for your particular server. Each one is different, so check the documentation to be sure. You also need all the Quartz libraries that are required when building a stand-alone Quartz application.

Категории