diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java index ac6243f1e..1f19b6f56 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java @@ -43,6 +43,10 @@ public class CoreNamespaceUtils { private static final String STEP_SCOPE_PROCESSOR_CLASS_NAME = "org.springframework.batch.core.scope.StepScope"; + private static final String JOB_SCOPE_PROCESSOR_BEAN_NAME = "org.springframework.batch.core.scope.internalJobScope"; + + private static final String JOB_SCOPE_PROCESSOR_CLASS_NAME = "org.springframework.batch.core.scope.JobScope"; + private static final String CUSTOM_EDITOR_CONFIGURER_CLASS_NAME = "org.springframework.beans.factory.config.CustomEditorConfigurer"; private static final String RANGE_ARRAY_CLASS_NAME = "org.springframework.batch.item.file.transform.Range[]"; @@ -53,28 +57,38 @@ public class CoreNamespaceUtils { public static void autoregisterBeansForNamespace(ParserContext parserContext, Object source) { checkForStepScope(parserContext, source); + checkForJobScope(parserContext, source); addRangePropertyEditor(parserContext); addCoreNamespacePostProcessor(parserContext); addStateTransitionComparator(parserContext); } private static void checkForStepScope(ParserContext parserContext, Object source) { - boolean foundStepScope = false; + checkForScope(parserContext, source, STEP_SCOPE_PROCESSOR_CLASS_NAME, STEP_SCOPE_PROCESSOR_BEAN_NAME); + } + + private static void checkForJobScope(ParserContext parserContext, Object source) { + checkForScope(parserContext, source, JOB_SCOPE_PROCESSOR_CLASS_NAME, JOB_SCOPE_PROCESSOR_BEAN_NAME); + } + + private static void checkForScope(ParserContext parserContext, Object source, String scopeClassName, + String scopeBeanName) { + boolean foundScope = false; String[] beanNames = parserContext.getRegistry().getBeanDefinitionNames(); for (String beanName : beanNames) { BeanDefinition bd = parserContext.getRegistry().getBeanDefinition(beanName); - if (STEP_SCOPE_PROCESSOR_CLASS_NAME.equals(bd.getBeanClassName())) { - foundStepScope = true; + if (scopeClassName.equals(bd.getBeanClassName())) { + foundScope = true; break; } } - if (!foundStepScope) { + if (!foundScope) { BeanDefinitionBuilder stepScopeBuilder = BeanDefinitionBuilder - .genericBeanDefinition(STEP_SCOPE_PROCESSOR_CLASS_NAME); + .genericBeanDefinition(scopeClassName); AbstractBeanDefinition abd = stepScopeBuilder.getBeanDefinition(); abd.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); abd.setSource(source); - parserContext.getRegistry().registerBeanDefinition(STEP_SCOPE_PROCESSOR_BEAN_NAME, abd); + parserContext.getRegistry().registerBeanDefinition(scopeBeanName, abd); } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java index 0373bece0..a8984d09c 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java @@ -38,6 +38,7 @@ import org.springframework.batch.core.launch.support.ExitCodeMapper; import org.springframework.batch.core.listener.CompositeJobExecutionListener; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.JobRestartException; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; import org.springframework.batch.core.step.StepLocator; import org.springframework.batch.repeat.RepeatException; import org.springframework.beans.factory.BeanNameAware; @@ -286,6 +287,8 @@ InitializingBean { logger.debug("Job execution starting: " + execution); + JobSynchronizationManager.register(execution); + try { jobParametersValidator.validate(execution.getJobParameters()); @@ -328,24 +331,27 @@ InitializingBean { execution.setStatus(BatchStatus.FAILED); execution.addFailureException(t); } finally { - - if (execution.getStatus().isLessThanOrEqualTo(BatchStatus.STOPPED) - && execution.getStepExecutions().isEmpty()) { - ExitStatus exitStatus = execution.getExitStatus(); - execution - .setExitStatus(exitStatus.and(ExitStatus.NOOP - .addExitDescription("All steps already completed or no steps configured for this job."))); - } - - execution.setEndTime(new Date()); - try { - listener.afterJob(execution); - } catch (Exception e) { - logger.error("Exception encountered in afterStep callback", e); - } + if (execution.getStatus().isLessThanOrEqualTo(BatchStatus.STOPPED) + && execution.getStepExecutions().isEmpty()) { + ExitStatus exitStatus = execution.getExitStatus(); + execution + .setExitStatus(exitStatus.and(ExitStatus.NOOP + .addExitDescription("All steps already completed or no steps configured for this job."))); + } - jobRepository.update(execution); + execution.setEndTime(new Date()); + + try { + listener.afterJob(execution); + } catch (Exception e) { + logger.error("Exception encountered in afterStep callback", e); + } + + jobRepository.update(execution); + } finally { + JobSynchronizationManager.release(); + } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/JobScope.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/JobScope.java new file mode 100644 index 000000000..e8de653a8 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/JobScope.java @@ -0,0 +1,159 @@ +/* + * 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.scope; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.core.scope.context.JobContext; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; +import org.springframework.batch.core.scope.util.ContextFactory; +import org.springframework.batch.core.scope.util.JobContextFactory; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.Scope; + +/** + * Scope for job context. Objects in this scope use the Spring container as an + * object factory, so there is only one instance of such a bean per executing + * job. All objects in this scope are <aop:scoped-proxy/> (no need to + * decorate the bean definitions).
+ *
+ * + * In addition, support is provided for late binding of references accessible + * from the {@link JobContext} using #{..} placeholders. Using this feature, + * bean properties can be pulled from the job or job execution context and the + * job parameters. E.g. + * + *
+ * <bean id="..." class="..." scope="job">
+ * 	<property name="name" value="#{jobParameters[input]}" />
+ * </bean>
+ * 
+ * <bean id="..." class="..." scope="job">
+ * 	<property name="name" value="#{jobExecutionContext['input.stem']}.txt" />
+ * </bean>
+ * 
+ * + * The {@link JobContext} is referenced using standard bean property paths (as + * per {@link BeanWrapper}). The examples above all show the use of the Map + * accessors provided as a convenience for job attributes. + * + * @author Dave Syer + * @author Jimmy Praet (create JobScope based on {@link StepScope}) + * @since 2.0 + */ +public class JobScope extends ScopeSupport { + + private Log logger = LogFactory.getLog(getClass()); + + private final Object mutex = new Object(); + + /** + * Context key for clients to use for conversation identifier. + */ + public static final String ID_KEY = "JOB_IDENTIFIER"; + + /** + * The ContextFactory. + */ + private static final ContextFactory CONTEXT_FACTORY = new JobContextFactory(); + + public JobScope() { + super("job", CONTEXT_FACTORY); + } + + /** + * If Spring 3.0 is available, this will be used to resolve expressions in + * job-scoped beans. This method is part of the Scope SPI in Spring 3.0, + * but should just be ignored by earlier versions of Spring. + */ + public Object resolveContextualObject(String key) { + JobContext context = getContext(); + // TODO: support for attributes as well maybe (setters not exposed yet + // so not urgent). + return new BeanWrapperImpl(context).getPropertyValue(key); + } + + /** + * @see Scope#get(String, ObjectFactory) + */ + public Object get(String name, ObjectFactory objectFactory) { + + JobContext context = getContext(); + Object scopedObject = context.getAttribute(name); + + if (scopedObject == null) { + + synchronized (mutex) { + scopedObject = context.getAttribute(name); + if (scopedObject == null) { + + logger.debug(String.format("Creating object in scope=%s, name=%s", this.getName(), name)); + + scopedObject = objectFactory.getObject(); + context.setAttribute(name, scopedObject); + + } + + } + + } + return scopedObject; + } + + /** + * @see Scope#getConversationId() + */ + public String getConversationId() { + JobContext context = getContext(); + return context.getId(); + } + + /** + * @see Scope#registerDestructionCallback(String, Runnable) + */ + public void registerDestructionCallback(String name, Runnable callback) { + JobContext context = getContext(); + logger.debug(String.format("Registered destruction callback in scope=%s, name=%s", this.getName(), name)); + context.registerDestructionCallback(name, callback); + } + + /** + * @see Scope#remove(String) + */ + public Object remove(String name) { + JobContext context = getContext(); + logger.debug(String.format("Removing from scope=%s, name=%s", this.getName(), name)); + return context.removeAttribute(name); + } + + /** + * Get an attribute accessor in the form of a {@link JobContext} that can + * be used to store scoped bean instances. + * + * @return the current job context which we can use as a scope storage + * medium + */ + private JobContext getContext() { + JobContext context = JobSynchronizationManager.getContext(); + if (context == null) { + throw new IllegalStateException("No context holder available for job scope"); + } + return context; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/ScopeSupport.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/ScopeSupport.java new file mode 100644 index 000000000..ded838484 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/ScopeSupport.java @@ -0,0 +1,316 @@ +/* + * 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.scope; + +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.batch.core.scope.util.ContextFactory; +import org.springframework.batch.core.scope.util.PlaceholderProxyFactoryBean; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.BeanDefinitionVisitor; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.Ordered; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringValueResolver; + +/** + * ScopeSupport. + * + * @author Dave Syer + */ +public abstract class ScopeSupport implements Scope, BeanFactoryPostProcessor, Ordered { + + private static boolean springThreeDetected; + + private static boolean cachedSpringThreeResult; + + private int order = Ordered.LOWEST_PRECEDENCE; + + private String name; + + private boolean proxyTargetClass = false; + + private ContextFactory contextFactory; + + /** + * ScopeSupport constructor. + * + * @param defaultScopeName the default scope name + * @param contextFactory the ContextFactory + */ + protected ScopeSupport(String defaultScopeName, ContextFactory contextFactory) { + this.name = defaultScopeName; + this.contextFactory = contextFactory; + } + + /** + * Flag to indicate that proxies should use dynamic subclassing. This allows + * classes with no interface to be proxied. Defaults to false. + * + * @param proxyTargetClass set to true to have proxies created using dynamic + * subclasses + */ + public void setProxyTargetClass(boolean proxyTargetClass) { + this.proxyTargetClass = proxyTargetClass; + } + + /** + * @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; + } + + public int getOrder() { + return order; + } + + /** + * Public setter for the name property. This can then be used as a bean + * definition attribute, e.g. scope="step". + * + * @param name the name to set for this scope. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Return the scope name. + * + * @return the scope name + */ + public String getName() { + return name; + } + + /** + * Register this scope with the enclosing BeanFactory. + * + * @see BeanFactoryPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory) + * + * @param beanFactory the BeanFactory to register with + * @throws BeansException if there is a problem. + */ + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + + beanFactory.registerScope(name, this); + + Assert.state(beanFactory instanceof BeanDefinitionRegistry, + "BeanFactory was not a BeanDefinitionRegistry, so scope cannot be used."); + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition definition = beanFactory.getBeanDefinition(beanName); + // Replace this or any of its inner beans with scoped proxy if it + // has this scope + boolean scoped = name.equals(definition.getScope()); + Scopifier scopifier = new Scopifier(registry, name, proxyTargetClass, scoped, contextFactory); + scopifier.visitBeanDefinition(definition); + if (scoped) { + if (!isSpringThree()) { + new ExpressionHider(name, scoped).visitBeanDefinition(definition); + } + createScopedProxy(beanName, definition, registry, proxyTargetClass, contextFactory); + } + } + + } + + private static boolean isSpringThree() { + if (!cachedSpringThreeResult) { + springThreeDetected = ReflectionUtils.findMethod(Scope.class, "resolveContextualObject", + new Class[] { String.class }) != null; + cachedSpringThreeResult = true; + } + return springThreeDetected; + } + + /** + * Wrap a target bean definition in a proxy that defers initialization until + * after the context is available. Amounts to adding + * <aop-auto-proxy/> to a scoped bean. Also if Spring EL is not + * available will enable a weak version of late binding as described in the + * class-level docs. + * + * @param beanName the bean name to replace + * @param definition the bean definition to replace + * @param registry the enclosing {@link BeanDefinitionRegistry} + * @param proxyTargetClass true if we need to force use of dynamic + * subclasses + * @param contextFactory the ContextFactory + * @return a {@link BeanDefinitionHolder} for the new representation of the + * target. Caller should register it if needed to be visible at top level in + * bean factory. + */ + private static BeanDefinitionHolder createScopedProxy(String beanName, BeanDefinition definition, + BeanDefinitionRegistry registry, boolean proxyTargetClass, ContextFactory contextFactory) { + + BeanDefinitionHolder proxyHolder; + + if (isSpringThree()) { + proxyHolder = ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder(definition, beanName), registry, + proxyTargetClass); + } + else { + // Create the scoped proxy... + proxyHolder = PlaceholderProxyFactoryBean.createScopedProxy(new BeanDefinitionHolder(definition, beanName), + registry, proxyTargetClass, contextFactory); + // ...and register it under the original target name + } + registry.registerBeanDefinition(beanName, proxyHolder.getBeanDefinition()); + + return proxyHolder; + + } + + /** + * Helper class to scan a bean definition hierarchy and force the use of + * auto-proxy for scoped beans. + * + * @author Dave Syer + * + */ + private static class Scopifier extends BeanDefinitionVisitor { + + private final boolean proxyTargetClass; + + private final BeanDefinitionRegistry registry; + + private final String scope; + + private final boolean scoped; + + private final ContextFactory contextFactory; + + public Scopifier(BeanDefinitionRegistry registry, String scope, boolean proxyTargetClass, boolean scoped, + ContextFactory contextFactory) { + super(new StringValueResolver() { + public String resolveStringValue(String value) { + return value; + } + }); + this.registry = registry; + this.proxyTargetClass = proxyTargetClass; + this.scope = scope; + this.scoped = scoped; + this.contextFactory = contextFactory; + } + + @Override + protected Object resolveValue(Object value) { + + BeanDefinition definition = null; + String beanName = null; + if (value instanceof BeanDefinition) { + definition = (BeanDefinition) value; + beanName = BeanDefinitionReaderUtils.generateBeanName(definition, registry); + } + else if (value instanceof BeanDefinitionHolder) { + BeanDefinitionHolder holder = (BeanDefinitionHolder) value; + definition = holder.getBeanDefinition(); + beanName = holder.getBeanName(); + } + + if (definition != null) { + boolean nestedScoped = scope.equals(definition.getScope()); + boolean scopeChangeRequiresProxy = !scoped && nestedScoped; + if (!isSpringThree()) { + new ExpressionHider(scope, nestedScoped).visitBeanDefinition(definition); + } + if (scopeChangeRequiresProxy) { + // Exit here so that nested inner bean definitions are not + // analysed + return createScopedProxy(beanName, definition, registry, proxyTargetClass, contextFactory); + } + } + + // Nested inner bean definitions are recursively analysed here + value = super.resolveValue(value); + return value; + + } + + } + + /** + * Helper class to scan a bean definition hierarchy and hide placeholders + * from Spring EL. + * + * @author Dave Syer + * + */ + private static class ExpressionHider extends BeanDefinitionVisitor { + + private static final String PLACEHOLDER_PREFIX = "#{"; + + private static final String PLACEHOLDER_SUFFIX = "}"; + + private static final String REPLACEMENT_PREFIX = "%{"; + + private final String scope; + + private final boolean scoped; + + private ExpressionHider(String scope, final boolean scoped) { + super(new StringValueResolver() { + public String resolveStringValue(String value) { + if (scoped && value.contains(PLACEHOLDER_PREFIX) && value.contains(PLACEHOLDER_SUFFIX)) { + value = value.replace(PLACEHOLDER_PREFIX, REPLACEMENT_PREFIX); + } + return value; + } + }); + this.scope = scope; + this.scoped = scoped; + } + + @Override + protected Object resolveValue(Object value) { + BeanDefinition definition = null; + if (value instanceof BeanDefinition) { + definition = (BeanDefinition) value; + } + else if (value instanceof BeanDefinitionHolder) { + BeanDefinitionHolder holder = (BeanDefinitionHolder) value; + definition = holder.getBeanDefinition(); + } + if (definition != null) { + String otherScope = definition.getScope(); + boolean scopeChange = !scope.equals(otherScope); + if (scopeChange) { + new ExpressionHider(otherScope == null ? scope : otherScope, !scoped) + .visitBeanDefinition(definition); + // Exit here so that nested inner bean definitions are not + // analysed by both visitors + return value; + } + } + // Nested inner bean definitions are recursively analysed here + value = super.resolveValue(value); + return value; + } + + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepScope.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepScope.java index 718630a68..3be3bce90 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepScope.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepScope.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2013 the original author or authors. + * 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. @@ -17,24 +17,14 @@ package org.springframework.batch.core.scope; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.batch.core.scope.context.StepContext; import org.springframework.batch.core.scope.context.StepSynchronizationManager; +import org.springframework.batch.core.scope.util.ContextFactory; +import org.springframework.batch.core.scope.util.StepContextFactory; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanDefinitionHolder; -import org.springframework.beans.factory.config.BeanDefinitionVisitor; -import org.springframework.beans.factory.config.BeanFactoryPostProcessor; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.Scope; -import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.core.Ordered; -import org.springframework.util.Assert; -import org.springframework.util.StringValueResolver; /** * Scope for step context. Objects in this scope use the Spring container as an @@ -42,97 +32,62 @@ import org.springframework.util.StringValueResolver; * step. All objects in this scope are <aop:scoped-proxy/> (no need to * decorate the bean definitions).
*
- * + * * In addition, support is provided for late binding of references accessible * from the {@link StepContext} using #{..} placeholders. Using this feature, * bean properties can be pulled from the step or job execution context and the * job parameters. E.g. - * + * *
  * <bean id="..." class="..." scope="step">
  * 	<property name="parent" ref="#{stepExecutionContext[helper]}" />
  * </bean>
- *
+ * 
  * <bean id="..." class="..." scope="step">
  * 	<property name="name" value="#{stepExecutionContext['input.name']}" />
  * </bean>
- *
+ * 
  * <bean id="..." class="..." scope="step">
  * 	<property name="name" value="#{jobParameters[input]}" />
  * </bean>
- *
+ * 
  * <bean id="..." class="..." scope="step">
  * 	<property name="name" value="#{jobExecutionContext['input.stem']}.txt" />
  * </bean>
  * 
- * + * * The {@link StepContext} is referenced using standard bean property paths (as * per {@link BeanWrapper}). The examples above all show the use of the Map * accessors provided as a convenience for step and job attributes. - * + * * @author Dave Syer - * @author Michael Minella * @since 2.0 */ -public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { - - private static final String TARGET_NAME_PREFIX = "scopedTarget."; +public class StepScope extends ScopeSupport { private Log logger = LogFactory.getLog(getClass()); - private int order = Ordered.LOWEST_PRECEDENCE; - - private boolean autoProxy = true; - private final 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; - } - - @Override - public int getOrder() { - return order; - } - /** * Context key for clients to use for conversation identifier. */ public static final String ID_KEY = "STEP_IDENTIFIER"; - private String name = "step"; - - private boolean proxyTargetClass = false; - /** - * Flag to indicate that proxies should use dynamic subclassing. This allows - * classes with no interface to be proxied. Defaults to false. - * - * @param proxyTargetClass set to true to have proxies created using dynamic - * subclasses + * The ContextFactory. */ - public void setProxyTargetClass(boolean proxyTargetClass) { - this.proxyTargetClass = proxyTargetClass; + private static final ContextFactory CONTEXT_FACTORY = new StepContextFactory(); + + public StepScope() { + super("step", CONTEXT_FACTORY); } /** - * Flag to indicate that bean definitions need not be auto proxied. This gives control back to the declarer of the - * bean definition (e.g. in an @Configuration class). - * - * @param autoProxy the flag value to set (default true) + * If Spring 3.0 is available, this will be used to resolve expressions in + * step-scoped beans. This method is part of the Scope SPI in Spring 3.0, + * but should just be ignored by earlier versions of Spring. */ - public void setAutoProxy(boolean autoProxy) { - this.autoProxy = autoProxy; - } - - /** - * This will be used to resolve expressions in step-scoped beans. - */ - @Override public Object resolveContextualObject(String key) { StepContext context = getContext(); // TODO: support for attributes as well maybe (setters not exposed yet @@ -143,9 +98,8 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { /** * @see Scope#get(String, ObjectFactory) */ - @SuppressWarnings("rawtypes") - @Override public Object get(String name, ObjectFactory objectFactory) { + StepContext context = getContext(); Object scopedObject = context.getAttribute(name); @@ -155,7 +109,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { scopedObject = context.getAttribute(name); if (scopedObject == null) { - logger.debug(String.format("Creating object in scope=%s, name=%s", this.name, name)); + logger.debug(String.format("Creating object in scope=%s, name=%s", this.getName(), name)); scopedObject = objectFactory.getObject(); context.setAttribute(name, scopedObject); @@ -171,7 +125,6 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { /** * @see Scope#getConversationId() */ - @Override public String getConversationId() { StepContext context = getContext(); return context.getId(); @@ -180,27 +133,25 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { /** * @see Scope#registerDestructionCallback(String, Runnable) */ - @Override public void registerDestructionCallback(String name, Runnable callback) { StepContext context = getContext(); - logger.debug(String.format("Registered destruction callback in scope=%s, name=%s", this.name, name)); + logger.debug(String.format("Registered destruction callback in scope=%s, name=%s", this.getName(), name)); context.registerDestructionCallback(name, callback); } /** * @see Scope#remove(String) */ - @Override public Object remove(String name) { StepContext context = getContext(); - logger.debug(String.format("Removing from scope=%s, name=%s", this.name, name)); + logger.debug(String.format("Removing from scope=%s, name=%s", this.getName(), name)); return context.removeAttribute(name); } /** * Get an attribute accessor in the form of a {@link StepContext} that can * be used to store scoped bean instances. - * + * * @return the current step context which we can use as a scope storage * medium */ @@ -212,142 +163,4 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { return context; } - /** - * Register this scope with the enclosing BeanFactory. - * - * @see BeanFactoryPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory) - * - * @param beanFactory the BeanFactory to register with - * @throws BeansException if there is a problem. - */ - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - - beanFactory.registerScope(name, this); - - if(!autoProxy) { - return; - } - - Assert.state(beanFactory instanceof BeanDefinitionRegistry, - "BeanFactory was not a BeanDefinitionRegistry, so StepScope cannot be used."); - BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; - - for (String beanName : beanFactory.getBeanDefinitionNames()) { - if (!beanName.startsWith(TARGET_NAME_PREFIX)) { - BeanDefinition definition = beanFactory.getBeanDefinition(beanName); - // Replace this or any of its inner beans with scoped proxy if it - // has this scope - boolean scoped = name.equals(definition.getScope()); - Scopifier scopifier = new Scopifier(registry, name, proxyTargetClass, scoped); - scopifier.visitBeanDefinition(definition); - - if (scoped && !definition.isAbstract()) { - createScopedProxy(beanName, definition, registry, proxyTargetClass); - } - } - } - - } - - /** - * 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. - */ - public void setName(String name) { - this.name = name; - } - - /** - * Wrap a target bean definition in a proxy that defers initialization until - * after the {@link StepContext} is available. Amounts to adding - * <aop-auto-proxy/> to a step scoped bean. - * - * @param beanName the bean name to replace - * @param definition the bean definition to replace - * @param registry the enclosing {@link BeanDefinitionRegistry} - * @param proxyTargetClass true if we need to force use of dynamic - * subclasses - * @return a {@link BeanDefinitionHolder} for the new representation of the - * target. Caller should register it if needed to be visible at top level in - * bean factory. - */ - private static BeanDefinitionHolder createScopedProxy(String beanName, BeanDefinition definition, - BeanDefinitionRegistry registry, boolean proxyTargetClass) { - - BeanDefinitionHolder proxyHolder; - - proxyHolder = ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder(definition, beanName), registry, - proxyTargetClass); - - registry.registerBeanDefinition(beanName, proxyHolder.getBeanDefinition()); - - return proxyHolder; - - } - - /** - * Helper class to scan a bean definition hierarchy and force the use of - * auto-proxy for step scoped beans. - * - * @author Dave Syer - * - */ - private static class Scopifier extends BeanDefinitionVisitor { - - private final boolean proxyTargetClass; - - private final BeanDefinitionRegistry registry; - - private final String scope; - - private final boolean scoped; - - public Scopifier(BeanDefinitionRegistry registry, String scope, boolean proxyTargetClass, boolean scoped) { - super(new StringValueResolver() { - @Override - public String resolveStringValue(String value) { - return value; - } - }); - this.registry = registry; - this.proxyTargetClass = proxyTargetClass; - this.scope = scope; - this.scoped = scoped; - } - - @Override - protected Object resolveValue(Object value) { - - BeanDefinition definition = null; - String beanName = null; - if (value instanceof BeanDefinition) { - definition = (BeanDefinition) value; - beanName = BeanDefinitionReaderUtils.generateBeanName(definition, registry); - } - else if (value instanceof BeanDefinitionHolder) { - BeanDefinitionHolder holder = (BeanDefinitionHolder) value; - definition = holder.getBeanDefinition(); - beanName = holder.getBeanName(); - } - - if (definition != null) { - boolean nestedScoped = scope.equals(definition.getScope()); - boolean scopeChangeRequiresProxy = !scoped && nestedScoped; - if (scopeChangeRequiresProxy) { - // Exit here so that nested inner bean definitions are not - // analysed - return createScopedProxy(beanName, definition, registry, proxyTargetClass); - } - } - - // Nested inner bean definitions are recursively analysed here - value = super.resolveValue(value); - return value; - - } - - } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobContext.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobContext.java new file mode 100644 index 000000000..acdd5e0b1 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobContext.java @@ -0,0 +1,233 @@ +/* + * 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.scope.context; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; + +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobInstance; +import org.springframework.batch.core.JobParameter; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.UnexpectedJobExecutionException; +import org.springframework.batch.core.scope.StepScope; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.repeat.context.SynchronizedAttributeAccessor; +import org.springframework.util.Assert; + +/** + * A context object that can be used to interrogate the current {@link JobExecution} and some of its associated + * properties using expressions + * based on bean paths. Has public getters for the job execution and + * convenience methods for accessing commonly used properties like the {@link ExecutionContext} associated with the job + * execution. + * + * @author Dave Syer + * @author Jimmy Praet (create JobContext based on {@link StepContext}) + */ +public class JobContext extends SynchronizedAttributeAccessor { + + private JobExecution jobExecution; + + private Map> callbacks = new HashMap>(); + + public JobContext(JobExecution jobExecution) { + super(); + Assert.notNull(jobExecution, "A JobContext must have a non-null JobExecution"); + this.jobExecution = jobExecution; + } + + /** + * Convenient accessor for current job name identifier. + * + * @return the job name identifier of the enclosing {@link JobInstance} associated with the current + * {@link JobExecution} + */ + public String getJobName() { + Assert.state(jobExecution.getJobInstance() != null, "StepExecution does not have a JobInstance"); + return jobExecution.getJobInstance().getJobName(); + } + + /** + * Convenient accessor for System properties to make it easy to access them + * from placeholder expressions. + * + * @return the current System properties + */ + public Properties getSystemProperties() { + return System.getProperties(); + } + + /** + * @return a map containing the items from the job {@link ExecutionContext} + */ + public Map getJobExecutionContext() { + Map result = new HashMap(); + for (Entry entry : jobExecution.getExecutionContext().entrySet()) { + result.put(entry.getKey(), entry.getValue()); + } + return Collections.unmodifiableMap(result); + } + + /** + * @return a map containing the items from the {@link JobParameters} + */ + public Map getJobParameters() { + Map result = new HashMap(); + for (Entry entry : jobExecution.getJobInstance().getJobParameters().getParameters() + .entrySet()) { + result.put(entry.getKey(), entry.getValue().getValue()); + } + return Collections.unmodifiableMap(result); + } + + /** + * Allow clients to register callbacks for clean up on close. + * + * @param name + * the callback id (unique attribute key in this context) + * @param callback + * a callback to execute on close + */ + public void registerDestructionCallback(String name, Runnable callback) { + synchronized (callbacks) { + Set set = callbacks.get(name); + if (set == null) { + set = new HashSet(); + callbacks.put(name, set); + } + set.add(callback); + } + } + + private void unregisterDestructionCallbacks(String name) { + synchronized (callbacks) { + callbacks.remove(name); + } + } + + /** + * Override base class behaviour to ensure destruction callbacks are + * unregistered as well as the default behaviour. + * + * @see SynchronizedAttributeAccessor#removeAttribute(String) + */ + @Override + public Object removeAttribute(String name) { + unregisterDestructionCallbacks(name); + return super.removeAttribute(name); + } + + /** + * Clean up the context at the end of a step execution. Must be called once + * at the end of a step execution to honour the destruction callback + * contract from the {@link StepScope}. + */ + public void close() { + + List errors = new ArrayList(); + + Map> copy = Collections.unmodifiableMap(callbacks); + + for (Entry> entry : copy.entrySet()) { + Set set = entry.getValue(); + for (Runnable callback : set) { + if (callback != null) { + /* + * The documentation of the interface says that these + * callbacks must not throw exceptions, but we don't trust + * them necessarily... + */ + try { + callback.run(); + } catch (RuntimeException t) { + errors.add(t); + } + } + } + } + + if (errors.isEmpty()) { + return; + } + + Exception error = errors.get(0); + if (error instanceof RuntimeException) { + throw (RuntimeException) error; + } else { + throw new UnexpectedJobExecutionException("Could not close step context, rethrowing first of " + + errors.size() + " exceptions.", error); + } + } + + /** + * The current {@link JobExecution} that is active in this context. + * + * @return the current {@link JobExecution} + */ + public JobExecution getJobExecution() { + return jobExecution; + } + + /** + * @return unique identifier for this context based on the step execution + */ + public String getId() { + Assert.state(jobExecution.getId() != null, "JobExecution has no id. " + + "It must be saved before it can be used in job scope."); + return "jobExecution#" + jobExecution.getId(); + } + + /** + * Extend the base class method to include the job execution itself as a key + * (i.e. two contexts are only equal if their job executions are the same). + */ + @Override + public boolean equals(Object other) { + if (!(other instanceof JobContext)) + return false; + if (other == this) + return true; + JobContext context = (JobContext) other; + if (context.jobExecution == jobExecution) { + return true; + } + return jobExecution.equals(context.jobExecution); + } + + /** + * Overrides the default behaviour to provide a hash code based only on the + * job execution. + */ + @Override + public int hashCode() { + return jobExecution.hashCode(); + } + + @Override + public String toString() { + return super.toString() + ", jobExecutionContext=" + getJobExecutionContext() + ", jobParameters=" + + getJobParameters(); + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobScopeManager.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobScopeManager.java new file mode 100644 index 000000000..1007c18e5 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobScopeManager.java @@ -0,0 +1,46 @@ +/* + * 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.scope.context; + +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; + +/** + * Convenient aspect to wrap a single threaded job execution, where the + * implementation of the {@link Job} is not job scope aware (i.e. not the ones + * provided by the framework). + * + * @author Dave Syer + * @author Jimmy Praet + */ +@Aspect +public class JobScopeManager { + + @Around("execution(void org.springframework.batch.core.Job+.execute(*)) && target(job) && args(jobExecution)") + public void execute(Job job, JobExecution jobExecution) { + JobSynchronizationManager.register(jobExecution); + try { + job.execute(jobExecution); + } + finally { + JobSynchronizationManager.release(); + } + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobSynchronizationManager.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobSynchronizationManager.java new file mode 100644 index 000000000..3d056aa08 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobSynchronizationManager.java @@ -0,0 +1,94 @@ +/* + * 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.scope.context; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; + +/** + * Central convenience class for framework use in managing the job scope + * context. Generally only to be used by implementations of {@link Job}. N.B. + * it is the responsibility of every {@link Job} implementation to ensure that + * a {@link JobContext} is available on every thread that might be involved in + * a job execution, including worker threads from a pool. + * + * @author Dave Syer + * @author Jimmy Praet + */ +public class JobSynchronizationManager { + + private static final SynchronizationManagerSupport synchronizationManager = + new SynchronizationManagerSupport() { + + @Override + protected JobContext createNewContext(JobExecution execution) { + return new JobContext(execution); + } + + @Override + protected void close(JobContext context) { + context.close(); + } + }; + + /* + * We have to deal with single and multi-threaded execution, with a single + * and with multiple job execution instances. That's 2x2 = 4 scenarios. + */ + + /** + * Getter for the current context if there is one, otherwise returns null. + * + * @return the current {@link JobContext} or null if there is none (if one + * has not been registered for this thread). + */ + public static JobContext getContext() { + return synchronizationManager.getContext(); + } + + /** + * Register a context with the current thread - always put a matching {@link #close()} call in a finally block to + * ensure that the correct context is available in the enclosing block. + * + * @param jobExecution the job context to register + * @return a new {@link JobContext} or the current one if it has the same {@link JobExecution} + */ + public static JobContext register(JobExecution jobExecution) { + return synchronizationManager.register(jobExecution); + } + + /** + * Method for de-registering the current context - should always and only be + * used by in conjunction with a matching {@link #register(JobExecution)} to ensure that {@link #getContext()} + * always returns the correct value. + * Does not call {@link JobContext#close()} - that is left up to the caller + * because he has a reference to the context (having registered it) and only + * he has knowledge of when the job actually ended. + */ + public static void close() { + synchronizationManager.close(); + } + + /** + * A convenient "deep" close operation. Call this instead of {@link #close()} if the job execution for the current + * context is ending. + * Delegates to {@link JobContext#close()} and then ensures that {@link #close()} is also called in a finally block. + */ + public static void release() { + synchronizationManager.release(); + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java index 71a6c509f..11f25bf09 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java @@ -15,11 +15,6 @@ */ package org.springframework.batch.core.scope.context; -import java.util.HashMap; -import java.util.Map; -import java.util.Stack; -import java.util.concurrent.atomic.AtomicInteger; - import org.springframework.batch.core.Step; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.jsr.configuration.support.BatchPropertyContext; @@ -37,33 +32,25 @@ import org.springframework.batch.core.jsr.configuration.support.BatchPropertyCon */ public class StepSynchronizationManager { + private static final SynchronizationManagerSupport synchronizationManager = + new SynchronizationManagerSupport() { + + @Override + protected StepContext createNewContext(StepExecution execution) { + return new StepContext(execution); + } + + @Override + protected void close(StepContext context) { + context.close(); + } + }; + /* * We have to deal with single and multi-threaded execution, with a single * and with multiple step execution instances. That's 2x2 = 4 scenarios. */ - /** - * Storage for the current step execution; has to be ThreadLocal because it - * is needed to locate a StepContext in components that are not part of a - * Step (like when re-hydrating a scoped proxy). Doesn't use - * InheritableThreadLocal because there are side effects if a step is trying - * to run multiple child steps (e.g. with partitioning). The Stack is used - * to cover the single threaded case, so that the API is the same as - * multi-threaded. - */ - private static final ThreadLocal> executionHolder = new ThreadLocal>(); - - /** - * Reference counter for each step execution: how many threads are using the - * same one? - */ - private static final Map counts = new HashMap(); - - /** - * Simple map from a running step execution to the associated context. - */ - private static final Map contexts = new HashMap(); - /** * Getter for the current context if there is one, otherwise returns null. * @@ -71,12 +58,7 @@ public class StepSynchronizationManager { * has not been registered for this thread). */ public static StepContext getContext() { - if (getCurrent().isEmpty()) { - return null; - } - synchronized (contexts) { - return contexts.get(getCurrent().peek()); - } + return synchronizationManager.getContext(); } /** @@ -89,20 +71,7 @@ public class StepSynchronizationManager { * {@link StepExecution} */ public static StepContext register(StepExecution stepExecution) { - if (stepExecution == null) { - return null; - } - getCurrent().push(stepExecution); - StepContext context; - synchronized (contexts) { - context = contexts.get(stepExecution); - if (context == null) { - context = new StepContext(stepExecution); - contexts.put(stepExecution, context); - } - } - increment(); - return context; + return synchronizationManager.register(stepExecution); } /** @@ -140,46 +109,7 @@ public class StepSynchronizationManager { * he has knowledge of when the step actually ended. */ public static void close() { - StepContext oldSession = getContext(); - if (oldSession == null) { - return; - } - decrement(); - } - - private static void decrement() { - StepExecution current = getCurrent().pop(); - if (current != null) { - int remaining = counts.get(current).decrementAndGet(); - if (remaining <= 0) { - synchronized (contexts) { - contexts.remove(current); - counts.remove(current); - } - } - } - } - - private static void increment() { - StepExecution current = getCurrent().peek(); - if (current != null) { - AtomicInteger count; - synchronized (counts) { - count = counts.get(current); - if (count == null) { - count = new AtomicInteger(); - counts.put(current, count); - } - } - count.incrementAndGet(); - } - } - - private static Stack getCurrent() { - if (executionHolder.get() == null) { - executionHolder.set(new Stack()); - } - return executionHolder.get(); + synchronizationManager.close(); } /** @@ -189,15 +119,7 @@ public class StepSynchronizationManager { * {@link #close()} is also called in a finally block. */ public static void release() { - StepContext context = getContext(); - try { - if (context != null) { - context.close(); - } - } - finally { - close(); - } + synchronizationManager.release(); } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/SynchronizationManagerSupport.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/SynchronizationManagerSupport.java new file mode 100644 index 000000000..647c01e25 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/SynchronizationManagerSupport.java @@ -0,0 +1,171 @@ +/* + * 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.scope.context; + +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Central convenience class for framework use in managing the scope + * context. + * + * @author Dave Syer + * @author Jimmy Praet + */ +public abstract class SynchronizationManagerSupport { + + /* + * We have to deal with single and multi-threaded execution, with a single + * and with multiple step execution instances. That's 2x2 = 4 scenarios. + */ + + /** + * Storage for the current execution; has to be ThreadLocal because it + * is needed to locate a context in components that are not part of a + * step/job (like when re-hydrating a scoped proxy). Doesn't use + * InheritableThreadLocal because there are side effects if a step is trying + * to run multiple child steps (e.g. with partitioning). The Stack is used + * to cover the single threaded case, so that the API is the same as + * multi-threaded. + */ + private final ThreadLocal> executionHolder = new ThreadLocal>(); + + /** + * Reference counter for each execution: how many threads are using the + * same one? + */ + private final Map counts = new HashMap(); + + /** + * Simple map from a running execution to the associated context. + */ + private final Map contexts = new HashMap(); + + /** + * Getter for the current context if there is one, otherwise returns null. + * + * @return the current context or null if there is none (if one + * has not been registered for this thread). + */ + public C getContext() { + if (getCurrent().isEmpty()) { + return null; + } + synchronized (contexts) { + return contexts.get(getCurrent().peek()); + } + } + + /** + * Register a context with the current thread - always put a matching {@link #close()} call in a finally block to + * ensure that the correct + * context is available in the enclosing block. + * + * @param execution the execution to register + * @return a new context or the current one if it has the same + * execution + */ + public C register(E execution) { + if (execution == null) { + return null; + } + getCurrent().push(execution); + C context; + synchronized (contexts) { + context = contexts.get(execution); + if (context == null) { + context = createNewContext(execution); + contexts.put(execution, context); + } + } + increment(); + return context; + } + + /** + * Method for de-registering the current context - should always and only be + * used by in conjunction with a matching {@link #register(E)} to ensure that {@link #getContext()} always returns + * the correct value. + * Does not call close on the context - that is left up to the caller + * because he has a reference to the context (having registered it) and only + * he has knowledge of when the execution actually ended. + */ + public void close() { + C oldSession = getContext(); + if (oldSession == null) { + return; + } + decrement(); + } + + private void decrement() { + E current = getCurrent().pop(); + if (current != null) { + int remaining = counts.get(current).decrementAndGet(); + if (remaining <= 0) { + synchronized (contexts) { + contexts.remove(current); + counts.remove(current); + } + } + } + } + + private void increment() { + E current = getCurrent().peek(); + if (current != null) { + AtomicInteger count; + synchronized (counts) { + count = counts.get(current); + if (count == null) { + count = new AtomicInteger(); + counts.put(current, count); + } + } + count.incrementAndGet(); + } + } + + private Stack getCurrent() { + if (executionHolder.get() == null) { + executionHolder.set(new Stack()); + } + return executionHolder.get(); + } + + /** + * A convenient "deep" close operation. Call this instead of {@link #close()} if the execution for the current + * context is ending. + * Delegates to {@link context#close()} and then ensures that {@link #close()} is also called in a finally block. + */ + public void release() { + C context = getContext(); + try { + if (context != null) { + close(context); + } + } finally { + close(); + } + } + + protected abstract void close(C context); + + protected abstract C createNewContext(E execution); + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/JobContextFactory.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/JobContextFactory.java new file mode 100644 index 000000000..7e5fe1fcc --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/JobContextFactory.java @@ -0,0 +1,40 @@ +/* + * 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.scope.util; + +import org.springframework.batch.core.scope.context.JobContext; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; + +/** + * Implementation of {@link ContextFactory} that provides the current + * {@link JobContext} as a context object. + * + * @author Dave Syer + * @author Jimmy Praet + */ +public class JobContextFactory implements ContextFactory { + + public Object getContext() { + return JobSynchronizationManager.getContext(); + } + + public String getContextId() { + JobContext context = JobSynchronizationManager.getContext(); + return context!=null ? (String) context.getId() : "sysinit"; + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeTests.java new file mode 100644 index 000000000..7bd8fe1f1 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2006-2009 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.configuration.xml; + +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.batch.core.scope.JobScope; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + + +/** + * @author Thomas Risberg + * @author Jimmy Praet + */ +public class AutoRegisteringJobScopeTests { + + @Test + public void testJobElement() throws Exception { + ConfigurableApplicationContext ctx = + new ClassPathXmlApplicationContext( + "org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForJobElementTests-context.xml"); + @SuppressWarnings("unchecked") + Map beans = ctx.getBeansOfType(JobScope.class); + assertTrue("JobScope not defined properly", beans.size() == 1); + } + + @Test + public void testStepElement() throws Exception { + ConfigurableApplicationContext ctx = + new ClassPathXmlApplicationContext( + "org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForStepElementTests-context.xml"); + @SuppressWarnings("unchecked") + Map beans = ctx.getBeansOfType(JobScope.class); + assertTrue("JobScope not defined properly", beans.size() == 1); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests.java new file mode 100644 index 000000000..7c56d4d88 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests.java @@ -0,0 +1,150 @@ +package org.springframework.batch.core.scope; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.scope.context.JobContext; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class AsyncJobScopeIntegrationTests implements BeanFactoryAware { + + private Log logger = LogFactory.getLog(getClass()); + + @Autowired + @Qualifier("simple") + private Collaborator simple; + + private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + + private ListableBeanFactory beanFactory; + + private int beanCount; + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (ListableBeanFactory) beanFactory; + } + + @Before + public void countBeans() { + JobSynchronizationManager.release(); + beanCount = beanFactory.getBeanDefinitionCount(); + } + + @After + public void cleanUp() { + JobSynchronizationManager.close(); + // Check that all temporary bean definitions are cleaned up + assertEquals(beanCount, beanFactory.getBeanDefinitionCount()); + } + + @Test + public void testSimpleProperty() throws Exception { + JobExecution jobExecution = new JobExecution(11L); + ExecutionContext executionContext = jobExecution.getExecutionContext(); + executionContext.put("foo", "bar"); + JobSynchronizationManager.register(jobExecution); + assertEquals("bar", simple.getName()); + } + + @Test + public void testGetMultipleInMultipleThreads() throws Exception { + + List> tasks = new ArrayList>(); + + for (int i = 0; i < 12; i++) { + final String value = "foo" + i; + final Long id = 123L + i; + FutureTask task = new FutureTask(new Callable() { + public String call() throws Exception { + JobExecution jobExecution = new JobExecution(id); + ExecutionContext executionContext = jobExecution.getExecutionContext(); + executionContext.put("foo", value); + JobContext context = JobSynchronizationManager.register(jobExecution); + logger.debug("Registered: " + context.getJobExecutionContext()); + try { + return simple.getName(); + } + finally { + JobSynchronizationManager.close(); + } + } + }); + tasks.add(task); + taskExecutor.execute(task); + } + + int i = 0; + for (FutureTask task : tasks) { + assertEquals("foo" + i, task.get()); + i++; + } + + } + + @Test + public void testGetSameInMultipleThreads() throws Exception { + + List> tasks = new ArrayList>(); + final JobExecution jobExecution = new JobExecution(11L); + ExecutionContext executionContext = jobExecution.getExecutionContext(); + executionContext.put("foo", "foo"); + JobSynchronizationManager.register(jobExecution); + assertEquals("foo", simple.getName()); + + for (int i = 0; i < 12; i++) { + final String value = "foo" + i; + FutureTask task = new FutureTask(new Callable() { + public String call() throws Exception { + ExecutionContext executionContext = jobExecution.getExecutionContext(); + executionContext.put("foo", value); + JobContext context = JobSynchronizationManager.register(jobExecution); + logger.debug("Registered: " + context.getJobExecutionContext()); + try { + return simple.getName(); + } + finally { + JobSynchronizationManager.close(); + } + } + }); + tasks.add(task); + taskExecutor.execute(task); + } + + int i = 0; + for (FutureTask task : tasks) { + assertEquals("foo", task.get()); + i++; + } + + // Don't close the outer scope until all tasks are finished. This should + // always be the case if using an AbstractJob + JobSynchronizationManager.close(); + + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests.java new file mode 100644 index 000000000..79af85f9f --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests.java @@ -0,0 +1,87 @@ +package org.springframework.batch.core.scope; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.StringUtils; + +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class JobScopeDestructionCallbackIntegrationTests { + + @Autowired + @Qualifier("proxied") + private Job proxied; + + @Autowired + @Qualifier("nested") + private Job nested; + + @Autowired + @Qualifier("ref") + private Job ref; + + @Autowired + @Qualifier("foo") + private Collaborator foo; + + @Before + @After + public void resetMessage() throws Exception { + TestDisposableCollaborator.message = "none"; + TestAdvice.names.clear(); + } + + @Test + public void testDisposableScopedProxy() throws Exception { + assertNotNull(proxied); + proxied.execute(new JobExecution(1L)); + assertEquals(1, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "destroyed")); + } + + @Test + public void testDisposableInnerScopedProxy() throws Exception { + assertNotNull(nested); + nested.execute(new JobExecution(1L)); + assertEquals(1, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "destroyed")); + } + + @Test + public void testProxiedScopedProxy() throws Exception { + assertNotNull(nested); + nested.execute(new JobExecution(1L)); + assertEquals(4, TestAdvice.names.size()); + assertEquals("bar", TestAdvice.names.get(0)); + assertEquals(1, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "destroyed")); + } + + @Test + public void testRefScopedProxy() throws Exception { + assertNotNull(ref); + ref.execute(new JobExecution(1L)); + assertEquals(4, TestAdvice.names.size()); + assertEquals("spam", TestAdvice.names.get(0)); + assertEquals(2, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "destroyed")); + assertEquals(1, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "bar:destroyed")); + assertEquals(1, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "spam:destroyed")); + } + + @Test + public void testProxiedNormalBean() throws Exception { + assertNotNull(nested); + String name = foo.getName(); + assertEquals(1, TestAdvice.names.size()); + assertEquals(name, TestAdvice.names.get(0)); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeIntegrationTests.java new file mode 100644 index 000000000..cffe4eafe --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeIntegrationTests.java @@ -0,0 +1,115 @@ +package org.springframework.batch.core.scope; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class JobScopeIntegrationTests { + + @Autowired + @Qualifier("vanilla") + private Job vanilla; + + @Autowired + @Qualifier("proxied") + private Job proxied; + + @Autowired + @Qualifier("nested") + private Job nested; + + @Autowired + @Qualifier("enhanced") + private Job enhanced; + + @Autowired + @Qualifier("double") + private Job doubleEnhanced; + + @Before + @After + public void start() { + JobSynchronizationManager.close(); + TestJob.reset(); + } + + @Test + public void testScopeCreation() throws Exception { + vanilla.execute(new JobExecution(11L)); + assertNotNull(TestJob.getContext()); + assertNull(JobSynchronizationManager.getContext()); + } + + @Test + public void testScopedProxy() throws Exception { + proxied.execute(new JobExecution(11L)); + assertTrue(TestJob.getContext().attributeNames().length > 0); + String collaborator = (String) TestJob.getContext().getAttribute("collaborator"); + assertNotNull(collaborator); + assertEquals("bar", collaborator); + assertTrue("Scoped proxy not created", ((String) TestJob.getContext().getAttribute("collaborator.class")) + .startsWith("class $Proxy")); + } + + @Test + public void testNestedScopedProxy() throws Exception { + nested.execute(new JobExecution(11L)); + assertTrue(TestJob.getContext().attributeNames().length > 0); + String collaborator = (String) TestJob.getContext().getAttribute("collaborator"); + assertNotNull(collaborator); + assertEquals("foo", collaborator); + String parent = (String) TestJob.getContext().getAttribute("parent"); + assertNotNull(parent); + assertEquals("bar", parent); + assertTrue("Scoped proxy not created", ((String) TestJob.getContext().getAttribute("parent.class")) + .startsWith("class $Proxy")); + } + + @Test + public void testExecutionContext() throws Exception { + JobExecution stepExecution = new JobExecution(11L); + ExecutionContext executionContext = new ExecutionContext(); + executionContext.put("name", "spam"); + stepExecution.setExecutionContext(executionContext); + proxied.execute(stepExecution); + assertTrue(TestJob.getContext().attributeNames().length > 0); + String collaborator = (String) TestJob.getContext().getAttribute("collaborator"); + assertNotNull(collaborator); + assertEquals("bar", collaborator); + } + + @Test + public void testScopedProxyForReference() throws Exception { + enhanced.execute(new JobExecution(11L)); + assertTrue(TestJob.getContext().attributeNames().length > 0); + String collaborator = (String) TestJob.getContext().getAttribute("collaborator"); + assertNotNull(collaborator); + assertEquals("bar", collaborator); + } + + @Test + public void testScopedProxyForSecondReference() throws Exception { + doubleEnhanced.execute(new JobExecution(11L)); + assertTrue(TestJob.getContext().attributeNames().length > 0); + String collaborator = (String) TestJob.getContext().getAttribute("collaborator"); + assertNotNull(collaborator); + assertEquals("bar", collaborator); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests.java new file mode 100644 index 000000000..1ca9c8465 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests.java @@ -0,0 +1,33 @@ +package org.springframework.batch.core.scope; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.Job; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + + +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class JobScopeNestedIntegrationTests { + + @Autowired + @Qualifier("proxied") + private Job proxied; + + @Autowired + @Qualifier("parent") + private Collaborator parent; + + @Test + public void testNestedScopedProxy() throws Exception { + assertNotNull(proxied); + assertEquals("foo", parent.getName()); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests.java new file mode 100644 index 000000000..0df49e8c4 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests.java @@ -0,0 +1,155 @@ +package org.springframework.batch.core.scope; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class JobScopePlaceholderIntegrationTests implements BeanFactoryAware { + + @Autowired + @Qualifier("simple") + private Collaborator simple; + + @Autowired + @Qualifier("compound") + private Collaborator compound; + + @Autowired + @Qualifier("value") + private Collaborator value; + + @Autowired + @Qualifier("ref") + private Collaborator ref; + + @Autowired + @Qualifier("scopedRef") + private Collaborator scopedRef; + + @Autowired + @Qualifier("list") + private Collaborator list; + + @Autowired + @Qualifier("bar") + private Collaborator bar; + + @Autowired + @Qualifier("nested") + private Collaborator nested; + + private JobExecution jobExecution; + + private ListableBeanFactory beanFactory; + + private int beanCount; + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (ListableBeanFactory) beanFactory; + } + + @Before + public void start() { + start("bar"); + } + + private void start(String foo) { + + JobSynchronizationManager.close(); + jobExecution = new JobExecution(123L); + + ExecutionContext executionContext = new ExecutionContext(); + executionContext.put("foo", foo); + executionContext.put("parent", bar); + + jobExecution.setExecutionContext(executionContext); + JobSynchronizationManager.register(jobExecution); + + beanCount = beanFactory.getBeanDefinitionCount(); + + } + + @After + public void stop() { + JobSynchronizationManager.close(); + // Check that all temporary bean definitions are cleaned up + assertEquals(beanCount, beanFactory.getBeanDefinitionCount()); + } + + @Test + public void testSimpleProperty() throws Exception { + assertEquals("bar", simple.getName()); + // Once the job context is set up it should be baked into the proxies + // so changing it now should have no effect + jobExecution.getExecutionContext().put("foo", "wrong!"); + assertEquals("bar", simple.getName()); + } + + @Test + public void testCompoundProperty() throws Exception { + assertEquals("bar-bar", compound.getName()); + } + + @Test + public void testCompoundPropertyTwice() throws Exception { + + assertEquals("bar-bar", compound.getName()); + + JobSynchronizationManager.close(); + jobExecution = new JobExecution(123L); + + ExecutionContext executionContext = new ExecutionContext(); + executionContext.put("foo", "spam"); + + jobExecution.setExecutionContext(executionContext); + JobSynchronizationManager.register(jobExecution); + + assertEquals("spam-bar", compound.getName()); + + } + + @Test + public void testParentByRef() throws Exception { + assertEquals("bar", ref.getParent().getName()); + } + + @Test + public void testParentByValue() throws Exception { + assertEquals("bar", value.getParent().getName()); + } + + @Test + public void testList() throws Exception { + assertEquals("[bar]", list.getList().toString()); + } + + @Test + public void testNested() throws Exception { + assertEquals("bar", nested.getParent().getName()); + } + + @Test + public void testScopedRef() throws Exception { + assertEquals("bar", scopedRef.getParent().getName()); + stop(); + start("spam"); + assertEquals("spam", scopedRef.getParent().getName()); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests.java new file mode 100644 index 000000000..2f23d70d8 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests.java @@ -0,0 +1,71 @@ +package org.springframework.batch.core.scope; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class JobScopeProxyTargetClassIntegrationTests implements BeanFactoryAware { + + @Autowired + @Qualifier("simple") + private TestCollaborator simple; + + private JobExecution jobExecution; + + private ListableBeanFactory beanFactory; + + private int beanCount; + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (ListableBeanFactory) beanFactory; + } + + @Before + public void start() { + + JobSynchronizationManager.close(); + jobExecution = new JobExecution(123L); + + ExecutionContext executionContext = new ExecutionContext(); + executionContext.put("foo", "bar"); + + jobExecution.setExecutionContext(executionContext); + JobSynchronizationManager.register(jobExecution); + + beanCount = beanFactory.getBeanDefinitionCount(); + + } + + @After + public void cleanUp() { + JobSynchronizationManager.close(); + // Check that all temporary bean definitions are cleaned up + assertEquals(beanCount, beanFactory.getBeanDefinitionCount()); + } + + @Test + public void testSimpleProperty() throws Exception { + assertEquals("bar", simple.getName()); + // Once the job context is set up it should be baked into the proxies + // so changing it now should have no effect + jobExecution.getExecutionContext().put("foo", "wrong!"); + assertEquals("bar", simple.getName()); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests.java new file mode 100644 index 000000000..e096de058 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests.java @@ -0,0 +1,17 @@ +package org.springframework.batch.core.scope; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + + +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class JobScopeStartupIntegrationTests { + + @Test + public void testScopedProxyDuringStartup() throws Exception { + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeTests.java new file mode 100644 index 000000000..f62598154 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeTests.java @@ -0,0 +1,168 @@ +/* + * 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.scope; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.scope.context.JobContext; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.context.support.StaticApplicationContext; + +/** + * @author Dave Syer + * @author Jimmy Praet + */ +public class JobScopeTests { + + private JobScope scope = new JobScope(); + + private JobExecution jobExecution = new JobExecution(0L); + + private JobContext context; + + @Before + public void setUp() throws Exception { + context = JobSynchronizationManager.register(jobExecution); + } + + @After + public void tearDown() throws Exception { + JobSynchronizationManager.release(); + } + + @Test + public void testGetWithNoContext() throws Exception { + final String foo = "bar"; + JobSynchronizationManager.release(); + try { + scope.get("foo", new ObjectFactory() { + public Object getObject() throws BeansException { + return foo; + } + }); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + + } + + @Test + public void testGetWithNothingAlreadyThere() { + final String foo = "bar"; + Object value = scope.get("foo", new ObjectFactory() { + public Object getObject() throws BeansException { + return foo; + } + }); + assertEquals(foo, value); + assertTrue(context.hasAttribute("foo")); + } + + @Test + public void testGetWithSomethingAlreadyThere() { + context.setAttribute("foo", "bar"); + Object value = scope.get("foo", new ObjectFactory() { + public Object getObject() throws BeansException { + return null; + } + }); + assertEquals("bar", value); + assertTrue(context.hasAttribute("foo")); + } + + @Test + public void testGetConversationId() { + String id = scope.getConversationId(); + assertNotNull(id); + } + + @Test + public void testRegisterDestructionCallback() { + final List list = new ArrayList(); + context.setAttribute("foo", "bar"); + scope.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("foo"); + } + }); + assertEquals(0, list.size()); + // When the context is closed, provided the attribute exists the + // callback is called... + context.close(); + assertEquals(1, list.size()); + } + + @Test + public void testRegisterAnotherDestructionCallback() { + final List list = new ArrayList(); + context.setAttribute("foo", "bar"); + scope.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("foo"); + } + }); + scope.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + } + }); + assertEquals(0, list.size()); + // When the context is closed, provided the attribute exists the + // callback is called... + context.close(); + assertEquals(2, list.size()); + } + + @Test + public void testRemove() { + context.setAttribute("foo", "bar"); + scope.remove("foo"); + assertFalse(context.hasAttribute("foo")); + } + + @Test + public void testOrder() throws Exception { + assertEquals(Integer.MAX_VALUE, scope.getOrder()); + scope.setOrder(11); + assertEquals(11, scope.getOrder()); + } + + @Test + public void testName() throws Exception { + scope.setName("foo"); + StaticApplicationContext beanFactory = new StaticApplicationContext(); + scope.postProcessBeanFactory(beanFactory.getDefaultListableBeanFactory()); + String[] scopes = beanFactory.getDefaultListableBeanFactory().getRegisteredScopeNames(); + assertEquals(1, scopes.length); + assertEquals("foo", scopes[0]); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobStartupRunner.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobStartupRunner.java index d67b72a15..30fb1c6fd 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobStartupRunner.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobStartupRunner.java @@ -1,22 +1,21 @@ package org.springframework.batch.core.scope; +import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.StepExecution; import org.springframework.beans.factory.InitializingBean; public class JobStartupRunner implements InitializingBean { - private Step step; + private Job job; - public void setStep(Step step) { - this.step = step; + public void setJob(Job job) { + this.job = job; } @Override public void afterPropertiesSet() throws Exception { - StepExecution stepExecution = new StepExecution("step", new JobExecution(1L), 0L); - step.execute(stepExecution); + JobExecution jobExecution = new JobExecution(11L); + job.execute(jobExecution); // expect no errors } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepStartupRunner.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepStartupRunner.java new file mode 100644 index 000000000..87876c6c7 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepStartupRunner.java @@ -0,0 +1,22 @@ +package org.springframework.batch.core.scope; + +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.StepExecution; +import org.springframework.beans.factory.InitializingBean; + +public class StepStartupRunner implements InitializingBean { + + private Step step; + + public void setStep(Step step) { + this.step = step; + } + + public void afterPropertiesSet() throws Exception { + StepExecution stepExecution = new StepExecution("step", new JobExecution(1L), 0L); + step.execute(stepExecution); + // expect no errors + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/TestJob.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/TestJob.java new file mode 100644 index 000000000..f28b374bd --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/TestJob.java @@ -0,0 +1,61 @@ +package org.springframework.batch.core.scope; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParametersIncrementer; +import org.springframework.batch.core.JobParametersValidator; +import org.springframework.batch.core.scope.context.JobContext; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; + +public class TestJob implements Job { + + private static JobContext context; + + private Collaborator collaborator; + + public void setCollaborator(Collaborator collaborator) { + this.collaborator = collaborator; + } + + public static JobContext getContext() { + return context; + } + + public static void reset() { + context = null; + } + + public void execute(JobExecution stepExecution) { + context = JobSynchronizationManager.getContext(); + setContextFromCollaborator(); + stepExecution.getExecutionContext().put("foo", "changed but it shouldn't affect the collaborator"); + setContextFromCollaborator(); + } + + private void setContextFromCollaborator() { + if (context != null) { + context.setAttribute("collaborator", collaborator.getName()); + context.setAttribute("collaborator.class", collaborator.getClass().toString()); + if (collaborator.getParent()!=null) { + context.setAttribute("parent", collaborator.getParent().getName()); + context.setAttribute("parent.class", collaborator.getParent().getClass().toString()); + } + } + } + + public String getName() { + return "foo"; + } + + public boolean isRestartable() { + return false; + } + + public JobParametersIncrementer getJobParametersIncrementer() { + return null; + } + + public JobParametersValidator getJobParametersValidator() { + return null; + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobContextTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobContextTests.java new file mode 100644 index 000000000..ed50c3e0b --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobContextTests.java @@ -0,0 +1,173 @@ +/* + * 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.scope.context; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobInstance; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.item.ExecutionContext; + +/** + * @author Dave Syer + * @author Jimmy Praet + */ +public class JobContextTests { + + private List list = new ArrayList(); + + private JobExecution jobExecution = new JobExecution(new JobInstance(2L, null, "job"), 1L); + + private JobContext context = new JobContext(jobExecution); + + @Test + public void testGetJobExecution() { + context = new JobContext(jobExecution); + assertNotNull(context.getJobExecution()); + } + + @Test + public void testNullJobExecution() { + try { + context = new JobContext(null); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testEqualsSelf() { + assertEquals(context, context); + } + + @Test + public void testNotEqualsNull() { + assertFalse(context.equals(null)); + } + + @Test + public void testEqualsContextWithSameJobExecution() { + assertEquals(new JobContext(jobExecution), context); + } + + @Test + public void testDestructionCallbackSunnyDay() throws Exception { + context.setAttribute("foo", "FOO"); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + } + }); + context.close(); + assertEquals(1, list.size()); + assertEquals("bar", list.get(0)); + } + + @Test + public void testDestructionCallbackMissingAttribute() throws Exception { + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + } + }); + context.close(); + // Yes the callback should be called even if the attribute is missing - + // for inner beans + assertEquals(1, list.size()); + } + + @Test + public void testDestructionCallbackWithException() throws Exception { + context.setAttribute("foo", "FOO"); + context.setAttribute("bar", "BAR"); + context.registerDestructionCallback("bar", new Runnable() { + public void run() { + list.add("spam"); + throw new RuntimeException("fail!"); + } + }); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + throw new RuntimeException("fail!"); + } + }); + try { + context.close(); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + // We don't care which one was thrown... + assertEquals("fail!", e.getMessage()); + } + // ...but we do care that both were executed: + assertEquals(2, list.size()); + assertTrue(list.contains("bar")); + assertTrue(list.contains("spam")); + } + + @Test + public void testJobName() throws Exception { + assertEquals("job", context.getJobName()); + } + + @Test + public void testJobExecutionContext() throws Exception { + ExecutionContext executionContext = jobExecution.getExecutionContext(); + executionContext.put("foo", "bar"); + assertEquals("bar", context.getJobExecutionContext().get("foo")); + } + + @Test + public void testSystemProperties() throws Exception { + System.setProperty("foo", "bar"); + assertEquals("bar", context.getSystemProperties().getProperty("foo")); + } + + @Test + public void testJobParameters() throws Exception { + JobParameters jobParameters = new JobParametersBuilder().addString("foo", "bar").toJobParameters(); + JobInstance jobInstance = new JobInstance(0L, jobParameters, "foo"); + jobExecution.setJobInstance(jobInstance); + assertEquals("bar", context.getJobParameters().get("foo")); + } + + @Test + public void testContextId() throws Exception { + assertEquals("jobExecution#1", context.getId()); + } + + @Test(expected = IllegalStateException.class) + public void testIllegalContextId() throws Exception { + JobParameters jobParameters = new JobParametersBuilder().addString("foo", "bar").toJobParameters(); + JobInstance jobInstance = new JobInstance(0L, jobParameters, "foo"); + context = new JobContext(new JobExecution(jobInstance)); + context.getId(); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobSynchronizationManagerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobSynchronizationManagerTests.java new file mode 100644 index 000000000..7ab4d6008 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobSynchronizationManagerTests.java @@ -0,0 +1,133 @@ +package org.springframework.batch.core.scope.context; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.batch.core.JobExecution; +import org.springframework.util.ReflectionUtils; + +/** + * JobSynchronizationManagerTests. + * + * @author Jimmy Praet + */ +public class JobSynchronizationManagerTests { + + private JobExecution jobExecution = new JobExecution(0L); + + @Before + @After + public void start() { + while (JobSynchronizationManager.getContext() != null) { + JobSynchronizationManager.close(); + } + } + + @Test + public void testGetContext() { + assertNull(JobSynchronizationManager.getContext()); + JobSynchronizationManager.register(jobExecution); + assertNotNull(JobSynchronizationManager.getContext()); + } + + @Test + public void testClose() throws Exception { + final List list = new ArrayList(); + JobContext context = JobSynchronizationManager.register(jobExecution); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("foo"); + } + }); + JobSynchronizationManager.close(); + assertNull(JobSynchronizationManager.getContext()); + assertEquals(0, list.size()); + // check for possible memory leak + assertEquals(0, extractStaticMap("counts").size()); + assertEquals(0, extractStaticMap("contexts").size()); + } + + private Map extractStaticMap(String name) throws IllegalAccessException { + Field field = ReflectionUtils.findField(JobSynchronizationManager.class, "synchronizationManager"); + ReflectionUtils.makeAccessible(field); + SynchronizationManagerSupport synchronizationManager = + (SynchronizationManagerSupport) field.get(JobSynchronizationManager.class); + field = ReflectionUtils.findField(SynchronizationManagerSupport.class, name); + ReflectionUtils.makeAccessible(field); + Map map = (Map) field.get(synchronizationManager); + return map; + } + @Test + public void testMultithreaded() throws Exception { + JobContext context = JobSynchronizationManager.register(jobExecution); + ExecutorService executorService = Executors.newFixedThreadPool(2); + FutureTask task = new FutureTask(new Callable() { + public JobContext call() throws Exception { + try { + JobSynchronizationManager.register(jobExecution); + JobContext context = JobSynchronizationManager.getContext(); + context.setAttribute("foo", "bar"); + return context; + } + finally { + JobSynchronizationManager.close(); + } + } + }); + executorService.execute(task); + executorService.awaitTermination(1, TimeUnit.SECONDS); + assertEquals(context.attributeNames().length, task.get().attributeNames().length); + JobSynchronizationManager.close(); + assertNull(JobSynchronizationManager.getContext()); + } + + @Test + public void testRelease() { + JobContext context = JobSynchronizationManager.register(jobExecution); + final List list = new ArrayList(); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("foo"); + } + }); + // On release we expect the destruction callbacks to be called + JobSynchronizationManager.release(); + assertNull(JobSynchronizationManager.getContext()); + assertEquals(1, list.size()); + } + + @Test + public void testRegisterNull() { + assertNull(JobSynchronizationManager.getContext()); + JobSynchronizationManager.register(null); + assertNull(JobSynchronizationManager.getContext()); + } + + @Test + public void testRegisterTwice() { + JobSynchronizationManager.register(jobExecution); + JobSynchronizationManager.register(jobExecution); + JobSynchronizationManager.close(); + // if someone registers you have to assume they are going to close, so + // the last thing you want is for the close to remove another context + // that someone else has registered + assertNotNull(JobSynchronizationManager.getContext()); + JobSynchronizationManager.close(); + assertNull(JobSynchronizationManager.getContext()); + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/StepSynchronizationManagerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/StepSynchronizationManagerTests.java index 1a4836373..5c277bf5d 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/StepSynchronizationManagerTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/StepSynchronizationManagerTests.java @@ -86,9 +86,13 @@ public class StepSynchronizationManagerTests { } private Map extractStaticMap(String name) throws IllegalAccessException { - Field field = ReflectionUtils.findField(StepSynchronizationManager.class, name); + Field field = ReflectionUtils.findField(StepSynchronizationManager.class, "synchronizationManager"); ReflectionUtils.makeAccessible(field); - Map map = (Map) field.get(StepSynchronizationManager.class); + SynchronizationManagerSupport synchronizationManager = + (SynchronizationManagerSupport) field.get(StepSynchronizationManager.class); + field = ReflectionUtils.findField(SynchronizationManagerSupport.class, name); + ReflectionUtils.makeAccessible(field); + Map map = (Map) field.get(synchronizationManager); return map; } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/JobContextFactoryTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/JobContextFactoryTests.java new file mode 100644 index 000000000..97622691d --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/JobContextFactoryTests.java @@ -0,0 +1,38 @@ +package org.springframework.batch.core.scope.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import org.junit.After; +import org.junit.Test; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.scope.context.JobContext; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; + +public class JobContextFactoryTests { + + private JobContextFactory factory = new JobContextFactory(); + + @After + public void cleanUp() { + JobSynchronizationManager.close(); + JobSynchronizationManager.close(); + } + + @Test + public void testGetContext() { + JobExecution jobExecution = new JobExecution(11L); + JobContext context = JobSynchronizationManager.register(jobExecution); + assertEquals(context, factory.getContext()); + } + + @Test + public void testGetContextId() { + JobSynchronizationManager.register(new JobExecution(11L)); + Object id1 = factory.getContextId(); + JobSynchronizationManager.register(new JobExecution(12L)); + Object id2 = factory.getContextId(); + assertFalse(id2.equals(id1)); + } + +} diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForJobElementTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForJobElementTests-context.xml new file mode 100644 index 000000000..093b8dfd0 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForJobElementTests-context.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForStepElementTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForStepElementTests-context.xml new file mode 100644 index 000000000..1d906944c --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForStepElementTests-context.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests-context.xml new file mode 100644 index 000000000..9b88a4e42 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests-context.xml new file mode 100644 index 000000000..e839d3c77 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests-context.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeIntegrationTests-context.xml new file mode 100644 index 000000000..a6e50fb3b --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeIntegrationTests-context.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests-context.xml new file mode 100644 index 000000000..438be3d1f --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests-context.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests-context.xml new file mode 100644 index 000000000..526fa6c15 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests-context.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #{jobExecutionContext[foo]} + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests-context.xml new file mode 100644 index 000000000..ac9c2d66a --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests-context.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests-context.xml new file mode 100644 index 000000000..c5a32b6de --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests-context.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/StepScopeStartupIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/StepScopeStartupIntegrationTests-context.xml index 3162bba5b..b2b1614a0 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/StepScopeStartupIntegrationTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/StepScopeStartupIntegrationTests-context.xml @@ -11,7 +11,7 @@ - + diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestExecutionListener.java b/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestExecutionListener.java new file mode 100644 index 000000000..685ff91c6 --- /dev/null +++ b/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestExecutionListener.java @@ -0,0 +1,181 @@ +/* + * Copyright 2006-2010 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.test; + +import java.lang.reflect.Method; + +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.scope.context.JobContext; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; +import org.springframework.batch.item.adapter.HippyMethodInvoker; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodCallback; + +/** + * A {@link TestExecutionListener} that sets up job-scope context for + * dependency injection into unit tests. A {@link JobContext} will be created + * for the duration of a test method and made available to any dependencies that + * are injected. The default behaviour is just to create a {@link JobExecution} with fixed properties. Alternatively it + * can be provided by the test case as a + * factory methods returning the correct type. Example: + * + *
+ * @ContextConfiguration
+ * @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, JobScopeTestExecutionListener.class })
+ * @RunWith(SpringJUnit4ClassRunner.class)
+ * public class JobScopeTestExecutionListenerIntegrationTests {
+ * 
+ * 	// A job-scoped dependency configured in the ApplicationContext
+ * 	@Autowired
+ * 	private ItemReader<String> reader;
+ * 
+ * 	public JobExecution getJobExecution() {
+ * 		JobExecution execution = MetaDataInstanceFactory.createJobExecution();
+ * 		execution.getExecutionContext().putString("foo", "bar");
+ * 		return execution;
+ * 	}
+ * 
+ * 	@Test
+ * 	public void testJobScopedReader() {
+ * 		// Job context is active here so the reader can be used,
+ * 		// and the job execution context will contain foo=bar...
+ * 		assertNotNull(reader.read());
+ * 	}
+ * 
+ * }
+ * 
+ * + * @author Dave Syer + * @author Jimmy Praet + */ +public class JobScopeTestExecutionListener implements TestExecutionListener { + + private static final String JOB_EXECUTION = JobScopeTestExecutionListener.class.getName() + ".JOB_EXECUTION"; + + /** + * Set up a {@link JobExecution} as a test context attribute. + * + * @param testContext the current test context + * @throws Exception if there is a problem + * @see TestExecutionListener#prepareTestInstance(TestContext) + */ + public void prepareTestInstance(TestContext testContext) throws Exception { + JobExecution jobExecution = getJobExecution(testContext); + if (jobExecution != null) { + testContext.setAttribute(JOB_EXECUTION, jobExecution); + } + } + + /** + * @param testContext the current test context + * @throws Exception if there is a problem + * @see TestExecutionListener#beforeTestMethod(TestContext) + */ + public void beforeTestMethod(org.springframework.test.context.TestContext testContext) throws Exception { + if (testContext.hasAttribute(JOB_EXECUTION)) { + JobExecution jobExecution = (JobExecution) testContext.getAttribute(JOB_EXECUTION); + JobSynchronizationManager.register(jobExecution); + } + + } + + /** + * @param testContext the current test context + * @throws Exception if there is a problem + * @see TestExecutionListener#afterTestMethod(TestContext) + */ + public void afterTestMethod(TestContext testContext) throws Exception { + if (testContext.hasAttribute(JOB_EXECUTION)) { + JobSynchronizationManager.close(); + } + } + + /* + * Support for Spring 3.0 (empty). + */ + public void afterTestClass(TestContext testContext) throws Exception { + } + + /* + * Support for Spring 3.0 (empty). + */ + public void beforeTestClass(TestContext testContext) throws Exception { + } + + /** + * Discover a {@link JobExecution} as a field in the test case or create + * one if none is available. + * + * @param testContext the current test context + * @return a {@link JobExecution} + */ + protected JobExecution getJobExecution(TestContext testContext) { + + Object target = testContext.getTestInstance(); + + ExtractorMethodCallback method = new ExtractorMethodCallback(JobExecution.class, "getJobExecution"); + ReflectionUtils.doWithMethods(target.getClass(), method); + if (method.getName() != null) { + HippyMethodInvoker invoker = new HippyMethodInvoker(); + invoker.setTargetObject(target); + invoker.setTargetMethod(method.getName()); + try { + invoker.prepare(); + return (JobExecution) invoker.invoke(); + } + catch (Exception e) { + throw new IllegalArgumentException("Could not create job execution from method: " + method.getName(), + e); + } + } + + return MetaDataInstanceFactory.createJobExecution(); + } + + /** + * Look for a method returning the type provided, preferring one with the + * name provided. + */ + private final class ExtractorMethodCallback implements MethodCallback { + private String preferredName; + + private final Class preferredType; + + private Method result; + + public ExtractorMethodCallback(Class preferredType, String preferredName) { + super(); + this.preferredType = preferredType; + this.preferredName = preferredName; + } + + public String getName() { + return result == null ? null : result.getName(); + } + + public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + Class type = method.getReturnType(); + if (preferredType.isAssignableFrom(type)) { + if (result == null || method.getName().equals(preferredName)) { + result = method; + } + } + } + } + +} diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestUtils.java b/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestUtils.java new file mode 100644 index 000000000..baa63f0fb --- /dev/null +++ b/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright 2006-2010 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.test; + +import java.util.concurrent.Callable; + +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.scope.JobScope; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; + +/** + * Utility class for creating and manipulating {@link JobScope} in unit tests. + * This is useful when you want to use the Spring test support and inject + * dependencies into your test case that happen to be job scoped in the + * application context. + * + * @author Dave Syer + * @author Jimmy Praet + */ +public class JobScopeTestUtils { + + public static T doInJobScope(JobExecution jobExecution, Callable callable) throws Exception { + try { + JobSynchronizationManager.register(jobExecution); + return callable.call(); + } + finally { + JobSynchronizationManager.close(); + } + } + +} diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests.java new file mode 100644 index 000000000..5678a82b9 --- /dev/null +++ b/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests.java @@ -0,0 +1,48 @@ +package org.springframework.batch.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.ItemStream; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; + +/** + * @author Dave Syer + * @since 2.1 + */ +@ContextConfiguration +@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, JobScopeTestExecutionListener.class }) +@RunWith(SpringJUnit4ClassRunner.class) +public class JobScopeTestExecutionListenerIntegrationTests { + + @Autowired + private ItemReader reader; + + @Autowired + private ItemStream stream; + + public JobExecution getJobExection() { + // Assert that dependencies are already injected... + assertNotNull(reader); + // Then create the execution for the job scope... + JobExecution execution = MetaDataInstanceFactory.createJobExecution(); + execution.getExecutionContext().putString("input.file", "classpath:/org/springframework/batch/test/simple.txt"); + return execution; + } + + @Test + public void testJob() throws Exception { + stream.open(new ExecutionContext()); + assertEquals("foo", reader.read()); + } + +} diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerTests.java new file mode 100644 index 000000000..3b38a5262 --- /dev/null +++ b/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerTests.java @@ -0,0 +1,103 @@ +package org.springframework.batch.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Test; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.scope.context.JobContext; +import org.springframework.batch.core.scope.context.JobSynchronizationManager; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestContextManager; + +/** + * @author Dave Syer + * @since 2.1 + */ +@ContextConfiguration +public class JobScopeTestExecutionListenerTests { + + private JobScopeTestExecutionListener listener = new JobScopeTestExecutionListener(); + + @Test + public void testDefaultJobContext() throws Exception { + TestContext testContext = getTestContext(new Object()); + listener.prepareTestInstance(testContext); + listener.beforeTestMethod(testContext); + JobContext context = JobSynchronizationManager.getContext(); + assertNotNull(context); + listener.afterTestMethod(testContext); + assertNull(JobSynchronizationManager.getContext()); + } + + @Test + public void testWithJobExecutionFactory() throws Exception { + testExecutionContext(new WithJobExecutionFactory()); + } + + @Test + public void testWithParameters() throws Exception { + testJobParameters(new WithJobExecutionFactory()); + } + + private void testExecutionContext(Object target) throws Exception { + TestContext testContext = getTestContext(target); + listener.prepareTestInstance(testContext); + try { + listener.beforeTestMethod(testContext); + JobContext context = JobSynchronizationManager.getContext(); + assertNotNull(context); + assertEquals("bar", context.getJobExecutionContext().get("foo")); + } + finally { + listener.afterTestMethod(testContext); + } + assertNull(JobSynchronizationManager.getContext()); + } + + private void testJobParameters(Object target) throws Exception { + TestContext testContext = getTestContext(target); + listener.prepareTestInstance(testContext); + try { + listener.beforeTestMethod(testContext); + JobContext context = JobSynchronizationManager.getContext(); + assertNotNull(context); + assertEquals("spam", context.getJobParameters().get("foo")); + } + finally { + listener.afterTestMethod(testContext); + } + assertNull(JobSynchronizationManager.getContext()); + } + + @SuppressWarnings("unused") + private static class WithJobExecutionFactory { + public JobExecution getJobExecution() { + JobExecution jobExecution = MetaDataInstanceFactory.createJobExecution("job", 11L, 123L, + new JobParametersBuilder().addString("foo", "spam").toJobParameters()); + jobExecution.getExecutionContext().putString("foo", "bar"); + return jobExecution; + } + } + + private TestContext getTestContext(Object target) throws Exception { + return new MockTestContextManager(target, getClass()).getContext(); + } + + private final class MockTestContextManager extends TestContextManager { + + private MockTestContextManager(Object target, Class testClass) throws Exception { + super(testClass); + prepareTestInstance(target); + } + + public TestContext getContext() { + return getTestContext(); + } + + } + +} diff --git a/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests-context.xml b/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests-context.xml new file mode 100644 index 000000000..3323c84a5 --- /dev/null +++ b/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests-context.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerTests-context.xml b/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerTests-context.xml new file mode 100644 index 000000000..cacc056e3 --- /dev/null +++ b/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerTests-context.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file