Introduction to AOP in Spring Boot – Understanding the Basics

Aspects-Oriented Programming (AOP) in Spring is an addition to object-oriented programming. Understanding how it works would require certain knowledge of a process called weaving, and other AOP-specific concepts. If you’re not already familiar with them, don’t worry! Just keep on reading, and I’m pretty sure you’ll end up with a much better grasp of the topic. And, if you worked with Spring or Spring Boot, you may have already worked with AOP unknowingly given its deep integration within the framework. That integration is what I will focus on going further with this blog.

But, before I start with some examples and get all technical, let’s first define AOP in theory.

What is AOP?

AOP is a paradigm that increases the modularity of applications by managing cross-cutting concerns like transactions, logging, security, and caching.
Now you might ask: what are cross-cutting concerns?
In simple terms, they are parts of an application that affects multiple modules.
This paradigm allows us to handle those concerns in one place instead of reusing the same logic throughout multiple layers of the application. Sounds good right?

Now, let’s talk about weaving and proxy classes.

Weaving and Proxy Classes

AOP is achieved through a technique known as “weaving”. This process involves introducing new behaviors into existing code, often by using proxy classes. While Spring generally uses runtime weaving with so-called proxy classes, it’s worth noting that there are multiple strategies for weaving. The idea behind all of them is the same, but for now, just keep in mind that there must be a way to introduce the mentioned new behavior without changing our code. The example below should give us more context on how this works.

The @Transactional Annotation

A commonly encountered annotation in Spring applications is @Transactional. You might be familiar with its function, but did you know Spring utilizes AOP to make your database operations transactional?

For instance, consider a method annotated with @Transactional:

@Service
public class ExampleClass {

    private final MyCustomRepository repository;

    public ExampleClass(MyCustomRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public void save(Object object) {
        repository.save(object);
    }
}
Copy

Here, you can see a simple class that injects a repository and has a save method that, you guessed it, saves an object into the database. Since @Transactional is present on the method, Spring will not invoke your actual save method but create a proxy class that will override your method. Let’s call that class ProxyExampleClass.

public class ProxyExampleClass extends ExampleClass {

    private TransactionManager transactionManager;

    public ProxyExampleClass(TransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    @Override
    public void save(Object object) {
        TransactionStatus status = transactionManager.begin();
        try {
            super.save(object); transactionManager.commit(status);
         } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }
 }
Copy

It is immediately visible how proxying works. We have a proxy class, which extends your class and overrides it by wrapping it in a transaction. There are many examples of how Spring uses these kinds of classes with different types of annotations like @Async or @PreAuthorize. Keep in mind that for a method to be proxied, it needs to be called outside of its own class. The reason for this is that a proxy is created for the entire class, and calling additional methods inside the same class would bypass the proxy layer.

Writing Your Own Aspects

As long as the spring-boot-starter-aop dependency is part of your project’s classpath either by introducing it manually or as a part of some transitive dependency, like popular spring-boot-starter-web, you will be able to write your own aspects.

One of the clear indicators of how deeply aspects are integrated into Spring is that they are essentially specialized types of Spring beans, and as such, they follow the same lifecycle as any other bean in the Spring ecosystem.

Let’s look at an example to illustrate this:

@Component
@Aspect
public class LogMetricAspect {

    @Autowired
    private LogMetricRepository logMetricRepository;

    @After("execution(* io.tacta.SomeClass.someMethod(..))")
    public void logBefore(JoinPoint joinPoint) {
        logMetricRepository.saveMetric(joinPoint);
    }
}
Copy

In this example, our LogMetricAspect is annotated with @Component, making it a Spring bean (note that it will not work if it weren’t a bean).
This allows us to leverage other features like dependency injection (the @Autowired annotation in the example). Since it’s a bean, you also have the flexibility to change its scope and use lifecycle methods like @PostConstruct and @PreDestroy.

Understanding Aspect Terminology

To create your own aspects, you need to understand three key terms: Advice, Pointcut, and Join Point. Based on the example above, let’s try to understand each of them.

Advice: The code that gets executed. In our case saving the log in the database after the actual method invocation. This one is pretty straightforward, but there is a catch! There are multiple types of Advice. In the example, you can see the @After Advice meaning that this code will be executed after the invocation of your actual method. We still have

@Before – before invocation,

@AfterReturning – after returning without any error,

@AfterThrowing – after the error was thrown during invocation,

@Around – The most powerful one. With this Advice you are in control of method invocation, allowing you to put code before, and after and use a try-catch block to handle errors.

Pointcut: A predicate that matches join points, specifying where the Advice should be applied. The line “execution(* io.tacta.SomeClass.someMethod(..))” is considered a Pointcut. There is a special type of syntax you would have to learn in order to write them and I could write an entire blog only about it. But for now, it is enough to know that the line tells us to execute our Advice on every execution of a method called someMethod in SomeClass in package io.tacta, regardless of its return type or parameters.

Join Point: The point in the program’s execution where an Aspect should be applied. It is a specific location of the program’s execution.
To be exact, it would look like this:

public class SomeClass {
     public void someMethod() {
         // join point
    }
}
Copy

Combining these elements, you get an Aspect. A powerful tool to handle your cross-cutting concerns across the entire application.

I could go on and on about the terminology and examples, but I think it would be easier to understand the concepts and examples if I divided them into multiple blogs on this topic. So, next time we can dig deeper into subjects such as weaving, proxying, and customizing aspects.