Sunday, March 21, 2010

Scheduling with Quartz and Spring

Scheduling with Quartz and Spring

This has been covered in various forms for quite some time but here is my preferred solution to running a job from your app server.  Typical use cases for this are reports, system status checks or cleanup operations.


What is the JavaBean/POJO to be called?

 <bean name="systemMonitorBean" class="com.myco.HostPortMonitor">

        <property name="serverList" value="${system.monitoring.serverport.locations}"/>
        <property name="timeout" value="${system.monitoring.serverport.timeout}"/>
        <property name="emailTo" value="${system.monitoring.serverport.emailto}"/>
        <property name="emailSender" ref="emailSender"/>


 The properties above are grabbed from a file on the classpath: 

<context:property-placeholder location="classpath*:myapp.properties"/>


I find the best way to call the Java class above is to get Spring to call a method on it:

   <bean id="methodInvokingJob" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <property name="targetObject" ref="systemMonitorBean"/>
        <property name="targetMethod" value="checkSystems"/>
    </bean>


Now this effectively is a Quartz "job", ie the pojo and its specified method have become the subject of a Quartz job and can therefore be called by Quartz.  So the rest of the setup is to have a trigger, in this case a simple check every hour (time is defined in milliseconds):


    
    <bean id="serverPortMonitoringTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
        <property name="jobDetail" ref="methodInvokingJob"/>
        <property name="startDelay" value="10000"/>
        <property name="repeatInterval" value="3600000"/>
    </bean>




You could use a CRON trigger as well: http://www.quartz-scheduler.org/docs/tutorials/crontrigger.html

Final step is to have a scheduler running that can call multiple triggers:

    <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="serverPortMonitoringTrigger"/>
            </list>
        </property>
    </bean>

Very easy.

Used in this were some other helper classes.  The Java class being called is just a simple check if a server:port is running and email out:  

public class HostPortMonitor extends BaseSystemMonitor {

    private String serverList;
    private Integer timeout;

    private List<ServerPort> serverPortList = new ArrayList<ServerPort>();

    @Override
    public void checkSystems() {
        if (ObjectUtil.isEmpty(timeout) || timeout < 100) {
            timeout = 10000;
        }
        for (ServerPort serverPort : serverPortList) {
            log.info("Checking system up/down for: " + serverPort.getServerName() + ":" + serverPort.getPort());
            try {
                if (NetworkUtils.checkServerPort(serverPort.getServerName(), serverPort.getPort(), timeout)) {
                    String checkStatus = "OK";
                    sendEmail(serverPort, checkStatus);
                } else {
                    String checkStatus = "FAILED - No Connect";
                    sendEmail(serverPort, checkStatus);
                }
            } catch (UtilException e) {
                log.error("Could not check server:port - " + serverPort.getServerName() + ":" + serverPort.getPort(), e);
                String checkStatus = "FAILED - " + e.getMessage();
                sendEmail(serverPort, checkStatus);
            }
        }
    }

    private void sendEmail(ServerPort serverPort, String checkStatus) {
        StringBuilder body = buildMessageBody(serverPort, checkStatus);
        try {
            log.info("Sending email for system up/down: " + body);
            emailSender.sendGenericEmail(emailTo, "Monitor: " + checkStatus, body.toString());
        } catch (MessagingException e) {
            log.warn("While emailing system monitor report I received error: " + e.getMessage());
        }
    }

    private StringBuilder buildMessageBody(ServerPort serverPort, String checkStatus) {
        StringBuilder body = new StringBuilder();
        body.append("System check: ").append(checkStatus).append(TextUtil.getLineSeparator());
        body.append("To server:port - ").append(serverPort.getServerName());
        body.append("At time: ").append(new Date()).append(TextUtil.getLineSeparator());
        body.append(":").append(serverPort.getPort()).append(TextUtil.getLineSeparator());
        return body;
    }

    public Integer getTimeout() {
        return timeout;
    }

    public void setTimeout(Integer timeout) {
        this.timeout = timeout;
    }

    public String getServerList() {
        return serverList;
    }

    public void setServerList(String serverList) {
        this.serverList = serverList;
        String[] serverPorts = serverList.split(";");
        for (String serverPort : serverPorts) {
            String[] split = serverPort.split(":", 2);
            serverPortList.add(new ServerPort(split[0], split[1]));
        }
    }

    class ServerPort {
        String serverName;
        Integer port;

        ServerPort(String serverName, String portString) {
            this.serverName = serverName;

            if (ObjectUtil.isEmpty(portString)) {
                this.port = 80;
            } else {
                try {
                    this.port = Integer.parseInt(portString);
                } catch (NumberFormatException e) {
                    this.port = 80;
                }
            }
        }

        public String getServerName() {
            return serverName;
        }

        public void setServerName(String serverName) {
            this.serverName = serverName;
        }

        public Integer getPort() {
            return port;
        }

        public void setPort(Integer port) {
            this.port = port;
        }
    }
}

The properties are read from a file included in the classpath using standard Java properties syntax

system.monitoring.serverport.locations=goolge.com:80
system.monitoring.serverport.timeout=3000
# etc...


The basic code for the EmailSender is:

public class EmailSender extends JavaMailSenderImpl {


    public void sendGenericEmail(String emailIds, String subject, String body, EmailAttachment... attachments) throws MessagingException {
        MimeMessage message = super.createMimeMessage();

        // use the true flag to indicate you need a multipart message
        MimeMessageHelper helper = new MimeMessageHelper(message, true);
        helper.setTo(emailIds);

        // use the true flag to indicate the text included is HTML
        helper.setSubject(subject);
        helper.setText(body, false);

        for (EmailAttachment attachment : attachments) {
            helper.addAttachment(attachment.getName(), attachment.getBodyAsStream());
        }

        // let's include the infamous windows Sample file (this time copied to c:/)
        /*FileSystemResource res = new FileSystemResource(new File("c:/Sample.jpg"));
        helper.addInline("identifier1234", res);*/
        send(message);
    }
}


   



system.monitoring.serverport.locations=goolge.com:80
system.monitoring.serverport.timeout=3000
# etc...

No comments: