Dealing with database deadlocks, retries, and profiling of methods is often a challenge, especially when you have multiple callers form multiple libraries. A specific example that comes to mind is the heavy usage of our database at BlackRock, with concurrent reads and writes from many applications, which causes a lack of response or deadlock exceptions. One of the most fundamental concepts in the Spring framework is Aspect Oriented Programming (AOP). The primary aim of AOP is to increase modularity and separation of concerns across modules in a project or multiple projects. AOP on services and other utilities is achieved with the introduction of an advice. An advice modifies the behavior of the code without changing the code itself. This means you can change the behavior before and after the actual code is executed without changing the actual method implementation.
Advices are reliant on the JDK’s API for proxies (see java.lang.reflect.Proxy) which are the foundations for all AOP based frameworks. These dynamic proxies introduced the concept of an InvocationHandler which is the most basic kind of pointcut, simply execute a piece of code “on the way” to the actual method call by manipulating the underlying bytecode on the fly. Almost all dependency injection frameworks such as Spring and Guice have expanded on the basic InvocationHandler and introduced the concept of advices, that allow for chaining of multiple advices, conditionals if’s to execute the method call or not, and if to execute with what behavior, and other useful tools that make this concept very powerful.
What is it good for?
An advice on a method call can help achieve reusability for a set of operations that are to be performed for a set of instructions to be done before and after. Let’s look for an example at the popular @Transactional annotation that is introduced in the spring-tx package. Once this annotation is placed on a method call, the framework will actually inject a dynamic proxy and not an actual instance of the class into the field. The fact that a proxy is injected and not an object instance of the class, allows the invocation handler of the proxy to define a set of instructions as a pointcut. Behind the scenes the framework actually injects a proxy to an injectable field, and the proxy delegates the work to a pointcut which opens a transaction before the actual method invocation, and commits or rolls back after the method is finished. We changed the method’s behavior without changing its code!
Of course there are other ways to introduce advices aside from annotations. Spring can use AspectJ expressions, method names which are regular expression based, or you can have an advice on every method call.
Spring’s advice framework is compatible with both AspectJ and the aopalliance library which is the standard set of APIs for aop related applications. This makes Spring’s and AspectJ’s advice framework very flexible and allowing for the migration among frameworks a very easy task. From a developer’s point of view the overhead of switching between the different AOP frameworks can be done with relative ease and minimal code change in your application. If the application is designed properly, this area of concern can be decoupled from the business logic and still achieve the same effect.
How do we use it?
We have some in house advices that are used to boost the resiliency of our application and to ensure optimal performance. For example, we profile service calls and show statistics how long the method call took, this is especially relevant for services that rely on IO to find bottlenecks. Since our applications must be kept at optimal efficiency for trading such techniques allow us to identify such problems early on in our testing phase and reduce the risk for potential production problems.
Let us take the example of a problem we faced at BlackRock. Since our database is heavily used with concurrent reads and writes from many applications, we sometimes find ourselves in situations in which the database either doesn’t respond in a timely manner, or responds with a deadlock exception. This means that our data is not persisted, and in applications which rely on asynchronistic logic, the user cannot simply retry the operation without starting again. In order to minimize occurrences of such instances, we created a generic @ActionRetry annotation as follows:
And its corresponding advice would look as follows:
This advice allows to wrap any action (in our case database persistence actions) thereby allowing us to change the behavior of the method call, by introducing a retry mechanism to it without having to call another utility class and repeating ourselves in every caller of that method.
What’s the downside?
Of course there are no pros without cons. Advices rely heavily on reflection, and the overuse of advice on the way to a method call may cause some performance issues. The ordering of the advice is often another area of concern. If your advices are dependent on each other, you will wind up trying to create a DAG and its maintenance can be taxing.
Code readability is another area that should be taken into account. Since you are essentially changing the behavior of the method in the service without actually changing the code itself, certain operations can take place that affect the operations inside the method itself. The change in behavior can sometimes be difficult to trace if not properly documented (which is a great example of how annotations can be useful, because they are documented!).
Logging is another area that is highly encouraged in this area. If the advice is not properly logged the change of behavior cannot be traced and may make debugging much harder to achieve if you are reading the code for the first time. It is therefore best practice to log at least the entering and exiting of an advice.
Advices are a very powerful tool to employ. They are flexible, well documented, widely used, reduce code redundancy, and are supported as a standard by the JDK. They should however be used carefully since over reliance on them can at times cause headaches and cause developers that are not familiar with the code base to use extra time to investigate issues with methods that are advised. It is therefore highly recommended that all advices be properly documented and logged. However, if used properly can highly increase the efficiency of the application by reusing the proper behavior across multiple services, and reduce the amount of testing needed since the behavior needs to be tested only once and not risk duplication or mutation of behaviors.
Enjoy advising your services!