Skip to content

Commit 2436d84

Browse files
jpraetmminella
authored andcommitted
BATCH-1701: Introduce job scope
1 parent e5fc066 commit 2436d84

File tree

43 files changed

+3142
-336
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3142
-336
lines changed

spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ public class CoreNamespaceUtils {
4343

4444
private static final String STEP_SCOPE_PROCESSOR_CLASS_NAME = "org.springframework.batch.core.scope.StepScope";
4545

46+
private static final String JOB_SCOPE_PROCESSOR_BEAN_NAME = "org.springframework.batch.core.scope.internalJobScope";
47+
48+
private static final String JOB_SCOPE_PROCESSOR_CLASS_NAME = "org.springframework.batch.core.scope.JobScope";
49+
4650
private static final String CUSTOM_EDITOR_CONFIGURER_CLASS_NAME = "org.springframework.beans.factory.config.CustomEditorConfigurer";
4751

4852
private static final String RANGE_ARRAY_CLASS_NAME = "org.springframework.batch.item.file.transform.Range[]";
@@ -53,28 +57,38 @@ public class CoreNamespaceUtils {
5357

5458
public static void autoregisterBeansForNamespace(ParserContext parserContext, Object source) {
5559
checkForStepScope(parserContext, source);
60+
checkForJobScope(parserContext, source);
5661
addRangePropertyEditor(parserContext);
5762
addCoreNamespacePostProcessor(parserContext);
5863
addStateTransitionComparator(parserContext);
5964
}
6065

6166
private static void checkForStepScope(ParserContext parserContext, Object source) {
62-
boolean foundStepScope = false;
67+
checkForScope(parserContext, source, STEP_SCOPE_PROCESSOR_CLASS_NAME, STEP_SCOPE_PROCESSOR_BEAN_NAME);
68+
}
69+
70+
private static void checkForJobScope(ParserContext parserContext, Object source) {
71+
checkForScope(parserContext, source, JOB_SCOPE_PROCESSOR_CLASS_NAME, JOB_SCOPE_PROCESSOR_BEAN_NAME);
72+
}
73+
74+
private static void checkForScope(ParserContext parserContext, Object source, String scopeClassName,
75+
String scopeBeanName) {
76+
boolean foundScope = false;
6377
String[] beanNames = parserContext.getRegistry().getBeanDefinitionNames();
6478
for (String beanName : beanNames) {
6579
BeanDefinition bd = parserContext.getRegistry().getBeanDefinition(beanName);
66-
if (STEP_SCOPE_PROCESSOR_CLASS_NAME.equals(bd.getBeanClassName())) {
67-
foundStepScope = true;
80+
if (scopeClassName.equals(bd.getBeanClassName())) {
81+
foundScope = true;
6882
break;
6983
}
7084
}
71-
if (!foundStepScope) {
85+
if (!foundScope) {
7286
BeanDefinitionBuilder stepScopeBuilder = BeanDefinitionBuilder
73-
.genericBeanDefinition(STEP_SCOPE_PROCESSOR_CLASS_NAME);
87+
.genericBeanDefinition(scopeClassName);
7488
AbstractBeanDefinition abd = stepScopeBuilder.getBeanDefinition();
7589
abd.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
7690
abd.setSource(source);
77-
parserContext.getRegistry().registerBeanDefinition(STEP_SCOPE_PROCESSOR_BEAN_NAME, abd);
91+
parserContext.getRegistry().registerBeanDefinition(scopeBeanName, abd);
7892
}
7993
}
8094

spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.batch.core.listener.CompositeJobExecutionListener;
3939
import org.springframework.batch.core.repository.JobRepository;
4040
import org.springframework.batch.core.repository.JobRestartException;
41+
import org.springframework.batch.core.scope.context.JobSynchronizationManager;
4142
import org.springframework.batch.core.step.StepLocator;
4243
import org.springframework.batch.repeat.RepeatException;
4344
import org.springframework.beans.factory.BeanNameAware;
@@ -286,6 +287,8 @@ public final void execute(JobExecution execution) {
286287

287288
logger.debug("Job execution starting: " + execution);
288289

290+
JobSynchronizationManager.register(execution);
291+
289292
try {
290293

291294
jobParametersValidator.validate(execution.getJobParameters());
@@ -328,25 +331,28 @@ public final void execute(JobExecution execution) {
328331
execution.setStatus(BatchStatus.FAILED);
329332
execution.addFailureException(t);
330333
} finally {
334+
try {
335+
if (execution.getStatus().isLessThanOrEqualTo(BatchStatus.STOPPED)
336+
&& execution.getStepExecutions().isEmpty()) {
337+
ExitStatus exitStatus = execution.getExitStatus();
338+
execution
339+
.setExitStatus(exitStatus.and(ExitStatus.NOOP
340+
.addExitDescription("All steps already completed or no steps configured for this job.")));
341+
}
331342

332-
if (execution.getStatus().isLessThanOrEqualTo(BatchStatus.STOPPED)
333-
&& execution.getStepExecutions().isEmpty()) {
334-
ExitStatus exitStatus = execution.getExitStatus();
335-
execution
336-
.setExitStatus(exitStatus.and(ExitStatus.NOOP
337-
.addExitDescription("All steps already completed or no steps configured for this job.")));
338-
}
343+
execution.setEndTime(new Date());
339344

340-
execution.setEndTime(new Date());
345+
try {
346+
listener.afterJob(execution);
347+
} catch (Exception e) {
348+
logger.error("Exception encountered in afterStep callback", e);
349+
}
341350

342-
try {
343-
listener.afterJob(execution);
344-
} catch (Exception e) {
345-
logger.error("Exception encountered in afterStep callback", e);
351+
jobRepository.update(execution);
352+
} finally {
353+
JobSynchronizationManager.release();
346354
}
347355

348-
jobRepository.update(execution);
349-
350356
}
351357

352358
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2006-2007 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.scope;
17+
18+
import org.apache.commons.logging.Log;
19+
import org.apache.commons.logging.LogFactory;
20+
import org.springframework.batch.core.scope.context.JobContext;
21+
import org.springframework.batch.core.scope.context.JobSynchronizationManager;
22+
import org.springframework.batch.core.scope.util.ContextFactory;
23+
import org.springframework.batch.core.scope.util.JobContextFactory;
24+
import org.springframework.beans.BeanWrapper;
25+
import org.springframework.beans.BeanWrapperImpl;
26+
import org.springframework.beans.factory.ObjectFactory;
27+
import org.springframework.beans.factory.config.Scope;
28+
29+
/**
30+
* Scope for job context. Objects in this scope use the Spring container as an
31+
* object factory, so there is only one instance of such a bean per executing
32+
* job. All objects in this scope are <aop:scoped-proxy/> (no need to
33+
* decorate the bean definitions).<br/>
34+
* <br/>
35+
*
36+
* In addition, support is provided for late binding of references accessible
37+
* from the {@link JobContext} using #{..} placeholders. Using this feature,
38+
* bean properties can be pulled from the job or job execution context and the
39+
* job parameters. E.g.
40+
*
41+
* <pre>
42+
* &lt;bean id=&quot;...&quot; class=&quot;...&quot; scope=&quot;job&quot;&gt;
43+
* &lt;property name=&quot;name&quot; value=&quot;#{jobParameters[input]}&quot; /&gt;
44+
* &lt;/bean&gt;
45+
*
46+
* &lt;bean id=&quot;...&quot; class=&quot;...&quot; scope=&quot;job&quot;&gt;
47+
* &lt;property name=&quot;name&quot; value=&quot;#{jobExecutionContext['input.stem']}.txt&quot; /&gt;
48+
* &lt;/bean&gt;
49+
* </pre>
50+
*
51+
* The {@link JobContext} is referenced using standard bean property paths (as
52+
* per {@link BeanWrapper}). The examples above all show the use of the Map
53+
* accessors provided as a convenience for job attributes.
54+
*
55+
* @author Dave Syer
56+
* @author Jimmy Praet (create JobScope based on {@link StepScope})
57+
* @since 2.0
58+
*/
59+
public class JobScope extends ScopeSupport {
60+
61+
private Log logger = LogFactory.getLog(getClass());
62+
63+
private final Object mutex = new Object();
64+
65+
/**
66+
* Context key for clients to use for conversation identifier.
67+
*/
68+
public static final String ID_KEY = "JOB_IDENTIFIER";
69+
70+
/**
71+
* The ContextFactory.
72+
*/
73+
private static final ContextFactory CONTEXT_FACTORY = new JobContextFactory();
74+
75+
public JobScope() {
76+
super("job", CONTEXT_FACTORY);
77+
}
78+
79+
/**
80+
* If Spring 3.0 is available, this will be used to resolve expressions in
81+
* job-scoped beans. This method is part of the Scope SPI in Spring 3.0,
82+
* but should just be ignored by earlier versions of Spring.
83+
*/
84+
public Object resolveContextualObject(String key) {
85+
JobContext context = getContext();
86+
// TODO: support for attributes as well maybe (setters not exposed yet
87+
// so not urgent).
88+
return new BeanWrapperImpl(context).getPropertyValue(key);
89+
}
90+
91+
/**
92+
* @see Scope#get(String, ObjectFactory)
93+
*/
94+
public Object get(String name, ObjectFactory objectFactory) {
95+
96+
JobContext context = getContext();
97+
Object scopedObject = context.getAttribute(name);
98+
99+
if (scopedObject == null) {
100+
101+
synchronized (mutex) {
102+
scopedObject = context.getAttribute(name);
103+
if (scopedObject == null) {
104+
105+
logger.debug(String.format("Creating object in scope=%s, name=%s", this.getName(), name));
106+
107+
scopedObject = objectFactory.getObject();
108+
context.setAttribute(name, scopedObject);
109+
110+
}
111+
112+
}
113+
114+
}
115+
return scopedObject;
116+
}
117+
118+
/**
119+
* @see Scope#getConversationId()
120+
*/
121+
public String getConversationId() {
122+
JobContext context = getContext();
123+
return context.getId();
124+
}
125+
126+
/**
127+
* @see Scope#registerDestructionCallback(String, Runnable)
128+
*/
129+
public void registerDestructionCallback(String name, Runnable callback) {
130+
JobContext context = getContext();
131+
logger.debug(String.format("Registered destruction callback in scope=%s, name=%s", this.getName(), name));
132+
context.registerDestructionCallback(name, callback);
133+
}
134+
135+
/**
136+
* @see Scope#remove(String)
137+
*/
138+
public Object remove(String name) {
139+
JobContext context = getContext();
140+
logger.debug(String.format("Removing from scope=%s, name=%s", this.getName(), name));
141+
return context.removeAttribute(name);
142+
}
143+
144+
/**
145+
* Get an attribute accessor in the form of a {@link JobContext} that can
146+
* be used to store scoped bean instances.
147+
*
148+
* @return the current job context which we can use as a scope storage
149+
* medium
150+
*/
151+
private JobContext getContext() {
152+
JobContext context = JobSynchronizationManager.getContext();
153+
if (context == null) {
154+
throw new IllegalStateException("No context holder available for job scope");
155+
}
156+
return context;
157+
}
158+
159+
}

0 commit comments

Comments
 (0)