View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.orchestra.conversation.spring;
21  
22  import org.aopalliance.aop.Advice;
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  import org.apache.myfaces.orchestra.conversation.Conversation;
26  import org.apache.myfaces.orchestra.conversation.ConversationAware;
27  import org.apache.myfaces.orchestra.conversation.ConversationBindingEvent;
28  import org.apache.myfaces.orchestra.conversation.ConversationBindingListener;
29  import org.apache.myfaces.orchestra.conversation.ConversationContext;
30  import org.apache.myfaces.orchestra.conversation.ConversationFactory;
31  import org.apache.myfaces.orchestra.conversation.ConversationManager;
32  import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
33  import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
34  import org.springframework.aop.scope.ScopedProxyFactoryBean;
35  import org.springframework.beans.BeansException;
36  import org.springframework.beans.factory.BeanFactory;
37  import org.springframework.beans.factory.BeanFactoryAware;
38  import org.springframework.beans.factory.ObjectFactory;
39  import org.springframework.beans.factory.config.BeanDefinition;
40  import org.springframework.beans.factory.config.BeanPostProcessor;
41  import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
42  import org.springframework.beans.factory.config.Scope;
43  import org.springframework.context.ApplicationContext;
44  import org.springframework.context.ApplicationContextAware;
45  import org.springframework.context.ConfigurableApplicationContext;
46  
47  import java.util.HashMap;
48  import java.util.Map;
49  
50  /**
51   * Abstract basis class for all the Orchestra scopes.
52   * <p>
53   * A scope object has two quite different roles:
54   * <ol>
55   * <li>It handles the lookup of beans in a scope, and creates them if needed</li>
56   * <li>It handles the creation of Conversation objects, using the spring properties
57   * configured on the scope object.</li>
58   * </ol>
59   * <p>
60   * This base class handles item 1 above, and leaves item 2 to a subclass. The
61   * declaration of interface ConversationFactory needs to be on this class, however,
62   * as the createBean method needs to invoke it.
63   */
64  public abstract class AbstractSpringOrchestraScope implements ConversationFactory,
65      Scope, BeanFactoryAware, ApplicationContextAware
66  {
67      private final Log log = LogFactory.getLog(AbstractSpringOrchestraScope.class);
68      private static final String POST_PROCESSOR_BEAN_NAME =
69          AbstractSpringOrchestraScope.class.getName() + "_BeanPostProcessor";
70  
71      private ConfigurableApplicationContext applicationContext;
72      private Advice[] advices;
73      private boolean autoProxy = true;
74  
75      public AbstractSpringOrchestraScope()
76      {
77      }
78  
79      /**
80       * The advices (interceptors) which will be applied to the conversation scoped bean.
81       * These are applied whenever a method is invoked on the bean [1].
82       * <p>
83       * An application's spring configuration uses this method to control what advices are
84       * applied to beans generated from this scope. One commonly applied advice is the
85       * Orchestra persistence interceptor, which ensures that whenever a method on a
86       * conversation-scoped bean is invoked the "global persistence context" is set
87       * to the context for the conversation that bean is in.
88       * <p>
89       * Note [1]: the advices are only applied when the bean is invoked via its proxy. If
90       * invoked via the "this" pointer of the object the interceptors will not run. This
91       * is generally a good thing, as they are not wanted when a method on the bean invokes
92       * another method on the same bean. However it is bad if the bean passes "this" as a
93       * parameter to some other object that makes a callback on it at some later time. In
94       * that case, the bean must take care to pass its proxy to the remote object, not
95       * itself. See method ConversationUtils.getCurrentBean().
96       */
97      public void setAdvices(Advice[] advices)
98      {
99          this.advices = advices;
100     }
101 
102     /**
103      * @since 1.1
104      */
105     protected Advice[] getAdvices()
106     {
107         return advices;
108     }
109 
110     /**
111      * Configuration for a scope object to control whether "scoped proxy" objects are
112      * automatically wrapped around each conversation bean.
113      * <p>
114      * Automatically applying scope proxies solves a lot of tricky problems with "stale"
115      * beans, and should generally be used. However it does require CGLIB to be present
116      * in the classpath. It also can impact performance in some cases. Where this is a
117      * problem, this flag can turn autoproxying off. Note that the standard spring
118      * aop:scoped-proxy bean can then be used on individual beans to re-enable
119      * proxying for specific beans if desired.
120      * <p>
121      * This defaults to true.
122      *
123      * @since 1.1
124      */
125     public void setAutoProxy(boolean state)
126     {
127         autoProxy = state;
128     }
129 
130     /**
131      * Return the conversation context id.
132      * <p>
133      * Note: This conversationId is something spring requires. It has nothing to do with the Orchestra
134      * conversation id.
135      * <p>
136      * TODO: what does Spring use this for????
137      */
138     public String getConversationId()
139     {
140         ConversationManager manager = ConversationManager.getInstance();
141         ConversationContext ctx = manager.getCurrentConversationContext();
142         if (ctx != null)
143         {
144             return Long.toString(ctx.getId(), 10);
145         }
146 
147         return null;
148     }
149 
150     /**
151      * This is invoked by Spring whenever someone calls getBean(name) on a bean-factory
152      * and the bean-definition for that bean has a scope attribute that maps to an
153      * instance of this class.
154      * <p>
155      * In the normal case, this method returns an internally-created proxy object
156      * that fetches the "real" bean every time a method is invoked on the proxy
157      * (see method getRealBean). This does obviously have some performance impact.
158      * However it is necessary when beans from one conversation are referencing beans
159      * from another conversation as the conversation lifetimes are not the same;
160      * without this proxying there are many cases where "stale" references end up
161      * being used. Most references to conversation-scoped objects are made via EL
162      * expressions, and in this case the performance impact is not significant
163      * relative to the overhead of EL. Note that there is one case where this
164      * proxying is not "transparent" to user code: if a proxied object passes a
165      * "this" pointer to a longer-lived object that retains that pointer then
166      * that reference can be "stale", as it points directly to an instance rather
167      * than to the proxy.
168      * <p>
169      * When the Spring aop:scoped-proxy feature is applied to conversation-scoped
170      * beans, then this functionality is disabled as aop:scoped-proxy has a very
171      * similar effect. Therefore, when this method detects that it has been invoked
172      * by a proxy object generated by aop:scoped-proxy then it returns the real
173      * object (see getRealBean) rather than another proxy. Using aop:scoped-proxy
174      * is somewhat less efficient than Orchestra's customised proxying.
175      * <p>
176      * And when the orchestra proxy needs to access the real object, it won't
177      * call this method; instead, getRealBean is called directly. See class
178      * ScopedBeanTargetSource.
179      */
180     public Object get(String name, ObjectFactory objectFactory)
181     {
182         if (log.isDebugEnabled())
183         {
184             log.debug("Method get called for bean " + name);
185         }
186 
187         if (_SpringUtils.isModifiedBeanName(name))
188         {
189             // Backwards compatibility with aop:scoped-proxy tag.
190             //
191             // The user must have included an aop:scoped-proxy within the bean definition,
192             // and here the proxy is firing to try to get the underlying bean. In this
193             // case, return a non-proxied instance of the referenced bean.
194             try
195             {
196                 String originalBeanName = _SpringUtils.getOriginalBeanName(name);
197                 String conversationName = getConversationNameForBean(name);
198                 return getRealBean(conversationName, originalBeanName, objectFactory);
199             }
200             catch(RuntimeException e)
201             {
202                 log.error("Exception while accessing bean '" + name + "'");
203                 throw e;
204             }
205         }
206         else if (!autoProxy)
207         {
208             String conversationName = getConversationNameForBean(name);
209             return getRealBean(conversationName, name, objectFactory);
210         }
211         else
212         {
213             // A call has been made by the user to the Spring getBean method
214             // (directly, or via an EL expression). Or the bean is being fetched
215             // as part of spring injection into another object.
216             //
217             // In all these cases, just return a proxy.
218             return getProxy(name, objectFactory);
219         }
220     }
221 
222     /**
223      * Return a CGLIB-generated proxy class for the beanclass that is
224      * specified by the provided beanName.
225      * <p>
226      * When any method is invoked on this proxy, it invokes method
227      * getRealBean on this same instance in order to locate a proper
228      * instance, then forwards the method call to it.
229      * <p>
230      * There is a separate proxy instance per beandef (shared across all
231      * instances of that bean). This instance is created when first needed,
232      * and cached for reuse.
233      *
234      * @since 1.1
235      */
236     protected Object getProxy(String beanName, ObjectFactory objectFactory)
237     {
238         if (log.isDebugEnabled())
239         {
240             log.debug("getProxy called for bean " + beanName);
241         }
242 
243         BeanDefinition beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName);
244         String conversationName = getConversationNameForBean(beanName);
245 
246         // deal with proxies required for multiple conversations.
247         // This is required to make the viewController scope work where proxies are
248         // required for each conversation a bean has been requested.
249         Map proxies = (Map) beanDefinition.getAttribute(ScopedBeanTargetSource.class.getName());
250         if (proxies == null)
251         {
252             proxies = new HashMap();
253             beanDefinition.setAttribute(ScopedBeanTargetSource.class.getName(), proxies);
254         }
255 
256         Object proxy = proxies.get(conversationName);
257         if (proxy == null)
258         {
259             if (log.isDebugEnabled())
260             {
261                 log.debug("getProxy: creating proxy for " + beanName);
262             }
263             BeanFactory beanFactory = applicationContext.getBeanFactory();
264             proxy = _SpringUtils.newProxy(this, conversationName, beanName, objectFactory, beanFactory);
265             proxies.put(conversationName, proxy);
266         }
267 
268         // Register the proxy in req scope. The first lookup of a variable using an EL expression during a
269         // request will therefore take the "long" path through JSF's VariableResolver and Spring to get here.
270         // But later lookups of this variable in the same request find the proxy directly in the request scope.
271         // The proxy could potentially be placed in the session or app scope, as there is just one instance
272         // for this spring context, and there is normally only one spring context for a webapp. However
273         // using the request scope is almost as efficient and seems safer.
274         //
275         // Note that the framework adapter might not be initialised during the Spring context initialisation
276         // phase (ie instantiation of singletons during startup), so just skip registration in those cases.
277         FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
278         if (fa != null)
279         {
280             fa.setRequestAttribute(beanName, proxy);
281         }
282 
283 
284         return proxy;
285     }
286 
287     /**
288      * Get a real bean instance (not a scoped-proxy).
289      * <p>
290      * The appropriate Conversation is retrieved; if it does not yet exist then
291      * it is created and started. The conversation name is either specified on the
292      * bean-definition via a custom attribute, or defaults to the bean name.
293      * <p>
294      * Then if the bean already exists in the Conversation it is returned. Otherwise
295      * a new instance is created, stored into the Conversation and returned.
296      * <p>
297      * When a bean is created, a proxy is actually created for it which has one or
298      * more AOP "advices" (ie method interceptors). The CurrentConversationAdvice class
299      * is always attached. Note that if the bean definition contains the aop:proxy
300      * tag (and most do) then the bean that spring creates is already a proxy, ie
301      * what is returned is a proxy of a proxy.
302      *
303      * @param conversationName
304      * @param beanName is the key within the conversation of the bean we are interested in.
305      *
306      * @since 1.1
307      */
308     protected Object getRealBean(String conversationName, String beanName, ObjectFactory objectFactory)
309     {
310         if (log.isDebugEnabled())
311         {
312             log.debug("getRealBean called for bean " + beanName);
313         }
314         ConversationManager manager = ConversationManager.getInstance();
315         Conversation conversation;
316 
317         // check if we have a conversation
318         synchronized(manager)
319         {
320             conversation = manager.getConversation(conversationName);
321             if (conversation == null)
322             {
323                 // Start the conversation. This eventually results in a
324                 // callback to the createConversation method on this class.
325                 conversation = manager.startConversation(conversationName, this);
326             }
327             else
328             {
329                 // sanity check: verify that two beans with the different scopes
330                 // do not declare the same conversationName.
331                 assertSameScope(beanName, conversation);
332             }
333         }
334 
335         // get the conversation
336         notifyAccessConversation(conversation);
337         synchronized(conversation)
338         {
339             if (!conversation.hasAttribute(beanName))
340             {
341                 Object value;
342 
343                 // Set the magic property that forces all proxies of this bean to be CGLIB proxies.
344                 // It doesn't matter if we do this multiple times..
345                 BeanDefinition beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName);
346                 beanDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
347 
348                 try
349                 {
350                     // Create the new bean. Note that this will run the
351                     // OrchestraAdvisorBeanPostProcessor processor, which
352                     // will cause the returned object to actually be a proxy
353                     // with the CurrentConversationAdvice (at least) attached to it.
354                     value = objectFactory.getObject();
355                 }
356                 catch(org.springframework.aop.framework.AopConfigException e)
357                 {
358                     throw new IllegalStateException(
359                         "Unable to create Orchestra proxy"
360                             + " for bean " + beanName, e);
361                 }
362 
363                 conversation.setAttribute(beanName, value);
364 
365                 if (value instanceof ConversationAware)
366                 {
367                     ((ConversationAware) value).setConversation(conversation);
368                 }
369             }
370         }
371 
372         // get the bean
373         return conversation.getAttribute(beanName);
374     }
375 
376     /**
377      * Verify that the specified conversation was created by this scope object.
378      *
379      * @param beanName is just used when generating an error message on failure.
380      * @param conversation is the conversation to validate.
381      */
382     protected void assertSameScope(String beanName, Conversation conversation)
383     {
384         // Check that the conversation's factory is this one.
385         //
386         // This handles the case where two different beans declare themselves
387         // as belonging to the same named conversation but with different scope
388         // objects. Allowing that would be nasty, as the conversation
389         // properties (eg lifetime of access or manual) would depend upon which
390         // bean got created first; some other ConversationFactory would have
391         // created the conversation using its configured properties then
392         // we are now adding to that conversation a bean that really wants
393         // the conversation properties defined on this ConversationFactory.
394         //
395         // Ideally the conversation properties would be defined using
396         // the conversation name, not the scope name; this problem would
397         // then not exist. However that would lead to some fairly clumsy
398         // configuration, particularly where lots of beans without explicit
399         // conversationName attributes are used.
400 
401         if (conversation.getFactory() != this)
402         {
403             throw new IllegalArgumentException(
404                 "Inconsistent scope and conversation name detected for bean "
405                     + beanName);
406         }
407     }
408 
409     protected void notifyAccessConversation(Conversation conversation)
410     {
411     }
412 
413     /**
414      * Invoked by Spring to notify this object of the BeanFactory it is associated with.
415      * <p>
416      * This method is redundant as we also have setApplicationContext. However as this
417      * method (and the BeanFactoryAware interface on this class) were present in release
418      * 1.0 this needs to be kept for backwards compatibility.
419      */
420     public void setBeanFactory(BeanFactory beanFactory) throws BeansException
421     {
422     }
423 
424     /**
425      * Register any BeanPostProcessors that are needed by this scope.
426      * <p>
427      * This is an alternative to requiring users to also add an orchestra BeanPostProcessor element
428      * to their xml configuration file manually.
429      * <p>
430      * When a bean <i>instance</i> is created by Spring, it always runs every single BeanPostProcessor
431      * that has been registered with it.
432      *
433      * @since 1.1
434      */
435     public void defineBeanPostProcessors(ConfigurableListableBeanFactory cbf) throws BeansException
436     {
437         if (!cbf.containsSingleton(POST_PROCESSOR_BEAN_NAME))
438         {
439             BeanPostProcessor processor = new OrchestraAdvisorBeanPostProcessor(applicationContext);
440 
441             // Adding the bean to the singletons set causes it to be picked up by the standard
442             // AbstractApplicationContext.RegisterBeanPostProcessors method; that calls
443             // getBeanNamesForType(BeanPostProcessor.class, ...) which finds stuff in the
444             // singleton map even when there is no actual BeanDefinition for it.
445             //
446             // We cannot call cbf.addBeanPostProcessor even if we want to, as the singleton
447             // registration will be added again, making the processor run twice on each bean.
448             // And we need the singleton registration in order to avoid registering this once
449             // for each scope object defined in spring.
450             cbf.registerSingleton(POST_PROCESSOR_BEAN_NAME, processor);
451         }
452     }
453 
454     /**
455      * Get the conversation for the given beanName.
456      * Returns null if the conversation does not exist.
457      */
458     protected Conversation getConversationForBean(String beanDefName)
459     {
460         ConversationManager manager = ConversationManager.getInstance();
461         String conversationName = getConversationNameForBean(beanDefName);
462         Conversation conversation = manager.getConversation(conversationName);
463         return conversation;
464     }
465 
466     /**
467      * Get the conversation-name for bean instances created using the specified
468      * bean definition.
469      */
470     public String getConversationNameForBean(String beanName)
471     {
472         if (applicationContext == null)
473         {
474             throw new IllegalStateException("Null application context");
475         }
476 
477         // Look up the definition with the specified name.
478         BeanDefinition beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName);
479 
480         if (ScopedProxyFactoryBean.class.getName().equals(beanDefinition.getBeanClassName()))
481         {
482             // Handle unusual case.
483             //
484             // The user bean must have been defined like this:
485             //  <bean name="foo" class="example.Foo">
486             //    <....>
487             //    <aop:scopedProxy/>
488             //  </bean>
489             // In this case, Spring's ScopedProxyUtils class creates two BeanDefinition objects, one
490             // with name "foo" that creates a proxy object, and one with name "scopedTarget.foo"
491             // that actually defines the bean of type example.Foo.
492             //
493             // So what we do here is find the renamed bean definition and look there.
494             //
495             // This case does not occur when this method is invoked from within this class; the
496             // spring scope-related callbacks always deal with the beandef that is scoped to
497             // this scope - which is the original (though renamed) beandef.
498             beanName = _SpringUtils.getModifiedBeanName(beanName);
499             beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName); // NON-NLS
500         }
501 
502         String convName = getExplicitConversationName(beanDefinition);
503         if (convName == null)
504         {
505             // The beanname might have been of form "scopedTarget.foo" (esp from registerDestructionCallback).
506             // But in this case, the conversation name will just be "foo", so strip the prefix off.
507             //
508             // Note that this does happen quite often for calls from within this class when aop:scoped-proxy
509             // is being used (which is not recommended but is supported).
510             convName = _SpringUtils.getOriginalBeanName(beanName);
511         }
512         return convName;
513     }
514 
515     /**
516      * Return the explicit conversation name for this bean definition, or null if there is none.
517      * <p>
518      * This is a separate method so that subclasses can determine conversation names via alternate methods.
519      * In particular, a subclass may want to look for an annotation on the class specified by the definition.
520      *
521      * @since 1.1
522      */
523     protected String getExplicitConversationName(BeanDefinition beanDef)
524     {
525         String attr = (String) beanDef.getAttribute(
526                 BeanDefinitionConversationNameAttrDecorator.CONVERSATION_NAME_ATTRIBUTE);
527         return attr;
528     }
529 
530     /**
531      * Strip off any Spring namespace (eg scopedTarget).
532      * <p>
533      * This method will simply strip off anything before the first dot.
534      *
535      * @deprecated Should not be necessary in user code.
536      */
537     protected String buildBeanName(String name)
538     {
539         if (name == null)
540         {
541             return null;
542         }
543 
544         int pos = name.indexOf('.');
545         if (pos < 0)
546         {
547             return name;
548         }
549 
550         return name.substring(pos + 1);
551     }
552 
553     public Object remove(String name)
554     {
555         throw new UnsupportedOperationException();
556     }
557 
558     /**
559      * Add the given runnable wrapped within an
560      * {@link org.apache.myfaces.orchestra.conversation.ConversationBindingListener} to
561      * the conversation map.
562      * <p>
563      * This ensures it will be called during conversation destroy.
564      * <p>
565      * Spring calls this method whenever a bean in this scope is created, if that bean
566      * has a "destroy method". Note however that it appears that it can also call it even
567      * for beans that do not have a destroy method when there is a "destruction aware"
568      * BeanPostProcessor attached to the context (spring version 2.5 at least).
569      * <p>
570      * When an aop:scoped-proxy has been used inside the bean, then the "new" definition
571      * does not have any scope attribute, so orchestra is not invoked for it. However
572      * the "renamed" bean does, and so this is called.
573      */
574     public void registerDestructionCallback(String name, final Runnable runnable)
575     {
576         if (log.isDebugEnabled())
577         {
578             log.debug("registerDestructionCallback for [" + name + "]");
579         }
580 
581         Conversation conversation = getConversationForBean(name);
582         if (conversation == null)
583         {
584             // This should never happen because this should only be called after the bean
585             // instance has been created via scope.getBean, which always creates the
586             // conversation for the bean.
587             throw new IllegalStateException("No conversation for bean [" + name + "]");
588         }
589         if (runnable == null)
590         {
591             throw new IllegalStateException("No runnable object for bean [" + name + "]");
592         }
593 
594         // Add an object to the conversation as a bean so that when the conversation is removed
595         // its valueUnbound method will be called. However we never need to retrieve this object
596         // from the context by name, so use a totally unique name as the bean key.
597         conversation.setAttribute(
598             runnable.getClass().getName() + "@" + System.identityHashCode(runnable),
599             new ConversationBindingListener()
600             {
601                 public void valueBound(ConversationBindingEvent event)
602                 {
603                 }
604 
605                 public void valueUnbound(ConversationBindingEvent event)
606                 {
607                     runnable.run();
608                 }
609             }
610         );
611     }
612 
613     /**
614      * Get an ApplicationContext injected by Spring. See ApplicationContextAware interface.
615      */
616     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
617     {
618         if (!(applicationContext instanceof ConfigurableApplicationContext))
619         {
620             throw new IllegalArgumentException("a ConfigurableApplicationContext is required");
621         }
622 
623         this.applicationContext = (ConfigurableApplicationContext) applicationContext;
624         defineBeanPostProcessors(this.applicationContext.getBeanFactory());
625     }
626 
627     /**
628      * @since 1.2
629      */
630     protected ConfigurableApplicationContext getApplicationContext()
631     {
632         return applicationContext;
633     }
634 }