Chapter 18. Spring in the Enterprise

In this chapter, you will learn about Spring's support for three common technologies on the Java EE platform: Java Management Extensions (JMX), sending e-mail, and scheduling tasks.

JMX is a technology or managing and monitoring system resources such as devices, applications, objects, and service-driven networks. The specification offers powerful features for managing systems at runtime and for adapting legacy systems. These resources are represented by managed beans (MBeans). Originally, JMX was distributed separately, but it has been part of Java SE since version 5.0. JMX has seen many improvements, but the original specification JSR 03 is very old! Spring supports JMX by allowing you to export any Spring beans as model MBeans (a kind of dynamic MBean), without programming against the JMX API. In addition, Spring enables you to access remote MBeans easily.

JavaMail is the standard API and implementation for sending e-mail in Java. Spring further provides an abstract layer for you to send e-mail in an implementation-independent fashion.

There are two main options for scheduling tasks on the Java platform: JDK Timer and Quartz Scheduler (http://www.opensymphony.com/quartz/). JDK Timer offers simple task scheduling features that you can use conveniently because the features are bundled with JDK. Compared with JDK Timer, Quartz offers more powerful job scheduling features. For both options, Spring supplies utility classes for you to configure scheduling tasks in the bean configuration file, without programming against their APIs.

Upon finishing this chapter, you will be able to export and access MBeans in a Spring application. You will also be able to utilize Spring's supporting features to simplify sending e-mail and scheduling tasks.

Exporting Spring Beans as JMX MBeans

Problem

You want to register an object from your Java application as a JMX MBean to allow management and monitoring. In this sense, management is the capability to look at services that are running and manipulate their runtime state on the fly. Imagine being able to do these tasks from a web page: rerun batch jobs, invoke methods, and change configuration metadata that you'd normally be able to do only at runtime. However, if you use the JMX API for this purpose, a lot of coding will be required, and you'll have to deal with JMX's complexity.

Solution

Spring supports JMX by allowing you to export any beans in its IoC container as model MBeans. This can be done simply by declaring an MBeanExporter instance. With Spring's JMX support, you no longer need to deal with the JMX API directly, so you can write code that is not JMX specific. In addition, Spring enables you to declare JSR-160 (Java Management Extensions Remote API) connectors to expose your MBeans for remote access over a specific protocol by using a factory bean. Spring provides factory beans for both servers and clients.

Spring's JMX support comes with other mechanisms by which you can assemble an MBean's management interface. These options include using exporting beans by method names, interfaces, and annotations. Spring can also detect and export your MBeans automatically from beans declared in the IoC container and annotated with JMX-specific annotations defined by Spring. The MBeanExporter class exports beans, delegating to an instance of MBeanInfoAssembler to do the heavy lifting.

How It Works

Suppose that you are developing a utility for replicating files from a source directory to a destination directory. Let's design the interface for this utility as follows:

package com.apress.springrecipes.replicator;
...
public interface FileReplicator {

    public String getSrcDir();
    public void setSrcDir(String srcDir);

    public String getDestDir();
    public void setDestDir(String destDir);

    public void replicate() throws IOException;
}

The source and destination directories are designed as properties of a replicator object, not method arguments. That means each file replicator instance replicates files only for a particular source and destination directory. You can create multiple replicator instances in your application.

Before you implement this replicator, you need another class that copies a file from one directory to another, given its name.

package com.apress.springrecipes.replicator;
...
public interface FileCopier {

    public void copyFile(String srcDir, String destDir, String filename)
            throws IOException;
}

There are many strategies for implementing this file copier. For instance, you can make use of the FileCopyUtils class provided by Spring.

package com.apress.springrecipes.replicator;
...
import org.springframework.util.FileCopyUtils;

public class FileCopierJMXImpl implements FileCopier {

    public void copyFile(String srcDir, String destDir, String filename)
            throws IOException {
        File srcFile = new File(srcDir, filename);
        File destFile = new File(destDir, filename);
        FileCopyUtils.copy(srcFile, destFile);
    }
}

With the help of a file copier, you can implement your file replicator, as shown in the following code sample. Each time you call the replicate() method, all files in the source directory will be replicated to the destination directory. To avoid unexpected problems caused by concurrent replication, you declare this method as synchronized.

package com.apress.springrecipes.replicator;

import java.io.File;
import java.io.IOException;

public class FileReplicatorImpl implements FileReplicator {

    private String srcDir;
    private String destDir;
    private FileCopier fileCopier;

    // accessors ...
    // mutators ...

    public void setSrcDir(String srcDir) {
        this.srcDir = srcDir;
        revaluateDirectories();
    }

    public void setDestDir(String destDir) {
        this.destDir = destDir;
        revaluateDirectories();
    }

    public void setFileCopier(FileCopier fileCopier) {
                this.fileCopier = fileCopier;
    }
public synchronized void replicate() throws IOException {
        File[] files = new File(srcDir).listFiles();
        for (File file : files) {
            if (file.isFile()) {
                fileCopier.copyFile(srcDir, destDir, file.getName());
            }
        }
    }
    private void revaluateDirectories() {
        File src = new File(srcDir);
        File dest = new File(destDir);
        if (!src.exists())
            src.mkdirs();
        if (!dest.exists())
            dest.mkdirs();
    }
}

Now, you can configure one or more file replicator instances in the bean configuration file for your needs (in the example, this file is called beans-jmx.xml). The documentReplicator instance needs references to two directories: a source directory from which files are read and a target directory to which files are backed up. The code in this example attempts to read from a directory called docs in your operating system user's home directory and then copy to a folder called docs_backup in your operating system user's home directory. When this bean starts up, it creates the two directories if they don't already exist there.

Tip

The "home directory" is different for each operating system, but typically on Unix it's the directory that ~ resolves to. On a Linux box, the folder might be /home/user. On Mac OS X, the folder might be /Users/user, and on Windows it might be similar to C:Documents and Settingsuser.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="fileCopier"
        class="com.apress.springrecipes.replicator.FileCopierJMXImpl" />

    <bean id="documentReplicator"
        class="com.apress.springrecipes.replicator.FileReplicatorImpl">
        <property name="srcDir" value="#{systemProperties['user.home']}/docs" />
       <property name="destDir" value="#{systemProperties['user.home']}
How It Works
/docs_backup" /> <property name="fileCopier" ref="fileCopier" /> </bean> </beans>

Registering MBeans Without Spring's Support

First, let's see how to register a model MBean using the JMX API directly. In the following Main class, you get the documentReplicator bean from the IoC container and register it as an MBean for management and monitoring. All properties and methods are included in the MBean's management interface.

package com.apress.springrecipes.replicator;
...
import java.lang.management.ManagementFactory;

import javax.management.Descriptor;
import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.modelmbean.DescriptorSupport;
import javax.management.modelmbean.InvalidTargetObjectTypeException;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanInfo;
import javax.management.modelmbean.ModelMBeanInfoSupport;
import javax.management.modelmbean.ModelMBeanOperationInfo;
import javax.management.modelmbean.RequiredModelMBean;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void main(String[] args) throws IOException {
        ApplicationContext context =
            new ClassPathXmlApplicationContext("beans-jmx.xml");

        FileReplicator documentReplicator =
            (FileReplicator) context.getBean("documentReplicator");

        try {
            MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
            ObjectName objectName = new ObjectName("bean:name=documentReplicator");

            RequiredModelMBean mbean = new RequiredModelMBean();
            mbean.setManagedResource(documentReplicator, "objectReference");

            Descriptor srcDirDescriptor = new DescriptorSupport(new String[] {
                    "name=SrcDir", "descriptorType=attribute",
                    "getMethod=getSrcDir", "setMethod=setSrcDir" });
            ModelMBeanAttributeInfo srcDirInfo = new ModelMBeanAttributeInfo(
                    "SrcDir", "java.lang.String", "Source directory",
                    true, true, false, srcDirDescriptor);

            Descriptor destDirDescriptor = new DescriptorSupport(new String[] {
                    "name=DestDir", "descriptorType=attribute",
                    "getMethod=getDestDir", "setMethod=setDestDir" });
ModelMBeanAttributeInfo destDirInfo = new ModelMBeanAttributeInfo(
                    "DestDir", "java.lang.String", "Destination directory",
                    true, true, false, destDirDescriptor);

            ModelMBeanOperationInfo getSrcDirInfo = new ModelMBeanOperationInfo(
                    "Get source directory",
                    FileReplicator.class.getMethod("getSrcDir"));
            ModelMBeanOperationInfo setSrcDirInfo = new ModelMBeanOperationInfo(
                    "Set source directory",
                    FileReplicator.class.getMethod("setSrcDir", String.class));
            ModelMBeanOperationInfo getDestDirInfo = new ModelMBeanOperationInfo(
                    "Get destination directory",
                    FileReplicator.class.getMethod("getDestDir"));
            ModelMBeanOperationInfo setDestDirInfo = new ModelMBeanOperationInfo(
                    "Set destination directory",
                    FileReplicator.class.getMethod("setDestDir", String.class));
            ModelMBeanOperationInfo replicateInfo = new ModelMBeanOperationInfo(
                    "Replicate files",
                    FileReplicator.class.getMethod("replicate"));

            ModelMBeanInfo mbeanInfo = new ModelMBeanInfoSupport(
                    "FileReplicator", "File replicator",
                    new ModelMBeanAttributeInfo[] { srcDirInfo, destDirInfo },
                    null,
                    new ModelMBeanOperationInfo[] { getSrcDirInfo, setSrcDirInfo,
                            getDestDirInfo, setDestDirInfo, replicateInfo },
                    null);
            mbean.setModelMBeanInfo(mbeanInfo);

            mbeanServer.registerMBean(mbean, objectName);
        } catch (JMException e) {
            ...
        } catch (InvalidTargetObjectTypeException e) {
            ...
        } catch (NoSuchMethodException e) {
            ...
        }

        System.in.read();
    }
}

