Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The Old "@Transactional from within the same class" Situation

Synopsis of the original question: Using standard Spring Transactions with AOP proxying, it is not possible to call an @Transactional-marked method from a non-@Transactional-marked method in the same class and be within a transaction (specifically due to the aforementioned proxy). This is supposedly possible with Spring Transactions in AspectJ mode, but how is it done?

Edit: The full rundown for Spring Transactions in AspectJ mode using Load-Time Weaving:

Add the following to META-INF/spring/applicationContext.xml:

<tx:annotation-driven mode="aspectj" />

<context:load-time-weaver />

(I'll assume you already have an AnnotationSessionFactoryBean and a HibernateTransactionManager set up in the application context. You can add transaction-manager="transactionManager" as an attribute to your <tx:annotation-driven /> tag, but if the value of your transaction manager bean's id attribute is actually "transactionManager", then it's redundant, as "transactionManager" is that attribute's default value.)

Add META-INF/aop.xml. Contents are as follows:

<aspectj>
  <aspects>
    <aspect name="org.springframework.transaction.aspectj.AnnotationTransactionAspect" />
  </aspects>
  <weaver>
    <include within="my.package..*" /><!--Whatever your package space is.-->
  </weaver>
</aspectj>

Add aspectjweaver-1.7.0.jar and spring-aspects-3.1.2.RELEASE.jar to your classpath. I use Maven as my build tool, so here are the <dependency /> declarations for your project's POM.xml file:

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.7.0</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>3.1.2.RELEASE</version>
</dependency>

spring-instrument-3.1.2.RELEASE.jar is not needed as a <dependency /> on your classpath, but you still need it somewhere so that you can point at it with the -javaagent JVM flag, as follows:

-javaagent:full\path\of\spring-instrument-3.1.2.RELEASE.jar

I'm working in Eclipse Juno, so to set this I went to Window -> Preferences -> Java -> Installed JREs. Then I clicked on the checked JRE in the list box and clicked the "Edit..." button to the right of the list box. The third text box in the resulting popup window is labeled "Default VM arguments:". This is where the -javaagent flag should be typed or copy+pasted in.

Now for my actual test code classes. First, my main class, TestMain.java:

package my.package;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestMain {
  public static void main(String[] args) {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("META-INF/spring/applicationContext.xml");
    TestClass testClass = applicationContext.getBean(TestClass.class);
    testClass.nonTransactionalMethod();
  }
}

And then my transactional class, TestClass.java:

package my.package;

import my.package.TestDao;
import my.package.TestObject;
import org.springframework.transaction.annotation.Transactional;

public void TestClass {
  private TestDao testDao;

  public void setTestDao(TestDao testDao) {
    this.testDao = testDao;
  }

  public TestDao getTestDao() {
    return testDao;
  }

  public void nonTransactionalMethod() {
    transactionalMethod();
  }

  @Transactional
  private void transactionalMethod() {
    TestObject testObject = new TestObject();
    testObject.setId(1L);
    testDao.save(testObject);
  }
}

The trick here is that if the TestClass is a field in TestMain its class will be loaded by the ClassLoader before the application context is loaded. Since the weaving is at the load-time of the class, and this weaving is done by Spring through the application context, it won't get woven because the class is already loaded before the application context is loaded and aware of it.

The further particulars of TestObject and TestDao are unimportant. Assume they are wired up with JPA and Hibernate annotations and use Hibernate for persistence (because they are, and they do), and that all the requisite <bean />'s are set up in the application context file.

Edit: The full rundown for Spring Transactions in AspectJ mode using Compile-Time Weaving:

Add the following to META-INF/spring/applicationContext.xml:

<tx:annotation-driven mode="aspectj" />

(I'll assume you already have an AnnotationSessionFactoryBean and a HibernateTransactionManager set up in the application context. You can add transaction-manager="transactionManager" as an attribute to your <tx:annotation-driven /> tag, but if the value of your transaction manager bean's id attribute is actually "transactionManager", then it's redundant, as "transactionManager" is that attribute's default value.)

Add spring-aspects-3.1.2.RELEASE.jar and aspectjrt-1.7.0.jar to your classpath. I use Maven as my build tool, so here's the <dependency /> declarations for the POM.xml file:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>3.1.2.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.7.0</version>
</dependency>

In Eclipse Juno: Help -> Eclipse Marketplace -> text box labeled "Find:" -> type "ajdt" -> hit [Enter] -> "AspectJ Development Tools (Juno)" -> Install -> Etc.

After restarting Eclipse (it will make you), right-click your project to bring up the context menu. Look near the bottom: Configure -> Convert to AspectJ Project.

Add the following <plugin /> declaration in your POM.xml (again with the Maven!):

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>aspectj-maven-plugin</artifactId>
  <version>1.4</version>
  <configuration>
    <aspectLibraries>
      <aspectLibrary>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
      </aspectLibrary>
    </aspectLibraries>
  </configuration>
  <executions>
    <execution>
      <goals>
        <goal>compile</goal>
        <goal>test-compile</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Alternative: Right-click your project to bring up the context menu. Look near the bottom: AspectJ Tools -> Configure AspectJ Build Path -> Aspect Path tab -> press "Add External JARs..." -> locate the full/path/of/spring-aspects-3.1.2.RELEASE.jar -> press "Open" -> press "OK".

If you took the Maven route, the <plugin /> above should be freaking out. To fix this: Help -> Install New Software... -> press "Add..." -> type whatever you like in the text box labeled "Name:" -> type or copy+paste http://dist.springsource.org/release/AJDT/configurator/ in the text box labeled "Location:" -> press "OK" -> Wait a second -> check the parent checkbox next to "Maven Integration for Eclipse AJDT Integration" -> press "Next >" -> Install -> Etc.

When the plugin is installed, and you've restarted Eclipse, the errors in your POM.xml file should have gone away. If not, right-click your project to bring up the context menu: Maven -> Update Project -> press "OK".

Now for my actual test code class. Only one this time, TestClass.java:

package my.package;

import my.package.TestDao;
import my.package.TestObject;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.transaction.annotation.Transactional;

public void TestClass {
  private TestDao testDao;

  public void setTestDao(TestDao testDao) {
    this.testDao = testDao;
  }

  public TestDao getTestDao() {
    return testDao;
  }

  public static void main(String[] args) {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("META-INF/spring/applicationContext.xml");
    TestClass testClass = applicationContext.getBean(TestClass.class);
    testClass.nonTransactionalMethod();
  }

  public void nonTransactionalMethod() {
    transactionalMethod();
  }

  @Transactional
  private void transactionalMethod() {
    TestObject testObject = new TestObject();
    testObject.setId(1L);
    testDao.save(testObject);
  }
}

There is no trick to this one; since the weaving happens at compile time, which is before both the class loading and application context loading, the order of these two things no longer matters. This means that everything can go in the same class. In Eclipse, your code is constantly being re-compiled each time you hit Save (ever wondered what it was doing while it says "Building workspace: (XX%)"?), so it's woven and ready to go whenever you are.

Just like in the Load-Time example: the further particulars of TestObject and TestDao are unimportant. Assume they are wired up with JPA and Hibernate annotations and use Hibernate for persistence (because they are, and they do), and that all the requisite <bean />'s are set up in the application context file.

like image 988
Random Human Avatar asked Jul 23 '12 15:07

Random Human


1 Answers

By reading your question it's not really clear where you are stuck, so I am going to briefly list what is needed to get AspectJ intercept your @Transactional methods.

  1. <tx:annotation-driven mode="aspectj"/> in your Spring configuration file.
  2. <context:load-time-weaver/> as well in your Spring configuration file.
  3. An aop.xml located in the META-INF folder directly in your classpath. The format of this is also explained here. It should contain an aspect definition for that handles the @Transactional annotation: <aspect name="org.springframework.transaction.aspectj.AnnotationTransactionAspect"/>
  4. The weaver element in that same file should also have an include clause that tells it which classes to weave: <include within="foo.*"/>
  5. aspectjrt.jar, aspectjweaver.jar, spring-aspects.jar and spring-aop.jar in the classpath
  6. Starting the application using the flag -javaagent:/path/to/spring-instrument.jar (or spring-agent, as it is called in earlier releases)

The final step may not be necessary. It is a really simple class that enables using the InstrumentationLoadTimeWeaver, but if not available Spring will try to use another load time weaver. I have never tried that, though.

Now, if you think you have fulfilled all steps and still are having problems, I can recommend enabling some options on the weaver (defined in aop.xml):

<weaver options="-XnoInline -Xreweavable -verbose -debug -showWeaveInfo">

This makes the weaver output a bunch of information what is being weaved. If you see classes being weaved, you can look for your TestClass there. Then you at least have a starting point to continue troubleshooting.


Regarding your second edit, "It's almost like the weaving isn't happening fast enough to be woven before the class tries to execute.", the answer is yes, this can happen. I experienced a situation like this before.

I am a little rusty on the specifics, but basically it is something in the lines that Spring will not be able to weave classes that are loaded before the application context is being created. How are you creating your application context? If you are doing it programatically, and that class has a direct reference to TestClass, then this problem could occur, since TestClass will be loaded too early.

Unfortunately, I have found that debugging AspectJ is hell.

like image 155
waxwing Avatar answered Nov 05 '22 21:11

waxwing