Spring AOP (Aspect-Oriented Programming) allows developers to modularize cross-cutting concerns like logging, transactions, or security. However, a common pitfall developers encounter is when they try to apply AOP to a method call within the same class. Let us delve into understanding how Spring AOP method calls within the same class and why it behaves differently from typical method interceptions.
1. What is Spring AOP?
Spring AOP (Aspect-Oriented Programming) is a powerful feature of the Spring Framework that enables the modularization of cross-cutting concerns such as logging, security, transaction management, and performance monitoring. Instead of embedding these concerns into core business logic, Spring AOP allows you to define them separately and weave them into your application using proxies.
Internally, Spring AOP is proxy-based. It creates a proxy object that wraps your original bean. This proxy intercepts method calls and applies the appropriate advice. Depending on whether your target object implements interfaces, Spring uses either:
- JDK Dynamic Proxies – Used when the bean implements one or more interfaces. (Java Proxy API)
- CGLIB Proxies – Used when the bean does not implement any interface. (CGLIB GitHub)
These proxies wrap the target bean with interceptors that apply behavior such as @Before, @After, or @Around advice, enabling behavior injection at runtime without changing the source code.
1.1 Core AOP concepts
- Join Point: A specific point in the execution of a program, such as the execution of a method or the handling of an exception. (Join Point – Spring Docs)
- Advice: The action taken at a particular join point. It can be executed before, after, or around the method execution. Examples include logging or authorization. (Advice – Spring Docs)
- Pointcut: An expression that selects one or more join points where advice should be applied. (Pointcut – Spring Docs)
- Aspect: A class that encapsulates pointcut and advice logic. It defines both what to do and where to do it. (Aspect – Spring Docs)
2. Code Example
This section walks through a complete Spring Boot example that demonstrates how Spring AOP behaves differently when a method is called internally versus via a proxy. It will help clarify the “Spring AOP method call within same class” behavior using a simple timing aspect.
2.1 Add Dependencies (pom.xml)
To get started, we need to add the required Spring Boot dependencies to our Maven project. The main ones include the core Spring Boot starter and the AOP starter, which enables proxy-based aspect weaving.
<!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- Spring Boot AOP Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2.2 LogExecutionTime.java
This custom annotation is used to mark any method we want to time. It has runtime retention and targets method-level elements, making it ideal for use with an @Around aspect.
package com.example.aopdemo.annotation;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {
}
2.3 LoggingAspect.java
This class defines the actual aspect using Spring AOP. It intercepts any method annotated with @LogExecutionTime and measures its execution time using an @Around advice.
package com.example.aopdemo.aspect;
import com.example.aopdemo.annotation.LogExecutionTime;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Around("@annotation(logExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint, LogExecutionTime logExecutionTime) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - start;
System.out.println(joinPoint.getSignature() + " executed in " + duration + "ms");
return result;
}
}
2.4 UserService.java
This service class contains a method marked with the @LogExecutionTime annotation. It also includes a method to demonstrate two types of calls to internalMethod – one directly (which will skip AOP) and one via the Spring proxy (which will apply AOP).
package com.example.aopdemo.service;
import com.example.aopdemo.annotation.LogExecutionTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private ApplicationContext context;
public void callMethods() {
System.out.println("Calling internal method directly (no AOP):");
this.internalMethod(); // internal call - no AOP
System.out.println("\nCalling internal method via proxy (AOP will apply):");
UserService proxy = context.getBean(UserService.class);
proxy.internalMethod();
}
@LogExecutionTime
public void internalMethod() {
System.out.println("Inside internalMethod()");
}
}
2.5 AopDemoApplication.java
This is the Spring Boot main application class. It loads the Spring context and retrieves the UserService bean to run the test scenario for method interception.
package com.example.aopdemo;
import com.example.aopdemo.service.UserService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class AopDemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(AopDemoApplication.class, args);
UserService service = context.getBean(UserService.class);
service.callMethods();
}
}
2.6 Code Run and Output
The output clearly demonstrates the difference between a direct internal method call and one made through a proxy. Only the proxy-based call triggers the aspect logic and logs the execution time.
Calling internal method directly (no AOP): Inside internalMethod() Calling internal method via proxy (AOP will apply): Inside internalMethod() void com.example.aopdemo.service.UserService.internalMethod() executed in 2ms
When the application is run, Spring creates a proxy of the UserService bean and wraps the annotated method with advice defined in LoggingAspect. However, when internalMethod is called via this.internalMethod(), it bypasses the proxy since internal calls within the same class do not go through Spring’s AOP proxy. As a result, the @LogExecutionTime annotation is ignored in that case. On the other hand, when internalMethod is called via the application context proxy (i.e., context.getBean(UserService.class)), the call is routed through the proxy, allowing the aspect to intercept the call and log the execution time. This behavior illustrates the limitation of Spring AOP when it comes to internal method calls and shows why developers sometimes need workarounds such as exposing self-references via proxy-aware context access. This example effectively demonstrates the subtle but important distinction that arises when using Spring AOP for method calls within the same class.
3. Conclusion
Spring AOP is a powerful tool, but developers must be cautious when invoking methods internally within the same class. Since such calls bypass the proxy, the associated advice is not applied. By using workarounds like retrieving the proxy from the application context or refactoring code into separate beans, you can ensure AOP advice is consistently executed.
Thank you!
We will contact you soon.

This site uses Akismet to reduce spam. Learn how your comment data is processed.