To register an MBean, you need an instance of the interface javax.managment.MBeanServer. In JDK 1.5, you can call the static method ManagementFactory.getPlatformMBeanServer() to locate a platform MBean server. It will create an MBean server if none exists and then register this server instance for future use. Each MBean requires an MBean object name that includes a domain. The preceding MBean is registered under the domain bean with the name documentReplicator.

From the preceding code, you can see that for each MBean attribute and MBean operation, you need to create a ModelMBeanAttributeInfo object and a ModelMBeanOperationInfo object for describing it. After those, you have to create a ModelMBeanInfo object for describing the MBean's management interface by assembling the preceding information. For details about using these classes, you can consult their Javadocs.

Moreover, you have to handle the JMX-specific exceptions when calling the JMX API. These exceptions are checked exceptions that you must handle.

Note that you must prevent your application from terminating before you look inside it with a JMX client tool. Requesting a key from the console using System.in.read() would be a good choice.

Finally, you have to add the VM argument -Dcom.sun.management.jmxremote to enable local monitoring of this application. You should also include all other options for your command, such as the classpath, as necessary.

Java –classpath ... -Dcom.sun.management.jmxremote com.apress.springrecipes.replicator.Main

Now, you can use any JMX client tools to monitor your MBeans locally. The simplest one may be JConsole, which comes with JDK 1.5.

Note

To start JConsole, just execute the jconsole executable file (located in the bin directory of the JDK installation).

When JConsole starts, you can see a list of JMX-enabled applications on the Local tab of the connection window. After connecting to the replicator application, you can see your documentReplicator MBean under the bean domain. If you want to invoke replicate(), simply click the button "replicate."

Exporting Spring Beans as MBeans

To export beans configured in the Spring IoC container as MBeans, you simply declare an MBeanExporter instance and specify the beans to export, with their MBean object names as the keys.

<bean id="mbeanExporter"
    class="org.springframework.jmx.export.MBeanExporter">
    <property name="beans">
        <map>
            <entry key="bean:name=documentReplicator"
                value-ref="documentReplicator" />
        </map>
    </property>
</bean>

The preceding configuration exports the documentReplicator bean as an MBean, under the domain bean and with the name documentReplicator. By default, all public properties are included as attributes and all public methods (with the exception of those from java.lang.Object) are included as operations in the MBean's management interface.

MBeanExporter attempts to locate an MBean server instance and register your MBeans with it implicitly. If your application is running in an environment that provides an MBean server (e.g., most Java EE application servers), MBeanExporter will be able to locate this MBean server instance.

However, in an environment with no MBean server available, you have to create one explicitly using Spring's MBeanServerFactoryBean. To make your application portable to different runtime environments, you should enable the locateExistingServerIfPossible property so that this factory bean will create an MBean server only if none is available.

Note

JDK 1.5 will create an MBean server for the first time when you locate it. So, if you're using JDK 1.5 or above, you needn't create an MBean server explicitly.

<bean id="mbeanServer"
        class="org.springframework.jmx.support.MBeanServerFactoryBean">
        <property name="locateExistingServerIfPossible" value="true" />
    </bean>

If, on the other hand, you have multiple MBeans servers running, you need to tell the mbeanServer bean to which server it should bind. You do this by specifying the agentId of the server. To figure out the agentId of a given server, browse to the JMImplementation/MBeanServerDelegate/Attributes/MBeanServerId node of the server you're inspecting in JConsole. There, you'll see the string value. On our local machine, the value is workstation_1253860476443. To enable it, configure the agentId property of the MBeanServer.

<bean id="mbeanServer"
        class="org.springframework.jmx.support.MBeanServerFactoryBean">
        <property name="locateExistingServerIfPossible" value="true" />
        <property name="agentId" value="workstation_1253860476443" />
    </bean>

If you have multiple MBean server instances in your context, you can explicitly specify a specific MBean server for MBeanExporter to export your MBeans to. In this case, MBeanExporter will not locate an MBean server; it will use the specified MBean server instance. This property is for you to specify a particular MBean server when more than one is available.

