DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4
  • How Spring Boot Starters Integrate With Your Project
  • A Practical Guide to Creating a Spring Modulith Project
  • Structured Logging in Spring Boot 3.4 for Improved Logs

Trending

  • Automating Data Pipelines: Generating PySpark and SQL Jobs With LLMs in Cloudera
  • 5 Subtle Indicators Your Development Environment Is Under Siege
  • Testing SingleStore's MCP Server
  • The Human Side of Logs: What Unstructured Data Is Trying to Tell You
  1. DZone
  2. Coding
  3. Frameworks
  4. Spring Boot Annotations: Behind the Scenes and the Self-Invocation Problem

Spring Boot Annotations: Behind the Scenes and the Self-Invocation Problem

Spring uses proxies to add extra logic to methods marked with annotations like @Cacheable and others. But using proxies could lead to issues like self-invocation problem

By 
Aleksei Chaika user avatar
Aleksei Chaika
·
Aug. 24, 23 · Analysis
Likes (13)
Comment
Save
Tweet
Share
8.0K Views

Join the DZone community and get the full member experience.

Join For Free

When developing Spring Boot applications, we often use annotations such as @Transactional, @Cacheable, @Retryable, @Validated, @Async, and so on. Through these annotations, we imbue our beans with supplementary logic, such as encapsulating database operations within transactions or implementing caching mechanisms.

However, not everyone wonders how they work under the hood and what issues may arise from using them. In this article, let's embark on a journey to explore how the most popular annotations work and what the self-invocation problem is.

Spring Beans and Proxies

When some annotations, like @Transactional , are applied to methods in beans. Spring creates proxy objects that intercept the actual method calls and execute additional logic before or after that. 

Spring utilizes AOP (Aspect-Oriented Programming) through these proxies to address cross-cutting concerns like logging, security, and transactions, distinct from the main logic. This allows us to keep the core business logic clean and concentrate solely on it without being distracted by other matters. 

Understanding things is often easier when we look at an example. So, let's consider the service below:

Java
 
@Service
public class CacheableService {
  
    @Cacheable(cacheNames = "cache")
    public String getFromCache() {
        return "This value will be moved to cache and next time used from there";
    }
  
}


In order to make CacheableService start using a cache, we marked the method getFromCache() with @Cacheable annotation. Spring undertakes the task of enhancing the bean by introducing supplementary logic:

  • If the value is absent in the cache, it has to be retrieved and placed into the cache.
  • If the value is already presents in the cache, it has to be directly retrieved from there.

This process involves the utilization of proxies. In the runtime, Spring not only creates a new instance of the CacheableService, but also generates a proxy class to accompany it.

The simplified sequence appears as follows:

proxy for cacheableservice

So, Spring generates a proxy object, which mirrors the existing methods while incorporating supplementary logic.

If the class implements an interface, the proxy object also implements it, and dynamic proxy is used in this case. Otherwise, the proxy extends the target class using CGLib.

Examples of How the Most Popular Annotations Work Under the Hood

Classes for proxies are generated at runtime. In the case of our example, CacheableService, the generated proxy looks like the following:

Java
 
public class CacheableService$$SpringCGLIB$$0 extends CacheableService implements SpringProxy, Advised, Factory {
    ...
    private MethodInterceptor CGLIB$CALLBACK_0;
    ...
    @Override
    public final String getFromCache() {
        //execute using method interceptor
    }
    ...
}


We can see that CGLib was used because CacheableService implements no interfaces, and the generated proxy simply extends it. If CacheableService implemented some interface, a dynamic proxy would be used instead, and it would implement the same interface.

Spring employs MethodInterceptors within the proxies, and these interceptors are responsible for adding supplementary logic around the actual method invocations

Let's take a look at what happens inside method interceptors, which are used with the most common annotations:

@Cacheable

Let's begin with the @Cacheable annotation, as demonstrated in this article with the CacheableService example. In this scenario, the proxy that's created employs the CacheInterceptor:

Java
 
if (cacheHit != null && !hasCachePut(contexts)) {
    // If there are no put requests, just use the cache hit
    cacheValue = cacheHit.get();
    returnValue = wrapCacheValue(method, cacheValue);
} else {
    // Invoke the method if we don't have a cache hit
    returnValue = invokeOperation(invoker);
    cacheValue = unwrapReturnValue(returnValue);
}


The method interceptor operates as follows: it examines the cache for a specific value and directly returns it if it's found. In the event that the value is not present in the cache, the interceptor orchestrates its computation through the invokeOperation (the target method call). Following the computation, the interceptor stores the value in the cache and subsequently returns it.

