RESOLVED - issue BATCH-503: Not applying declarative transactions around repository causes unstable execution

This commit is contained in:
dsyer
2008-05-26 12:16:05 +00:00
parent 11b4e2a012
commit 7025ff4831
6 changed files with 380 additions and 35 deletions

View File

@@ -18,6 +18,10 @@ package org.springframework.batch.core.repository.support;
import javax.sql.DataSource;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.dao.AbstractJdbcBatchMetadataDao;
import org.springframework.batch.core.repository.dao.JdbcJobExecutionDao;
import org.springframework.batch.core.repository.dao.JdbcJobInstanceDao;
@@ -27,21 +31,30 @@ import org.springframework.batch.core.repository.dao.JobInstanceDao;
import org.springframework.batch.core.repository.dao.StepExecutionDao;
import org.springframework.batch.item.database.support.DataFieldMaxValueIncrementerFactory;
import org.springframework.batch.item.database.support.DefaultDataFieldMaxValueIncrementerFactory;
import org.springframework.batch.support.PropertiesConverter;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.interceptor.TransactionInterceptor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link FactoryBean} that automates the creation of a {@link SimpleJobRepository}. Requires the
* user to describe what kind of database they are using.
* A {@link FactoryBean} that automates the creation of a
* {@link SimpleJobRepository}. Requires the user to describe what kind of
* database they are using.
*
* @author Ben Hale
* @author Lucas Ward
*/
public class JobRepositoryFactoryBean implements FactoryBean, InitializingBean {
/**
* Default value for isolation level in create* method.
*/
private static final String DEFAULT_ISOLATION_LEVEL = "ISOLATION_SERIALIZABLE";
private DataSource dataSource;
private String databaseType;
@@ -50,6 +63,35 @@ public class JobRepositoryFactoryBean implements FactoryBean, InitializingBean {
private DataFieldMaxValueIncrementerFactory incrementerFactory;
private PlatformTransactionManager transactionManager;
private ProxyFactory proxyFactory;
private String isolationLevelForCreate = DEFAULT_ISOLATION_LEVEL;
/**
* Public setter for the {@link PlatformTransactionManager}.
* @param transactionManager the transactionManager to set
*/
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
/**
* Public setter for the isolation level to be used for the transaction when
* job execution entities are initially created. The default is
* ISOLATION_SERIALIZABLE, which prevents accidental concurrent execution of
* the same job (ISOLATION_REPEATABLE_READ would work as well).
*
* @param isolationLevelForCreate the isolation level name to set
*
* @see SimpleJobRepository#createJobExecution(org.springframework.batch.core.Job,
* org.springframework.batch.core.JobParameters)
*/
public void setIsolationLevelForCreate(String isolationLevelForCreate) {
this.isolationLevelForCreate = isolationLevelForCreate;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@@ -67,18 +109,39 @@ public class JobRepositoryFactoryBean implements FactoryBean, InitializingBean {
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(dataSource, "Datasource must not be null.");
Assert.notNull(dataSource, "DataSource must not be null.");
Assert.notNull(transactionManager, "TransactionManager must not be null.");
if (incrementerFactory == null) {
incrementerFactory = new DefaultDataFieldMaxValueIncrementerFactory(dataSource);
}
Assert.isTrue(incrementerFactory.isSupportedIncrementerType(databaseType), "'" + databaseType
+ "' is an unsupported database type. The supported database types are "
+ StringUtils.arrayToCommaDelimitedString(incrementerFactory.getSupportedIncrementerTypes()));
+ "' is an unsupported database type. The supported database types are "
+ StringUtils.arrayToCommaDelimitedString(incrementerFactory.getSupportedIncrementerTypes()));
initializeProxy();
}
public Object getObject() throws Exception {
protected void initializeProxy() throws Exception {
proxyFactory = new ProxyFactory();
TransactionInterceptor advice = new TransactionInterceptor(transactionManager, PropertiesConverter
.stringToProperties("create*=PROPAGATION_REQUIRES_NEW," + isolationLevelForCreate
+ "\n*=PROPAGATION_REQUIRED"));
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(advice);
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.addMethodName("*");
advisor.setPointcut(pointcut);
proxyFactory.addAdvisor(advisor);
proxyFactory.setProxyTargetClass(false);
proxyFactory.addInterface(JobRepository.class);
proxyFactory.setTarget(getTarget());
}
/**
* @return a SimpleJobRepository
*/
private SimpleJobRepository getTarget() throws Exception {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
JobInstanceDao jobInstanceDao = createJobInstanceDao(jdbcTemplate);
JobExecutionDao jobExecutionDao = createJobExecutionDao(jdbcTemplate);
@@ -86,8 +149,25 @@ public class JobRepositoryFactoryBean implements FactoryBean, InitializingBean {
return new SimpleJobRepository(jobInstanceDao, jobExecutionDao, stepExecutionDao);
}
/**
* Get a concrete {@link JobRepository}. The repository will be a proxy
* containing transaction advice using the supplied transaction manager.
*
* @return a {@link JobRepository}
* @see org.springframework.beans.factory.FactoryBean#getObject()
*/
public Object getObject() throws Exception {
return proxyFactory.getProxy();
}
/**
* The type of object to be returned from {@link #getObject()}.
*
* @return JobRepository.class
* @see org.springframework.beans.factory.FactoryBean#getObjectType()
*/
public Class getObjectType() {
return SimpleJobRepository.class;
return JobRepository.class;
}
public boolean isSingleton() {
@@ -106,7 +186,8 @@ public class JobRepositoryFactoryBean implements FactoryBean, InitializingBean {
private JobExecutionDao createJobExecutionDao(JdbcTemplate jdbcTemplate) throws Exception {
JdbcJobExecutionDao dao = new JdbcJobExecutionDao();
dao.setJdbcTemplate(jdbcTemplate);
dao.setJobExecutionIncrementer(incrementerFactory.getIncrementer(databaseType, tablePrefix + "JOB_EXECUTION_SEQ"));
dao.setJobExecutionIncrementer(incrementerFactory.getIncrementer(databaseType, tablePrefix
+ "JOB_EXECUTION_SEQ"));
dao.setTablePrefix(tablePrefix);
dao.afterPropertiesSet();
return dao;
@@ -115,7 +196,8 @@ public class JobRepositoryFactoryBean implements FactoryBean, InitializingBean {
private StepExecutionDao createStepExecutionDao(JdbcTemplate jdbcTemplate) throws Exception {
JdbcStepExecutionDao dao = new JdbcStepExecutionDao();
dao.setJdbcTemplate(jdbcTemplate);
dao.setStepExecutionIncrementer(incrementerFactory.getIncrementer(databaseType, tablePrefix + "STEP_EXECUTION_SEQ"));
dao.setStepExecutionIncrementer(incrementerFactory.getIncrementer(databaseType, tablePrefix
+ "STEP_EXECUTION_SEQ"));
dao.setTablePrefix(tablePrefix);
dao.afterPropertiesSet();
return dao;

View File

@@ -19,6 +19,7 @@ import org.springframework.batch.core.Step;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.validator.Validator;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
@@ -33,7 +34,7 @@ import org.springframework.util.Assert;
*
*/
public abstract class AbstractStepFactoryBean implements FactoryBean, BeanNameAware {
private String name;
private int startLimit = Integer.MAX_VALUE;
@@ -50,6 +51,8 @@ public abstract class AbstractStepFactoryBean implements FactoryBean, BeanNameAw
private boolean singleton = true;
private Validator jobRepositoryValidator = new TransactionInterceptorValidator(1);
/**
*
*/
@@ -160,8 +163,8 @@ public abstract class AbstractStepFactoryBean implements FactoryBean, BeanNameAw
Assert.notNull(getItemReader(), "ItemReader must be provided");
Assert.notNull(getItemWriter(), "ItemWriter must be provided");
Assert.notNull(jobRepository, "JobRepository must be provided");
Assert.notNull(transactionManager, "TransactionManager must be provided");
jobRepositoryValidator .validate(jobRepository);
step.setItemHandler(new SimpleItemHandler(itemReader, itemWriter));
step.setTransactionManager(transactionManager);

View File

@@ -0,0 +1,87 @@
/*
* 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.core.step.item;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.batch.item.validator.ValidationException;
import org.springframework.batch.item.validator.Validator;
import org.springframework.transaction.interceptor.TransactionInterceptor;
import org.springframework.util.Assert;
/**
* Simple validator for internal use only (package private to make it testable).
* Asserts that its argument has no more than the specified number of
* transaction interceptors in its advice chain.
*
* @author Dave Syer
*
*/
class TransactionInterceptorValidator implements Validator {
protected Log logger = LogFactory.getLog(getClass());
private final int maxCount;
/**
* @param maxCount
*/
public TransactionInterceptorValidator(int maxCount) {
super();
this.maxCount = maxCount;
}
/**
* Assert that the object passed in has no more than the maximum number of
* transaction interceptors in its advice chain.
*
* @see org.springframework.batch.item.validator.Validator#validate(java.lang.Object)
*/
public void validate(Object value) throws ValidationException {
Assert.notNull(value, "JobRepository must be provided");
Assert.state(countTransactionInterceptors(value) <= maxCount,
"JobRepository has more than one transaction interceptor. "
+ "Do not declare a separate transaction advice if using the JobRepositoryFactoryBean.");
}
/**
* @param object an Object, possibly advised
* @return the number of transaction interceptors in the advice chain
*/
private int countTransactionInterceptors(Object object) {
int count = 0;
Object target = object;
while (target instanceof Advised) {
Advised advised = (Advised) target;
Advisor[] interceptors = advised.getAdvisors();
for (int i = 0; i < interceptors.length; i++) {
if (interceptors[i].getAdvice() instanceof TransactionInterceptor) {
count++;
}
}
try {
target = advised.getTargetSource().getTarget();
}
catch (Exception e) {
logger.warn("Target could not be obtained from advised instance.", e);
}
}
return count;
}
}