<beans ...>
    ...
    <bean id="mbeanServer"
        class="org.springframework.jmx.support.MBeanServerFactoryBean">
        <property name="locateExistingServerIfPossible" value="true" />
    </bean>

    <bean id="mbeanExporter"
        class="org.springframework.jmx.export.MBeanExporter">
        ...
        <property name="server" ref="mbeanServer" />
    </bean>
</beans>

The Main class for exporting an MBean can be simplified as shown following. You have to retain the key-requesting statement to prevent your application from terminating.

package com.apress.springrecipes.replicator;
...
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void main(String[] args) throws IOException {
        new ClassPathXmlApplicationContext("beans-jmx.xml");
        System.in.read();
    }
}

Exposing MBeans for Remote Access

If you want your MBeans to be accessed remotely, you need to enable a remoting protocol for JMX. JSR-160 defines a standard for JMX remoting through a JMX connector. Spring allows you to create a JMX connector server through ConnectorServerFactoryBean.

By default, ConnectorServerFactoryBean creates and starts a JMX connector server bound to the service URL service:jmx:jmxmp://localhost:9875, which exposes the JMX connector through the JMX Messaging Protocol (JMXMP). However, most JMX implementations, including JDK 1.5's, don't support JMXMP. Therefore, you should choose a widely supported remoting protocol for your JMX connector, such as RMI. To expose your JMX connector through a specific protocol, you just provide the service URL for it.

<beans ...>
    ...
    <bean id="rmiRegistry"
        class="org.springframework.remoting.rmi.RmiRegistryFactoryBean" />

    <bean id="connectorServer"
        class="org.springframework.jmx.support.ConnectorServerFactoryBean"
        depends-on="rmiRegistry">
        <property name="serviceUrl" value=
            "service:jmx:rmi://localhost/jndi/rmi://localhost:1099/replicator" />
    </bean>
</beans>

You specify the preceding URL to bind your JMX connector to an RMI registry listening on port 1099 of localhost. If no RMI registry has been created externally, you should create one by using RmiRegistryFactoryBean. The default port for this registry is 1099, but you can specify another one in its port property. Note that ConnectorServerFactoryBean must create the connector server after the RMI registry is created and ready. You can set the depends-on attribute for this purpose.

Now, your MBeans can be accessed remotely via RMI. When JConsole starts, you can enter the following service URL on the Advanced tab of the connection window.

service:jmx:rmi://localhost/jndi/rmi://localhost:1099/replicator

Assembling the Management Interface of MBeans

Recall that, by default, the Spring MBeanExporter exports all public properties of a bean as MBean attributes and all public methods as MBean operations. In fact, you can assemble the management interface of your MBeans using an MBean assembler. The simplest MBean assembler in Spring is MethodNameBasedMBeanInfoAssembler, which allows you to specify the names of the methods to export.

<beans ...>
    ...
    <bean id="mbeanExporter"
        class="org.springframework.jmx.export.MBeanExporter">
        ...
        <property name="assembler" ref="assembler" />
    </bean>

    <bean id="assembler" class="org.springframework.jmx.export.assembler.
        MethodNameBasedMBeanInfoAssembler">
        <property name="managedMethods">
            <list>
                <value>getSrcDir</value>
                <value>setSrcDir</value>
                <value>getDestDir</value>
                <value>setDestDir</value>
                <value>replicate</value>
            </list>
        </property>
    </bean>
</beans>

Another MBean assembler is InterfaceBasedMBeanInfoAssembler, which exports all methods defined in the interfaces you specified.

<bean id="assembler" class="org.springframework.jmx.export.assembler.
InterfaceBasedMBeanInfoAssembler">
    <property name="managedInterfaces">
        <list>
            <value>com.apress.springrecipes.replicator.FileReplicator</value>
        </list>
    </property>
</bean>

Spring also provides MetadataMBeanInfoAssembler to assemble an MBean's management interface based on the metadata in the bean class. It supports two types of metadata: JDK annotations and Apache Commons Attributes (behind the scenes, this is accomplished using a strategy interface JmxAttributeSource). For a bean class annotated with JDK annotations, you specify an AnnotationJmxAttributeSource instance as the attribute source of MetadataMBeanInfoAssembler.

<bean id="assembler" class="org.springframework.jmx.export.assembler.
Assembling the Management Interface of MBeans
MetadataMBeanInfoAssembler"> <property name="attributeSource"> <bean class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource" /> </property> </bean>

Then, you annotate your bean class and methods with the annotations @ManagedResource, @ManagedAttribute, and @ManagedOperation for MetadataMBeanInfoAssembler to assemble the management interface for this bean. The annotations are easily interpreted. They expose the element that they annotate. If you have a JavaBeans-compliant property, JMX will use the term attribute. Classes themselves are referred to as resources. In JMX, methods will be called operations. Knowing that, it's easy to see what the following code does:

package com.apress.springrecipes.replicator;
...
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;

@ManagedResource(description = "File replicator")
public class FileReplicatorImpl implements FileReplicator {
    ...
    @ManagedAttribute(description = "Get source directory")
    public String getSrcDir() {
        ...
    }

    @ManagedAttribute(description = "Set source directory")
    public void setSrcDir(String srcDir) {
        ...
    }

    @ManagedAttribute(description = "Get destination directory")
    public String getDestDir() {
        ...
    }

    @ManagedAttribute(description = "Set destination directory")
    public void setDestDir(String destDir) {
        ...
    }

        ...

    @ManagedOperation(description = "Replicate files")
    public synchronized void replicate() throws IOException {
        ...
    }
}

Auto-Detecting MBeans by Annotations

In addition to exporting a bean explicitly with MBeanExporter, you can simply configure its subclass AnnotationMBeanExporter to auto-detect MBeans from beans declared in the IoC container. You needn't configure an MBean assembler for this exporter, because it uses MetadataMBeanInfoAssembler with AnnotationJmxAttributeSource by default. You can delete the previous beans and assembler properties for this exporter.

<bean id="mbeanExporter"
    class="org.springframework.jmx.export.annotation.AnnotationMBeanExporter">
    ...
</bean>

AnnotationMBeanExporter detects any beans configured in the IoC container with the @ManagedResource annotation and exports them as MBeans. By default, this exporter exports a bean to the domain whose name is the same as its package name. Also, it uses the bean's name in the IoC container as its MBean name, and the bean's short class name as its type. So your documentReplicator bean will be exported under the following MBean object name:

com.apress.springrecipes.replicator:name=documentReplicator,
type=FileReplicatorImpl

If you don't want to use the package name as the domain name, you can set the default domain for this exporter.

<bean id="mbeanExporter"
    class="org.springframework.jmx.export.annotation.AnnotationMBeanExporter">
    ...
    <property name="defaultDomain" value="bean" />
</bean>

After setting the default domain to bean, the documentReplicator bean will be exported under the following MBean object name:

bean:name=documentReplicator,type=FileReplicatorImpl

Moreover, you can specify a bean's MBean object name in the objectName attribute of the @ManagedResource annotation. For example, you can export your file copier as an MBean by annotating it with the following annotations:

