Untangling the Web: Migrating Spring AOP to Quarkus Interceptors
A clear-eyed guide to moving from Spring AOP to Quarkus CDI interceptors. Covering custom annotations, library dependencies, and design shifts for modern Java applications.
Porting a Spring application to Quarkus can dramatically improve performance and resource usage, but migrating Spring AOP. Especially with custom annotations and embedded aspect logic it does require strategic planning. This article explores the equivalent constructs in Quarkus using CDI interceptors, explains how to deal with complex pointcuts and library dependencies, and outlines pragmatic approaches including annotation binding, facade layers, ecosystem alternatives, and last-resort compatibility techniques. The goal isn’t to mimic Spring, but to rethink your AOP usage in the idiomatic, performant style Quarkus enables.
Why Migrate at All?
Migrating a mature Spring application to Quarkus brings tangible benefits:
Fast startup (great for serverless and container platforms)
Low memory footprint (ideal for Kubernetes)
Ahead-of-time (AOT) compilation and native image support
Clean, CDI-based dependency injection model
But when your application is full of Spring AOP with custom annotations, @Aspect
classes, and third-party libraries using Spring internals the migration can get tricky.
Let’s break it down and give you a roadmap.
AOP in Context: From Spring to Quarkus
At its core, Aspect-Oriented Programming (AOP) is about modularizing cross-cutting concerns: logging, security, transactions, retries, caching. Spring AOP (especially with AspectJ syntax) makes it easy to declare where and how this logic should be applied.
Quarkus takes a different approach. It doesn’t use AspectJ or expression-based pointcuts. Instead, it leans on CDI Interceptors: A Jakarta EE standard.
Comparing Spring AOP and Quarkus CDI Interceptors
Expression-Based Pointcuts
Spring AOP:
Supports rich, expression-based pointcuts using AspectJ syntax. You can match methods using patterns likeexecution(* com.example..*(..))
, which enables fine-grained targeting.Quarkus CDI Interceptors:
Does not support expression-based pointcuts. Instead, it uses annotation binding—interceptors are applied based on the presence of specific annotations or configuration inbeans.xml
.
Advice Types and Lifecycle Hooks
Spring AOP:
Offers a variety of advice annotations:@Before
@After
@AfterReturning
@AfterThrowing
@Around
Quarkus CDI Interceptors:
Supports lifecycle and method-level interception:@AroundInvoke
for method calls@PostConstruct
and@PreDestroy
for lifecycle hooks
Dependency Injection Models
Spring AOP:
Built on Spring's proprietary DI container and bean lifecycle.Quarkus CDI Interceptors:
Uses Jakarta CDI (via ArC)—a standard, type-safe, and portable DI model that works at build time in Quarkus.
Native Compilation Compatibility
Spring AOP:
Not designed with native compilation in mind. Requires runtime proxies and reflection-heavy behavior, which can complicate GraalVM native image generation.Quarkus CDI Interceptors:
Fully compatible with native image builds. The interception model is build-time friendly, reflection-free where possible, and optimized for ahead-of-time (AOT) compilation.
Custom Annotation Support
Spring AOP:
Supports custom annotations to trigger advice logic, either directly or via pointcut expressions.Quarkus CDI Interceptors:
Also supports custom annotations, using@InterceptorBinding
to link annotations to interceptor classes in a standard-compliant way.
So the real question becomes:
How do you map Spring AOP to Quarkus interceptors? Especially when custom annotations and libraries are involved.
Navigating the Migration
Custom Annotations — A Smooth Transition
If you’re using custom annotations like @LogExecutionTime
, @Audit
, or @TrackAccess
, you’re in luck. These are the easiest to migrate.
Migration Flow:
Define your annotation with
@InterceptorBinding
.
@InterceptorBinding
@Retention(RUNTIME)
@Target({ METHOD, TYPE })
public @interface Audit {}
Implement the interceptor logic.
@Audit
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class AuditInterceptor {
@AroundInvoke
public Object logAudit(InvocationContext ctx) throws Exception {
System.out.println("Calling: " + ctx.getMethod());
return ctx.proceed();
}
}
Use it like any other annotation.
@Audit
public void registerUser(User user) {
// ...
}
Easy win. You get the same modularization benefits without losing native compatibility.
When AOP Is Sprinkled All Over
This is where things get complicated. If your application uses dozens of aspects, declared using Spring’s @Aspect
and AspectJ-style pointcuts, you’ll need a strategy.
Audit First
Start by cataloging your existing aspects:
What does each one do?
Where is it applied?
Can some be removed or refactored into regular services?
Group and Prioritize
Critical logic (like security or transaction control) gets top priority.
Nice-to-have aspects like logging can be refactored or simplified.
Migrate to Quarkus Interceptors
Replace each Spring aspect with a CDI interceptor. Instead of relying on expression-based pointcuts, you'll bind logic to annotations.
What if I can't annotate every method?
Apply the interceptor broadly (e.g., at the class level) and filter inside:
@AroundInvoke
public Object conditionalApply(InvocationContext ctx) throws Exception {
if (ctx.getMethod().isAnnotationPresent(SensitiveAction.class)) {
// Intercept logic here
}
return ctx.proceed();
}
AOP Buried in Libraries
This is the hardest scenario. Especially when those libraries depend on Spring internals.
Internal Libraries (You Control)
Best: Refactor them to use CDI interceptors and eliminate Spring dependencies.
Fallback: Add a facade or adapter layer in your Quarkus app that exposes the expected behavior using Quarkus-native APIs underneath.
Third-Party Libraries (You Don’t Control)
Your choices:
Find Quarkus-native alternatives.
Micrometer, Hibernate Validator, Elytron, SmallRye Fault Tolerance, and many others are natively supported.
Use compatibility layers.
quarkus-spring-di
can support basic Spring DI, but doesn’t cover full AOP semantics.Good for beans, bad for aspects.
Replace or isolate the logic.
If the AOP-based behavior is critical, try to isolate it and replicate using interceptors or utility classes.
As a last resort: Embed a minimal Spring
ApplicationContext
for that one component.🚨 This breaks the Quarkus startup model and should be avoided unless absolutely necessary.
Strategic Tips for a Cleaner Migration
Understand the "Why" behind each aspect. Replace with Quarkus-native idioms whenever possible (
@Transactional
,@Retry
,@Counted
, etc.).Migrate incrementally. Start with simple cases and build experience.
Test extensively. AOP behavior can subtly affect logic paths. Unit and integration tests are critical.
Refactor when possible. Many aspects are legacy workarounds and you can use this as opportunity to simplify them.
Consider CDI Events and Bean Validation.
Quarkus supports asynchronous and synchronous CDI events for decoupling logic.
Hibernate Validator is tightly integrated for validation concerns.
A Final Word: Rethink, Don’t Just Rewire
Spring AOP is expressive and mature, but also complex and tightly coupled to Spring’s internal model. Quarkus offers a leaner, more standard-based approach. Migrating isn’t about 1:1 translation, it’s about rethinking your architecture.
CDI interceptors won’t cover every Spring AOP use case. But they will cover most. And they do it in a way that works well with Quarkus's build-time magic, fast startup, and native image support.
Takeaways
Quarkus CDI interceptors can replace most Spring AOP use cases: Cleanly and natively.
Focus on rewriting custom annotations, mapping advice to
@AroundInvoke
methods.Handle complex pointcuts by rethinking the design and leveraging annotations and filtering.
For third-party Spring-dependent libraries, prefer replacement or isolation over compatibility hacks.
Migration is a chance to improve your code, not just port it.