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.
Step-1: Add the dependencies
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>
Step-2: Create the sample classes
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 { }
Step-3: Create Pointcut expressions and advice
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; } }
Step-4: Test the example
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.
I usually wrap Object retVal = pjp.proceed(); with a try catch to account for methods returning null or void. This works out well since we don’t require the output of that methods to be returned and there is no need of changing each of the “source” classes to not return NULL. What do you think about that?