How to Test a Spring AOP Aspect
1. Introduction
Aspect-Oriented Programming (AOP) is one of programming paradigms that separates cross-cutting concerns as aspects. It complements Object-Oriented Programming (OOP) by enabling the encapsulation of behaviors that affect multiple classes into reusable aspects. Spring AOP framework supports AOP with AspectJ annotations. In this example, I will create a custom aspect with several advices including tracking the method execution time around the advice. Then I will demonstrate the Spring AOP test aspect with both unit and integration tests via a spring bean. The spring bean’s method’s execution will be weaved with the desired Aspect’s advice.
2. Setup
In this step, I will create a gradle project with spring-boot-starter
, spring-boot-starter-aop
, AspectJ
, and Junit
libraries.
build.gradle
plugins { id 'java' id 'org.springframework.boot' version '3.3.0' id 'io.spring.dependency-management' version '1.1.5' } group = 'org.zheng.demo' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-aop' // AspectJ weaver implementation 'org.aspectj:aspectjweaver:1.9.22.1' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
3. Source Code
3.1 Spring AOP Aspect
Spring AOP supports method execution join points for spring beans. It relies on the annotations defined by the aspectjweaver
library. Spring AOP includes the following key concepts:
@Aspect
: defines the aspect component that encapsulates a concern that cuts across multiple classes, such as logging, security, or transaction management. Make sure to include both@Component
and@Aspect
annotations.- Join Point: A point in the execution of a program, such as method execution or object creation, where an aspect can be applied.
@Pointcut
: An expression that matches join points and determines whether an advice should be applied at that join point. Pointcuts can be based on method names, annotations, or other attributes.@Before
,@After
,@AfterReturning
, and@AfterThrowing
Advices: Advice is the action taken by an aspect at a particular join point.
In this step, I will create a MyAopExample.java
class with the above annotations.
MyAopExample.java
package org.zheng.demo.aop; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class MyAopExample { @After("execution(* org.zheng.demo.service.*.*(..))") public void afterAdvice() { System.out.println("AOP_afterAdvise is executed."); } @AfterReturning(pointcut = "afterReturningPC()", returning = "retV") public void afterReturningAdvice(String retV) { System.out.println("AOP_afterReturningAdvise is executed. " + retV); } @Pointcut("execution(* org.zheng.demo.service.*.*(..))") public void afterReturningPC() { } @AfterThrowing(pointcut = "matchingAll()", throwing = "e") public void afterThrowingAdvice(RuntimeException e) { Thread.setDefaultUncaughtExceptionHandler((t, e1) -> System.out.println("Caught " + e1.getMessage())); System.out.println("AOP_afterThrowingAdvice is executed"); } @Before("execution(* org.zheng.demo.service.MyService.myMethod(..))") public void beforeAdvice() { System.out.println("AOP_beforeAdvice is executed"); } @Around("execution(* org.zheng.demo.service.*.*(..))") public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object proceed = joinPoint.proceed(); long endTime = System.currentTimeMillis(); long executionTime = endTime - startTime; System.out.println("AOP_aroundAdvice " + joinPoint.getSignature() + " executed in " + executionTime + "ms"); return proceed; } @Pointcut(value = "execution(public * org.zheng..*.*(..))") public void matchingAll() { } }
- Line 13, 14: define a spring aspect component.
- Line 17: define an
@After
advice for any service’s any method at the “org.zheng.demo.service
” package. - Line 22: define an
@AfterReturning
advice for any service’s any method at the “org.zheng.demo.service
” package. - Line 27: define a
@Pointcut
for any service’s any method at the “org.zheng.demo.service
” package. - Line 31: define a
@AfterThrowing
advice formatchingAll()
pointcut. - Line 37: define a
@Before
advice fororg.zheng.demo.service.MyService.myMethod
. - Line 42: defines a
@Around
advice to track the method’s execution time. - Line 55: defines a
@Pointcut
formatchingAll()
.
3.2 Spring Service
In this step, I will create a MyService.java
class which annotates with the @Service
annotation and has two methods.
MyService.java
package org.zheng.demo.service; import org.springframework.stereotype.Service; @Service public class MyService { public void myMethod() { System.out.println("Executing myMethod"); } public String returnData(String input) { System.out.printf("Executing returnData input=%s\n", input); Integer.parseInt(input); return input.toUpperCase(); } }
- Line 14: the
Integer.parseInt
will throw an exception when the input is not a numeric string.
3.3 Spring Boot Application
In this step, I will create a SpringAopExampleApplication.java
which includes @EnableAspectJAutoProxy
. Please note, spring boot automatically discovers if you have missed it. Recommended added for a clarity purpose.
SpringAopExampleApplication.java
package org.zheng.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.EnableAspectJAutoProxy; @SpringBootApplication @EnableAspectJAutoProxy public class SpringAopExampleApplication { public static void main(String[] args) { SpringApplication.run(SpringAopExampleApplication.class, args); } }
- Line 8:
@EnableAspectJAutoProxy
is used to enable AOP.
4. Spring AOP Test Aspect
4.1 Unit Tests
In this step, I will create a MyAspectUnitTest.java
class which mocked the AOP’s proceed
and getSignature
methods.
MyAspectUnitTest.java
package org.zheng.demo; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.zheng.demo.aop.MyAopExample; @ExtendWith(MockitoExtension.class) public class MyAspectUnitTest { @Mock private ProceedingJoinPoint proceedingJoinPoint; @InjectMocks private MyAopExample testAspect; @Test public void testLogExecutionTime() throws Throwable { // Given when(proceedingJoinPoint.proceed()).thenAnswer(invocation -> { Thread.sleep(100); // Simulate method execution time return null; }); when(proceedingJoinPoint.getSignature()).thenReturn(mock(MethodSignature.class)); // When testAspect.logExecutionTime(proceedingJoinPoint); // Then verify(proceedingJoinPoint, times(1)).proceed(); // Additional assertions can be made to check the log output if needed } }
Run the Junit test and capture the output:
MyAspectUnitTest Output
AOP_aroundAdvice Mock for MethodSignature, hashCode: 219638321 executed in 109ms
4.2 Integration Tests
In this step, I will create a MyAspectIntegrationTest.java
class which verifies that Spring service’s methods are weaved with the MyAopExample
‘s advice defined at step 3.1 based on the matching join points.
MyAspectIntegrationTest.java
package org.zheng.demo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.zheng.demo.service.MyService; @SpringBootTest public class MyAspectIntegrationTest { @Autowired MyService myService; @Test void test_aop_before_after() { myService.myMethod(); } @Test void test_aop_afterReturning() { String ret = myService.returnData("123"); assertEquals("123", ret); } @Test void test_aop_afterThrowing() { NumberFormatException ex = assertThrows(NumberFormatException.class, () -> { myService.returnData("mary"); }); assertEquals("For input string: \"mary\"", ex.getMessage()); } }
Run the Junit test and capture the output. As you can see from the output, the five advice methods: before
, after
, around
, afterThrowing
, afterReturning
were wearved as expected.
MyAspectIntegrationTest Output
2024-06-15T13:04:33.577-05:00 INFO 40688 --- [spring-aop-example] [ main] org.zheng.demo.MyAspectIntegrationTest : Started MyAspectIntegrationTest in 1.35 seconds (process running for 2.425) Executing returnData input=123 AOP_afterReturningAdvise is executed. 123 AOP_afterAdvise is executed. AOP_aroundAdvice String org.zheng.demo.service.MyService.returnData(String) executed in 1ms Executing returnData input=mary AOP_afterThrowingAdvice is executed AOP_afterAdvise is executed. AOP_beforeAdvice is executed Executing myMethod AOP_afterAdvise is executed. AOP_aroundAdvice void org.zheng.demo.service.MyService.myMethod() executed in 0ms
Line 3, 4: both afterReturning
and after
advice methods are weaved as they match the pointcut expression.
Run the tests and capture the status as the following screenshot:
5. Conclusion
In this example, I created an AOP aspect with aspectJ annotations and weaved it into Spring bean’s methods. I also demonstrated the spring AOP test aspect via both unit and integration tests. As you saw in this example, there are few benefits of AOP:
- Modularity: separates cross-cutting concerns from the main business logic, making code easier to maintain and understand.
- Reusability: aspects can be reused across different parts of the application.
- Maintainability: changes to cross-cutting concerns (like logging or security) can be made in one place.
6. Download
This was an example of a gradle project which created a Spring AOP Aspect and tested it via both unit and integration tests.
You can download the full source code of this example here: How to Test a Spring AOP Aspect