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 }