ICE09 . playing with java, scala, groovy and spring .

Playing with Spring

Archive for August, 2010

Spring-batch with GMail-Notification using Velocity

Posted by ice09 on August 15, 2010

The sources to this blog entry are available here. The SpringSource Tool Suite (STS) or an accordingly configured Eclipse (with a mandatory Maven plugin and an optional Spring IDE plugin) has to be installed.

The problem

The Spring Batch project is really useful in creating new batch jobs. The framework is straightforward and gained quite a lot usefulness and user-friendliness with the 2.x branch. However, even with the great reference documentation and the getting started section, it takes its time to create a simple project with success/error-mail notification.
I will create a really simple batch job using the new 2.x features, adding the almost always necessary mail notification after a successful or failed job execution. The mail context should be rendered using Velocity. This post does not explain the Spring (Batch) components used, please refer to the Spring Batch reference documentation for detailed explanations.

The setup

The structure is taken from the Template Project in STS, the howto is available in the getting started page.

project structure

It is important to note the separation of concerns in the two spring contexts launch-context.xml and module-context.xml. The first one contains all beans relevant to the job’s infrastructure, there is no business logic contained. The module config is included using import resource. The module config just contains business logic, the actual batch job logic is configured here.

module-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:batch="http://www.springframework.org/schema/batch"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd">

	<batch:job id="job1" xmlns="http://www.springframework.org/schema/batch">
		<batch:step id="step1" parent="simpleStep">
			<batch:tasklet ref="processorTasklet">
				<batch:listeners>
					<batch:listener ref="errorlistener"/>
				</batch:listeners>
			</batch:tasklet>
		</batch:step>
		<batch:listeners>
			<batch:listener ref="samplemailnotification"/>
		</batch:listeners>
	</batch:job>
	
	<bean id="samplemailnotification" parent="mailnotification">
		<property name="errorMailPath" value="mailtemplate.vm"/>
		<property name="successMailPath" value="mailtemplate.vm"/>
		<property name="subject" value="Mail from batch-sample"/>
	</bean>
	
	<bean id="processorTasklet" class="org.springframework.sample.batch.example.ExampleProcessor"/>

</beans>

The processor used just writes a data item to a shared Map. This Map is initialized and written into the job execution context by the StatusMailJobListener in its JobExecutionListener.beforeJob(...) method. In afterJob(...) this Map is evaluated and the following logic is applied: if an error-element is present, remove all provided data and just set the error-element in the Velocity context. Otherwise, copy all data from the values-element into the Velocity context. The origin of the values-element is explained below.

package org.springframework.sample.batch.example;

//[imports excluded]
public class StatusMailJobListener implements JobExecutionListener {
	
	private String recipient;
	private String sender;
	private String successMailPath;
	private String errorMailPath;
	private String subject;
	
	@Autowired
	private JavaMailSenderImpl mailSender;
	@Autowired
	private VelocityEngine velocityEngine;

	public void afterJob(JobExecution jobExecution) {
		String exitCode = jobExecution.getExitStatus().getExitCode();
		if (exitCode.equals(ExitStatus.COMPLETED.getExitCode())) {
			sendMail(successMailPath, "[OK] " + subject, jobExecution.getExecutionContext());
		} else {
			sendMail(errorMailPath, "[ERROR] " + subject, jobExecution.getExecutionContext());
		}
	}

	private void sendMail(final String messagePath, final String subject, final ExecutionContext executionContext) {
		MimeMessagePreparator preparator = new MimeMessagePreparator() {
			public void prepare(MimeMessage mimeMessage) throws Exception {
				MimeMessageHelper message = new MimeMessageHelper(mimeMessage);
				message.setSubject(subject);
				message.setTo(recipient);
				message.setFrom(sender);
				Map<String, String> values = (Map<String, String>) executionContext.get("values");
				if (executionContext.containsKey("error")) {
					values.clear();
					values.put("error", executionContext.getString("error"));
				}
				String text = 
					VelocityEngineUtils.
						mergeTemplateIntoString(velocityEngine, messagePath, values);
				message.setText(text, true);
			}
		};
		mailSender.send(preparator);
	}

	public void beforeJob(JobExecution jobExecution) {
		Map<String, String> values = new HashMap<String, String>();
		jobExecution.getExecutionContext().put("values", values);
	}