package com.apress.springrecipes.replicator;
...
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
import org.springframework.jmx.export.annotation.ManagedResource;
@ManagedResource(
    objectName = "bean:name=fileCopier,type=FileCopierImpl",
    description = "File Copier")
public class FileCopierImpl implements FileCopier {

    @ManagedOperation(
        description = "Copy file from source directory to destination directory")
    @ManagedOperationParameters( {
        @ManagedOperationParameter(
            name = "srcDir", description = "Source directory"),
        @ManagedOperationParameter(
            name = "destDir", description = "Destination directory"),
        @ManagedOperationParameter(
            name = "filename", description = "File to copy") })
    public void copyFile(String srcDir, String destDir, String filename)
            throws IOException {
        ...
    }
}

However, specifying the object name in this way works only for classes that you're going to create a single instance of in the IoC container (e.g., file copier), not for classes that you may create multiple instances of (e.g., file replicator). This is because you can only specify a single object name for a class. As a result, you shouldn't try and run the same server multiple times without changing the names.

You can simply declare a <context:mbean-export> element in your bean configuration file, instead of the AnnotationMBeanExporter declaration, which you can omit.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:mbean-export server="mbeanServer" default-domain="bean" />
    ...
</beans>

You can specify an MBean server and a default domain name for this element through the server and default-domain attributes. However, you won't be able to set other MBean exporter properties such as notification listener mappings. Whenever you have to set these properties, you need to declare an AnnotationMBeanExporter instance explicitly.

Publishing and Listening to JMX Notifications

Problem

You want to publish JMX notifications from your MBeans and listen to them with JMX notification listeners.

Solution

Spring allows your beans to publish JMX notifications through the NotificationPublisher interface. You can also register standard JMX notification listeners in the IoC container to listen to JMX notifications.

How It Works

Publishing JMX Notifications

