Monday, 1 September 2014

Limitations of compile time woven spring-aspects

The compile time woven aspectj spring-aspects provides some very useful features. The features I was after was:
  • transactional boundaries between methods in the same bean. e.g. nonTransactionMethod() calling requiresNewMethod() without having to use self references or manually handling the transactions.
  • consistent results between testing in Eclipse, testing via maven and running on the server (instead of using the loadtime weaver)
During testing in both eclipse and maven I started getting exceptions of the likes "NoTransactionException". The individual test would pass fine but when running all the tests together they would fail. Debugging through the tests showed that transaction manager was still being called but it was the wrong one! Decompiling the aspectj woven code shows the reason. My simple class:
package net.devgrok;

import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.aspectj.AbstractTransactionAspect;
import org.springframework.transaction.aspectj.AnnotationTransactionAspect;

public class AspectWoven
{
  public void noTransacted()
  {
    doInTransaction();
  }
  
  @Transactional
  private void doInTransaction()
  {
    Object[] arrayOfObject = new Object[1];arrayOfObject[0] = this;AnnotationTransactionAspect.aspectOf().ajc$around$org_springframework_transaction_aspectj_AbstractTransactionAspect$1$2a73e96c(this, new AspectWoven.AjcClosure1(arrayOfObject), ajc$tjp_0);
  }
  
  static {}
}
The spring classes:
public class AnnotationTransactionAspect
  extends AbstractTransactionAspect
{
  public static boolean hasAspect()
  {
    return ajc$perSingletonInstance != null;
  }
  
  public static AnnotationTransactionAspect aspectOf()
  {
    if (ajc$perSingletonInstance == null) {
      throw new NoAspectBoundException("org_springframework_transaction_aspectj_AnnotationTransactionAspect", ajc$initFailureCause);
    }
    return ajc$perSingletonInstance;
  }
  
  static
  {
    try
    {
      
    }
    catch (Throwable localThrowable)
    {
      ajc$initFailureCause = localThrowable;
    }
  }
  
  public AnnotationTransactionAspect()
  {
    super(new AnnotationTransactionAttributeSource(false));
  }
}
public class AspectJTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {

 @Bean(name=TransactionManagementConfigUtils.TRANSACTION_ASPECT_BEAN_NAME)
 @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 public AnnotationTransactionAspect transactionAspect() {
  AnnotationTransactionAspect txAspect = AnnotationTransactionAspect.aspectOf();
  if (this.txManager != null) {
   txAspect.setTransactionManager(this.txManager);
  }
  return txAspect;
 }
}
Following the flow through this in reverse:
  • On creation of the context #1, Spring gets a reference to the static AnnotationTransactionAspect.aspectOf and injects the transaction manager
  • Calling the transacted method on a bean from context #1, calls the static AnnotationTransactionAspect.aspectOf referring to the transaction manager from context #1
  • On creation of the context #2, Spring gets a reference to the static AnnotationTransactionAspect.aspectOf and injects the transaction manager, overwriting the transaction manager from context #1
  • Calling the transacted method on a bean from context #1, calls the static AnnotationTransactionAspect.aspectOf referring to the transaction manager from context #2
And there in lies the problem - the transaction manager and the aspect itself is a static singleton. It is mentioned on the spring documentation but not the implications it has on testing. Working with multiple application contexts
The AnnotationBeanConfigurerAspect used to implement the @Configurable support is an AspectJ singleton aspect. The scope of a singleton aspect is the same as the scope of static members, that is to say there is one aspect instance per classloader that defines the type. This means that if you define multiple application contexts within the same classloader hierarchy you need to consider where to define the @EnableSpringConfigured bean and where to place spring-aspects.jar on the classpath.


Consider a typical Spring web-app configuration with a shared parent application context defining common business services and everything needed to support them, and one child application context per servlet containing definitions particular to that servlet. All of these contexts will co-exist within the same classloader hierarchy, and so the AnnotationBeanConfigurerAspect can only hold a reference to one of them. In this case we recommend defining the @EnableSpringConfigured bean in the shared (parent) application context: this defines the services that you are likely to want to inject into domain objects. A consequence is that you cannot configure domain objects with references to beans defined in the child (servlet-specific) contexts using the @Configurable mechanism (probably not something you want to do anyway!).


When deploying multiple web-apps within the same container, ensure that each web-application loads the types in spring-aspects.jar using its own classloader (for example, by placing spring-aspects.jar in 'WEB-INF/lib'). If spring-aspects.jar is only added to the container wide classpath (and hence loaded by the shared parent classloader), all web applications will share the same aspect instance which is probably not what you want.

Which is a massive limitation when trying to do unit tests. It is very rare that your unit test will only have one context (set of configuration files). The only solution to this would be to write your own implementation of the aspects to remember which spring context created each bean.