@Transactional

In the case of Transactional annotation, the proxy uses another MethodInterceptor implementation — TransactionInterceptor:

Java
 
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
    // Target method invocation
    retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
    // target invocation exception -> transaction rollback
    completeTransactionAfterThrowing(txInfo, ex); // leads to transactionManager.rollback(...);
    throw ex;
} finally {
    cleanupTransactionInfo(txInfo);
}


We can see that, actually, this is not that difficult. A new transaction is initiated prior to the method invocation, and it is committed if no errors occur; otherwise, a transaction rollback takes place.

@Retryable

When it comes to the @Retryable annotation, the proxy that's generated utilizes the RetryOperationsInterceptor along with the RetryTemplate behind the scenes:

Java
 
while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
    try {
        ...
        T result = retryCallback.doWithRetry(context);
        doOnSuccessInterceptors(retryCallback, context, result);
        return result;
    } catch (Throwable e) {
        registerThrowable(retryPolicy, state, context, e);
        ...
    }
    ...
}
    


The actual target method is executed in a loop until success or the error limit is exceeded.

@Validated

If we annotate certain components with the @Validated annotation, and some method arguments are also marked with validation annotations like @NotBlank, @Min, @Email, the MethodValidationInterceptor will be added to a generated proxy. The interceptor works in the following way:

Java
 
try {
    result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
} catch (IllegalArgumentException ex) {
    ...
    result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) {
    throw new ConstraintViolationException(result);
}


@Async

In the case of this annotation, the proxy invokes the target method using AsyncExecutionInterceptor and AsyncTaskExecutor:

Java
 
if (CompletableFuture.class.isAssignableFrom(returnType)) {
    return executor.submitCompletable(task);
} else if (... some other cases ...){
} else if (Future.class.isAssignableFrom(returnType)) {
    return executor.submit(task);
} else if (void.class == returnType) {
    executor.submit(task);
    return null;
} else {
    throw new IllegalArgumentException("Invalid return type for async method (only Future and void supported): " + returnType);
}


Self-Invocation Problem

We have already learned that Spring creates proxy objects that mirror the existing bean methods and execute supplementary logic before or after the target method invocation.

But what would happen if we called the method marked with an annotation from another method in the same bean? Let's explore this case using an example:

Java
 
@Service
public class CacheableService {
  
    public String selfInvoke() {
        return getFromCache();
    }
  
    @Cacheable(cacheNames = "cache")
    public String getFromCache() {
        return "This value will be moved to cache and next time used from there";
    }
  
}


Now, the CacheableService includes an additional method called selfInvoke(), which is not marked with any annotation. This method simply calls getFromCache(), a method that is marked with the @Cacheable annotation and enhanced in the proxy to utilize the cache.

So, the question arises: will the selfInvoke() method retrieve a value from the cache?

Many developers expect that this should work and the cache will be utilized. However, in reality, it doesn't function as expected. To grasp this, let's analyze a sequence diagram that encompasses the updated CacheableService, its proxy, and the invocation of the selfInvoke() method.


Self-Invocation Problem

So, when we call the selfInvoke() method, we only access the getFromCache() method in the target bean. However, all the enhancements made to enable caching for this method exist in the overridden getFromCache() method within the proxy, which, unfortunately, remains unvisited.

The same issue applies to other annotations as well, such as @Transactional, @Retryable, @Validated, @Async, and others.

Indeed, several options exist to fix this issue. One approach involves refactoring the code to prevent self-invocation of methods marked with annotations and to call them from other beans only. Alternatively, transitioning to AspectJ for proxy creation (compile-time weaving) instead of relying on CGLib and dynamic proxies can be considered. Additionally, a technique known as self-injection could be employed, where the bean is injected into itself, and the target method is invoked on the injected bean. However, it's important to note that this latter solution is essentially a workaround.

Conclusion

In this article, we've explained how Spring uses proxies to add extra logic to methods marked with annotations like @Transactional, @Cacheable, @Retryable, @Validated, @Async, and others.

We've also dived into how these annotations work behind the scenes with some code examples.

However, using proxies can sometimes cause issues like the self-invocation problem. We've discussed what it is and ways to fix it.

It's important to get a grip on these concepts, and I really hope you found this article interesting. 

Thanks for reading!

Spring Framework Spring Boot

Opinions expressed by DZone contributors are their own.

Related

  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4
  • How Spring Boot Starters Integrate With Your Project
  • A Practical Guide to Creating a Spring Modulith Project
  • Structured Logging in Spring Boot 3.4 for Improved Logs

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!