RESOLVED - issue BATCH-88: StatisticsProvider is a leaky abstraction

http://jira.springframework.org/browse/BATCH-88
This commit is contained in:
dsyer
2008-01-24 16:22:56 +00:00
parent d8f2ccb354
commit 00ca5ce850
29 changed files with 522 additions and 866 deletions

View File

@@ -1,263 +0,0 @@
/*
* Copyright 2006-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.bootstrap.support;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.domain.Job;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobLocator;
import org.springframework.batch.core.domain.JobParameters;
import org.springframework.batch.core.executor.ExitCodeExceptionClassifier;
import org.springframework.batch.core.runtime.JobParametersFactory;
import org.springframework.batch.execution.launch.JobLauncher;
import org.springframework.batch.execution.step.simple.SimpleExitCodeExceptionClassifier;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.util.StringUtils;
/**
* <p>
* Basic launcher for starting jobs from the command line. In general, it is
* assumed that this launcher will primarily be used to start a job via a script
* from an Enterprise Scheduler. Therefore, exit codes are mapped to integers so
* that schedulers can use the returned values to determine the next course of
* action. The returned values can also be useful to operations teams in
* determining what should happen upon failure. For example, a returned code of
* 5 might mean that some resource wasn't available and the job should be
* restarted. However, a code of 10 might mean that something critical has
* happened and the issue should be escalated.
* </p>
*
* <p>
* With any launch of a batch job within Spring Batch, a Spring context
* containing the Job and the 'Execution Environment' has to be created. This
* command line launcher can be used to load that context from a single
* location. It can also load the job as well All dependencies of the launcher
* will then be satisfied by autowiring by type from the combined application
* context. Default values are provided for all fields except the
* {@link JobLauncher} and {@link JobLocator}. Therefore, if autowiring fails
* to set it (it should be noted that dependency checking is disabled because
* most of the fields have default values and thus don't require dependencies to
* be fulfilled via autowiring) then an exception will be thrown. It should also
* be noted that even if an exception is thrown by this class, it will be mapped
* to an integer and returned.
* </p>
*
* <p>
* Notice a property is available to set the {@link SystemExiter}. This class
* is used to exit from the main method, rather than calling System.exit()
* directly. This is because unit testing a class the calls System.exit() is
* impossible without kicking off the test within a new Jvm, which it is
* possible to do, however it is a complex solution, much more so than
* strategizing the exiter.
* </p>
*
* <p>
* The arguments to this class are roughly as follows:
* </p>
*
* <code>
* java jobPath jobName jobLauncherPath jobParameters...
* </code>
*
* <p>
* <ul>
* <li>jobPath: the xml application context containing a {@link Job}
* <li>jobName: the bean id of the job.
* <li>jobLauncherPath: the xml application context containing a
* {@link JobLauncher}
* <li>jobParameters: 0 to many parameters that will be used to launch a job.
* </ul>
* </p>
*
* <p>
* The combined application context must only contain one instance of a
* {@link JobLauncher}. The job parameters passed in to the command line will
* be converted to {@link Properties} by assuming that each individual element
* is one parameter that is separated by an equals sign. For example,
* "vendor.id=290232". Below is an example arguments list: "
*
* <p>
* <code>
* java org.springframework.batch.execution.bootstrap.support.CommandLineJobRunner testJob.xml
* testJob standard-job-launcher.xml schedule.date=2008/01/24 vendor.id=3902483920
* <code></p>
*
* <p>Once arguments have been successfully parsed, autowiring will be used to set
* various dependencies. The {@JobLauncher} for example, will be loaded this way. If
* none is contained in the bean factory (it searches by type) then a
* {@link BeanDefinitionStoreException} will be thrown. The same exception will also
* be thrown if there is more than one present. Assuming the JobLauncher has been
* set correctly, the jobName argument will be used to obtain an actual {@link Job}.
* If a {@link JobLocator} has been set, then it will be used, if not the beanFactory
* will be asked, using the jobName as the bean id.</p>
*
* @author Dave Syer
* @author Lucas Ward
* @since 1.0
*/
public class CommandLineJobRunner {
protected static final Log logger = LogFactory
.getLog(CommandLineJobRunner.class);
private ExitCodeMapper exitCodeMapper = new SimpleJvmExitCodeMapper();
private ExitCodeExceptionClassifier exceptionClassifier = new SimpleExitCodeExceptionClassifier();
private JobLauncher launcher;
private JobLocator jobLocator;
private SystemExiter systemExiter = new JvmSystemExiter();
private JobParametersFactory jobParametersFactory = new DefaultJobParametersFactory();
/**
* Injection setter for the {@link JobLauncher}.
*
* @param launcher
* the launcher to set
*/
public void setLauncher(JobLauncher launcher) {
this.launcher = launcher;
}
/**
* Injection setter for the {@link ExitCodeExceptionClassifier}
*
* @param exceptionClassifier
*/
public void setExceptionClassifier(
ExitCodeExceptionClassifier exceptionClassifier) {
this.exceptionClassifier = exceptionClassifier;
}
/**
* Injection setter for the {@link JvmExitCodeMapper}.
*
* @param exitCodeMapper
* the exitCodeMapper to set
*/
public void setExitCodeMapper(ExitCodeMapper exitCodeMapper) {
this.exitCodeMapper = exitCodeMapper;
}
/**
* Injection setter for the {@link SystemExiter}.
*
* @param systemExitor
*/
public void setSystemExiter(SystemExiter systemExitor) {
this.systemExiter = systemExitor;
}
/**
* Delegate to the exiter to (possibly) exit the VM gracefully.
*
* @param status
*/
public void exit(int status) {
systemExiter.exit(status);
}
public void setJobLocator(JobLocator jobLocator) {
this.jobLocator = jobLocator;
}
/*
* Start a job by obtaining a combined classpath using the job launcher and
* job paths. If a JobLocator has been set, then use it to obtain an actual
* job, if not ask the context for it.
*/
int start(String jobPath, String jobLauncherPath, String jobName,
String[] parameters) {
try {
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[] { jobPath, jobLauncherPath });
context.getAutowireCapableBeanFactory().autowireBeanProperties(
this, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false);
Job job;
if (jobLocator != null) {
job = jobLocator.getJob(jobName);
} else {
job = (Job) context.getBean(jobName);
}
JobParameters jobParameters = jobParametersFactory
.getJobParameters(StringUtils
.splitArrayElementsIntoProperties(parameters, "="));
JobExecution jobExecution = launcher.run(job, jobParameters);
return exitCodeMapper.getExitCode(jobExecution.getExitStatus()
.getExitCode());
} catch (Throwable e) {
logger.error("Job Terminated in error:", e);
return exitCodeMapper.getExitCode(exceptionClassifier
.classifyForExitCode(e).getExitCode());
}
}
/**
* Launch a batch job using a {@link CommandLineJobRunner}. Creates a
* new Spring context for the job execution, and uses a common parent for
* all such contexts. No exception are thrown from this method, rather
* exceptions are logged and an integer returned through the exit status in
* a {@link JvmSystemExiter} (which can be overridden by defining one in the
* Spring context).
*
* @param args
* <p>
* <ul>
* <li>jobPath: the xml application context containing a
* {@link Job}
* <li>jobName: the bean id of the job.
* <li>jobLauncherPath: the xml application context containing a
* {@link JobLauncher}
* <li>jobParameters: 0 to many parameters that will be used to
* launch a job.
* </ul>
* </p>
*/
public static void main(String[] args) {
CommandLineJobRunner command = new CommandLineJobRunner();
if (args.length < 3) {
logger
.error("At least 3 arguments are required: JobPath, JobName, and ExecutionPath.");
command.exit(1);
}
String jobPath = args[0];
String jobName = args[1];
String jobLauncherPath = args[2];
String[] parameters = new String[args.length - 3];
System.arraycopy(args, 2, parameters, 0, args.length - 3);
int result = command.start(jobPath, jobLauncherPath, jobName,
parameters);
command.exit(result);
}
}