        //[setters excluded]

}

For this setup to work, a contract is defined: every step/tasklet, which wants to add information to the context which should be available in the Velocity context for rendering the mail content, must write to the predefined Map.
This can be done eg. by using ((Map)chunkContext.getStepContext().getJobExecutionContext().get("values")).put("content", "add this text"). Initially, this Map is created by the MailNotificationListener as described above (of course, you should check for existance of this Map first in real code).
The errorhandling is done magically by Spring, if an error occurs, the Listeners can just check the ExitStatus and react accordingly (compare the afterJob(...) method).

package org.springframework.sample.batch.example;

//[imports excluded]

public class ExampleProcessor implements Tasklet {

	public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
		//throw new RuntimeException("bla");
		((Map<String, String>)chunkContext.getStepContext().getJobExecutionContext().get("values")).put("test", "[successfully invoked exampleprocessor]");
		return RepeatStatus.FINISHED;
	}

}

How and when is the error-element set into the job execution context? This is done by the last missing item, the ErrorListener, which is a StepExecutionListener.

package org.springframework.sample.batch.example;

//[imports excluded]

public class ErrorListener implements StepExecutionListener {

	public ExitStatus afterStep(StepExecution stepExecution) {
		String exitCode = stepExecution.getExitStatus().getExitCode();
		if (exitCode.equals(ExitStatus.FAILED.getExitCode())) {
			StringBuilder messages = new StringBuilder();
			for (Throwable error : stepExecution.getFailureExceptions()) {
				messages.append(error.getMessage());
			}
			stepExecution.getJobExecution().getExecutionContext().put("error", messages.toString());
			return stepExecution.getExitStatus();
		}
		return ExitStatus.COMPLETED;
	}

	public void beforeStep(StepExecution stepExecution) {
		// Nothing to do here.
	}

}

Tips & Tricks

There is not much missing, however, for this thing to run with GMail, certain properties have to be provided, therefore I will list the complete launch-config here. However, this is available in the sources to this post as well. Just fill in the correct data (GMail-account, recipients) and it should work as excepted. The velocity template is so short, I attached it as well FWIW.

launch-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:batch="http://www.springframework.org/schema/batch"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
		http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch-2.0.xsd">

	<import resource="classpath:/META-INF/spring/module-context.xml" />

	<bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
    	<property name="transactionManager" ref="transactionManager"/>
	</bean>

	<bean id="jobLauncher"
		class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
		<property name="jobRepository" ref="jobRepository" />
	</bean>

	<bean id="transactionManager"
		class="org.springframework.batch.support.transaction.ResourcelessTransactionManager">
	</bean>

	<bean id="mailnotification" class="org.springframework.sample.batch.example.StatusMailJobListener" abstract="true">
		<property name="recipient" value="MAILADDRESS_OF_RECIPIENTS"/>
		<property name="sender" value="MAILADDRESS_OF_SENDER"/>
	</bean>
	
	<bean id="errorlistener" class="org.springframework.sample.batch.example.ErrorListener"/>

	<util:properties id="props">
		<prop key="mail.smtp.starttls.enable">true</prop>
	</util:properties>

   <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
      <property name="host" value="smtp.googlemail.com"/>
      <property name="username" value="YOUR_GMAIL_ACCOUNT"/>
      <property name="password" value="YOUR_GMAIL_PASSWORD"/>
      <property name="javaMailProperties" ref="props"/>
   </bean>

	<bean id="simpleStep"
		class="org.springframework.batch.core.step.item.SimpleStepFactoryBean"
		abstract="true">
		<property name="transactionManager" ref="transactionManager" />
		<property name="jobRepository" ref="jobRepository" />
		<property name="startLimit" value="100" />
		<property name="commitInterval" value="1" />
	</bean>
	
	<bean id="velocityEngine" class="org.springframework.ui.velocity.VelocityEngineFactoryBean">
      <property name="velocityProperties">
         <value>
          resource.loader=class
          class.resource.loader.class=org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
         </value>
      </property>
   </bean>

</beans>

mailtemplate.vm

#if ($error)
Sorry, there was an error, please check the logs.<br/>
exceptionmessages: $error 
#else
Cool, it worked, there is a message for you: $test
#end

Posted in Mail, Velocity, Spring Batch | 1 Comment »

 
Follow

Get every new post delivered to your Inbox.