The Spring IoC container supports the beans that are going to be exported as MBeans to publish JMX notifications. These beans must implement the NotificationPublisherAware interface (as you might implement ApplicationContextAware to receive a reference to the current bean's containing ApplicatonContext instance) to get access to NotificationPublisher so that they can publish notifications.

package com.apress.springrecipes.replicator;
...
import javax.management.Notification;

import org.springframework.jmx.export.notification.NotificationPublisher;
import org.springframework.jmx.export.notification.NotificationPublisherAware;

@ManagedResource(description = "File replicator")
public class FileReplicatorImpl implements FileReplicator,
        NotificationPublisherAware {
    ...
    private int sequenceNumber;
    private NotificationPublisher notificationPublisher;

    public void setNotificationPublisher(
            NotificationPublisher notificationPublisher) {
        this.notificationPublisher = notificationPublisher;
    }
  @ManagedOperation(description = "Replicate files")
    public void replicate() throws IOException {
        notificationPublisher.sendNotification(
                new Notification("replication.start", this, sequenceNumber));
        ...
notificationPublisher.sendNotification(
                new Notification("replication.complete", this, sequenceNumber));
        sequenceNumber++;
    }
}

In this file replicator, you send a JMX notification whenever a replication starts or completes. The notification is visible both in the standard output in the console as well as in the Notifications node for your service in JConsole. To see them, you must click Subscribe. Then, invoke the replicate() method, and you'll see two new notifications arrive, much like your e-mail's inbox. The first argument in the Notification constructor is the notification type, while the second is the notification source. Each notification requires a sequence number. You can use the same sequence for a notification pair to keep track of them.

Listening to JMX Notifications

Now, let's create a notification listener to listen to JMX notifications. Because a listener will be notified of many different types of notifications, such as javax.management.AttributeChangeNotification when an MBean's attribute has changed, you have to filter those notifications that you are interested in handling.

package com.apress.springrecipes.replicator;

import javax.management.Notification;
import javax.management.NotificationListener;

public class ReplicationNotificationListener implements NotificationListener {

    public void handleNotification(Notification notification, Object handback) {
        if (notification.getType().startsWith("replication")) {
            System.out.println(
                    notification.getSource() + " " +
                    notification.getType() + " #" +
                    notification.getSequenceNumber());
        }
    }
}

Then, you can register this notification listener with your MBean exporter to listen to notifications emitted from certain MBeans.

<bean id="mbeanExporter"
    class="org.springframework.jmx.export.annotation.AnnotationMBeanExporter">
    <property name="defaultDomain" value="bean" />
    <property name="notificationListenerMappings">
        <map>
            <entry key="bean:name=documentReplicator,type=FileReplicatorImpl">
                <bean class="com.apress.springrecipes.replicator.
Listening to JMX Notifications
ReplicationNotificationListener" /> </entry> </map> </property> </bean>

Accessing Remote JMX MBeans in Spring

Problem

You want to access JMX MBeans running on a remote MBean server exposed by a JMX connector. When accessing remote MBeans directly with the JMX API, you have to write complex JMX-specific code.

Solution

Spring offers two approaches to simplify your remote MBean access. First, it provides a factory bean for you to create an MBean server connection declaratively. With this server connection, you can query and update an MBean's attributes, as well as invoke its operations. Second, Spring provides another factory bean that allows you to create a proxy for a remote MBean. With this proxy, you can operate a remote MBean as if it were a local bean.

How It Works

Accessing Remote MBeans Through an MBean Server Connection

A JMX client requires an MBean server connection to access MBeans running on a remote MBean server. Spring provides org.springframework.jmx.support.MBeanServerConnectionFactoryBean for you to create a connection to a remote JSR-160–enabled MBean server declaratively. You only have to provide the service URL for it to locate the MBean server. Now let's declare this factory bean in your client bean configuration file (e.g., beans-jmx-client.xml).

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="mbeanServerConnection"
        class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
        <property name="serviceUrl" value=
            "service:jmx:rmi://localhost/jndi/rmi://localhost:1099/replicator" />
    </bean>
</beans>

With the MBean server connection created by this factory bean, you can access and operate the MBeans running on this server. For example, you can query and update an MBean's attributes through the getAttribute() and setAttribute() methods, giving the MBean's object name and attribute name. You can also invoke an MBean's operations by using the invoke() method.

package com.apress.springrecipes.replicator;

import javax.management.Attribute;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Client {

    public static void main(String[] args) throws Exception {
        ApplicationContext context =
            new ClassPathXmlApplicationContext("beans-jmx-client.xml");

        MBeanServerConnection mbeanServerConnection =
            (MBeanServerConnection) context.getBean("mbeanServerConnection");

        ObjectName mbeanName = new ObjectName(
                "bean:name=documentReplicator,type=FileReplicatorImpl");

        String srcDir = (String) mbeanServerConnection.getAttribute(
                mbeanName, "SrcDir");

        mbeanServerConnection.setAttribute(
                mbeanName, new Attribute("DestDir", srcDir + "_1"));

        mbeanServerConnection.invoke(
                mbeanName, "replicate", new Object[] {}, new String[] {});
    }
}

Suppose that you've created the following JMX notification listener, which listens to file replication notifications:

package com.apress.springrecipes.replicator;

import javax.management.Notification;
import javax.management.NotificationListener;

public class ReplicationNotificationListener implements NotificationListener {

    public void handleNotification(Notification notification, Object handback) {
        if (notification.getType().startsWith("replication")) {
            System.out.println(
                    notification.getSource() + " " +
                    notification.getType() + " #" +
                    notification.getSequenceNumber());
        }
    }
}

You can register this notification listener to the MBean server connection to listen to notifications emitted from this MBean server.

package com.apress.springrecipes.replicator;
...
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;

public class Client {

    public static void main(String[] args) throws Exception {
        ...
        MBeanServerConnection mbeanServerConnection =
            (MBeanServerConnection) context.getBean("mbeanServerConnection");

        ObjectName mbeanName = new ObjectName(
                "bean:name=documentReplicator,type=FileReplicatorImpl");

        mbeanServerConnection.addNotificationListener(
                mbeanName, new ReplicationNotificationListener(), null, null);
        ...
    }
}

After you run this, check JConsole again under the Notifications node. You'll see the same two notifications as before and an interesting, new notification of type jmx.attribute.change.

Accessing Remote MBeans Through an MBean Proxy

Another approach that Spring offers for remote MBean access is through MBeanProxy, which can be created by MBeanProxyFactoryBean.

<beans ...>
    <bean id="mbeanServerConnection"
        class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
        <property name="serviceUrl" value=
            "service:jmx:rmi://localhost/jndi/rmi://localhost:1099/replicator" />
    </bean>

    <bean id="fileReplicatorProxy"
        class="org.springframework.jmx.access.MBeanProxyFactoryBean">
        <property name="server" ref="mbeanServerConnection" />
        <property name="objectName"
            value="bean:name=documentReplicator,type=FileReplicatorImpl" />
        <property name="proxyInterface"
            value="com.apress.springrecipes.replicator.FileReplicator" />
    </bean>
</beans>

You need to specify the object name and the server connection for the MBean you are going to proxy. The most important is the proxy interface, whose local method calls will be translated into remote MBean calls behind the scenes.

Now, you can operate the remote MBean through this proxy as if it were a local bean. The preceding MBean operations invoked on the MBean server connection directly can be simplified as follows:

package com.apress.springrecipes.replicator;
...
public class Client {

    public static void main(String[] args) throws Exception {
        ...
        FileReplicator fileReplicatorProxy =
            (FileReplicator) context.getBean("fileReplicatorProxy");

        String srcDir = fileReplicatorProxy.getSrcDir();
        fileReplicatorProxy.setDestDir(srcDir + "_1");
        fileReplicatorProxy.replicate();
    }
}

Sending E-mail with Spring's E-mail Support

Problem

Many applications need to send e-mail. In a Java application, you can send e-mail with the JavaMail API. However, when using JavaMail, you have to handle the JavaMail-specific mail sessions and exceptions. As a result, your application becomes JavaMail dependent and hard to switch to another e-mail API.

Solution

Spring's e-mail support makes it easier to send e-mail by providing an abstract and implementation-independent API for sending e-mail. The core interface of Spring's e-mail support is MailSender.

The JavaMailSender interface is a subinterface of MailSender that includes specialized JavaMail features such as Multipurpose Internet Mail Extensions (MIME) message support. To send an e-mail message with HTML content, inline images, or attachments, you have to send it as a MIME message.

How It Works

Suppose that you want your file replicator application to notify the administrator of any error. First, you create the following ErrorNotifier interface, which includes a method for notifying of a file copy error:

package com.apress.springrecipes.replicator;

public interface ErrorNotifier {

    public void notifyCopyError(String srcDir, String destDir, String filename);
}

Note

Invoking this notifier in case of error is left for you to accomplish. As you can consider error handling a crosscutting concern, AOP would be an ideal solution to this problem. You can write an after throwing advice to invoke this notifier.

Next, you can implement this interface to send a notification in a way of your choice. The most common way is to send e-mail. Before you implement the interface in this way, you may need a local e-mail server that supports the Simple Mail Transfer Protocol (SMTP) for testing purposes. We recommend installing Apache James Server (http://james.apache.org/server/index.html), which is very easy to install and configure.

Note

You can download Apache James Server (e.g., version 2.3.2) from the Apache James web site and extract it to a directory of your choice to complete the installation. To start it, just execute the run script (located in the bin directory).

Let's create two user accounts for sending and receiving e-mail with this server. By default, the remote manager service of James listens on port 4555. You can telnet, using a console, to this port and run the following commands (displayed in bold) to add the users system and admin, whose passwords are 12345:

JAMES Remote Administration Tool 2.3.1
Please enter your login and password
Login id:
root
Password:
root
Welcome root. HELP for a list of commands
adduser system 12345
User system added
adduser admin 12345
User admin added
listusers
Existing accounts 2
user: admin
user: system
quit
Bye

Sending E-mail Using the JavaMail API

Now, let's take a look at how to send e-mail using the JavaMail API. You can implement the ErrorNotifier interface to send e-mail notifications in case of errors.

Note

To use JavaMail in your application, you need the JavaMail library, as well as the Activation library. If you are using Maven, add the following dependency to your project.

<dependency>
   <groupId>javax.mail</groupId>
   <artifactId>mail</artifactId>
   <version>1.4</version>
 </dependency>
package com.apress.springrecipes.replicator;

import java.util.Properties;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
public class EmailErrorNotifier implements ErrorNotifier {

    public void notifyCopyError(String srcDir, String destDir, String filename) {
        Properties props = new Properties();
        props.put("mail.smtp.host", "localhost");
        props.put("mail.smtp.port", "25");
        props.put("mail.smtp.username", "system");
        props.put("mail.smtp.password", "12345");
        Session session = Session.getDefaultInstance(props, null);
        try {
            Message message = new MimeMessage(session);
            message.setFrom(new InternetAddress("system@localhost"));
            message.setRecipients(Message.RecipientType.TO,
                    InternetAddress.parse("admin@localhost"));
            message.setSubject("File Copy Error");
            message.setText(
                "Dear Administrator,

" +
                "An error occurred when copying the following file :
" +
                "Source directory : " + srcDir + "
" +
                "Destination directory : " + destDir + "
" +
                "Filename : " + filename);
            Transport.send(message);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }
}

You first open a mail session connecting to an SMTP server by defining the properties. Then, you create a message from this session for constructing your e-mail. After that, you send the e-mail by making a call to Transport.send(). When dealing with the JavaMail API, you have to handle the checked exception MessagingException. Note that all these classes, interfaces, and exceptions are defined by JavaMail.

Next, declare an instance of EmailErrorNotifier in the Spring IoC container for sending e-mail notifications in case of file replication errors.

<bean id="errorNotifier"
    class="com.apress.springrecipes.replicator.EmailErrorNotifier" />

You can write the following Main class to test EmailErrorNotifier. After running it, you can configure your e-mail application to receive the e-mail from your James Server via POP3.

package com.apress.springrecipes.replicator;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {

    public static void main(String[] args) {
        ApplicationContext context =
            new ClassPathXmlApplicationContext("beans.xml");

        ErrorNotifier errorNotifier =
            (ErrorNotifier) context.getBean("errorNotifier");
        errorNotifier.notifyCopyError(
            "c:/documents", "d:/documents", "spring.doc");
    }
}

Sending E-mail with Spring's MailSender

Now, let's look at how to send e-mail with the help of Spring's MailSender interface, which can send SimpleMailMessage in its send() method. With this interface, your code is no longer JavaMail specific, and now it's easier to test.

package com.apress.springrecipes.replicator;

import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;

public class EmailErrorNotifier implements ErrorNotifier {

    private MailSender mailSender;

    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void notifyCopyError(String srcDir, String destDir, String filename) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom("system@localhost");
        message.setTo("admin@localhost");
        message.setSubject("File Copy Error");
        message.setText(
                "Dear Administrator,

" +
                "An error occurred when copying the following file :
" +
                "Source directory : " + srcDir + "
" +
                "Destination directory : " + destDir + "
" +
                "Filename : " + filename);
        mailSender.send(message);
    }
}

Next, you have to configure a MailSender implementation in the bean configuration file and inject it into EmailErrorNotifier. In Spring, the unique implementation of this interface is JavaMailSenderImpl, which uses JavaMail to send e-mail.

<beans ...>
    ...
    <bean id="mailSender"
        class="org.springframework.mail.javamail.JavaMailSenderImpl">
        <property name="host" value="localhost" />
        <property name="port" value="25" />
        <property name="username" value="system" />
        <property name="password" value="12345" />
    </bean>

    <bean id="errorNotifier"
        class="com.apress.springrecipes.replicator.EmailErrorNotifier">
        <property name="mailSender" ref="mailSender" />
    </bean>
</beans>

The default port used by JavaMailSenderImpl is the standard SMTP port 25, so if your e-mail server listens on this port for SMTP, you can simply omit this property. Also, if your SMTP server doesn't require user authentication, you needn't set the username and password.

If you have a JavaMail session configured in your Java EE application server, you can first look it up with the help of JndiObjectFactoryBean.

<bean id="mailSession"
    class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="mail/Session" />
</bean>

Or you can look up a JavaMail session through the <jee:jndi-lookup> element if you are using Spring 2.0 or later.

<jee:jndi-lookup id="mailSession" jndi-name="mail/Session" />

You can inject the JavaMail session into JavaMailSenderImpl for its use. In this case, you no longer need to set the host, port, username, or password.

<bean id="mailSender"
    class="org.springframework.mail.javamail.JavaMailSenderImpl">
    <property name="session" ref="mailSession" />
</bean>

Defining an E-mail Template

Constructing an e-mail message from scratch in the method body is not efficient, because you have to hard-code the e-mail properties. Also, you may have difficulty in writing the e-mail text in terms of Java strings. You can consider defining an e-mail message template in the bean configuration file and construct a new e-mail message from it.

<beans ...>
    ...
    <bean id="copyErrorMailMessage"
        class="org.springframework.mail.SimpleMailMessage">
        <property name="from" value="system@localhost" />
        <property name="to" value="admin@localhost" />
        <property name="subject" value="File Copy Error" />
        <property name="text">
            <value>
<![CDATA[
Dear Administrator,

An error occurred when copying the following file :
Source directory : %s
Destination directory : %s
Filename : %s
]]>
            </value>
        </property>
    </bean>

    <bean id="errorNotifier"
        class="com.apress.springrecipes.replicator.EmailErrorNotifier">
        <property name="mailSender" ref="mailSender" />
        <property name="copyErrorMailMessage" ref="copyErrorMailMessage" />
    </bean>
</beans>

Note that in the preceding message text, you include the placeholders %s, which will be replaced by message parameters through String.format(). Of course, you can also use a powerful templating language such as Velocity or FreeMarker to generate the message text according to a template. It's also a good practice to separate mail message templates from bean configuration files.

Each time you send e-mail, you can construct a new SimpleMailMessage instance from this injected template. Then you can generate the message text using String.format() to replace the %s placeholders with your message parameters.

package com.apress.springrecipes.replicator;
...
import org.springframework.mail.SimpleMailMessage;

public class EmailErrorNotifier implements ErrorNotifier {
    ...
    private SimpleMailMessage copyErrorMailMessage;

    public void setCopyErrorMailMessage(SimpleMailMessage copyErrorMailMessage) {
        this.copyErrorMailMessage = copyErrorMailMessage;
    }
public void notifyCopyError(String srcDir, String destDir, String filename) {
        SimpleMailMessage message = new SimpleMailMessage(copyErrorMailMessage);
        message.setText(String.format(
                copyErrorMailMessage.getText(), srcDir, destDir, filename));
        mailSender.send(message);
    }
}

Sending MIME Messages

So far, the SimpleMailMessage class you used can send only a simple plain text e-mail message. To send e-mail that contains HTML content, inline images, or attachments, you have to construct and send a MIME message instead. MIME is supported by JavaMail through the javax.mail.internet.MimeMessage class.

First of all, you have to use the JavaMailSender interface instead of its parent interface MailSender. The JavaMailSenderImpl instance you injected does implement this interface, so you needn't modify your bean configurations. The following notifier sends Spring's bean configuration file as an e-mail attachment to the administrator:

package com.apress.springrecipes.replicator;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.MailParseException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;

public class EmailErrorNotifier implements ErrorNotifier {

    private JavaMailSender mailSender;
    private SimpleMailMessage copyErrorMailMessage;

    public void setMailSender(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void setCopyErrorMailMessage(SimpleMailMessage copyErrorMailMessage) {
        this.copyErrorMailMessage = copyErrorMailMessage;
    }

    public void notifyCopyError(String srcDir, String destDir, String filename) {
        MimeMessage message = mailSender.createMimeMessage();
        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            helper.setFrom(copyErrorMailMessage.getFrom());
            helper.setTo(copyErrorMailMessage.getTo());
helper.setSubject(copyErrorMailMessage.getSubject());
            helper.setText(String.format(
                    copyErrorMailMessage.getText(), srcDir, destDir, filename));

            ClassPathResource config = new ClassPathResource("beans.xml");
            helper.addAttachment("beans.xml", config);
        } catch (MessagingException e) {
            throw new MailParseException(e);
        }
        mailSender.send(message);
    }
}

Unlike SimpleMailMessage, the MimeMessage class is defined by JavaMail, so you can only instantiate it by calling mailSender.createMimeMessage(). Spring provides the helper class MimeMessageHelper to simplify the operations of MimeMessage. It allows you to add an attachment from a Spring Resource object. However, the operations of this helper class still throw JavaMail's MessagingException. You have to convert this exception into Spring's mail runtime exception for consistency.

Spring offers another method for you to construct a MIME message, which is through implementing the MimeMessagePreparator interface.

package com.apress.springrecipes.replicator;
...
import javax.mail.internet.MimeMessage;

import org.springframework.mail.javamail.MimeMessagePreparator;

public class EmailErrorNotifier implements ErrorNotifier {
    ...
    public void notifyCopyError(
            final String srcDir, final String destDir, final String filename) {
        MimeMessagePreparator preparator = new MimeMessagePreparator() {

            public void prepare(MimeMessage mimeMessage) throws Exception {
                MimeMessageHelper helper =
                    new MimeMessageHelper(mimeMessage, true);
                helper.setFrom(copyErrorMailMessage.getFrom());
                helper.setTo(copyErrorMailMessage.getTo());
                helper.setSubject(copyErrorMailMessage.getSubject());
                helper.setText(String.format(
                    copyErrorMailMessage.getText(), srcDir, destDir, filename));

                ClassPathResource config = new ClassPathResource("beans.xml");
                helper.addAttachment("beans.xml", config);
            }
        };
        mailSender.send(preparator);
    }
}

In the prepare() method, you can prepare the MimeMessage object, which is precreated for JavaMailSender. If there's any exception thrown, it will be converted into Spring's mail runtime exception automatically.

Scheduling with Spring's Quartz Support

Problem

Your application has an advanced scheduling requirement that you want to fulfill using Quartz Scheduler. Such a requirement might be something seemingly complex like the ability to run at arbitrary times, or at strange intervals ("every other Thursday, but only after 10 am and before 2 pm"). Moreover, you want to configure your scheduling jobs in a declarative way.

Solution

Spring provides utility classes for Quartz to enable you to configure scheduling jobs in the bean configuration file, without programming against the Quartz API.

How It Works

Using Quartz Without Spring's Support

To use Quartz for scheduling, first create your job by implementing the Job interface. For example, the following job executes the replicate() method of a file replicator, retrieved from the job data map through the JobExecutionContext object that's passed in.

Note

To use Quartz in your application, you must add it to your classpath. If you are using Maven, add the following dependency to your project.

<dependency>
   <groupId>org.opensymphony.quartz</groupId>
   <artifactId>quartz</artifactId>
   <version>1.6.1</version>
 </dependency>
package com.apress.springrecipes.replicator;
...
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class FileReplicationJob implements Job {

    public void execute(JobExecutionContext context)
            throws JobExecutionException {
        Map dataMap = context.getJobDetail().getJobDataMap();
        FileReplicator fileReplicator =
            (FileReplicator) dataMap.get("fileReplicator");
        try {
            fileReplicator.replicate();
        } catch (IOException e) {
            throw new JobExecutionException(e);
        }
    }
}

After creating the job, you configure and schedule it with the Quartz API. For instance, the following scheduler runs your file replication job every 60 seconds with a 5-second delay for the first time of execution:

package com.apress.springrecipes.replicator;
...
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void main(String[] args) throws Exception {
        ApplicationContext context =
            new ClassPathXmlApplicationContext("beans.xml");

        FileReplicator documentReplicator =
            (FileReplicator) context.getBean("documentReplicator");

        JobDetail job = new JobDetail();
        job.setName("documentReplicationJob");
        job.setJobClass(FileReplicationJob.class);
        Map dataMap = job.getJobDataMap();
        dataMap.put("fileReplicator", documentReplicator);
SimpleTrigger trigger = new SimpleTrigger();
        trigger.setName("documentReplicationJob");
        trigger.setStartTime(new Date(System.currentTimeMillis() + 5000));
        trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
        trigger.setRepeatInterval(60000);

        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        scheduler.start();
        scheduler.scheduleJob(job, trigger);
    }
}

In the Main class, you first configure the job details for your file replication job in a JobDetail object and prepare job data in its jobDataMap property. Next, you create a SimpleTrigger object to configure the scheduling properties. Finally, you create a scheduler to run your job using this trigger.

Quartz supports two types of triggers: SimpleTrigger and CronTrigger. SimpleTrigger allows you to set trigger properties such as start time, end time, repeat interval, and repeat count. CronTrigger accepts a Unix cron expression for you to specify the times to run your job. For example, you can replace the preceding SimpleTrigger with the following CronTrigger to run your job at 17:30 every day:

CronTrigger trigger = new CronTrigger();
trigger.setName("documentReplicationJob");
trigger.setCronExpression("0 30 17 * * ?");

A cron expression is made up of seven fields (the last field is optional), separated by spaces. Table 18-1 shows the field description for a cron expression.

Table 18.1. Field Description for a Cron Expression

Position

Field Name

Range

1

Second

0–59

2

Minute

0–59

3

Hour

0–23

4

Day of month

1–31

5

Month

1–12 or JAN–DEC

6

Day of week

1–7 or SUN–SAT

7

Year (optional)

1970–2099

Each part of a cron expression can be assigned a specific value (e.g. 3), a range (e.g. 1–5), a list (e.g. 1,3,5), a wildcard (* matches all values), or a question mark (? is used in either of the "Day of month" and "Day of week" fields for matching one of these fields but not both). For more information on the cron expressions supported by CronTrigger, refer to its Javadoc http://quartz.sourceforge.net/javadoc/org/quartz/CronTrigger.html).

Using Quartz with Spring's Support

When using Quartz, you can create a job by implementing the Job interface and retrieve job data from the job data map through JobExecutionContext. To decouple your job class from the Quartz API, Spring provides QuartzJobBean, which you can extend to retrieve job data through setter methods. QuartzJobBean converts the job data map into properties and injects them via the setter methods.

package com.apress.springrecipes.replicator;
...
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class FileReplicationJob extends QuartzJobBean {

    private FileReplicator fileReplicator;

    public void setFileReplicator(FileReplicator fileReplicator) {
        this.fileReplicator = fileReplicator;
    }

    protected void executeInternal(JobExecutionContext context)
            throws JobExecutionException {
        try {
            fileReplicator.replicate();
        } catch (IOException e) {
            throw new JobExecutionException(e);
        }
    }
}

Then, you can configure a Quartz JobDetail object in Spring's bean configuration file through JobDetailBean. By default, Spring uses this bean's name as the job name. You can modify it by setting the name property.

<bean name="documentReplicationJob"
    class="org.springframework.scheduling.quartz.JobDetailBean">
    <property name="jobClass"
        value="com.apress.springrecipes.replicator.FileReplicationJob" />
    <property name="jobDataAsMap">
        <map>
            <entry key="fileReplicator" value-ref="documentReplicator" />
        </map>
    </property>
</bean>

Spring also offers MethodInvokingJobDetailFactoryBean for you to define a job that executes a single method of a particular object. This saves you the trouble of creating a job class. You can use the following job detail to replace the previous:

<bean id="documentReplicationJob" class="org.springframework.
Using Quartz with Spring's Support
scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="targetObject" ref="documentReplicator" /> <property name="targetMethod" value="replicate" /> </bean>

You can configure a Quartz SimpleTrigger object in Spring's bean configuration file through SimpleTriggerBean, which requires a reference to a JobDetail object. This bean provides common default values for certain trigger properties, such as using the bean name as the job name and setting indefinite repeat count.

<bean id="documentReplicationTrigger"
    class="org.springframework.scheduling.quartz.SimpleTriggerBean">
    <property name="jobDetail" ref="documentReplicationJob" />
    <property name="repeatInterval" value="60000" />
    <property name="startDelay" value="5000" />
</bean>

You can also configure a Quartz CronTrigger object in the bean configuration file through CronTriggerBean.

<bean id="documentReplicationTrigger"
    class="org.springframework.scheduling.quartz.CronTriggerBean">
    <property name="jobDetail" ref="documentReplicationJob" />
    <property name="cronExpression" value=" 0 * * * * ? " />
</bean>

Finally, you can configure a SchedulerFactoryBean instance to create a Scheduler object for running your trigger. You can specify multiple triggers in this factory bean.

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <list>
            <ref bean="documentReplicationTrigger" />
           <!--    other triggers you have may be included here -->
        </list>
    </property>
</bean>

Now, you can simply start your scheduler with the following Main class. In this way, you don't require a single line of code for scheduling jobs.

package com.apress.springrecipes.replicator;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void main(String[] args) throws Exception {
        new ClassPathXmlApplicationContext("beans.xml");
    }
}

