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.StepContext; |
21 | import org.springframework.batch.core.scope.context.StepSynchronizationManager; |
22 | import org.springframework.batch.core.scope.util.PlaceholderProxyFactoryBean; |
23 | import org.springframework.beans.BeanWrapper; |
24 | import org.springframework.beans.BeansException; |
25 | import org.springframework.beans.factory.ObjectFactory; |
26 | import org.springframework.beans.factory.config.BeanDefinition; |
27 | import org.springframework.beans.factory.config.BeanDefinitionHolder; |
28 | import org.springframework.beans.factory.config.BeanDefinitionVisitor; |
29 | import org.springframework.beans.factory.config.BeanFactoryPostProcessor; |
30 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; |
31 | import org.springframework.beans.factory.config.Scope; |
32 | import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; |
33 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; |
34 | import org.springframework.core.Ordered; |
35 | import org.springframework.util.Assert; |
36 | import org.springframework.util.StringValueResolver; |
37 | |
38 | /** |
39 | * Scope for step context. Objects in this scope use the Spring container as an |
40 | * object factory, so there is only one instance of such a bean per executing |
41 | * step. All objects in this scope are <aop:scoped-proxy/> (no need to |
42 | * decorate the bean definitions).<br/> |
43 | * <br/> |
44 | * |
45 | * In addition, support is provided for late binding of references accessible |
46 | * from the {@link StepContext} using #{..} placeholders. Using this feature, |
47 | * bean properties can be pulled from the step or job execution context and the |
48 | * job parameters. E.g. |
49 | * |
50 | * <pre> |
51 | * <bean id="..." class="..." scope="step"> |
52 | * <property name="parent" ref="#{stepExecutionContext[helper]}" /> |
53 | * </bean> |
54 | * |
55 | * <bean id="..." class="..." scope="step"> |
56 | * <property name="name" value="#{stepExecutionContext['input.name']}" /> |
57 | * </bean> |
58 | * |
59 | * <bean id="..." class="..." scope="step"> |
60 | * <property name="name" value="#{jobParameters[input]}" /> |
61 | * </bean> |
62 | * |
63 | * <bean id="..." class="..." scope="step"> |
64 | * <property name="name" value="#{jobExecutionContext['input.stem']}.txt" /> |
65 | * </bean> |
66 | * </pre> |
67 | * |
68 | * The {@link StepContext} is referenced using standard bean property paths (as |
69 | * per {@link BeanWrapper}). The examples above all show the use of the Map |
70 | * accessors provided as a convenience for step and job attributes. |
71 | * |
72 | * @author Dave Syer |
73 | * @since 2.0 |
74 | */ |
75 | public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { |
76 | |
77 | private Log logger = LogFactory.getLog(getClass()); |
78 | |
79 | private int order = Ordered.LOWEST_PRECEDENCE; |
80 | |
81 | private final Object mutex = new Object(); |
82 | |
83 | /** |
84 | * @param order the order value to set priority of callback execution for |
85 | * the {@link BeanFactoryPostProcessor} part of this scope bean. |
86 | */ |
87 | public void setOrder(int order) { |
88 | this.order = order; |
89 | } |
90 | |
91 | public int getOrder() { |
92 | return order; |
93 | } |
94 | |
95 | /** |
96 | * Context key for clients to use for conversation identifier. |
97 | */ |
98 | public static final String ID_KEY = "STEP_IDENTIFIER"; |
99 | |
100 | private String name = "step"; |
101 | |
102 | private boolean proxyTargetClass = false; |
103 | |
104 | /** |
105 | * Flag to indicate that proxies should use dynamic subclassing. This allows |
106 | * classes with no interface to be proxied. Defaults to false. |
107 | * |
108 | * @param proxyTargetClass set to true to have proxies created using dynamic |
109 | * subclasses |
110 | */ |
111 | public void setProxyTargetClass(boolean proxyTargetClass) { |
112 | this.proxyTargetClass = proxyTargetClass; |
113 | } |
114 | |
115 | // Implement missing method from Spring 3.0 SPI. |
116 | public Object resolveContextualObject(String key) { |
117 | StepContext context = getContext(); |
118 | return context.getAttribute(key); |
119 | } |
120 | |
121 | /** |
122 | * @see Scope#get(String, ObjectFactory) |
123 | */ |
124 | public Object get(String name, ObjectFactory objectFactory) { |
125 | |
126 | StepContext context = getContext(); |
127 | Object scopedObject = context.getAttribute(name); |
128 | |
129 | if (scopedObject == null) { |
130 | |
131 | synchronized (mutex) { |
132 | scopedObject = context.getAttribute(name); |
133 | if (scopedObject == null) { |
134 | |
135 | logger.debug(String.format("Creating object in scope=%s, name=%s", this.name, name)); |
136 | |
137 | scopedObject = objectFactory.getObject(); |
138 | context.setAttribute(name, scopedObject); |
139 | |
140 | } |
141 | |
142 | } |
143 | |
144 | } |
145 | return scopedObject; |
146 | } |
147 | |
148 | /** |
149 | * @see Scope#getConversationId() |
150 | */ |
151 | public String getConversationId() { |
152 | StepContext context = getContext(); |
153 | return context.getId(); |
154 | } |
155 | |
156 | /** |
157 | * @see Scope#registerDestructionCallback(String, Runnable) |
158 | */ |
159 | public void registerDestructionCallback(String name, Runnable callback) { |
160 | StepContext context = getContext(); |
161 | logger.debug(String.format("Registered destruction callback in scope=%s, name=%s", this.name, name)); |
162 | context.registerDestructionCallback(name, callback); |
163 | } |
164 | |
165 | /** |
166 | * @see Scope#remove(String) |
167 | */ |
168 | public Object remove(String name) { |
169 | StepContext context = getContext(); |
170 | logger.debug(String.format("Removing from scope=%s, name=%s", this.name, name)); |
171 | return context.removeAttribute(name); |
172 | } |
173 | |
174 | /** |
175 | * Get an attribute accessor in the form of a {@link StepContext} that can |
176 | * be used to store scoped bean instances. |
177 | * |
178 | * @return the current step context which we can use as a scope storage |
179 | * medium |
180 | */ |
181 | private StepContext getContext() { |
182 | StepContext context = StepSynchronizationManager.getContext(); |
183 | if (context == null) { |
184 | throw new IllegalStateException("No context holder available for step scope"); |
185 | } |
186 | return context; |
187 | } |
188 | |
189 | /** |
190 | * Register this scope with the enclosing BeanFactory. |
191 | * |
192 | * @see BeanFactoryPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory) |
193 | * |
194 | * @param beanFactory the BeanFactory to register with |
195 | * @throws BeansException if there is a problem. |
196 | */ |
197 | public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { |
198 | |
199 | beanFactory.registerScope(name, this); |
200 | |
201 | Assert.state(beanFactory instanceof BeanDefinitionRegistry, |
202 | "BeanFactory was not a BeanDefinitionRegistry, so StepScope cannot be used."); |
203 | BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; |
204 | |
205 | for (String beanName : beanFactory.getBeanDefinitionNames()) { |
206 | BeanDefinition definition = beanFactory.getBeanDefinition(beanName); |
207 | // Replace this or any of its inner beans with scoped proxy if it |
208 | // has this scope |
209 | boolean scoped = name.equals(definition.getScope()); |
210 | Scopifier scopifier = new Scopifier(registry, name, proxyTargetClass, scoped); |
211 | scopifier.visitBeanDefinition(definition); |
212 | if (scoped) { |
213 | new ExpressionHider(name, scoped).visitBeanDefinition(definition); |
214 | createScopedProxy(beanName, definition, registry, proxyTargetClass); |
215 | } |
216 | } |
217 | |
218 | } |
219 | |
220 | /** |
221 | * Public setter for the name property. This can then be used as a bean |
222 | * definition attribute, e.g. scope="step". Defaults to "step". |
223 | * |
224 | * @param name the name to set for this scope. |
225 | */ |
226 | public void setName(String name) { |
227 | this.name = name; |
228 | } |
229 | |
230 | /** |
231 | * Wrap a target bean definition in a proxy that defers initialization until |
232 | * after the {@link StepContext} is available. Amounts to adding |
233 | * <aop-auto-proxy/> to a step scoped bean. Also if Spring EL is not |
234 | * available will enable a weak version of late binding as described in the |
235 | * class-level docs. |
236 | * |
237 | * @param beanName the bean name to replace |
238 | * @param definition the bean definition to replace |
239 | * @param registry the enclosing {@link BeanDefinitionRegistry} |
240 | * @param proxyTargetClass true if we need to force use of dynamic |
241 | * subclasses |
242 | * @return a {@link BeanDefinitionHolder} for the new representation of the |
243 | * target. Caller should register it if needed to be visible at top level in |
244 | * bean factory. |
245 | */ |
246 | private static BeanDefinitionHolder createScopedProxy(String beanName, BeanDefinition definition, |
247 | BeanDefinitionRegistry registry, boolean proxyTargetClass) { |
248 | |
249 | // TODO: (for Batch 2.1) detect presence of Spring 3.0 and use |
250 | // ScopedProxyUtils instead |
251 | |
252 | // Create the scoped proxy... |
253 | BeanDefinitionHolder proxyHolder = PlaceholderProxyFactoryBean.createScopedProxy(new BeanDefinitionHolder( |
254 | definition, beanName), registry, proxyTargetClass); |
255 | // ...and register it under the original target name |
256 | registry.registerBeanDefinition(beanName, proxyHolder.getBeanDefinition()); |
257 | |
258 | return proxyHolder; |
259 | |
260 | } |
261 | |
262 | /** |
263 | * Helper class to scan a bean definition hierarchy and force the use of |
264 | * auto-proxy for step scoped beans. |
265 | * |
266 | * @author Dave Syer |
267 | * |
268 | */ |
269 | private static class Scopifier extends BeanDefinitionVisitor { |
270 | |
271 | private final boolean proxyTargetClass; |
272 | |
273 | private final BeanDefinitionRegistry registry; |
274 | |
275 | private final String scope; |
276 | |
277 | private final boolean scoped; |
278 | |
279 | public Scopifier(BeanDefinitionRegistry registry, String scope, boolean proxyTargetClass, boolean scoped) { |
280 | super(new StringValueResolver() { |
281 | public String resolveStringValue(String value) { |
282 | return value; |
283 | } |
284 | }); |
285 | this.registry = registry; |
286 | this.proxyTargetClass = proxyTargetClass; |
287 | this.scope = scope; |
288 | this.scoped = scoped; |
289 | } |
290 | |
291 | @Override |
292 | protected Object resolveValue(Object value) { |
293 | |
294 | BeanDefinition definition = null; |
295 | String beanName = null; |
296 | if (value instanceof BeanDefinition) { |
297 | definition = (BeanDefinition) value; |
298 | beanName = BeanDefinitionReaderUtils.generateBeanName(definition, registry); |
299 | } |
300 | else if (value instanceof BeanDefinitionHolder) { |
301 | BeanDefinitionHolder holder = (BeanDefinitionHolder) value; |
302 | definition = holder.getBeanDefinition(); |
303 | beanName = holder.getBeanName(); |
304 | } |
305 | |
306 | if (definition != null) { |
307 | boolean nestedScoped = scope.equals(definition.getScope()); |
308 | boolean scopeChangeRequiresProxy = !scoped && nestedScoped; |
309 | new ExpressionHider(scope, nestedScoped).visitBeanDefinition(definition); |
310 | if (scopeChangeRequiresProxy) { |
311 | // Exit here so that nested inner bean definitions are not |
312 | // analysed |
313 | return createScopedProxy(beanName, definition, registry, proxyTargetClass); |
314 | } |
315 | } |
316 | |
317 | // Nested inner bean definitions are recursively analysed here |
318 | value = super.resolveValue(value); |
319 | return value; |
320 | |
321 | } |
322 | |
323 | } |
324 | |
325 | /** |
326 | * Helper class to scan a bean definition hierarchy and hide placeholders |
327 | * from Spring EL. |
328 | * |
329 | * @author Dave Syer |
330 | * |
331 | */ |
332 | private static class ExpressionHider extends BeanDefinitionVisitor { |
333 | |
334 | private static final String PLACEHOLDER_PREFIX = "#{"; |
335 | |
336 | private static final String PLACEHOLDER_SUFFIX = "}"; |
337 | |
338 | private static final String REPLACEMENT_PREFIX = "%{"; |
339 | |
340 | private final String scope; |
341 | |
342 | private final boolean scoped; |
343 | |
344 | private ExpressionHider(String scope, final boolean scoped) { |
345 | super(new StringValueResolver() { |
346 | public String resolveStringValue(String value) { |
347 | if (scoped && value.contains(PLACEHOLDER_PREFIX) && value.contains(PLACEHOLDER_SUFFIX)) { |
348 | value = value.replace(PLACEHOLDER_PREFIX, REPLACEMENT_PREFIX); |
349 | } |
350 | return value; |
351 | } |
352 | }); |
353 | this.scope = scope; |
354 | this.scoped = scoped; |
355 | } |
356 | |
357 | @Override |
358 | protected Object resolveValue(Object value) { |
359 | BeanDefinition definition = null; |
360 | if (value instanceof BeanDefinition) { |
361 | definition = (BeanDefinition) value; |
362 | } |
363 | else if (value instanceof BeanDefinitionHolder) { |
364 | BeanDefinitionHolder holder = (BeanDefinitionHolder) value; |
365 | definition = holder.getBeanDefinition(); |
366 | } |
367 | if (definition != null) { |
368 | boolean scopeChange = !scope.equals(definition.getScope()); |
369 | if (scopeChange) { |
370 | new ExpressionHider(definition.getScope(), !scoped).visitBeanDefinition(definition); |
371 | // Exit here so that nested inner bean definitions are not |
372 | // analysed by both vistors |
373 | return value; |
374 | } |
375 | } |
376 | // Nested inner bean definitions are recursively analysed here |
377 | value = super.resolveValue(value); |
378 | return value; |
379 | } |
380 | |
381 | } |
382 | |
383 | } |