Spring AOP Tutorial with Code Examples
This Spring AOP tutorial explains how to modularize cross‑cutting concerns such as timing by defining an aspect with @Before, @After, @AfterReturning, @AfterThrowing and @Around advice, demonstrates refactoring a calculation service, and compares Spring’s proxy mechanisms to a custom JDK dynamic proxy implementation.
Introducing AOP
Spring AOP is a core concept of the Spring framework. AOP (Aspect‑Oriented Programming) extends OOP by allowing cross‑cutting concerns to be modularized, reducing coupling and improving reusability.
AOP is a continuation of OOP, a hot topic in software development and an important part of Spring. By using AOP you can isolate business logic parts, lower coupling, and increase reuse and development efficiency.
Example Code
First we create a service interface:
public interface CalculateService {
int add(int x, int y);
int reduce(int x, int y);
int multi(int x, int y);
int division(int x, int y);
}Then the implementation class:
@Service
public class CalculateServiceImpl implements CalculateService {
@Override
public int add(int x, int y) {
System.out.println(x + " + " + y + " = " + (x + y));
return x + y;
}
@Override
public int reduce(int x, int y) {
System.out.println(x + " - " + y + " = " + (x - y));
return x - y;
}
@Override
public int multi(int x, int y) {
System.out.println(x + " * " + y + " = " + (x * y));
return x * y;
}
@Override
public int division(int x, int y) {
System.out.println(x + " / " + y + " = " + (x / y));
return x / y;
}
}Calling the bean from the Spring container:
public static void main(String[] args) throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
CalculateService calculateService = context.getBean("calculateServiceImpl", CalculateService.class);
calculateService.add(1, 1);
calculateService.reduce(1, 1);
calculateService.multi(1, 1);
calculateService.division(1, 1);
}Running result:
1 + 1 = 2
1 - 1 = 0
1 * 1 = 1
1 / 1 = 1Problem Statement
Now the requirement changes: we need to print the current system time before and after each calculation. The naïve solution is to hard‑code the time prints inside every method:
@Service
public class CalculateServiceImpl implements CalculateService {
@Override
public int add(int x, int y) {
System.out.println("Time before: " + LocalDateTime.now());
System.out.println(x + " + " + y + " = " + (x + y));
System.out.println("Time after: " + LocalDateTime.now());
return x + y;
}
// similar changes for reduce, multi, division
}Result shows duplicated time prints and poor maintainability.
Time before: 2022-01-21T14:35:21.806
1 + 1 = 2
Time after: 2022-01-21T14:35:21.806
... (similar output for other methods)Spring AOP Refactor
Spring AOP can extract the timing logic into an aspect:
@Aspect
@Component
public class CalculateAspectJ {
@Before("execution(* com.wwj.spring.demo.aop.CalculateService.add(..))")
public void printBefore() {
System.out.println("Time before: " + LocalDateTime.now());
}
}The pointcut expression matches the target method. Using a wildcard we can apply the advice to all methods:
execution(* com.wwj.spring.demo.aop.CalculateService.*(..))Running with this pointcut prints the time only for the add method, demonstrating the need for a broader expression.
Advice Types
@Before : runs before the target method.
@After : runs after the target method regardless of outcome.
@AfterReturning : runs after successful execution and can access the return value.
@AfterThrowing : runs when the target method throws an exception and can access the exception object.
@Around : surrounds the method execution, allowing full control (pre‑, post‑, return‑, and exception handling).
Examples of each advice are provided in the source code, showing how to obtain method name, arguments, return value, and exception via JoinPoint or ProceedingJoinPoint .
Custom Advice with JDK Dynamic Proxy
Because @Around behaves similarly to a JDK dynamic proxy, we can implement our own proxy:
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) { this.target = target; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
System.out.println("Before: " + method.getName() + ", args=" + Arrays.asList(args));
result = method.invoke(target, args);
System.out.println("AfterReturning: " + method.getName() + ", result=" + result);
} catch (Throwable e) {
System.out.println("AfterThrowing: " + method.getName() + ", ex=" + e);
} finally {
System.out.println("After: " + method.getName());
}
return result;
}
}Using the proxy:
public static void main(String[] args) throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
CalculateService calculateService = context.getBean("calculateServiceImpl", CalculateService.class);
MyInvocationHandler handler = new MyInvocationHandler(calculateService);
calculateService = (CalculateService) Proxy.newProxyInstance(
calculateService.getClass().getClassLoader(),
calculateService.getClass().getInterfaces(),
handler);
calculateService.add(1, 1);
// other method calls
}The output shows the same sequence of advice messages as Spring AOP. The limitation of JDK proxies (the target must implement an interface) is why Spring also supports CGLIB proxies, which create subclasses at runtime.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.