You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by sk...@apache.org on 2007/11/30 13:14:54 UTC
svn commit: r599792 - in
/myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring:
AbstractSpringOrchestraScope.java ScopedBeanTargetSource.java
_SpringUtils.java
Author: skitching
Date: Fri Nov 30 04:14:32 2007
New Revision: 599792
URL: http://svn.apache.org/viewvc?rev=599792&view=rev
Log:
Implement ORCHESTRA-10 - autocreate scope-proxy for conversation-scoped beans.
Note that there is a binary-incompatible change in AbstractSpringOrchestraScope; protected method getBean has been removed.
Added:
myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/ScopedBeanTargetSource.java
Modified:
myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/AbstractSpringOrchestraScope.java
myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/_SpringUtils.java
Modified: myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/AbstractSpringOrchestraScope.java
URL: http://svn.apache.org/viewvc/myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/AbstractSpringOrchestraScope.java?rev=599792&r1=599791&r2=599792&view=diff
==============================================================================
--- myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/AbstractSpringOrchestraScope.java (original)
+++ myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/AbstractSpringOrchestraScope.java Fri Nov 30 04:14:32 2007
@@ -29,6 +29,7 @@
import org.apache.myfaces.orchestra.conversation.ConversationFactory;
import org.apache.myfaces.orchestra.conversation.ConversationManager;
import org.apache.myfaces.orchestra.conversation.CurrentConversationAdvice;
+import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.scope.ScopedProxyFactoryBean;
import org.springframework.beans.BeansException;
@@ -62,8 +63,10 @@
{
private final Log log = LogFactory.getLog(AbstractSpringOrchestraScope.class);
+ private BeanFactory beanFactory;
private ConfigurableApplicationContext applicationContext;
private Advice[] advices;
+ private boolean autoProxy = true;
public AbstractSpringOrchestraScope()
{
@@ -71,6 +74,21 @@
/**
* The advices (interceptors) which will be applied to the conversation scoped bean.
+ * These are applied whenever a method is invoked on the bean [1].
+ * <p>
+ * An application's spring configuration uses this method to control what advices are
+ * applied to beans generated from this scope. One commonly applied advice is the
+ * Orchestra persistence interceptor, which ensures that whenever a method on a
+ * conversation-scoped bean is invoked the "global persistence context" is set
+ * to the context for the conversation that bean is in.
+ * <p>
+ * Note [1]: the advices are only applied when the bean is invoked via its proxy. If
+ * invoked via the "this" pointer of the object the interceptors will not run. This
+ * is generally a good thing, as they are not wanted when a method on the bean invokes
+ * another method on the same bean. However it is bad if the bean passes "this" as a
+ * parameter to some other object that makes a callback on it at some later time. In
+ * that case, the bean must take care to pass its proxy to the remote object, not
+ * itself. See method ConversationUtils.getCurrentBean().
*/
public void setAdvices(Advice[] advices)
{
@@ -78,10 +96,30 @@
}
/**
+ * Configuration for a scope object to control whether "scoped proxy" objects are
+ * automatically wrapped around each conversation bean.
+ * <p>
+ * Automatically applying scope proxies solves a lot of tricky problems with "stale"
+ * beans, and should generally be used. However it does require CGLIB to be present
+ * in the classpath. It also can impact performance in some cases. Where this is a
+ * problem, this flag can turn autoproxying off. Note that the standard spring
+ * aop:scoped-proxy bean can then be used on individual beans to re-enable
+ * proxying for specific beans if desired.
+ * <p>
+ * This defaults to true.
+ */
+ public void setAutoProxy(boolean state)
+ {
+ autoProxy = state;
+ }
+
+ /**
* Return the conversation context id.
* <p>
* Note: This conversationId is something spring requires. It has nothing to do with the Orchestra
* conversation id.
+ * <p>
+ * TODO: what does Spring use this for????
*/
public String getConversationId()
{
@@ -99,13 +137,158 @@
* and the bean-definition for that bean has a scope attribute that maps to an
* instance of this class.
* <p>
- * First, the appropriate ConversationContext is retrieved.
+ * In the normal case, this method returns an internally-created proxy object
+ * that fetches the "real" bean every time a method is invoked on the proxy
+ * (see method getRealBean). This does obviously have some performance impact.
+ * However it is necessary when beans from one conversation are referencing beans
+ * from another conversation as the conversation lifetimes are not the same;
+ * without this proxying there are many cases where "stale" references end up
+ * being used. Most references to conversation-scoped objects are made via EL
+ * expressions, and in this case the performance impact is not significant
+ * relative to the overhead of EL. Note that there is one case where this
+ * proxying is not "transparent" to user code: if a proxied object passes a
+ * "this" pointer to a longer-lived object that retains that pointer then
+ * that reference can be "stale", as it points directly to an instance rather
+ * than to the proxy.
+ * <p>
+ * When the Spring aop:scoped-proxy feature is applied to conversation-scoped
+ * beans, then this functionality is disabled as aop:scoped-proxy has a very
+ * similar effect. Therefore, when this method detects that it has been invoked
+ * by a proxy object generated by aop:scoped-proxy then it returns the real
+ * object (see getRealBean) rather than another proxy. Using aop:scoped-proxy
+ * is somewhat less efficient than Orchestra's customised proxying.
* <p>
- * Second, the appropriate Conversation is retrieved; if it does not yet exist then
+ * And when the orchestra proxy needs to access the real object, it won't
+ * call this method; instead, getRealBean is called directly. See class
+ * ScopedBeanTargetSource.
+ */
+ public Object get(String name, ObjectFactory objectFactory)
+ {
+ if (log.isDebugEnabled())
+ {
+ log.debug("Method get called for bean " + name);
+ }
+
+ if (_SpringUtils.isModifiedBeanName(name))
+ {
+ // Backwards compatibility with aop:scoped-proxy tag.
+ //
+ // The user must have included an aop:scoped-proxy within the bean definition,
+ // and here the proxy is firing to try to get the underlying bean. In this
+ // case, return a non-proxied instance of the referenced bean.
+ try
+ {
+ String originalBeanName = _SpringUtils.getOriginalBeanName(name);
+ String conversationName = getConversationNameForBean(name);
+ return getRealBean(conversationName, originalBeanName, objectFactory);
+ }
+ catch(RuntimeException e)
+ {
+ log.error("Exception while accessing bean '" + name + "'");
+ throw e;
+ }
+ }
+ else if (!autoProxy)
+ {
+ String conversationName = getConversationNameForBean(name);
+ return getRealBean(conversationName, name, objectFactory);
+ }
+ else
+ {
+ // A call has been made by the user to the Spring getBean method
+ // (directly, or via an EL expression). Or the bean is being fetched
+ // as part of spring injection into another object.
+ //
+ // In all these cases, just return a proxy.
+ return getProxy(name, objectFactory);
+ }
+ }
+
+ /**
+ * Return a CGLIB-generated proxy class for the beanclass that is
+ * specified by the provided beanName.
+ * <p>
+ * When any method is invoked on this proxy, it invokes method
+ * getRealBean on this same instance in order to locate a proper
+ * instance, then forwards the method call to it.
+ * <p>
+ * There is a separate proxy instance per beandef (shared across all
+ * instances of that bean). This instance is created when first needed,
+ * and cached for reuse.
+ */
+ protected Object getProxy(String beanName, ObjectFactory objectFactory)
+ {
+ // TODO: consider proxy scope
+ //
+ // Q: can proxies be added to the session or even app scope? It would seem
+ // so, as they always do a lookup each time. In that case, bean lookup
+ // to find the proxy will be much faster, and the lookup from the proxy will
+ // also be faster as it knows the Scope and the ConversationName. The only
+ // drawback is that session.containsKey will always be true even when the
+ // bean does not currently exist. And that app-variables that "shadow" the
+ // real bean will not work. But that's pretty ugly anyway.
+ //
+ // And if is stored in app scope, then there is no need to cache it inside the
+ // beandef..
+ //
+ // Or should the proxy then be stored in the spring "singleton" scope? Odd, as
+ // we have no bean definition for it...
+ //
+ // Hmm..maybe that is the reason for the ugly scoping "rename" hack - so that
+ // the proxy can have singleton scope even though the bean it references
+ // does not.
+ //
+
+ // Check for a cached proxy under a key that is the name of ScopedBeanTargetSource.class.
+ // The "proxy class" cannot be used as a key, because it really has no class of its own :-)
+
+ if (log.isDebugEnabled())
+ {
+ log.debug("getProxy called for bean " + beanName);
+ }
+
+ BeanDefinition beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName);
+ Object proxy = beanDefinition.getAttribute(ScopedBeanTargetSource.class.getName());
+ if (proxy == null)
+ {
+ if (log.isDebugEnabled())
+ {
+ log.debug("getProxy: creating proxy for " + beanName);
+ }
+ String conversationName = (String) beanDefinition.getAttribute(BeanDefinitionConversationNameAttrDecorator.CONVERSATION_NAME_ATTRIBUTE);
+ if (conversationName == null)
+ conversationName = beanName;
+ proxy = _SpringUtils.newProxy(this, conversationName, beanName, objectFactory, beanFactory);
+ beanDefinition.setAttribute(ScopedBeanTargetSource.class.getName(), proxy);
+ }
+
+ // Register the proxy in req scope. The first lookup of a variable using an EL expression during a
+ // request will therefore take the "long" path through JSF's VariableResolver and Spring to get here.
+ // But later lookups of this variable in the same request find the proxy directly in the request scope.
+ // The proxy could potentially be placed in the session or app scope, as there is just one instance
+ // for this spring context, and there is normally only one spring context for a webapp. However
+ // using the request scope is almost as efficient and seems safer.
+ //
+ // Note that the framework adapter might not be initialised during the Spring context initialisation
+ // phase (ie instantiation of singletons during startup), so just skip registration in those cases.
+ FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
+ if (fa != null)
+ {
+ fa.setRequestAttribute(beanName, proxy);
+ }
+
+
+ return proxy;
+ }
+
+ /**
+ * Get a real bean instance (not a scoped-proxy).
+ * <p>
+ * The appropriate Conversation is retrieved; if it does not yet exist then
* it is created and started. The conversation name is either specified on the
* bean-definition via a custom attribute, or defaults to the bean name.
* <p>
- * Then if the bean already exists in the Conversation then it is returned. Otherwise
+ * Then if the bean already exists in the Conversation it is returned. Otherwise
* a new instance is created, stored into the Conversation and returned.
* <p>
* When a bean is created, a proxy is actually created for it which has one or
@@ -113,19 +296,16 @@
* is always attached. Note that if the bean definition contains the aop:proxy
* tag (and most do) then the bean that spring creates is already a proxy, ie
* what is returned is a proxy of a proxy.
+ *
+ * @param conversationName
+ * @param beanName is the key within the conversation of the bean we are interested in.
*/
- public Object get(String name, ObjectFactory objectFactory)
+ protected Object getRealBean(String conversationName, String beanName, ObjectFactory objectFactory)
{
- name = buildBeanName(name);
-
- return getBean(name, objectFactory);
- }
-
- /** See method get(name, objectFactory). */
- protected Object getBean(String beanName, ObjectFactory objectFactory)
- {
- String conversationName = getConversationNameForBean(beanName);
-
+ if (log.isDebugEnabled())
+ {
+ log.debug("getRealBean called for bean " + beanName);
+ }
ConversationManager manager = ConversationManager.getInstance();
Conversation conversation;
@@ -136,11 +316,13 @@
if (conversation == null)
{
// Start the conversation. This eventually results in a
- // callback to the createConversation method on this class.
+ // callback to the createConversation method on t, beanFactoryhis class.
conversation = manager.startConversation(conversationName, this);
}
else
{
+ // sanity check: verify that two beans with the different scopes
+ // do not declare the same conversationName.
assertSameScope(beanName, conversation);
}
}
@@ -176,12 +358,18 @@
return conversation.getAttribute(beanName);
}
- protected void assertSameScope(String beanName, Conversation conversation)
+ /**
+ * Verify that the specified conversation was created by this scope object.
+ *
+ * @param beanName is just used when generating an error message on failure.
+ * @param conversation is the conversation to validate.
+ */
+ protected void assertSameScope(String beanName, Conversation conversation)
{
// Check that the conversation's factory is this one.
//
// This handles the case where two different beans declare themselves
- // as belonging to the same conversation but with different scope
+ // as belonging to the same named conversation but with different scope
// objects. Allowing that would be nasty as the conversation
// properties (eg flash or manual) would depend upon which bean
// got created first; some other ConversationFactory would have
@@ -208,8 +396,24 @@
}
/**
- * Set the {@link org.apache.myfaces.orchestra.conversation.Conversation} object to the bean if it implements the
- * {@link org.apache.myfaces.orchestra.conversation.ConversationAware} interface.
+ * When this scope object's declaration is first processed by Spring, take the opportunity to
+ * register a global BeanPostProcessor.
+ * <p>
+ * When a bean <i>instance</i> is created by Spring, it always runs every single BeanPostProcessor
+ * that has been registered with it.
+ * <p>
+ * The BeanPostProcessor implementation sets the {@link org.apache.myfaces.orchestra.conversation.Conversation}
+ * object on the bean if it implements the {@link org.apache.myfaces.orchestra.conversation.ConversationAware}
+ * interface.
+ * <p>
+ * Note: when an aop:scoped-proxy exists for the bean definition, then the scoped-proxy object is a
+ * singleton. So the BeanPostProcessor will run once for it (but not do anything). Any time user code
+ * references that (singleton) bean, an instance of the real bean will be fetched, which might trigger
+ * an instance creation. If it does, then the BeanPostProcessor also runs for that instance; in this
+ * case the beanName parameter passed to the methods will be the "scopedTarget" beanname as that is
+ * the object that the ScopedProxyFactoryBean has asked Spring to fetch.
+ * <p>
+ * For Orchestra's "internal" scoped proxying,
*/
public void setBeanFactory(BeanFactory beanFactory) throws BeansException
{
@@ -234,74 +438,97 @@
}
}
);
+
+ this.beanFactory = beanFactory;
+ }
+
+ /**
+ * Return the BeanFactory that this scope object exists within.
+ */
+ protected BeanFactory getBeanFactory()
+ {
+ return beanFactory;
}
/**
* Get the conversation for the given beanName.
* Returns null if the conversation does not exist.
*/
- protected Conversation getConversationForBean(String beanName)
+ protected Conversation getConversationForBean(String beanDefName)
{
ConversationManager manager = ConversationManager.getInstance();
- String conversationName = getConversationNameForBean(beanName);
+ String conversationName = getConversationNameForBean(beanDefName);
Conversation conversation = manager.getConversation(conversationName);
return conversation;
}
/**
- * Get the conversation name associated with the beanName.
+ * Get the conversation-name for bean instances created using the specified
+ * bean definition.
*/
protected String getConversationNameForBean(String beanName)
{
if (applicationContext == null)
{
- // TODO: when can this happen? A scope method is being invoked but no context exists??
- return beanName;
+ throw new IllegalStateException("Null application context");
}
- // First, look up the definition with the specified name.
+ // Look up the definition with the specified name.
BeanDefinition beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName);
- // When a definition is present in the config file like this:
- // <bean name="foo" class="example.Foo">
- // <....>
- // <aop:scopedProxy/>
- // </bean>
- // then what Spring actually does is create two BeanDefinition objects, one
- // with name "foo" that creates a proxy object, and one with name "scopedTarget.foo"
- // that actually defines the bean of type example.Foo.
- //
- // So what we do here is look up the definition by the provided name, then if that
- // def is creating a proxy then look up the "renamed" definition.
if (ScopedProxyFactoryBean.class.getName().equals(beanDefinition.getBeanClassName()))
{
- // bad hack to get access to the real definition
- // TODO: is there a better way to do this?
-
- // Note that we deliberately change the value that will be returned, so that if there is no
- // conversation name attribute then we return the
- beanName = _SpringUtils.getAlternateBeanName(beanName);
+ // Handle unusual case.
+ //
+ // The user bean must have been defined like this:
+ // <bean name="foo" class="example.Foo">
+ // <....>
+ // <aop:scopedProxy/>
+ // </bean>
+ // In this case, Spring's ScopedProxyUtils class creates two BeanDefinition objects, one
+ // with name "foo" that creates a proxy object, and one with name "scopedTarget.foo"
+ // that actually defines the bean of type example.Foo.
+ //
+ // So what we do here is find the renamed bean definition and look there.
+ //
+ // This case does not occur when this method is invoked from within this class; the
+ // spring scope-related callbacks always deal with the beandef that is scoped to
+ // this scope - which is the original (though renamed) beandef.
+ beanName = _SpringUtils.getModifiedBeanName(beanName);
beanDefinition = applicationContext.getBeanFactory().getBeanDefinition(beanName); // NON-NLS
}
- if (beanDefinition.hasAttribute(BeanDefinitionConversationNameAttrDecorator.CONVERSATION_NAME_ATTRIBUTE))
- {
- // The bean def explicitly included a conversation-name, so return that.
- return (String) beanDefinition.getAttribute(BeanDefinitionConversationNameAttrDecorator.CONVERSATION_NAME_ATTRIBUTE);
- }
- else
+ String convName = getExplicitConversationName(beanDefinition);
+ if (convName == null)
{
// The beanname might have been of form "scopedTarget.foo" (esp from registerDestructionCallback).
// But in this case, the conversation name will just be "foo", so strip the prefix off.
- String convName = _SpringUtils.getRealBeanName(beanName);
- return convName;
+ //
+ // Note that this does happen quite often for calls from within this class when aop:scoped-proxy
+ // is being used (which is not recommended but is supported).
+ convName = _SpringUtils.getOriginalBeanName(beanName);
}
+ return convName;
+ }
+
+ /**
+ * Return the explicit conversation name for this bean definition, or null if there is none.
+ * <p>
+ * This is a separate method so that subclasses can determine conversation names via alternate methods.
+ * In particular, a subclass may want to look for an annotation on the class specified by the definition.
+ */
+ protected String getExplicitConversationName(BeanDefinition beanDef)
+ {
+ String attr = (String) beanDef.getAttribute(BeanDefinitionConversationNameAttrDecorator.CONVERSATION_NAME_ATTRIBUTE);
+ return attr;
}
/**
* Strip off any Spring namespace (eg scopedTarget).
* <p>
* This method will simply strip off anything before the first dot.
+ *
+ * @deprecated Should not be necessary in user code.
*/
protected String buildBeanName(String name)
{
@@ -335,6 +562,10 @@
* has a "destroy method". Note however that it appears that it can also call it even
* for beans that do not have a destroy method when there is a "destruction aware"
* BeanPostProcessor attached to the context (spring version 2.5 at least).
+ * <p>
+ * When an aop:scoped-proxy has been used inside the bean, then the "new" definition
+ * does not have any scope attribute, so orchestra is not invoked for it. However
+ * the "renamed" bean does, and so this is called.
*/
public void registerDestructionCallback(String name, final Runnable runnable)
{
@@ -356,6 +587,9 @@
throw new IllegalStateException("No runnable object for bean [" + name + "]");
}
+ // Add an object to the conversation as a bean so that when the conversation is removed
+ // its valueUnbound method will be called. However we never need to retrieve this object
+ // from the context by name, so use a totally unique name as the bean key.
conversation.setAttribute(
runnable.getClass().getName() + "@" + System.identityHashCode(runnable),
new ConversationBindingListener()
Added: myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/ScopedBeanTargetSource.java
URL: http://svn.apache.org/viewvc/myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/ScopedBeanTargetSource.java?rev=599792&view=auto
==============================================================================
--- myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/ScopedBeanTargetSource.java (added)
+++ myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/ScopedBeanTargetSource.java Fri Nov 30 04:14:32 2007
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.orchestra.conversation.spring;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.aop.target.AbstractBeanFactoryBasedTargetSource;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.ObjectFactory;
+
+/**
+ * Used with a "scoping proxy" object as generated by _SpringUtils.newProxy.
+ * <p>
+ * When user code invokes any method on the proxy, it invokes getTarget on its
+ * source object to get the "real" object, then invokes the same method on the
+ * returned object.
+ * <p>
+ * Here the getTarget method is implemented by using an AbstractSpringOrchestraScope
+ * object to look up the ConversationContext for the user, then a particular
+ * Conversation instance (by name), then a bean within that Conversation.
+ */
+public class ScopedBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
+ private AbstractSpringOrchestraScope scope;
+ private String conversationName;
+ private String targetBeanName;
+ private ObjectFactory objectFactory;
+
+ public ScopedBeanTargetSource(
+ AbstractSpringOrchestraScope scope,
+ String conversationName,
+ String targetBeanName,
+ ObjectFactory objectFactory,
+ BeanFactory beanFactory) {
+
+ this.scope = scope;
+ this.conversationName = conversationName;
+ this.targetBeanName = targetBeanName;
+ this.objectFactory = objectFactory;
+
+ super.setTargetBeanName(targetBeanName);
+ super.setBeanFactory(beanFactory);
+ }
+
+ public Object getTarget() throws Exception {
+ final Log log = LogFactory.getLog(ScopedBeanTargetSource.class);
+ log.debug("getRealBean for " + targetBeanName);
+ return scope.getRealBean(conversationName, targetBeanName, objectFactory);
+ }
+}
Modified: myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/_SpringUtils.java
URL: http://svn.apache.org/viewvc/myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/_SpringUtils.java?rev=599792&r1=599791&r2=599792&view=diff
==============================================================================
--- myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/_SpringUtils.java (original)
+++ myfaces/orchestra/trunk/core/src/main/java/org/apache/myfaces/orchestra/conversation/spring/_SpringUtils.java Fri Nov 30 04:14:32 2007
@@ -18,26 +18,63 @@
*/
package org.apache.myfaces.orchestra.conversation.spring;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.aop.framework.AopInfrastructureBean;
+import org.springframework.aop.framework.ProxyFactory;
+import org.springframework.aop.scope.DefaultScopedObject;
+import org.springframework.aop.scope.ScopedObject;
+import org.springframework.aop.support.DelegatingIntroductionInterceptor;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.ObjectFactory;
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+
/**
* <p>Various Spring utilities used by the conversation framework</p>
* <p>Notice: this class is not meant to be used outside of this library</p>
*/
public final class _SpringUtils
{
+ // Equal to ScopedProxyUtils.TARGET_NAME_PREFIX, but that is private.
private final static String SCOPED_TARGET_PREFIX = "scopedTarget.";
private _SpringUtils()
{
}
- public static boolean isAlternateBeanName(String beanName)
+ /**
+ * Detect the case where a class includes a nested aop:scoped-target tag.
+ * <p>
+ * When a definition is present in the config file like this:
+ * <pre>
+ * <bean name="foo" class="example.Foo" orchestra:foo="foo">
+ * <....>
+ * <aop:scopedProxy/>
+ * </bean>
+ * </pre>
+ * then what Spring actually does is create two BeanDefinition objects, one
+ * with name "foo" that creates a proxy object, and one with name "scopedTarget.foo"
+ * that actually defines the bean of type example.Foo (see Spring class ScopedProxyUtils
+ * for details.
+ * <p>
+ * Using this pattern is very common with Orchestra, so we need to detect it and
+ * look for orchestra settings on the renamed bean definition rather than the
+ * one with the original name.
+ */
+ public static boolean isModifiedBeanName(String beanName)
{
return beanName.startsWith(SCOPED_TARGET_PREFIX);
}
- public static String getRealBeanName(String beanName)
+ /**
+ * Given the name of a BeanDefinition, if this is a fake name created
+ * by spring because an aop:scoped-proxy is now wrapping this object,
+ * then return the name of that scoped-proxy bean (ie the name that the
+ * user accesses this bean by).
+ */
+ public static String getOriginalBeanName(String beanName)
{
- if (beanName != null && isAlternateBeanName(beanName))
+ if (beanName != null && isModifiedBeanName(beanName))
{
return beanName.substring(SCOPED_TARGET_PREFIX.length());
}
@@ -45,9 +82,14 @@
return beanName;
}
- public static String getAlternateBeanName(String beanName)
+ /**
+ * Given a bean name "foo", if it refers to a scoped proxy then the bean
+ * definition that holds the original settings is now under another name,
+ * so return that other name so the original BeanDefinition can be found.
+ */
+ public static String getModifiedBeanName(String beanName)
{
- if (beanName != null && !isAlternateBeanName(beanName))
+ if (beanName != null && !isModifiedBeanName(beanName))
{
return SCOPED_TARGET_PREFIX + beanName;
}
@@ -55,26 +97,73 @@
return beanName;
}
- /*
- public static Set getConversationManagedScopeNames()
+ /** @deprecated use isModifiedBeanName instead. */
+ public static boolean isAlternateBeanName(String beanName)
+ {
+ return isModifiedBeanName(beanName);
+ }
+
+ /** @deprecated use getModifiedBeanName instead. */
+ public static String getAlternateBeanName(String beanName)
+ {
+ return getModifiedBeanName(beanName);
+ }
+
+ /** @deprecated use getOriginalBeanName instead. */
+ public static String getRealBeanName(String beanName)
{
- ConfigurableApplicationContext applicationContext = FrameworkAdapter.getInstance().getApplicationContext();
- ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
+ return getOriginalBeanName(beanName);
+ }
+
+ /**
+ * Create an object that subclasses the concrete class of the BeanDefinition
+ * for the specified targetBeanName, and when invoked delegates to an instance
+ * of that type fetched from a scope object.
+ * <p>
+ * The proxy returned currently also currently implements the standard Spring
+ * ScopedObject interface; this is experimental and may be removed if not useful.
+ */
+ public static Object newProxy(
+ AbstractSpringOrchestraScope scope,
+ String conversationName,
+ String targetBeanName,
+ ObjectFactory objectFactory,
+ BeanFactory beanFactory)
+ {
+ // based on code from Spring ScopedProxyFactoryBean (APL2.0)
+ final Log log = LogFactory.getLog(_SpringUtils.class);
+
+ log.debug("newProxy invoked for " + targetBeanName);
- Set managedScopes = new TreeSet();
- String[] scopes = beanFactory.getRegisteredScopeNames();
- for (int i = 0; i < scopes.length; i++)
+ if (!(beanFactory instanceof ConfigurableBeanFactory))
{
- String scopeName = scopes[i];
+ throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory);
+ }
+ ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory;
+
+ ScopedBeanTargetSource scopedTargetSource = new ScopedBeanTargetSource(
+ scope, conversationName, targetBeanName, objectFactory, beanFactory);
- Scope scope = beanFactory.getRegisteredScope(scopeName);
- if (scope instanceof SpringConversationScope)
- {
- managedScopes.add(scopeName);
- }
+ ProxyFactory pf = new ProxyFactory();
+ pf.setProxyTargetClass(true);
+ pf.setTargetSource(scopedTargetSource);
+
+ Class beanType = beanFactory.getType(targetBeanName);
+ if (beanType == null) {
+ throw new IllegalStateException("Cannot create scoped proxy for bean '" + targetBeanName +
+ "': Target type could not be determined at the time of proxy creation.");
}
- return managedScopes;
+ // Add an introduction that implements only the methods on ScopedObject.
+ // Not sure if this is useful...
+ ScopedObject scopedObject = new DefaultScopedObject(cbf, scopedTargetSource.getTargetBeanName());
+ pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject));
+
+ // Add the AopInfrastructureBean marker to indicate that the scoped proxy
+ // itself is not subject to auto-proxying! Only its target bean is.
+ // Not sure if this is needed....
+ pf.addInterface(AopInfrastructureBean.class);
+
+ return pf.getProxy(cbf.getBeanClassLoader());
}
- */
}