Scheduling With Spring 3.0's Scheduling Namespace

Problem

You want to schedule a method invocation in a consistent manner, using either a cron expression, an interval, or a rate, and you don't want to have to go through Quartz just to do it.

Solution

Spring 3.0 debuts new support for configuring TaskExecutors and TaskSchedulers. This capability, coupled with the ability to schedule method execution using the @Scheduled annotation, makes Spring 3.0 very capable of meeting this challenge. The scheduling support works with a minimal of fuss: all you need are a method, an annotation, and to have switched on the scanner for annotations, in the simplest case.

How It Works

Let's revisit the example in the last recipe: we want to schedule a call to the replication method on the bean using a cron expression. Our XML file looks familiar:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.0.xsd
        ">

<context:component-scan annotation-config="true"
                        base-package="com.apress.springrecipes.replicator"/>

        <task:scheduler id="scheduler" pool-size="10"/>
        <task:executor id="executor" pool-size="10"/>

        <task:annotation-driven scheduler="scheduler" executor="executor"/>

<bean id="fileCopier"
      class="com.apress.springrecipes.replicator.FileCopierJMXImpl" />

        <bean id="documentReplicator"
                class="com.apress.springrecipes.replicator.FileReplicatorImpl">
                <property name="srcDir" value="#{systemProperties['user.home']}/docs" />
                <property name="destDir"
                       value="#{systemProperties['user.home']}/docs_backup" />
                <property name="fileCopier" ref="fileCopier" />
        </bean>

