The Spring Framework: Ep. 6 - Spring AOP
In this article, we will talk about Aspect Oriented Programming(AOP) which is a powerful feature of Spring Framework. Transaction management, logging, and caching are some of the things that can be implemented using AOP.
A lot of people use AOP without even knowing it. So even if you don't plan on using AOP directly, it's still good to know how it works because Spring Boot uses it extensively under the hood.
Aspect Oriented Programming (AOP) is a programming paradigm that lets you modularize cross-cutting concerns in aspects. A cross-cutting concern is something that crosses the boundaries of a class—for example, logging, transaction management, and authentication.
As usual, let's start with an example. The code for this article is available on GitHub.
Let's say we have a class that performs some business logic and we want to log the time it takes to execute the method. We could do it like this:
public class BusinessService {
public void doSomething() {
long startTime = System.currentTimeMillis();
// do something
long endTime = System.currentTimeMillis();
System.out.println("Time taken: " + (endTime - startTime));
}
}
This works, but it's not very elegant. We have to repeat the same code in every method we want to log. Also, if we want to change the way we log the time, we have to change it in every method. This code also violates the Single Responsibility Principle(SOLID) as it's doing more than one thing.
We can probably do better using some kind of design pattern, but in one way or another, the logging code will still be mixed with the business logic. Here is where AOP comes in. Instead of working with classes, AOP works with aspects.
To use AOP, we need to add the spring-aspects dependency to our build.gradle
file.
implementation 'org.springframework:spring-aspects:6.0.4'
We also need to add the @EnableAspectJAutoProxy
annotation to our configuration class.
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
class MyConfiguration {
}
Now let's implement an aspect.
@Component
@Aspect
public class TimingAspect {
@Around("execution(* com.cristianrita.aop.business.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
System.out.println("Time taken: " + (endTime - startTime) + "ms");
return result;
}
}
I find the terminology used in AOP a bit confusing, so hopefully having an example will help you understand it better.
Aspect = a modularization of a concern that cuts across multiple classes. In our case, the TimingAspect class is an aspect.
Join point = a point in the execution of the application where we can plug in aspects. In Spring AOP, a join point is always the execution of a method.
Advice = the action taken by an aspect at a particular joint point. In our case, Around is an advice.
Other types of advice are: before, after, after-returning, after-throwing.
Pointcut = a predicate that matches join points. In our case, the execution(* com.cristianrita.aop.business..(..))
part is a point cut. It defines the methods where the aspect will be applied. It will be applied to all methods in the business package. The expression is written in the AspectJ expression language. You can find more details about it here. There are multiple types of pointcuts, another one being the annotation pointcut. It matches all the methods annotated with a specific annotation.
For a complete list of pointcut types, check out the Spring documentation.
Now that we have our aspect, we need to apply it to our business service.
@Service
public class MyBusinessLogic {
public void doSomething() throws InterruptedException {
Thread.sleep(1000);
}
public void doSomethingElse() throws InterruptedException {
Thread.sleep(5000);
}
}
public class AopApplication {
public static void main(String[] args) throws InterruptedException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
MyBusinessLogic businessLogic = context.getBean(MyBusinessLogic.class);
businessLogic.doSomething();
businessLogic.doSomethingElse();
}
}
If we run the application, we will see the following output:
Time taken: 1006ms
Time taken: 5005ms
Under the hood, Spring AOP uses proxies to apply the aspects. There are two types of proxies: JDK dynamic proxies
and CGLIB proxies
. If the class we want to apply the aspect to implements an interface, then Spring AOP will use a JDK dynamic proxy. Otherwise, it will use a CGLIB proxy. If you don't know what a proxy is, I wrote an article about it here.
A lot of information, right? Let's recap:
we wanted to execute some code before and after a method call, without polluting the business logic;
we created an aspect that contains the code we want to execute;
we created a pointcut that defines where we want to apply the aspect; in our case, we want to apply it to all the methods in the business package;
@Around is an advice that defines when we want to apply the aspect; in our case, we want to apply it before and after the method call
Let's try another example where we log the name of the method that is being executed and the arguments it receives.
@Component
@Aspect
public class MethodExecutionAspect {
@Before("@annotation(com.cristianrita.aop.LogMethodExecution)")
public void logMethodExecution(JoinPoint joinPoint) {
System.out.println("Method execution: " + joinPoint.getSignature().getName() + " with args: " + Arrays.stream(joinPoint.getArgs()).toList());
}
}
In this case, we are using the @Before
advice. This means that the aspect will be applied before the method call. We are also using the @annotation
pointcut. This means that the aspect will be applied to all the methods annotated with the @LogMethodExecution
annotation.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogMethodExecution {
}
@LogMethodExecution
public int sum(int a, int b) {
return a + b;
}
public static void main(String[] args) throws InterruptedException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
MyBusinessLogic businessLogic = context.getBean(MyBusinessLogic.class);
businessLogic.doSomething();
businessLogic.doSomethingElse();
businessLogic.sum(1, 2);
}
Although these examples are simple, I hope they helped you understand the basics of AOP.
To wrap up, let's see some of the advantages and disadvantages of AOP.
Advantages:
it helps you modularize cross-cutting concerns
it helps you avoid code duplication
it helps you keep your code clean and easy to maintain
Disadvantages:
it can be hard to understand debug and test
hard to keep track of all the aspects that are being applied to a method. Think about the execution pointcut. It will be applied to all the methods in the business package. Other developers might not be aware of this and might be surprised when they see some code being executed before and after their method call.
proxying is not free. It adds some overhead to the application.
We only scratched the surface of AOP. There are a lot of other things that we can do with it, but I think this is enough to get you started.