Spring’s AOP proxying is actually two distinct mechanisms, and understanding which one is active is the key to debugging AOP issues.
Let’s see it in action. Imagine we have a simple service and we want to log method calls.
@Service
public class MyService {
public String doSomething(String input) {
System.out.println("Executing MyService.doSomething with: " + input);
return "Processed: " + input;
}
}
And an aspect to log entry and exit:
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.demo.MyService.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Aspect: Before method " + joinPoint.getSignature().getName());
}
@AfterReturning(pointcut = "execution(* com.example.demo.MyService.*(..))", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("Aspect: After method " + joinPoint.getSignature().getName() + " returned: " + result);
}
}
When you inject MyService into another component, Spring doesn’t give you a direct MyService instance. Instead, it gives you a proxy that looks like a MyService but intercepts calls.
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
MyService myService = context.getBean(MyService.class);
myService.doSomething("test");
}
}
If you run this, you’ll see output from both the service and the aspect, demonstrating the interception.
The mental model of AOP in Spring is that when you ask for a bean that has AOP advice applied, Spring doesn’t give you the actual bean instance. It gives you a proxy object. This proxy object implements the same interfaces as the target bean, or if it doesn’t implement any interfaces (or if you’ve configured it to use CGLIB), it extends the target bean’s class. When a method is called on the proxy, the proxy’s code runs first. It can then decide, based on the AOP pointcut, whether to execute the original method on the target bean, run advice before or after the target method, or even skip the target method entirely.
The primary levers you control are:
- Pointcut expressions: These define when the advice should be applied.
execution(* com.example.demo.MyService.*(..))means "any method on any class in thecom.example.demopackage that has the nameMyService." You can get much more granular, matching by method name, arguments, annotations, etc. - Advice types:
@Before,@After,@Around,@AfterReturning,@AfterThrowingdefine what the advice does and when relative to the target method execution. - Proxying strategy: Spring defaults to JDK dynamic proxies if the bean implements interfaces, otherwise it falls back to CGLIB. You can force CGLIB with
@EnableAspectJAutoProxy(proxyTargetClass = true).
The key to AOP’s magic is that the proxy is created at runtime. When Spring creates the bean, it checks if any aspects apply to it. If they do, it wraps the actual bean instance (or creates a proxy that extends the bean’s class) with the proxy logic. This means AOP is highly dynamic and can be configured entirely through Spring’s context.
Most people don’t realize that when you inject a bean into another bean within the same Spring context, and that injected bean has AOP advice applied, the advice might not run. This happens because Spring injects the actual bean instance into the target bean, not the proxy. The proxy is only created when you ask for the bean from the Spring container (e.g., via @Autowired in a different bean or context.getBean()). To ensure AOP advice runs on self-invocations, you need to inject the bean through the Spring context again, rather than calling its methods directly on the this reference.
The next concept you’ll likely grapple with is how to handle transactional advice, especially when combined with other AOP concerns.