</beans>

We have our two beans from the previous example. At the top, however, we've added some configuration to support the scheduling features. We've drawn out the configuration of each element here, but you don't need to. Once that's done, we switch on general annotation support with the task:annotation-driven element. Let's look at our code now.

package com.apress.springrecipes.replicator;

import org.springframework.scheduling.annotation.Scheduled;

import java.io.File;
import java.io.IOException;


public class FileReplicatorImpl implements FileReplicator {
    private String srcDir;
    private String destDir;
    private FileCopier fileCopier;
public String getSrcDir() {
        return srcDir;
    }

    public void setSrcDir(String srcDir) {
        this.srcDir = srcDir;
        revaluateDirectories();
    }

    public String getDestDir() {
        return destDir;
    }

    public void setDestDir(String destDir) {
        this.destDir = destDir;
        revaluateDirectories();
    }

    public void setFileCopier(FileCopier fileCopier) {
        this.fileCopier = fileCopier;
    }

    @Scheduled(fixedDelay = 60 * 1000)
    public synchronized void replicate() throws IOException {
        File[] files = new File(srcDir).listFiles();

        for (File file : files) {
            if (file.isFile()) {
                fileCopier.copyFile(srcDir, destDir, file.getName());
            }
        }
    }

    private void revaluateDirectories() {
        File src = new File(srcDir);
        File dest = new File(destDir);

        if (!src.exists()) {
            src.mkdirs();
        }

        if (!dest.exists()) {
            dest.mkdirs();
        }
    }
}

