
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: May 14, 2025
There are many intricacies when working with Spring AOP. One common issue is handling method calls from within the same class as these calls circumvent the AOP functionality.
In this tutorial, we’ll learn a little bit about how Spring AOP works and what workarounds can be applied.
To start off, we’ll do a quick review of what proxy objects are and how they’re used in the Spring framework.
A proxy object can be thought of as a wrapper that can add functionality around calls to the target object.
In Spring, when a bean needs extra functionality, a proxy is created and the proxy is injected into other beans. For example, if a method has the Transactional annotation or any caching annotations, then this proxy is used to add the desired functionality around calls to the target object.
As mentioned, the proxy created by Spring adds the desired AOP functionality. Let’s look at a caching example to see how the internal and external method calls differ:
@Component
@CacheConfig(cacheNames = "addOne")
public class AddComponent {
private int counter = 0;
@Cacheable
public int addOne(int n) {
counter++;
return n + 1;
}
@CacheEvict
public void resetCache() {
counter = 0;
}
}
When Spring creates the AddComponent bean, a proxy object is created as well to add the AOP functionality for caching. When another object requires the AddComponent bean, Spring provides the proxy for injection.
Testing this out, let’s call addOne() from a separate component multiple times with the same input and verify the counter is only incremented once:
@SpringBootTest(classes = Application.class)
class AddComponentUnitTest {
@Resource
private AddComponent addComponent;
@Test
void whenExternalCall_thenCacheHit() {
addComponent.resetCache();
addComponent.addOne(0);
addComponent.addOne(0);
assertThat(addComponent.getCounter()).isEqualTo(1);
}
}
Now let’s add another method to AddComponent that calls addOne() internally:
public int addOneAndDouble(int n) {
return this.addOne(n) + this.addOne(n);
}
When this new method calls addOne(), the calls don’t go through the proxy, and the counter is incremented twice:
@Test
void whenInternalCall_thenCacheNotHit() {
addComponent.resetCache();
addComponent.addOneAndDouble(0);
assertThat(addComponent.getCounter()).isEqualTo(2);
}
Although method calls from within the same class won’t go through the desired AOP functionality, there are several workaround options.
One of the best ways is to refactor. In our AddComponent example, instead of adding addOneAndDouble() directly to the class, let’s create a new class with this method. The new class can have the AddComponent injected, or more precisely, the proxy for the AddComponent will be injected:
@Component
public class AddOneAndDoubleComponent {
@Resource
private AddComponent addComponent;
public int addOneAndDouble(int n) {
return addComponent.addOne(n) + addComponent.addOne(n);
}
}
As our test showed earlier, this only increments the counter once.
If refactoring isn’t possible, then we can try injecting the proxy directly into the class. This requires some care since this creates a direct circular dependency, which Spring no longer allows by default. However, there are many solutions to allow for Circular Dependencies in Spring and we’ll annotate the self-dependency with @Lazy:
@Component
@CacheConfig(cacheNames = "selfInjectionAddOne")
public class SelfInjection {
@Lazy
@Resource
private SelfInjection selfInjection;
private int counter = 0;
@Cacheable
public int addOne(int n) {
counter++;
return n + 1;
}
public int addOneAndDouble(int n) {
return selfInjection.addOne(n) + selfInjection.addOne(n);
}
@CacheEvict(allEntries = true)
public void resetCache() {
counter = 0;
}
}
With the injected proxy, addOneAndDouble() will now be using the caching functionality and the counter is only incremented once:
@Test
void whenCallingFromExternalClass_thenAopProxyIsUsed() {
selfInjection.resetCache();
selfInjection.addOneAndDouble(0);
assertThat(selfInjection.getCounter()).isEqualTo(1);
}
Spring AOP’s usage of creating proxies is an example of runtime weaving. AspectJ, on the other hand, makes use of a couple of other types of weaving so that refactoring and self-injection aren’t needed.
In this article, we discussed a couple of ways to handle Spring AOP calls from within the same class. Refactoring is often the preferred solution but, when this isn’t possible, we can use AspectJ or self-injection to implement the desired functionality.
As always, the code is available over on GitHub.
Follow the Spring Category