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