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