Spring AOP Meets Reactive Programming: A Survival Guide
Spring AOP in a reactive (WebFlux) environment requires special considerations because AOP in Spring is proxy-based, and most of its traditional use cases assume a synchronous execution flow. However, in a reactive world, execution can be deferred and asynchronous, which makes handling aspects like logging, security, and transactions more challenging.
If you’re coming from a traditional Spring MVC background and thought, “I’ll just slap an @Transactional or @Around and call it a day,” think again! WebFlux operates on an entirely different paradigm, and so should your AOP strategies.
How Spring AOP Works in WebFlux
The Proxy Mechanism & WebFlux Challenge
Spring AOP uses CGLIB (subclass proxying) or JDK dynamic proxies to intercept method calls. However, in WebFlux, methods often return Mono<T>
or Flux<T>
, and the actual execution of logic happens inside the reactive pipeline—not at method invocation.
Traditional AOP advice that wraps method execution won’t automatically intercept reactive chain operations. In other words, if you were hoping for AOP magic to just work as usual, surprise!
The Right Way to Apply AOP in a Reactive WebFlux Context
Rather than wrapping method calls, you should apply advice around the entire reactive pipeline. This ensures that the logic executes correctly without disrupting the non-blocking, event-driven nature of WebFlux.
Let’s dive into some practical examples.
Example 1: Logging Aspect for WebFlux (Mono & Flux)
Want to log method execution in WebFlux? Here’s how you can do it properly:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Aspect
@Component
public class ReactiveLoggingAspect {
@Around("@annotation(ReactiveLog)")
public Object logReactiveMethods(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
if (result instanceof Mono) {
return ((Mono<?>) result)
.doOnSubscribe(subscription -> System.out.println("Mono started: " + joinPoint.getSignature()))
.doOnTerminate(() -> System.out.println("Mono completed: " + joinPoint.getSignature()));
} else if (result instanceof Flux) {
return ((Flux<?>) result)
.doOnSubscribe(subscription -> System.out.println("Flux started: " + joinPoint.getSignature()))
.doOnTerminate(() -> System.out.println("Flux completed: " + joinPoint.getSignature()));
}
return result;
}
}
Custom Annotation for Logging
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReactiveLog {
}
Applying AOP in a WebFlux Controller
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api")
public class WebFluxController {
@ReactiveLog
@GetMapping("/hello")
public Mono<String> sayHello() {
return Mono.just("Hello, Reactive AOP!")
.map(String::toUpperCase);
}
}
Key Takeaways:
- Use
@Around
Advice to intercept methods returningMono
orFlux
. - Apply reactive operators (
doOnSubscribe
,doOnTerminate
) inside the AOP advice. - Avoid blocking calls inside the aspect (e.g.,
.block()
,.toFuture()
), as it will break the reactive model. - Works best for logging, monitoring, and metrics in WebFlux.
Example 2: Handling Transactions in WebFlux with MongoDB Using Spring AOP
MongoDB supports reactive transactions when using MongoDB 4.0+ with a replica set. However, unlike traditional relational databases, transactions in MongoDB must be explicitly managed using a reactive ClientSession
.
Why Not Just Use @Transactional
?
Spring AOP does not natively support MongoDB reactive transactions. You need to explicitly manage them using ReactiveMongoTemplate
and ClientSession
. If you thought you could just slap @Transactional
on a method and be done—well, Houston, we have a problem!
Step 1: Define the Aspect for MongoDB Transactions
import com.mongodb.reactivestreams.client.ClientSession;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Aspect
@Component
public class ReactiveMongoTransactionAspect {
private final ReactiveMongoTemplate mongoTemplate;
private final ReactiveMongoDatabaseFactory mongoDatabaseFactory;
public ReactiveMongoTransactionAspect(ReactiveMongoTemplate mongoTemplate, ReactiveMongoDatabaseFactory mongoDatabaseFactory) {
this.mongoTemplate = mongoTemplate;
this.mongoDatabaseFactory = mongoDatabaseFactory;
}
@Around("@annotation(ReactiveMongoTransactional)")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
return Mono.usingWhen(
mongoDatabaseFactory.getSession(),
session -> {
session.startTransaction();
try {
Object result = joinPoint.proceed();
if (result instanceof Mono) {
return ((Mono<?>) result)
.doOnSuccess(unused -> session.commitTransaction())
.doOnError(e -> session.abortTransaction());
} else if (result instanceof Flux) {
return ((Flux<?>) result)
.doOnComplete(session::commitTransaction)
.doOnError(e -> session.abortTransaction());
} else {
session.abortTransaction();
throw new UnsupportedOperationException("Reactive transactions only support Mono/Flux return types");
}
} catch (Throwable ex) {
session.abortTransaction();
return Mono.error(ex);
}
},
ClientSession::close
);
}
}
Step 2: Create a Custom @ReactiveMongoTransactional
Annotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReactiveMongoTransactional {
}
Step 3: Apply the Transactional AOP in a Service
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@ReactiveMongoTransactional
public Mono<User> createUser(String name, String email) {
User user = new User(null, name, email);
return userRepository.save(user);
}
}
Final Thoughts
Spring AOP and WebFlux can peacefully coexist — but only if you handle them correctly. Stick to non-blocking patterns, ensure aspects wrap reactive pipelines, and avoid traditional synchronous assumptions.
As they say in the reactive world: “Don’t block — just go with the flow!” 🚀
Thank you for reading!