1. Introduction
In this article, you will learn about the Around advice in Spring AOP that runs around a matched Joinpoint’s execution. Around advice is declared using the @Around annotation.
You already know about the 5 types of advice from the previous article as listed below.
- Before advice –
@Before
- After returning –
@AfterReturning
- After throwing –
@AfterThrowing
- After (finally) advice –
@After
- Around advice –
@Around
2. Around advice execution – @Around
- Around advice is declared by using the
@Around
annotation. It contains code which is executed before and after the matched method (JoinPoint) - The first parameter of the advice method must be of type
ProceedingJoinPoint
. - Within the body of the advice, calling
proceed()
on theProceedingJoinPoint
causes the underlying method to execute. - In most of the case, it is important to return a value from the advice method as the matched JoinPoint can have a return type.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("execution (* com.jbd.myapp.service.*.*(..))")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// Custom code before method execution
Object retVal = pjp.proceed(); //method execution
// Custom code after method execution
return retVal; //exit the advice
}
}
3. Example of Around advice – @Around
Problem statement – We will use the same UserRepository from previous examples and add a new capability to track the time taken by a method to complete its execution. Any method annotated with @ExecutionTime
will log the total execution time in the console. The @ExecutionTime
is a custom annotation.
Create a maven project and add the following dependencies in the pom.xml
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<!-- Step-1 Add the dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<dependencies>
We will create to the methods annotated with @ExecutionTime
. So we create the sample code – UserRepository, the configuration class, and the annotation as below. As we discussed before, the annotation @EnableAspectJAutoProxy
is used to enable the Spring-AOP.
package com.jbd.saop.around.dao;
import com.jbd.saop.around.ExecutionTime;
import org.springframework.stereotype.Repository;
//A very stupid demo repository
@Repository
public class UserRepository {
//Add a user
@ExecutionTime
public UserRepository add(String username) throws InterruptedException {
Thread.sleep(100);
if(username == null) {
throw new RuntimeException("username is null", new NullPointerException());
}
System.out.println("New user added: " + username);
return this;
}
//Update an user
public UserRepository update(String username, String email) {
System.out.println("Update email: " + email);
return this;
}
//Delete an user
@ExecutionTime
public boolean delete(String username){
if (username == null) {
throw new RuntimeException("username is null", new NullPointerException());
}
System.out.println("User deleted: " + username);
return true;
}
}
package com.jbd.saop.around;
import java.lang.annotation.*;
/**
* Tracks the execution time.
*/
//Allowed to use only on methods
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExecutionTime {
}
package com.jbd.saop.around;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.jbd.saop.around")
@EnableAspectJAutoProxy
public class ApplicationConfig {
}
The pointcut expression matches all methods that are annotated with @ExecutionTime
. If any of the methods throws RuntimeException
, it will be logged. Create the aspect class as shown below.
package com.jbd.saop.around.advice;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.Configuration;
import java.time.LocalTime;
@Configuration
@Aspect
public class ExecutionTimeLogger {
@Around("@annotation(com.jbd.saop.around.ExecutionTime)")
public Object logExecTime(ProceedingJoinPoint pJoinPoint){
System.out.println("Before method: "
+ pJoinPoint.getSignature().toShortString());
long beforeTime = System.currentTimeMillis();
Object result = null;
try {
result = pJoinPoint.proceed();//Important
//If method throws Exception or any error occurs
} catch (Throwable throwable) {
throwable.printStackTrace();
}
long afterTime = System.currentTimeMillis();
System.out.println("Time taken to execute: "
+ (afterTime - beforeTime) + "ms");
return result;
}
}
Finally, we will test the example to see its output. Add the test dependencies in the pom.xml
before running the test cases.
<dependencies>
...
<!-- Test Dependencies-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
...
<!-- Important for Jupiter-engine -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M3</version>
</plugin>
</plugins>
</build>
</project>
Always remember to upgrade the
maven-surefire-plugin
to 3.0.0-M3 or higher when using Junit-jupiter-*.
Once you have added the test dependencies, create the Test class inside src\test
.
package com.jbd.saop.around;
import com.jbd.saop.around.dao.UserRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(ApplicationConfig.class)
public class TestAroundAdvice {
@Autowired
private UserRepository userRepository;
@Test
public void testNull(){
Assertions.assertNotNull(userRepository);
}
@Test
public void testAddUser() throws InterruptedException {
userRepository.add("sample_username");
}
@Test
public void testAddUserFailure() throws InterruptedException {
userRepository.add(null);
}
}
The test cases should pass with the following output in the console. The order could be different. I suggest to run each test case separately and check the output.
Output:Before method: UserRepository.add(..) New user added: sample_username Time taken to execute: 124ms Before method: UserRepository.add(..) java.lang.RuntimeException: username is null at com.jbd.saop.around.dao.UserRepository.add(UserRepository.java:14) at com.jbd.saop.around.dao.UserRepository$$FastClassBySpringCGLIB$$dcb57c42.invoke() ... Caused by: java.lang.NullPointerException ... 86 more Time taken to execute: 120ms
Conclusion:
As you can see, the around advice in Spring AOP is quite powerful. You have more control over the matched method execution, especially due to ProceedingJoinPoint
. Always remember to call the proceed()
inside the advice method and return the Object value. We will be discussing about Joinpoint and ProceedingJoinPoint in a separate article for more inside.
Download the working code example from GitHub code repo.