View File

@@ -29,7 +29,6 @@ import java.util.Map.Entry;
import org.springframework.batch.core.domain.JobParameters;
import org.springframework.batch.core.domain.JobParametersBuilder;
import org.springframework.batch.core.runtime.JobParametersFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**

View File

@@ -27,7 +27,6 @@ import java.util.Map.Entry;
import org.springframework.batch.core.domain.JobParameters;
import org.springframework.batch.core.domain.JobParametersBuilder;
import org.springframework.batch.core.runtime.JobParametersFactory;
import org.springframework.util.Assert;
/**
* @author Lucas Ward

View File

@@ -21,10 +21,13 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.repeat.context.SynchronizedAttributeAccessor;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.batch.statistics.StatisticsService;
/**
* Simple implementation of {@link StepContext}.
@@ -33,25 +36,55 @@ import org.springframework.batch.repeat.context.SynchronizedAttributeAccessor;
*
*/
public class SimpleStepContext extends SynchronizedAttributeAccessor implements
StepContext {
StepContext, StatisticsProvider {
private Map callbacks = new HashMap();
private StepContext parent;
private StepExecution stepExecution;
private StatisticsService statisticsService;
/**
* Default constructor.
*/
public SimpleStepContext() {
this(null);
public SimpleStepContext(StepExecution stepExecution) {
this(stepExecution, null, null);
}
/**
* Default constructor.
*/
public SimpleStepContext(StepExecution stepExecution, StepContext parent) {
this(stepExecution, parent, null);
}
/**
* @param object
*/
public SimpleStepContext(StepContext parent) {
public SimpleStepContext(StepExecution stepExecution, StepContext parent, StatisticsService statisticsService) {
super();
this.parent = parent;
this.statisticsService = statisticsService;
this.stepExecution = stepExecution;
}
/* (non-Javadoc)
* @see org.springframework.batch.repeat.context.SynchronizedAttributeAccessor#setAttribute(java.lang.String, java.lang.Object)
*/
public void setAttribute(String name, Object value) {
super.setAttribute(name, value);
if (statisticsService!=null && (value instanceof StatisticsProvider)) {
statisticsService.register(this, (StatisticsProvider) value);
}
}
/* (non-Javadoc)
* @see org.springframework.batch.statistics.StatisticsProvider#getStatistics()
*/
public Properties getStatistics() {
if (statisticsService==null) {
return new Properties();
}
return statisticsService.getStatistics(this);
}
/*
@@ -86,10 +119,10 @@ public class SimpleStepContext extends SynchronizedAttributeAccessor implements
}
}
/*
* Package access because only needed internally.
/* (non-Javadoc)
* @see org.springframework.batch.execution.scope.StepContext#close()
*/
void close() {
public void close() {
List errors = new ArrayList();
@@ -133,13 +166,6 @@ public class SimpleStepContext extends SynchronizedAttributeAccessor implements
throw (RuntimeException) errors.get(0);
}
/**
* @param stepExecution
*/
public void setStepExecution(StepExecution stepExecution) {
this.stepExecution = stepExecution;
}
/*
* (non-Javadoc)
*

View File

@@ -16,6 +16,7 @@
package org.springframework.batch.execution.scope;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.core.AttributeAccessor;
/**
@@ -24,7 +25,7 @@ import org.springframework.core.AttributeAccessor;
* @author Dave Syer
*
*/
public interface StepContext extends AttributeAccessor {
public interface StepContext extends AttributeAccessor, StatisticsProvider {
/**
* Accessor for the {@link StepExecution} associated with the currently
@@ -45,4 +46,9 @@ public interface StepContext extends AttributeAccessor {
* Register a destruction callback for the end of life of the scope.
*/
void registerDestructionCallback(String name, Runnable callback);
/**
* Clean up any resources held during the context of the step.
*/
void close();
}

View File

@@ -36,10 +36,17 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
private Object mutex = new Object();
/**
* @param order the order value to set priority of callback execution for
* the {@link BeanFactoryPostProcessor} part of this scope bean.
*/
public void setOrder(int order) {
this.order = order;
}
/* (non-Javadoc)
* @see org.springframework.core.Ordered#getOrder()
*/
public int getOrder() {
return order;
}
@@ -48,16 +55,17 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
* Context key for clients to use for conversation identifier.
*/
public static final String ID_KEY = "JOB_IDENTIFIER";
private String name = "step";
/*
* (non-Javadoc)
*
* @see org.springframework.beans.factory.config.Scope#get(java.lang.String,
* org.springframework.beans.factory.ObjectFactory)
* org.springframework.beans.factory.ObjectFactory)
*/
public Object get(String name, ObjectFactory objectFactory) {
SimpleStepContext context = getContext();
StepContext context = getContext();
Object scopedObject = context.getAttribute(name);
if (scopedObject == null) {
synchronized (mutex) {
@@ -80,7 +88,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
* @see org.springframework.beans.factory.config.Scope#getConversationId()
*/
public String getConversationId() {
SimpleStepContext context = getContext();
StepContext context = getContext();
Object id = context.getAttribute(ID_KEY);
return "" + id;
}
@@ -89,7 +97,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
* (non-Javadoc)
*
* @see org.springframework.beans.factory.config.Scope#registerDestructionCallback(java.lang.String,
* java.lang.Runnable)
* java.lang.Runnable)
*/
public void registerDestructionCallback(String name, Runnable callback) {
StepContext context = getContext();
@@ -102,7 +110,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
* @see org.springframework.beans.factory.config.Scope#remove(java.lang.String)
*/
public Object remove(String name) {
SimpleStepContext context = getContext();
StepContext context = getContext();
return context.removeAttribute(name);
}
@@ -111,13 +119,12 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
* can be used to store scoped bean instances.
*
* @return the current step context which we can use as a scope storage
* medium
* medium
*/
private SimpleStepContext getContext() {
SimpleStepContext context = StepSynchronizationManager.getContext();
private StepContext getContext() {
StepContext context = StepSynchronizationManager.getContext();
if (context == null) {
throw new IllegalStateException(
"No context holder available for step scope");
throw new IllegalStateException("No context holder available for step scope");
}
return context;
}
@@ -125,13 +132,10 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
/**
* Register this scope with the enclosing BeanFactory.
*
* @param beanFactory
* the BeanFactory to register with
* @throws BeansException
* if there is a problem.
* @param beanFactory the BeanFactory to register with
* @throws BeansException if there is a problem.
*/
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory) throws BeansException {
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope(name, this);
}
@@ -139,8 +143,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
* Public setter for the name property. This can then be used as a bean
* definition attribute, e.g. scope="step". Defaults to "step".
*
* @param name
* the name to set for this scope.
* @param name the name to set for this scope.
*/
public void setName(String name) {
this.name = name;

View File

@@ -1,80 +0,0 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.scope;
/**
* @author Dave Syer
*
*/
public class StepSynchronizationManager {
private static final ThreadLocal contextHolder = new InheritableThreadLocal();
/**
* Getter for the current context..
*
* @return the current {@link SimpleStepContext} or null if there is none
* (if we are not in a step).
*/
public static SimpleStepContext getContext() {
return (SimpleStepContext) contextHolder.get();
}
/**
* Method for registering a context - should only be used by
* {@link StepExecutor} implementations to ensure that {@link #getContext()}
* always returns the correct value.
*
* @return a new context at the start of a batch.
*/
public static SimpleStepContext open() {
StepContext oldSession = getContext();
SimpleStepContext context = new SimpleStepContext(oldSession);
StepSynchronizationManager.contextHolder.set(context);
return context;
}
/**
* Method for de-registering the current context - should only be used by
* {@link StepExecutor} implementations to ensure that {@link #getContext()}
* always returns the correct value.
*
* @return the old value if there was one.
*/
public static StepContext close() {
SimpleStepContext oldSession = getContext();
if (oldSession == null) {
return null;
}
oldSession.close();
StepContext context = oldSession.getParent();
StepSynchronizationManager.contextHolder.set(context);
return context;
}
/**
* Used internally by {@link StepExecutor} implementations to clear the
* current context at the end of a batch.
*
* @return the old value if there was one.
*/
public static StepContext clear() {
StepContext context = getContext();
StepSynchronizationManager.contextHolder.set(null);
return context;
}
}

View File

@@ -30,6 +30,7 @@ import org.springframework.batch.core.executor.StepInterruptedException;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.tasklet.Tasklet;
import org.springframework.batch.execution.scope.SimpleStepContext;
import org.springframework.batch.execution.scope.StepContext;
import org.springframework.batch.execution.scope.StepScope;
import org.springframework.batch.execution.scope.StepSynchronizationManager;
import org.springframework.batch.execution.step.RepeatOperationsHolder;
@@ -47,7 +48,9 @@ import org.springframework.batch.repeat.support.RepeatTemplate;
import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.SimpleStatisticsService;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.batch.statistics.StatisticsService;
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
@@ -91,6 +94,26 @@ public class SimpleStepExecutor implements StepExecutor {
// Not for production use...
protected PlatformTransactionManager transactionManager = new ResourcelessTransactionManager();
private StatisticsService statisticsService = new SimpleStatisticsService();
/**
* Public setter for the {@link StatisticsService}. This will be used to
* create the {@link StepContext}, and hence any component that is a
* {@link StatisticsProvider} and in step scope will be registered with the
* service. The {@link StepContext} is then a source of aggregate statistics
* for the step.
*
* @param statisticsService the {@link StatisticsService} to set. Default is
* a {@link SimpleStatisticsService}.
*/
public void setStatisticsService(StatisticsService statisticsService) {
this.statisticsService = statisticsService;
}
/**
* Injected strategy for transaction management
* @param transactionManager
*/
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
@@ -142,8 +165,8 @@ public class SimpleStepExecutor implements StepExecutor {
* execution
* @see StepExecutor#process(Step, StepExecution)
*/
public ExitStatus process(final Step step, final StepExecution stepExecution)
throws BatchCriticalException, StepInterruptedException {
public ExitStatus process(final Step step, final StepExecution stepExecution) throws BatchCriticalException,
StepInterruptedException {
final StepInstance stepInstance = stepExecution.getStep();
boolean isRestart = stepInstance.getStepExecutionCount() > 0 ? true : false;
@@ -153,8 +176,10 @@ public class SimpleStepExecutor implements StepExecutor {
ExitStatus status = ExitStatus.FAILED;
final SimpleStepContext stepScopeContext = StepSynchronizationManager.open();
stepScopeContext.setStepExecution(stepExecution);
StepContext parentStepScopeContext = StepSynchronizationManager.getContext();
final StepContext stepScopeContext = new SimpleStepContext(stepExecution, parentStepScopeContext,
statisticsService);
StepSynchronizationManager.register(stepScopeContext);
// Add the job identifier so that it can be used to identify
// the conversation in StepScope
stepScopeContext.setAttribute(StepScope.ID_KEY, stepExecution.getJobExecution().getId());
@@ -198,8 +223,8 @@ public class SimpleStepExecutor implements StepExecutor {
// TODO: Statistics are not thread safe
// - we cannot guarantee that they are
// up to date. (Maybe we never can?)
Properties statistics = getStatistics(module);
// up to date. (Maybe we never can?)
Properties statistics = stepScopeContext.getStatistics();
contribution.setStatistics(statistics);
contribution.incrementCommitCount();
// Apply the contribution to the step
@@ -301,8 +326,8 @@ public class SimpleStepExecutor implements StepExecutor {
* outside this method, so subclasses that override do not need to create a
* transaction.
*
* @param step the current step containing the {@link Tasklet}
* with the business logic.
* @param step the current step containing the {@link Tasklet} with the
* business logic.
* @return true if there is more data to process.
*/
protected final ExitStatus processChunk(final Step step, final StepContribution contribution) {
@@ -378,15 +403,6 @@ public class SimpleStepExecutor implements StepExecutor {
}
}
private Properties getStatistics(Tasklet tasklet) {
if (tasklet instanceof StatisticsProvider) {
return ((StatisticsProvider) tasklet).getStatistics();
}
else {
return null;
}
}
/**
* Setter for the {@link StepInterruptionPolicy}. The policy is used to
* check whether an external request has been made to interrupt the job
@@ -414,9 +430,9 @@ public class SimpleStepExecutor implements StepExecutor {
* <ul>
* <li> If the configuration is a {@link RepeatOperationsHolder} then we use
* the provided {@link RepeatOperations} instances for chunk and step. </li>
* <li> If the configuration is a {@link SimpleStep} then we
* apply the commit interval at the chunk level and the exception handler at
* the step level, provided the existing repeat operations are instances of
* <li> If the configuration is a {@link SimpleStep} then we apply the
* commit interval at the chunk level and the exception handler at the step
* level, provided the existing repeat operations are instances of
* {@link RepeatTemplate}. In addition if there is a non-zero skip limit
* and no {@link ExceptionHandler} then we inject a
* {@link SimpleLimitExceptionHandler} with that limit.</li>
@@ -431,9 +447,7 @@ public class SimpleStepExecutor implements StepExecutor {
RepeatOperationsHolder holder = (RepeatOperationsHolder) step;
RepeatOperations chunkOperations = holder.getChunkOperations();
RepeatOperations stepOperations = holder.getStepOperations();
Assert
.state(chunkOperations != null,
"Chunk operations obtained from step must be non-null.");
Assert.state(chunkOperations != null, "Chunk operations obtained from step must be non-null.");
if (chunkOperations != null) {
setChunkOperations(chunkOperations);

View File

@@ -16,12 +16,6 @@
package org.springframework.batch.execution.tasklet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.springframework.batch.core.tasklet.Tasklet;
import org.springframework.batch.io.Skippable;
import org.springframework.batch.item.ItemProcessor;
@@ -32,7 +26,6 @@ import org.springframework.batch.retry.RetryPolicy;
import org.springframework.batch.retry.callback.ItemReaderRetryCallback;
import org.springframework.batch.retry.policy.ItemReaderRetryPolicy;
import org.springframework.batch.retry.support.RetryTemplate;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
@@ -40,13 +33,13 @@ import org.springframework.util.Assert;
* A concrete implementation of the {@link Tasklet} interface that provides
* 'split processing'. This type of processing is characterized by separating
* the reading and processing of batch data into two separate classes:
* {@link ItemReader} and {@link ItemProcessor}. The {@link ItemReader} class provides a solid means
* for re-usability and enforces good architecture practices. Because an object
* <em>must</em> be returned by the {@link ItemReader} to continue
* processing, (returning null indicates processing should end) a developer is
* forced to read in all relevant data, place it into a domain object, and
* return that object. The {@link ItemProcessor} will then use this object for
* calculations and output.<br/>
* {@link ItemReader} and {@link ItemProcessor}. The {@link ItemReader} class
* provides a solid means for re-usability and enforces good architecture
* practices. Because an object <em>must</em> be returned by the
* {@link ItemReader} to continue processing, (returning null indicates
* processing should end) a developer is forced to read in all relevant data,
* place it into a domain object, and return that object. The
* {@link ItemProcessor} will then use this object for calculations and output.<br/>
*
* If a {@link RetryPolicy} is provided it will be used to construct a stateful
* retry around the {@link ItemProcessor}, delegating identity concerns to the
@@ -75,8 +68,7 @@ import org.springframework.util.Assert;
* @author Robert Kasanicky
*
*/
public class ItemOrientedTasklet implements Tasklet, Skippable,
StatisticsProvider, InitializingBean {
public class ItemOrientedTasklet implements Tasklet, Skippable, InitializingBean {
/**
* Prefix added to statistics keys from processor if needed to avoid
@@ -115,13 +107,11 @@ public class ItemOrientedTasklet implements Tasklet, Skippable,
itemRecoverer = (ItemRecoverer) itemProvider;
}
ItemReaderRetryPolicy itemProviderRetryPolicy = new ItemReaderRetryPolicy(
retryPolicy);
ItemReaderRetryPolicy itemProviderRetryPolicy = new ItemReaderRetryPolicy(retryPolicy);
template.setRetryPolicy(itemProviderRetryPolicy);
if (retryPolicy != null) {
retryCallback = new ItemReaderRetryCallback(itemProvider,
itemProcessor);
retryCallback = new ItemReaderRetryCallback(itemProvider, itemProcessor);
retryCallback.setRecoverer(itemRecoverer);
}
@@ -150,7 +140,8 @@ public class ItemOrientedTasklet implements Tasklet, Skippable,
}
try {
itemProcessor.process(item);
} catch (Exception e) {
}
catch (Exception e) {
if (itemRecoverer != null) {
itemRecoverer.recover(item, e);
}
@@ -211,77 +202,10 @@ public class ItemOrientedTasklet implements Tasklet, Skippable,
}
}
/**
* If the provider and / or processor are {@link StatisticsProvider} then
* delegate to them in that order. If they both implement
* {@link StatisticsProvider} then the property keys are prepended with
* special prefixes to avoid potential ambiguity. The prefixes are only
* prepended in the case of a duplicate key shared between provider and
* processor.
*
* @see org.springframework.batch.io.Skippable#skip()
*/
public Properties getStatistics() {
Properties stats = new Properties();
if (this.itemProvider instanceof StatisticsProvider) {
stats = ((StatisticsProvider) this.itemProvider).getStatistics();
}
if (this.itemProcessor instanceof StatisticsProvider) {
Properties props = ((StatisticsProvider) this.itemProcessor)
.getStatistics();
if (!stats.isEmpty()) {
stats = prependKeys(stats, props, PROVIDER_STATISTICS_PREFIX,
PROCESSOR_STATISTICS_PREFIX);
} else {
stats.putAll(props);
}
}
return stats;
}
/**
* @param props1
* @param string
* @return
*/
private Properties prependKeys(Properties props1, Properties props2,
String prefix1, String prefix2) {
Properties result = new Properties();
Set duplicates = new HashSet();
for (Iterator iterator = props1.entrySet().iterator(); iterator
.hasNext();) {
Map.Entry entry = (Map.Entry) iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
if (props2.containsKey(key)) {
duplicates.add(key);
continue;
}
result.setProperty(key, value);
}
for (Iterator iterator = props2.entrySet().iterator(); iterator
.hasNext();) {
Map.Entry entry = (Map.Entry) iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
if (duplicates.contains(key)) {
continue;
}
result.setProperty(key, value);
}
for (Iterator iterator = duplicates.iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
result.setProperty(prefix1 + key, props1.getProperty(key));
result.setProperty(prefix2 + key, props2.getProperty(key));
}
return result;
}
/**
* Public setter for the retryPolicy.
*
* @param retyPolicy
* the retryPolicy to set
* @param retyPolicy the retryPolicy to set
*/
public void setRetryPolicy(RetryPolicy retryPolicy) {
this.retryPolicy = retryPolicy;

View File

@@ -1,154 +0,0 @@
/*
* Copyright 2006-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.bootstrap.support;
import junit.framework.TestCase;
import org.springframework.batch.core.domain.Job;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobParameters;
import org.springframework.batch.core.executor.ExitCodeExceptionClassifier;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.execution.launch.JobLauncher;
import org.springframework.batch.repeat.ExitStatus;
/**
* @author Lucas Ward
*
*/
public class CommandLineJobRunnerTests extends TestCase {
private static final String JOB = "org/springframework/batch/execution/bootstrap/support/job.xml";
private static final String TEST_BATCH_ENVIRONMENT = "org/springframework/batch/execution/bootstrap/support/test-environment.xml";
private static final String JOB_NAME = "test-job";
private String jobPath = JOB;
private String environmentPath = TEST_BATCH_ENVIRONMENT;
private String jobName = JOB_NAME;
private String jobKey = "job.Key=myKey";
private String scheduleDate = "schedule.Date=01/23/2008";
private String vendorId = "vendor.id=33243243";
private String[] args = new String[]{jobPath, jobName, environmentPath, jobKey, scheduleDate, vendorId};
private JobExecution jobExecution;
/* (non-Javadoc)
* @see junit.framework.TestCase#setUp()
*/
protected void setUp() throws Exception {
super.setUp();
jobExecution = new JobExecution(null, new Long(1));
ExitStatus exitStatus = ExitStatus.FINISHED;
jobExecution.setExitStatus(exitStatus);
}
public void testMain(){
StubJobLauncher.jobExecution = jobExecution;
CommandLineJobRunner.main(args);
assertEquals(0, StubSystemExiter.getStatus());
}
public void testJobAlreadyRunning(){
StubJobLauncher.throwExecutionRunningException = true;
CommandLineJobRunner.main(args);
assertTrue(StubExceptionClassifier.exception instanceof JobExecutionAlreadyRunningException);
}
//can't test because it will cause the system to exit.
// public void testInvalidArgs(){
//
// String[] args = new String[]{jobPath, jobName};
// CommandLineJobRunner.main(args);
// }
public void testWithNoParameters(){
String[] args = new String[]{jobPath, jobName, environmentPath};
CommandLineJobRunner.main(args);
assertEquals(new JobParameters(), StubJobLauncher.jobParameters);
}
protected void tearDown() throws Exception {
super.tearDown();
StubJobLauncher.tearDown();
}
public static class StubSystemExiter implements SystemExiter {
public static int status;
public void exit(int status) {
StubSystemExiter.status = status;
}
public static int getStatus() {
return status;
}
}
public static class StubJobLauncher implements JobLauncher{
public static JobExecution jobExecution;
public static boolean throwExecutionRunningException = false;
public static JobParameters jobParameters;
public JobExecution run(Job job, JobParameters jobParameters)
throws JobExecutionAlreadyRunningException {
StubJobLauncher.jobParameters = jobParameters;
if(throwExecutionRunningException){
throw new JobExecutionAlreadyRunningException("");
}
return jobExecution;
}
public static void tearDown(){
jobExecution = null;
throwExecutionRunningException = false;
jobParameters = null;
}
}
public static class StubExceptionClassifier implements ExitCodeExceptionClassifier{
public static Throwable exception;
public Object classify(Throwable throwable) {
return null;
}
public Object getDefault() {
return null;
}
public ExitStatus classifyForExitCode(Throwable throwable) {
exception = throwable;
return ExitStatus.FAILED;
}
}
}

View File

@@ -63,13 +63,12 @@ public class BatchResourceFactoryBeanTests extends TestCase {
resourceFactory.setRootDirectory(rootDir);
SimpleStepContext context = new SimpleStepContext();
jobInstance = new JobInstance(new Long(0), new JobParameters());
jobInstance.setJob(new Job("testJob"));
JobExecution jobExecution = new JobExecution(jobInstance);
StepInstance step = new StepInstance(jobInstance, "bar");
StepExecution stepExecution = new StepExecution(step, jobExecution, null);
context.setStepExecution(stepExecution);
SimpleStepContext context = new SimpleStepContext(stepExecution);
resourceFactory.setStepContext(context);
resourceFactory.afterPropertiesSet();

View File

@@ -16,47 +16,57 @@
package org.springframework.batch.execution.scope;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import junit.framework.TestCase;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.batch.statistics.StatisticsService;
import org.springframework.batch.support.PropertiesConverter;
/**
* @author Dave Syer
*
*
*/
public class SimpleStepContextTests extends TestCase {
private SimpleStepContext context = new SimpleStepContext(new SimpleStepContext());
private SimpleStepContext context = new SimpleStepContext(null, new SimpleStepContext(null));
/**
* Test method for {@link org.springframework.batch.execution.scope.SimpleStepContext#StepScopeContext()}.
* Test method for
* {@link org.springframework.batch.execution.scope.SimpleStepContext#StepScopeContext()}.
*/
public void testStepScopeContext() {
assertNull(new SimpleStepContext().getParent());
assertNull(new SimpleStepContext(null).getParent());
}
/**
* Test method for {@link org.springframework.batch.execution.scope.SimpleStepContext#getParent()}.
* Test method for
* {@link org.springframework.batch.execution.scope.SimpleStepContext#getParent()}.
*/
public void testGetParent() {
assertNotNull(context.getParent());
}
/**
* Test method for {@link org.springframework.batch.execution.scope.SimpleStepContext#getStepExecution()}.
* Test method for
* {@link org.springframework.batch.execution.scope.SimpleStepContext#getStepExecution()}.
*/
public void testGetJobIdentifier() {
public void testGetStepExecution() {
assertNull(context.getStepExecution());
context.setStepExecution(new StepExecution(null, null, null));
context = new SimpleStepContext(new StepExecution(null, null, null));
assertNotNull(context.getStepExecution());
}
private List list = new ArrayList();
/**
* Test method for {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}.
* Test method for
* {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}.
*/
public void testDestructionCallbackSunnyDay() throws Exception {
SimpleStepContext context = new SimpleStepContext(null);
@@ -72,7 +82,8 @@ public class SimpleStepContextTests extends TestCase {
}
/**
* Test method for {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}.
* Test method for
* {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}.
*/
public void testDestructionCallbackMissingAttribute() throws Exception {
SimpleStepContext context = new SimpleStepContext(null);
@@ -82,12 +93,14 @@ public class SimpleStepContextTests extends TestCase {
}
});
context.close();
// Yes the callback should be called even if the attribute is missing - for inner beans
// Yes the callback should be called even if the attribute is missing -
// for inner beans
assertEquals(1, list.size());
}
/**
* Test method for {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}.
* Test method for
* {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}.
*/
public void testDestructionCallbackWithException() throws Exception {
SimpleStepContext context = new SimpleStepContext(null);
@@ -108,7 +121,8 @@ public class SimpleStepContextTests extends TestCase {
try {
context.close();
fail("Expected RuntimeException");
} catch (RuntimeException e) {
}
catch (RuntimeException e) {
// We don't care which one was thrown...
assertEquals("fail!", e.getMessage());
}
@@ -117,5 +131,56 @@ public class SimpleStepContextTests extends TestCase {
assertTrue(list.contains("bar"));
assertTrue(list.contains("spam"));
}
public void testStatisticsWithNullService() throws Exception {
assertEquals(0, context.getStatistics().size());
}
public void testStatisticsWithNotNullService() throws Exception {
Map map = new HashMap();
context = new SimpleStepContext(null, null, new StubStatisticsService(map));
assertEquals(1, context.getStatistics().size());
assertEquals("bar", context.getStatistics().getProperty("foo"));
}
public void testStatisticsServiceRegistration() throws Exception {
Map map = new HashMap();
context = new SimpleStepContext(null, null, new StubStatisticsService(map));
StubStatisticsProvider provider = new StubStatisticsProvider();
context.setAttribute("foo", provider);
assertEquals(1, map.size());
assertEquals(context, map.keySet().iterator().next());
assertEquals(provider, map.values().iterator().next());
}
/**
* @author Dave Syer
*
*/
private class StubStatisticsService implements StatisticsService {
private final Map map;
private StubStatisticsService(Map map) {
this.map = map;
}
public Properties getStatistics(Object key) {
return PropertiesConverter.stringToProperties("foo=bar");
}
public void register(Object key, StatisticsProvider provider) {
map.put(key, provider);
}
}
/**
* @author Dave Syer
*
*/
private class StubStatisticsProvider implements StatisticsProvider {
public Properties getStatistics() {
return null;
}
}
}

View File

@@ -40,7 +40,7 @@ public class StepContextAwareStepScopeTests extends TestCase {
}
public void testScopedBean() throws Exception {
StepSynchronizationManager.open();
StepSynchronizationManager.register(new SimpleStepContext(null));
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass());
TestBean bean = (TestBean) applicationContext.getBean("bean");
assertNotNull(bean);
@@ -49,7 +49,7 @@ public class StepContextAwareStepScopeTests extends TestCase {
public void testScopedBeanWithDestroyCallback() throws Exception {
assertEquals(0, list.size());
StepSynchronizationManager.open();
StepSynchronizationManager.register(new SimpleStepContext(null));
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass());
TestBean bean = (TestBean) applicationContext.getBean("bean");
assertNotNull(bean);
@@ -58,7 +58,8 @@ public class StepContextAwareStepScopeTests extends TestCase {
}
public void testScopedBeanWithAware() throws Exception {
StepContext context = StepSynchronizationManager.open();
StepContext context = new SimpleStepContext(null);
StepSynchronizationManager.register(context);
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass());
TestBeanAware bean = (TestBeanAware) applicationContext.getBean("aware");
assertNotNull(bean);
@@ -67,7 +68,7 @@ public class StepContextAwareStepScopeTests extends TestCase {
}
public void testScopedBeanWithInner() throws Exception {
StepSynchronizationManager.open();
StepSynchronizationManager.register(new SimpleStepContext(null));
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(
"scope-tests.xml", getClass());
TestBean bean = ((TestBean) applicationContext.getBean("inner")).child;
@@ -78,7 +79,8 @@ public class StepContextAwareStepScopeTests extends TestCase {
}
public void testScopedBeanWithProxy() throws Exception {
StepContext context = StepSynchronizationManager.open();
StepContext context = new SimpleStepContext(null);
StepSynchronizationManager.register(context);
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass());
TestBeanAware bean = (TestBeanAware) applicationContext.getBean("proxy");
assertNotNull(bean);
@@ -89,7 +91,7 @@ public class StepContextAwareStepScopeTests extends TestCase {
}
public void testScopedBeanWithProxyInThread() throws Exception {
StepSynchronizationManager.open();
StepSynchronizationManager.register(new SimpleStepContext(null));
final ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass());
new Thread(new Runnable() {
public void run() {
@@ -109,7 +111,7 @@ public class StepContextAwareStepScopeTests extends TestCase {
}
public void testScopedBeanWithTwoProxiesInThreads() throws Exception {
StepSynchronizationManager.open();
StepSynchronizationManager.register(new SimpleStepContext(null));
final ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass());
new Thread(new Runnable() {
public void run() {

View File

@@ -41,7 +41,8 @@ public class StepScopeTests extends TestCase {
*/
protected void setUp() throws Exception {
super.setUp();
context = StepSynchronizationManager.open();
context = new SimpleStepContext(null);
StepSynchronizationManager.register(context);
}
/* (non-Javadoc)
@@ -104,7 +105,8 @@ public class StepScopeTests extends TestCase {
* {@link org.springframework.batch.execution.scope.StepScope#get(java.lang.String, org.springframework.beans.factory.ObjectFactory)}.
*/
public void testGetWithSomethingAlreadyInParentContext() {
SimpleStepContext context = StepSynchronizationManager.open();
StepContext context = new SimpleStepContext(null);
StepSynchronizationManager.register(context);
context.setAttribute("foo", "bar");
Object value = scope.get("foo", new ObjectFactory() {
public Object getObject() throws BeansException {

View File

@@ -18,7 +18,10 @@ package org.springframework.batch.execution.step.simple;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import junit.framework.TestCase;
@@ -50,8 +53,11 @@ import org.springframework.batch.repeat.policy.SimpleCompletionPolicy;
import org.springframework.batch.repeat.support.RepeatTemplate;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.batch.statistics.StatisticsService;
import org.springframework.batch.support.PropertiesConverter;
public class DefaultStepExecutorTests extends TestCase {
public class SimpleStepExecutorTests extends TestCase {
ArrayList processed = new ArrayList();
@@ -411,6 +417,43 @@ public class DefaultStepExecutorTests extends TestCase {
assertEquals(1, list.size());
}
public void testStatisticsService() throws Exception {
StepInstance step = new StepInstance(new Long(1));
step.setStepExecutionCount(1);
stepConfiguration.setTasklet(new Tasklet() {
public ExitStatus execute() throws Exception {
return ExitStatus.FINISHED;
}
});
stepConfiguration.setSaveRestartData(true);
JobExecution jobExecution = new JobExecution(jobInstance);
StepExecution stepExecution = new StepExecution(step, jobExecution);
assertEquals(null, stepExecution.getStatistics().getProperty("foo"));
final Map map = new HashMap();
stepExecutor.setStatisticsService(new StatisticsService() {
public Properties getStatistics(Object key) {
return PropertiesConverter.stringToProperties("foo=bar");
}
public void register(Object key, StatisticsProvider provider) {
map.put(key, provider);
}
});
try {
stepExecutor.process(stepConfiguration, stepExecution);
}
catch (Throwable t) {
fail();
}
// At least once in that process the statistics service was asked for statistics...
assertEquals("bar", stepExecution.getStatistics().getProperty("foo"));
// ...but nothing was registered because nothing with step scoped.
assertEquals(0, map.size());
}
private class MockRestartableTasklet implements Tasklet, Restartable {
private boolean getRestartDataCalled = false;

View File

@@ -159,41 +159,6 @@ public class ItemOrientedTaskletTests extends TestCase {
assertEquals(2, list.size());
}
public void testStatisticsProvider() throws Exception {
module.setItemReader(new SkippableItemReader());
Properties stats = module.getStatistics();
assertEquals(1, stats.size());
assertEquals("bar", stats.getProperty("foo"));
}
public void testStatisticsProcessor() throws Exception {
module.setItemProcessor(new SkippableItemProcessor());
Properties stats = module.getStatistics();
assertEquals(1, stats.size());
assertEquals("bar", stats.getProperty("foo"));
}
public void testStatisticsProviderProcessor() throws Exception {
module.setItemReader(new SkippableItemReader());
module.setItemProcessor(new SkippableItemProcessor());
Properties stats = module.getStatistics();
assertEquals(2, stats.size());
assertEquals("bar", stats.getProperty("provider.foo"));
assertEquals("bar", stats.getProperty("processor.foo"));
}
public void testStatisticsProviderProcessorMergeDuplicates()
throws Exception {
module.setItemReader(new SkippableItemReader());
module.setItemProcessor(new SkippableItemProcessor(
"foo=bar\nspam=bucket"));
Properties stats = module.getStatistics();
assertEquals(3, stats.size());
assertEquals("bar", stats.getProperty("provider.foo"));
assertEquals("bar", stats.getProperty("processor.foo"));
assertEquals("bucket", stats.getProperty("spam"));
}
public void testRecoverable() throws Exception {
// set up and call execute

View File

@@ -11,14 +11,13 @@ import org.springframework.batch.item.ItemReader;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
/**
* Runs a collection of ItemProcessors in fixed-order sequence.
*
* @author Robert Kasanicky
*/
public class CompositeItemProcessor implements ItemProcessor, Restartable, StatisticsProvider {
public class CompositeItemProcessor implements ItemProcessor, Restartable {
private static final String SEPARATOR = "#";
@@ -72,23 +71,6 @@ public class CompositeItemProcessor implements ItemProcessor, Restartable, Stati
}
/**
* @return Properties containing statistics of all injected ItemProcessors,
* property keys are prefixed with the list index of the ItemProcessor.
*/
public Properties getStatistics() {
return createCompoundProperties(new PropertiesExtractor() {
public Properties extractProperties(Object o) {
if (o instanceof StatisticsProvider) {
return ((StatisticsProvider) o).getStatistics();
}
else {
return null;
}
}
});
}
public void setItemProcessors(List itemProcessors) {
this.itemProcessors = itemProcessors;
}

View File

@@ -8,24 +8,23 @@ import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* Simple wrapper around {@link ItemWriter} providing {@link Restartable} and
* {@link StatisticsProvider} where the {@link ItemWriter} does.
* Simple wrapper around {@link ItemWriter} providing {@link Restartable} where
* the {@link ItemWriter} does. To make sure
*
* @author Dave Syer
* @author Robert Kasanicky
*/
public class ItemWriterItemProcessor implements ItemProcessor, Restartable, Skippable,
StatisticsProvider, InitializingBean {
public class ItemWriterItemProcessor implements ItemProcessor, Restartable, Skippable, InitializingBean {
private ItemWriter writer;
/**
* Calls {@link #doProcess(Object)} and then writes the result to the {@link ItemWriter}.
* Calls {@link #doProcess(Object)} and then writes the result to the
* {@link ItemWriter}.
*
* @see org.springframework.batch.item.ItemProcessor#process(java.lang.Object)
*/
@@ -35,8 +34,8 @@ public class ItemWriterItemProcessor implements ItemProcessor, Restartable, Skip
}
/**
* By default returns the argument. This method is an extension point
* meant to be overridden by subclasses that implement processing logic.
* By default returns the argument. This method is an extension point meant
* to be overridden by subclasses that implement processing logic.
*/
protected Object doProcess(Object item) throws Exception {
return item;
@@ -53,13 +52,13 @@ public class ItemWriterItemProcessor implements ItemProcessor, Restartable, Skip
* @see Restartable#getRestartData()
*/
public RestartData getRestartData() {
Assert.state(writer != null, "Source must not be null.");
if (writer instanceof Restartable) {
return ((Restartable) writer).getRestartData();
}
else{
else {
return new GenericRestartData(new Properties());
}
}
@@ -68,35 +67,21 @@ public class ItemWriterItemProcessor implements ItemProcessor, Restartable, Skip
* @see Restartable#restoreFrom(RestartData)
*/
public void restoreFrom(RestartData data) {
Assert.state(writer != null, "Source must not be null.");
if (writer instanceof Restartable) {
((Restartable) writer).restoreFrom(data);
}
}
/**
* @return delegates to the parent template of it is a
* {@link StatisticsProvider}, otherwise returns an empty
* {@link Properties} instance.
* @see StatisticsProvider#getStatistics()
*/
public Properties getStatistics() {
if (!(writer instanceof StatisticsProvider)) {
return new Properties();
}
return ((StatisticsProvider) writer).getStatistics();
}
public void skip() {
if (writer instanceof Skippable) {
((Skippable)writer).skip();
((Skippable) writer).skip();
}
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(writer);
}

View File

@@ -16,13 +16,10 @@
package org.springframework.batch.item.reader;
import java.util.Properties;
import org.springframework.batch.io.Skippable;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
@@ -33,7 +30,7 @@ import org.springframework.util.Assert;
*
* @author Dave Syer
*/
public class DelegatingItemReader extends AbstractItemReader implements Restartable, StatisticsProvider, Skippable, InitializingBean{
public class DelegatingItemReader extends AbstractItemReader implements Restartable, Skippable, InitializingBean{
private ItemReader inputSource;
@@ -73,19 +70,6 @@ public class DelegatingItemReader extends AbstractItemReader implements Restarta
((Restartable) inputSource).restoreFrom(data);
}
/**
* @return delegates to the parent template of it is a
* {@link StatisticsProvider}, otherwise returns an empty
* {@link Properties} instance.
* @see StatisticsProvider#getStatistics()
*/
public Properties getStatistics() {
if (!(inputSource instanceof StatisticsProvider)) {
return new Properties();
}
return ((StatisticsProvider) inputSource).getStatistics();
}
/**
* Setter for input source.
* @param source

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.statistics;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* Simple {@link StatisticsService} that makes no attempt to aggregate or
* resolve conflicts between key names. All the contributions registered are
* simply polled and added "as is" to the aggregate properties.
*
* @author Dave Syer
*
*/
public class SimpleStatisticsService implements StatisticsService {
private Map registry = new HashMap();
/**
* Simple aggregate statistics provider for the contributions registered
* under the given key.
*
* @see org.springframework.batch.statistics.StatisticsService#getStatistics(java.lang.Object)
*/
public Properties getStatistics(Object key) {
Set set = new LinkedHashSet();
synchronized (registry) {
Collection collection = (Collection) registry.get(key);
if (collection != null) {
set = new LinkedHashSet(collection);
}
}
return aggregate(set);
}
/**
* @param list a list of {@link StatisticsProvider}s
* @return aggregated statistics
*/
private Properties aggregate(Collection list) {
Properties result = new Properties();
for (Iterator iterator = list.iterator(); iterator.hasNext();) {
StatisticsProvider provider = (StatisticsProvider) iterator.next();
Properties properties = provider.getStatistics();
if (properties != null) {
result.putAll(properties);
}
}
return result;
}
/**
* Register a {@link StatisticsProvider} as one of the interesting providers
* under the provided key.
*
* @see org.springframework.batch.statistics.StatisticsService#register(java.lang.Object,
* org.springframework.batch.statistics.StatisticsProvider)
*/
public void register(Object key, StatisticsProvider provider) {
synchronized (registry) {
Set set = (Set) registry.get(key);
if (set == null) {
set = new LinkedHashSet();
registry.put(key, set);
}
set.add(provider);
}
}
}

View File

@@ -20,7 +20,7 @@ import java.util.Properties;
/**
* Provides statistics for a given module run. Any class that implements
* this interface is garunteeing that it will provide Statistics.
* this interface is guaranteeing that it will provide Statistics.
*
* @author Lucas Ward
*

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.statistics;
import java.util.Properties;
/**
* Generalised statistics aggregation strategy. Clients register
* {@link StatisticsProvider} instances under a well-known key, and then when
* they ask for statistics by that key, they receive an aggregate of all the
* values given by registered providers.
*
* @author Dave Syer
*
*/
public interface StatisticsService {
/**
* Register the {@link StatisticsProvider} instance as one of possibly
* several that are associated with the given key.
*
* @param key the key under which to add the provider
* @param provider a {@link StatisticsProvider}
*/
void register(Object key, StatisticsProvider provider);
/**
* Extract and aggregate the statistics from all providers under this key.
*
* @param key the key under which {@link StatisticsProvider} instances might
* have been registered.
* @return {@link Properties} summarising the statistics of all providers
* registered under this key, or empty otherwise.
*/
Properties getStatistics(Object key);
}

View File

@@ -58,24 +58,6 @@ public class CompositeItemProcessorTests extends TestCase {
}
}
/**
* Statistics of injected ItemProcessors should be returned under keys prefixed with their list index.
*/
public void testStatistics() {
final ItemProcessor p1 = new ItemProcessorStub();
final ItemProcessor p2 = new ItemProcessorStub();
List itemProcessors = new ArrayList(){{
add(p1);
add(p2);
}};
itemProcessor.setItemProcessors(itemProcessors);
Properties stats = itemProcessor.getStatistics();
assertEquals(String.valueOf(p1.hashCode()), stats.getProperty("0#" + ItemProcessorStub.STATS_KEY));
assertEquals(String.valueOf(p2.hashCode()), stats.getProperty("1#" + ItemProcessorStub.STATS_KEY));
}
/**
* All Restartable processors should be restarted, not-Restartable processors should be ignored.
*/

View File

@@ -23,7 +23,6 @@ import junit.framework.TestCase;
import org.springframework.batch.io.Skippable;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.processor.ItemWriterItemProcessor;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
@@ -56,14 +55,6 @@ public class ItemWriterItemProcessorTests extends TestCase {
assertEquals("test:foo", list.get(0));
}
/**
* Gets statistics from the input template
*/
public void testGetStatistics() {
Properties props = processor.getStatistics();
assertEquals("b", props.getProperty("a"));
}
/**
* Gets restart data from the input template
*/
@@ -111,15 +102,6 @@ public class ItemWriterItemProcessorTests extends TestCase {
// expected
}
}
/**
* Gets statistics from the input template
*/
public void testGetStatisticsWithoutStatisticsProvider() {
processor.setItemWriter(null);
Properties props = processor.getStatistics();
assertEquals(null, props.getProperty("a"));
}
public void testSkip() {
processor.skip();

View File

@@ -72,14 +72,6 @@ public class DelegatingItemReaderTests extends TestCase {
assertSame("domain object is provided by the input template", this, result);
}
/**
* Gets statistics from the input template
*/
public void testGetStatistics() {
Properties props = itemProvider.getStatistics();
assertEquals("b", props.getProperty("a"));
}
/**
* Gets restart data from the input template
*/

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.statistics;
import java.util.Properties;
import org.springframework.batch.support.PropertiesConverter;
import junit.framework.TestCase;
/**
* @author Dave Syer
*
*/
public class SimpleStatisticsServiceTests extends TestCase {
private SimpleStatisticsService service = new SimpleStatisticsService();
public void testRegistration() throws Exception {
service.register("FOO", new StubStatisticsProvider());
assertEquals("bar", service.getStatistics("FOO").getProperty("foo"));
}
public void testAggregation() throws Exception {
service.register("FOO", new StubStatisticsProvider());
service.register("FOO", new StubStatisticsProvider("spam=bucket"));
assertEquals("bar", service.getStatistics("FOO").getProperty("foo"));
assertEquals("bucket", service.getStatistics("FOO").getProperty("spam"));
}
public void testDoubleAggregationOrder() throws Exception {
StubStatisticsProvider provider = new StubStatisticsProvider();
service.register("FOO", provider);
service.register("FOO", provider);
assertEquals(1, service.getStatistics("FOO").size());
}
/**
* @author Dave Syer
*
*/
private class StubStatisticsProvider implements StatisticsProvider {
String values = "foo=bar";
public StubStatisticsProvider(String values) {
super();
this.values = values;
}
public StubStatisticsProvider() {
super();
}
public Properties getStatistics() {
return PropertiesConverter.stringToProperties(values);
}
}
}

View File

@@ -86,7 +86,7 @@ public class SimpleTradeTasklet implements Tasklet, StatisticsProvider {
public Properties getStatistics() {
Properties statistics = new Properties();
statistics.setProperty("Trade.Count", String.valueOf(tradeCount));
statistics.setProperty("trade.count", String.valueOf(tradeCount));
statistics.putAll(inputSource.getStatistics());
return statistics;
}

View File

@@ -11,8 +11,7 @@ import org.springframework.batch.execution.scope.StepSynchronizationManager;
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
import org.springframework.util.ClassUtils;
public class StagingItemProcessorTests extends
AbstractTransactionalDataSourceSpringContextTests {
public class StagingItemProcessorTests extends AbstractTransactionalDataSourceSpringContextTests {
private StagingItemProcessor processor;
@@ -21,25 +20,21 @@ public class StagingItemProcessorTests extends
}
protected String[] getConfigLocations() {
return new String[] { ClassUtils.addResourcePathToPackagePath(
StagingItemProcessor.class, "staging-test-context.xml") };
return new String[] { ClassUtils.addResourcePathToPackagePath(StagingItemProcessor.class,
"staging-test-context.xml") };
}
protected void prepareTestInstance() throws Exception {
SimpleStepContext stepScopeContext = StepSynchronizationManager
.open();
stepScopeContext.setStepExecution(new StepExecution(new StepInstance(
new Long(11)), new JobExecution(new JobInstance(new Long(12),
new JobParameters(), new Job("job")))));
SimpleStepContext stepScopeContext = new SimpleStepContext(new StepExecution(new StepInstance(new Long(11)),
new JobExecution(new JobInstance(new Long(12), new JobParameters(), new Job("job")))));
StepSynchronizationManager.register(stepScopeContext);
super.prepareTestInstance();
}
public void testProcessInsertsNewItem() throws Exception {
int before = getJdbcTemplate().queryForInt(
"SELECT COUNT(*) from BATCH_STAGING");
int before = getJdbcTemplate().queryForInt("SELECT COUNT(*) from BATCH_STAGING");
processor.process("FOO");
int after = getJdbcTemplate().queryForInt(
"SELECT COUNT(*) from BATCH_STAGING");
int after = getJdbcTemplate().queryForInt("SELECT COUNT(*) from BATCH_STAGING");
assertEquals(before + 1, after);
}

View File

@@ -6,21 +6,22 @@ import org.springframework.batch.core.domain.JobParameters;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.core.domain.StepInstance;
import org.springframework.batch.execution.scope.SimpleStepContext;
import org.springframework.batch.execution.scope.StepContext;
import org.springframework.batch.execution.scope.StepSynchronizationManager;
import org.springframework.batch.repeat.context.RepeatContextSupport;
import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager;
import org.springframework.batch.repeat.synch.RepeatSynchronizationManager;
import org.springframework.batch.sample.item.processor.StagingItemProcessor;
import org.springframework.batch.sample.item.reader.StagingItemReader;
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
import org.springframework.util.ClassUtils;
public class StagingItemReaderTests extends
AbstractTransactionalDataSourceSpringContextTests {
public class StagingItemReaderTests extends AbstractTransactionalDataSourceSpringContextTests {
private StagingItemProcessor processor;
private StagingItemReader provider;
private Long jobId;
private Long jobId = new Long(11);
public void setProcessor(StagingItemProcessor processor) {
this.processor = processor;
@@ -31,16 +32,14 @@ public class StagingItemReaderTests extends
}
protected String[] getConfigLocations() {
return new String[] { ClassUtils.addResourcePathToPackagePath(
StagingItemProcessor.class, "staging-test-context.xml") };
return new String[] { ClassUtils.addResourcePathToPackagePath(StagingItemProcessor.class,
"staging-test-context.xml") };
}
protected void prepareTestInstance() throws Exception {
SimpleStepContext stepScopeContext = StepSynchronizationManager.open();
jobId = new Long(11);
stepScopeContext.setStepExecution(new StepExecution(new StepInstance(
new Long(12)), new JobExecution(new JobInstance(jobId,
new JobParameters()))));
StepContext stepScopeContext = new SimpleStepContext(new StepExecution(new StepInstance(new Long(12)),
new JobExecution(new JobInstance(jobId, new JobParameters()))));
StepSynchronizationManager.register(stepScopeContext);
RepeatSynchronizationManager.register(new RepeatContextSupport(null));
super.prepareTestInstance();
}
@@ -59,36 +58,31 @@ public class StagingItemReaderTests extends
public void testReaderUpdatesProcessIndicator() throws Exception {
long id = getJdbcTemplate().queryForLong(
"SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?",
long id = getJdbcTemplate().queryForLong("SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?",
new Object[] { jobId });
String before = (String) getJdbcTemplate().queryForObject(
"SELECT PROCESSED from BATCH_STAGING where ID=?",
String before = (String) getJdbcTemplate().queryForObject("SELECT PROCESSED from BATCH_STAGING where ID=?",
new Object[] { new Long(id) }, String.class);
assertEquals(StagingItemProcessor.NEW, before);
Object item = provider.read();
assertEquals("FOO", item);
String after = (String) getJdbcTemplate().queryForObject(
"SELECT PROCESSED from BATCH_STAGING where ID=?",
String after = (String) getJdbcTemplate().queryForObject("SELECT PROCESSED from BATCH_STAGING where ID=?",
new Object[] { new Long(id) }, String.class);
assertEquals(StagingItemProcessor.DONE, after);
}
public void testUpdateProcessIndicatorAfterCommit() throws Exception {
testReaderUpdatesProcessIndicator();
setComplete();
endTransaction();
startNewTransaction();
long id = getJdbcTemplate().queryForLong(
"SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?",
long id = getJdbcTemplate().queryForLong("SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?",
new Object[] { jobId });
String before = (String) getJdbcTemplate().queryForObject(
"SELECT PROCESSED from BATCH_STAGING where ID=?",
String before = (String) getJdbcTemplate().queryForObject("SELECT PROCESSED from BATCH_STAGING where ID=?",
new Object[] { new Long(id) }, String.class);
assertEquals(StagingItemProcessor.DONE, before);
assertEquals(StagingItemProcessor.DONE, before);
}
public void testProviderRollsBackMultipleTimes() throws Exception {
@@ -96,14 +90,14 @@ public class StagingItemReaderTests extends
setComplete();
endTransaction();
startNewTransaction();
// After a rollback we have to resynchronize the TX to simulate a real batch
// After a rollback we have to resynchronize the TX to simulate a real
// batch
BatchTransactionSynchronizationManager.resynchronize();
int count = getJdbcTemplate().queryForInt(
"SELECT COUNT(*) from BATCH_STAGING where JOB_ID=? AND PROCESSED=?",
int count = getJdbcTemplate().queryForInt("SELECT COUNT(*) from BATCH_STAGING where JOB_ID=? AND PROCESSED=?",
new Object[] { jobId, StagingItemProcessor.NEW });
assertEquals(4, count);
Object item = provider.read();
assertEquals("FOO", item);
item = provider.read();
@@ -112,7 +106,7 @@ public class StagingItemReaderTests extends
endTransaction();
startNewTransaction();
BatchTransactionSynchronizationManager.resynchronize();
item = provider.read();
assertEquals("FOO", item);
item = provider.read();
@@ -123,25 +117,24 @@ public class StagingItemReaderTests extends
endTransaction();
startNewTransaction();
BatchTransactionSynchronizationManager.resynchronize();
item = provider.read();
assertEquals("FOO", item);
}
public void testProviderRollsBackProcessIndicator() throws Exception {
setComplete();
endTransaction();
startNewTransaction();
// After a rollback we have to resynchronize the TX to simulate a real batch
// After a rollback we have to resynchronize the TX to simulate a real
// batch
BatchTransactionSynchronizationManager.resynchronize();
long id = getJdbcTemplate().queryForLong(
"SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?",
long id = getJdbcTemplate().queryForLong("SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?",
new Object[] { jobId });
String before = (String) getJdbcTemplate().queryForObject(
"SELECT PROCESSED from BATCH_STAGING where ID=?",
String before = (String) getJdbcTemplate().queryForObject("SELECT PROCESSED from BATCH_STAGING where ID=?",
new Object[] { new Long(id) }, String.class);
assertEquals(StagingItemProcessor.NEW, before);
@@ -150,11 +143,11 @@ public class StagingItemReaderTests extends
endTransaction();
startNewTransaction();
// After a rollback we have to resynchronize the TX to simulate a real batch
// After a rollback we have to resynchronize the TX to simulate a real
// batch
BatchTransactionSynchronizationManager.resynchronize();
String after = (String) getJdbcTemplate().queryForObject(
"SELECT PROCESSED from BATCH_STAGING where ID=?",
String after = (String) getJdbcTemplate().queryForObject("SELECT PROCESSED from BATCH_STAGING where ID=?",
new Object[] { new Long(id) }, String.class);
assertEquals(StagingItemProcessor.NEW, after);