Note that we've annotated the replicate() method with a @Scheduled annotation. Here, we've told the scheduler to execute the method every 30 seconds, as measured from the completion time of the previous invocation. Alternateively, we might specify a fixedRate value for the @Scheduled annotation, which would measure the time between successive starts and then trigger another run.

@Scheduled(fixedRate = 60 * 1000)
    public synchronized void replicate() throws IOException {
        File[] files = new File(srcDir).listFiles();

        for (File file : files) {
            if (file.isFile()) {
                fileCopier.copyFile(srcDir, destDir, file.getName());
            }
        }
    }

Finally, we might want more complex control over the execution of the method. In this case, we can use a cron expression, just as we did in the Quartz example.

@Scheduled( cron = " 0 * * * * ? " )
    public synchronized void replicate() throws IOException {
        File[] files = new File(srcDir).listFiles();

        for (File file : files) {
            if (file.isFile()) {
                fileCopier.copyFile(srcDir, destDir, file.getName());
            }
        }
    }

There is support for configuring all of this in the XML too. This might be useful if you didn't want to, or couldn't, add an annotation to an existing bean method. Here's a look at how we might re-create the preceding annotation-centric examples using the Spring task XML namespace.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:task="http://www.springframework.org/schema/task"
How It Works
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context
How It Works
http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/task
How It Works
http://www.springframework.org/schema/task/spring-task-3.0.xsd "> <context:component-scan annotation-config="true"
How It Works
base-package="com.apress.springrecipes.replicator"/> <task:scheduler id="scheduler" pool-size="10"/> <task:executor id="executor" pool-size="10"/> <task:annotation-driven scheduler="scheduler" executor="executor"/>
<task:scheduled-tasks scheduler="scheduler">
        <task:scheduled ref="documentReplicator" method="replicate" fixed-rate="60000"/>
        <task:scheduled ref="documentReplicator" method="replicate" fixed-delay="60000"/>
        <task:scheduled ref="documentReplicator" method="replicate" cron="0 * * * * ? "/>
</task:scheduled-tasks>

<bean id="fileCopier" class="com.apress.springrecipes.replicator.FileCopierJMXImpl" />

        <bean id="documentReplicator"
                class="com.apress.springrecipes.replicator.FileReplicatorImpl">
                <property name="srcDir" value="#{systemProperties['user.home']}/docs" />
                <property name="destDir"
How It Works
value="#{systemProperties['user.home']}/docs_backup" /> <property name="fileCopier" ref="fileCopier" /> </bean> </beans>

Summary

This chapter discussed JMX and a few of the surrounding specifications. You learned how to export Spring beans as JMX MBeans and how to use those MBeans from a client, both remotely and locally by using Spring's proxies. You published and listened to notification events on a JMX server from Spring. You built a simple replicator and exposed its configuration through JMX. You learned how to schedule the replication using the Quartz Scheduler, as well as Spring 3.0's new task namespace.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset