Using AspectJ to weave Spring @Transactional in Kotlin
I just learnt the hard way (like many others) that Spring proxies can’t proxy calls to methods in the same instance. Spring proxies are wrappers around the real instance, and it is this wrapper that you actually get when a dependency is wired in. Therefore calls made to wrapper are proxied, but once you’re inside the wrapper and into the real instance any internal calls will not be.
This can easily trip you up, for example:
class ThingService {
...
fun updateThings() {
things.forEach { updateThing(it) }
}
@Transactional
fun updateThing() {
...
}
}
So if you wire in a ThingService
, then calling updateThing
(singular) will use a transaction, but calling updateThings
(plural) will not use transactions at all.
So what to do? - the answer could be AspectJ transactions. AspectJ uses a process they call weaving to incorporate the code for an aspect (like @Transactional
) into the method bytecode, so it will be used whether for an internal call or even for private
methods. AspectJ can weave in the aspects like this at compile time, or load time - the results are the same either way, but load-time weaving requires a classloader that knows how to weave.
Using AspectJ transactions in Spring boot
With either type of weaving, it is necessary to tell Spring that you are going to use AspectJ transactions by adding mode = AdviceMode.ASPECTJ
to @EnableTransactionManagement
, i.e.:
@SpringBootApplication
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
class Application
Compile-time weaving
Doing the weaving at compile-time ought to bring consistent results, and requires no further modifications to your application. When using gradle it can be as simple as the following:
- Add io.freefair.aspectj.post-compile-weaving plugin to
build.gradle.kts
i.e.:
plugins {
...
id("io.freefair.aspectj.post-compile-weaving") version "6.4.1"
}
- Add AspectJ dependencies to
build.gradle.kts
:
dependencies {
...
implementation("org.aspectj:aspectjrt:1.9.9.1")
implementation("org.aspectj:aspectjweaver:1.9.9.1")
}
- Name the module with aspects that you wish to weave (custom aspects in the host gradle module are automatically processed):
dependencies {
...
aspect("org.springframework:spring-aspects")
}
One drawback of compile-time weaving with Kotlin is that Kotlin class files are harder to decompile than Java ones. This can make it hard (compared to Java) to verify that you have correctly woven in the aspects. IntelliJ in particular shows the method body as { /* compiled code */ }
, I had some luck with other decompilers - enough to confirm that the weaving had worked at least.
Load-time weaving
If you can’t modify your build chain, load-time weaving may be a better option. However, it requires a custom classloader that can weave in the aspects. Some environments may provide a classloader that can already do this - for example Apache Tomcat according to the Spring docs. However, I couldn’t get this to work, at least with the embedded Tomcat in a Spring Boot application. I took the easy route to a proof of concept by using a java agent:
- Add AspectJ dependencies to
build.gradle.kts
:
dependencies {
...
implementation("org.aspectj:aspectjrt:1.9.9.1")
implementation("org.springframework.boot:spring-boot-starter-aop")
implementation("org.springframework:spring-aspects")
implementation("org.springframework:spring-instrument")
}
- Add
META-INF/aop.xml
to configure weaving, something like (the verbose output option is particularly useful):
<aspectj>
<weaver options="-verbose">
<include within="org.springframework.transaction.*"/>
</weaver>
</aspectj>
- Configure the javaagent for weaving classes, e.g.
-javaagent:<PATH_TO_GRADLE_CACHES>/modules-2/files-2.1/org.springframework/spring-instrument/5.3.18/8c8240406471d2d80482c32ecbdd918405350595/spring-instrument-5.3.18.jar
. This may need to be done in different ways for different use cases, for example tests can be configured as below:
tasks.test {
useJUnitPlatform()
jvmArgs = listOf("-javaagent:<PATH_TO_GRADLE_CACHES>/modules-2/files-2.1/org.springframework/spring-instrument/5.3.18/8c8240406471d2d80482c32ecbdd918405350595/spring-instrument-5.3.18.jar")
}
I find this approach less pleasant, as it’s necessary to supply the javaagent
argument in multiple places, as well as having to somehow scrape the value of it. As well as in tests, the actual running application needs the argument (or to get the Tomcat classloader doing the job), and similarly for any custom run configuration.
Another issue is that if @Transactional
weaving just didn’t occur in the real deployment, you might not even notice until your database was full of half-completed transactions. I suspect most live deployments won’t have monitoring set up to perform an action that will fail and verify that the transaction is rolled back.
Conclusion
AspectJ-style @Transactional
did exactly what it was supposed to do, and makes it a lot harder to accidentally have no transactions, as well as adding additional flexibility (e.g. annotating private methods).
Unfortunately I hit one strange issue in testing that - due to an abundance of caution - stops me from using it for the time being.