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 }