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
@Aroundannotation. 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 theProceedingJoinPointcauses 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-pluginto 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.