Spring makes building a reliable application much easier thanks to its declarative transaction management.
It also supports programmatic transaction management, but that’s not as common.
In this article, I want to focus on the declarative transaction management angle, since it seems much harder to debug compared to the programmatic approach. This is partially true. We can’t put a breakpoint on a transactional annotation.
But I’m getting ahead of myself!
What is Spring’s Method Declarative Transaction Management?
When writing a spring method or class, we can use annotations to declare that a method or a bean (class) is transactional. This annotation lets us tune transactional semantics using attributes. This lets us define behavior such as:
- Transaction isolation levels – lets us address issues such as dirty reads, non-repeatable reads, phantom reads, etc.
- Transaction Manager
- Propagation behavior – we can define whether the transaction is mandatory, required, etc. This shows whether the method expects to receive a transaction and how it behaves
- readOnly attribute – the DB does not always support a read-only transaction. But when it is supported, it’s an excellent performance/reliability tuning feature
And much more.
Isn’t the Transaction Related to the Database Driver?
The concept of transactional methods is very confusing to new spring developers. Transactions are a feature of the database driver/JDBC Connection, not of a method. Why declare it in the method?
There’s more to it. Other features, such as message queues, are also transactional. We might work with multiple databases. In those cases, if one transaction is rolled back, we need to rollback all the underlying transactions. As a result, we do the transaction management in user code and spring seamlessly propagates it into the various underlying transactional resource.
How can we Write Programmatic Transaction Management if we don’t use the Database API?
Spring includes a transaction manager that exposes the API’s we typically expect to see: begin, commit and rollback. This manager includes all the logic to orchestrate the various resources.
You can inject that manager to a typical spring class, but it’s much easier to just write declarative transaction management like this Java code:
@Transactional public void myMethod() { // ... }
I used the annotation on the method level, but I could have placed it on the class level. The class defines the default and the method can override it.
This allows for extreme flexibility and is great for separating business code from low level JDBC transaction details.
Dynamic Proxy, Aspect Oriented Programming and Annotations
The key to debugging transactions is the way spring implements this logic. Spring uses a proxy mechanism to implement the aspect oriented programming declarative capabilities. Effectively, this means that when you invoke myMethod
on MyObject
or MyClass
spring creates a proxy class and a proxy object instance between them.
Spring routes your invocation through the proxy types which implement all the declarative annotations. As such, a transactional proxy takes care of validating the transaction status and enforcing it.
Debugging a Spring Transaction Management using Lightrun
IMPORTANT: I assume you’re familiar with Lightrun basics. If not, please read this.
Programmatic transaction management is trivial. We can just place a snapshot where it begins or is rolled back to get the status.
But if an annotation fails, the method won’t be invoked and we won’t get a callback.
Annotations aren’t magic, though. Spring uses a proxy object, as we discussed above. That proxy mechanism invokes generic code, which we can use to bind a snapshot. Once we bind a snapshot there, we can detect the proxy types in the stack. Unfortunately, debugging proxying mechanisms is problematic since there’s no physical code to debug. Everything in proxying mechanisms is generated dynamically at runtime. Fortunately, this isn’t a big deal. We have enough hooks for debugging without this.
Finding the Actual Transaction Class
The first thing we need to do is look for the class that implements transaction functionality. Opening the IntelliJ/IDEA class view (Command-O or CTRL-O) lets us locate a class by name. Typing in “Transaction” resulted in the following view:
This might seem like a lot, but we need a concrete public class. So annotations and interfaces can be ignored. Since we only care about Spring classes, we can ignore other packages. Still, the class we are looking for was relatively low in the list, so it took me some time to find it.
In this case, the interesting class is TransactionAspectSupport
. Once we open the class, we need to select the option to download the class source code.
Once this is done, we can look for an applicable public method. getTransactionManager
seemed perfect, but it’s a bit too bare. Placing a snapshot there provided me a hint:
I don’t have much information here but the invokeWithinTransaction
method up the stack is perfect!
Moving on to that method, I would like to track information specific to a transaction on the findById
method:
To limit the scope only to findById
we add the condition:
method.getName().equals("findById")
Once the method is hit, we can see the details of the transaction in the stack.
If you scroll further in the method, you can see ideal locations to set snapshots in case of an exception in thread, etc. This is a great central point to debug transaction failures.
One of the nice things with snapshots is that they can easily debug concurrent transactions. Their non-blocking nature makes them the ideal tool for that.
TL;DR
Declarative configuration in Spring makes transactional operations much easier. This significantly simplifies the development of applications and separates the object logic from low level transactional behavior details.
Spring uses class-based proxies to implement annotations. Because they are generated, we can’t really debug them directly, but we can debug the classes, they use internally. Specifically: TransactionAspectSupport
is a great example.
An immense advantage of Lightrun is that it doesn’t suspend the current thread. This means issues related to concurrency can be reproduced in Lightrun. Everything discussed here can be accomplished with the free version of Lightrun.
The post Spring Transaction Debugging in Production with Lightrun appeared first on foojay.