Spring AOP: Implementing Advice Types
Spring AOP allows us to execute additional logic before, after, or around selected Spring bean methods without mixing that logic into the business code.
Typical cross-cutting concerns include:
- Logging
- Authorization
- Auditing
- Performance measurement
- Metrics
- Technical validation
- Context management
In this chapter, we will create a basic aspect and implement the five principal advice types provided by Spring AOP:
@Before@AfterReturning@AfterThrowing@After@Around
1. Add the AOP Dependency
The dependency name differs between Spring Boot 3 and Spring Boot 4.
Spring Boot 3.x
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Spring Boot 4.x
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aspect</artifactId>
</dependency>
Once the appropriate starter is present, Spring Boot configures the required AOP infrastructure automatically. In a normal Spring Boot application, we do not need to manually add @EnableAspectJAutoProxy.
2. Recommended Package Structure
The aspect class should normally be placed inside the main application package or one of its subpackages. This allows Spring Boot's component scanning to discover it automatically.
3. Create the Target Bean
A target bean is the ordinary Spring bean whose method we want to intercept.
package com.coderarmy.studentmanagement.service;
import org.springframework.stereotype.Service;
@Service
public class StudentService {
public String createStudent() {
System.out.println("Actual createStudent() method executed");
return "Student created successfully";
}
public String getStudent() {
System.out.println("Actual getStudent() method executed");
return "Student details";
}
}
Here:
StudentServiceis the target class.- The Spring-managed
StudentServiceinstance is the target object. createStudent()andgetStudent()are possible target methods.
4. Create the Aspect Class
package com.coderarmy.studentmanagement.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(String com.coderarmy.studentmanagement.service.StudentService.createStudent())")
public void logBeforeMethodExecution() {
System.out.println("A StudentService method is about to execute");
}
}
This class contains three important annotations.
@Aspect
Declares that the class contains AOP configuration, such as pointcuts and advice. It tells Spring:
This class describes cross-cutting behaviour that must be applied to selected method executions.
@Component
Registers the aspect as a Spring bean.
@Aspect describes the role of the class, but it does not by itself register an object in the Spring container. @Component makes the class discoverable through component scanning.
@Before
Declares that the advice method must execute before every method matched by its pointcut expression.
5. Understanding the Pointcut Expression
execution(String com.coderarmy.studentmanagement.service.StudentService.createStudent())
The expression can be read from left to right:
The complete string inside @Before is a pointcut expression.
The method containing the additional behaviour is the advice:
public void logBeforeMethodExecution()
The matched business method is the target method:
public String createStudent()
6. How Does the Advice Execute Automatically?
The StudentService class does not explicitly call LoggingAspect. Still, the advice executes before the service method.
Spring makes this possible by placing a proxy object in front of the target object.
Conceptually, the proxy behaves like a wrapper:
public String createStudent() {
loggingAspect.logBeforeMethodExecution();
return target.createStudent();
}
The real proxy created by Spring is generic and considerably more sophisticated, but this is the correct mental model.
7. Target Object
The target object contains the actual business implementation.
public class StudentService {
public String createStudent() {
return "Student created";
}
}
Conceptually:
Target object = actual StudentService instance
8. Proxy Object
The proxy is the Spring-generated object that stands in front of the target object and intercepts matching method calls.
The proxy can run additional behaviour before or after delegating the call to the target.
Are the target and proxy two different objects?
Conceptually, yes. Spring may maintain:
- A proxy object
- A target object behind that proxy
However, they should not be imagined as two ordinary, independently injectable application beans. The normal bean name exposes the proxy:
The target is held behind the proxy. The Spring container manages the advised service as one logical bean, while callers normally interact with the proxy.
9. What Is an Advice?
An advice answers one fundamental question:
What additional logic should run before, after, or around a matched method execution?
The general flow can be represented as:
Spring provides five principal advice types:
@Before@AfterReturning@AfterThrowing@After@Around
10. The try-catch-finally Mental Model
The advice types can be understood by comparing them with ordinary Java control flow.
beforeLogic();
try {
Object result = targetMethod();
afterReturningLogic(result);
return result;
} catch (Throwable exception) {
afterThrowingLogic(exception);
throw exception;
} finally {
afterLogic();
}
Approximate mapping:
@Around is more powerful than simply combining the other four. It controls whether, when, and how the remaining invocation chain proceeds.
@Before Advice
11. What Does @Before Do?
@Before executes before the matched target method begins.
@Before("execution(* com.coderarmy.studentmanagement.service.PaymentService.*(..))")
public void logMethodEntry(JoinPoint joinPoint) {
System.out.println("Entering method: " + joinPoint.getSignature().getName());
System.out.println("Arguments: " + Arrays.toString(joinPoint.getArgs()));
}
Common uses include:
- Logging method entry
- Reading method arguments
- Checking authorization
- Checking technical preconditions
- Populating request or tracing context
- Validating a common technical policy
12. Using JoinPoint
JoinPoint represents the current intercepted method execution. It allows the advice to inspect contextual information such as:
joinPoint.getSignature()joinPoint.getSignature().getName()joinPoint.getArgs()joinPoint.getTarget()joinPoint.getThis()
Common methods
JoinPoint allows inspection, but it does not allow the advice to continue, skip, or repeat the target invocation.
13. Can @Before Prevent the Target Method?
@Before does not have a doNotProceed() operation because it does not directly control the invocation.
However, the advice can throw an exception:
@Before("execution(* com.coderarmy.studentmanagement.service.PaymentService.makePayment(...))")
public void checkAccess() {
boolean allowed = false;
if (!allowed) {
throw new SecurityException("You cannot make this payment");
}
}
If the advice throws an exception, execution stops before the target method is reached.
Therefore:
@Beforecannot directly skip the method and return an alternative result.- It can stop execution by throwing an exception.
- Use
@Aroundwhen controlled short-circuiting is required.
14. Can @Before Modify Method Arguments?
It can read the arguments:
Object[] arguments = joinPoint.getArgs();
However, @Before does not provide a controlled mechanism for replacing the arguments that will be used for the target invocation.
For argument replacement, use @Around:
joinPoint.proceed(modifiedArguments);
@AfterReturning Advice
15. When Does @AfterReturning Execute?
@AfterReturning executes only when the matched method completes normally.
@AfterReturning("execution(* com.coderarmy.studentmanagement.service.PaymentService.*(..))")
public void logSuccessfulExecution() {
System.out.println("Method completed successfully");
}
Successful execution:
Exceptional execution:
16. Accessing the Returned Value
Use the returning attribute to bind the returned value to an advice parameter.
@AfterReturning(
pointcut = "execution(* com.coderarmy.studentmanagement.service.PaymentService.*(..))",
returning = "result"
)
public void logReturnedValue(String result) {
System.out.println("Method returned: " + result);
}
The value of returning must match the parameter name:
returning = "result"
public void logReturnedValue(String result)
The parameter type also affects matching.
public void logReturnedValue(String result)
This applies only when the returned value is compatible with String.
For a generic advice, use Object:
public void logReturnedValue(Object result)
17. Can @AfterReturning Replace the Returned Value?
No.
@AfterReturning(
pointcut = "...",
returning = "result"
)
public Object replaceResult(Object result) {
return "Different result"; // Ignored
}
The value returned by the advice method is ignored. It does not replace the value returned by the target method.
Therefore, an @AfterReturning method should normally return void:
public void logResult(Object result)
Use @Around when the caller must receive a changed, wrapped, or completely different result.
18. What About Mutating the Returned Object?
Suppose the target returns a mutable object:
PaymentReceipt receipt
The advice receives the same object reference and could technically modify it:
receipt.setStatus("CHANGED BY ASPECT");
This is normally poor design because it hides meaningful data transformation outside the visible business flow.
Use @AfterReturning mainly for observation:
- Logging a returned value
- Recording a metric
- Writing an audit record
- Publishing a success-related event
Use @Around when deliberate result transformation is genuinely required.
19. Normal Completion Means How the Method Exits
Consider a target method that catches an internal exception:
public String makePayment(double amount) {
try {
throw new RuntimeException("Gateway failed");
} catch (RuntimeException exception) {
return "Fallback response";
}
}
An exception occurred inside the method, but it did not escape. The method ultimately returned a value.
Therefore:
@AfterReturningexecutes@AfterThrowingdoes not execute
Advice reacts to how the target method exits, not to every internal event that occurs inside it.
@AfterThrowing Advice
20. When Does @AfterThrowing Execute?
@AfterThrowing runs when the matched target method exits by throwing an exception.
@AfterThrowing("execution(* com.coderarmy.studentmanagement.service.PaymentService.*(..))")
public void logFailure() {
System.out.println("PaymentService method failed");
}
Execution flow:
21. Accessing the Exception
Use the throwing attribute to bind the exception to an advice parameter.
@AfterThrowing(
pointcut = "execution(* com.coderarmy.studentmanagement.service.PaymentService.*(..))",
throwing = "exception"
)
public void logException(Throwable exception) {
System.out.println("Exception type: " + exception.getClass().getSimpleName());
System.out.println("Exception message: " + exception.getMessage());
}
The throwing value must match the parameter name:
throwing = "exception"
public void logException(Throwable exception)
22. Restricting Advice to a Specific Exception Type
The parameter type can restrict when the advice executes.
@AfterThrowing(
pointcut = "execution(* com.coderarmy.studentmanagement.service.PaymentService.*(..))",
throwing = "exception"
)
public void handleInvalidAmount(IllegalArgumentException exception) {
System.out.println("Invalid payment request: " + exception.getMessage());
}
This advice executes for:
IllegalArgumentException- Compatible subclasses
It does not execute for unrelated exception types.
23. Does @AfterThrowing Handle the Exception?
Not automatically.
It observes the failed exit, but the original exception still propagates.
The advice may itself throw another exception, but merely returning from an @AfterThrowing method does not consume the original failure.
24. Can @AfterThrowing Return a Fallback Value?
No.
@AfterThrowing("...")
public String returnFallback(Exception exception) {
return "Fallback";
}
The target method has already completed exceptionally. The advice method's return value is not used as a recovery result.
Use @Around when the invocation must be caught and converted into a fallback value. This must be done carefully because silently swallowing failures can create incorrect application behaviour.
25. Suitable Uses of @AfterThrowing
Use it for behaviour that applies specifically to failed method execution:
- Error logging
- Failure metrics
- Failed-operation audit records
- Alert creation
- Recording exception type and message
- Technical cleanup that applies only to failure
@After Advice
26. @After Means After-Finally
The name can be misleading. @After does not mean "after successful completion."
It behaves like a finally block:
try {
targetMethod();
} finally {
afterAdvice();
}
It executes whether the target method:
- Returns successfully
- Throws an exception
Spring therefore describes it as after-finally advice.
27. @After Example
@After("execution(* com.coderarmy.studentmanagement.service.PaymentService.*(..))")
public void clearContext() {
System.out.println("Clearing method context");
}
Execution on success:
Execution on failure:
28. Comparing the Three After Advices
@After vs @AfterReturning
@After→ success or failure@AfterReturning→ success only
@After vs @AfterThrowing
@After→ every outcome@AfterThrowing→ failure only
29. Suitable Uses of @After
Use @After for logic that must happen regardless of outcome:
-
Releasing a lock
-
Clearing contextual data
-
Removing a
ThreadLocalvalue -
Releasing a technical resource
-
Generic method-exit logging
-
Use
@AfterReturningwhen the returned value is needed. -
Use
@AfterThrowingwhen the thrown exception is needed.
@After is intentionally outcome-neutral.
@Around Advice
30. Why Is @Around the Most Powerful Advice?
@Around surrounds the complete invocation.
Unlike the other advice types, it controls the target method's execution.
It can:
- Run logic before the target method
- Run logic after the target method
- Inspect and replace arguments
- Inspect and replace the returned value
- Catch, rethrow, wrap, or replace exceptions
- Skip the target method entirely
- Invoke the method more than once
- Measure execution duration
Because it has this level of control, it should be used only when that control is actually required.
31. Basic @Around Syntax
@Around("...")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before target method");
Object result = joinPoint.proceed();
System.out.println("After target method");
return result;
}
Execution flow:
32. What Is ProceedingJoinPoint?
ProceedingJoinPoint represents the current intercepted invocation and provides the ability to continue that invocation.
It is used specifically with @Around advice.
ProceedingJoinPoint extends JoinPoint, so it provides the normal contextual information plus control methods such as:
proceed()proceed(Object[] arguments)
The core distinction is:
JoinPoint→ inspect the current invocationProceedingJoinPoint→ inspect and control the current invocation
33. What Does proceed() Actually Do?
Object result = joinPoint.proceed();
This means:
Continue with the remaining interceptor chain and eventually invoke the actual target method.
It is not always equivalent to directly calling the business method because other interceptors or advice may still remain inside the chain.
Therefore, proceed() means: Continue to the next step in the current invocation chain.
34. What Happens When proceed() Is Not Called?
@Around("...")
public Object blockExecution(ProceedingJoinPoint joinPoint) {
System.out.println("Method blocked");
return "Blocked response";
}
The target method never executes.
This ability can support patterns such as:
- Caching
- Feature flags
- Access rejection
- Short-circuiting
- Idempotency
- Circuit-breaking
It can also make behaviour difficult to understand because the source code appears to call a method that an aspect silently prevents from running.
35. Why Must We Return the Result of proceed()?
Consider this incorrect advice:
@Around("...")
public Object incorrectAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
joinPoint.proceed();
return null;
}
Even when the target returns:
Payment successful
the caller receives:
null
The value returned by the @Around advice becomes the value observed by the caller.
Therefore, the normal pattern is:
Object result = joinPoint.proceed();
return result;
Return something else only when deliberately replacing the target's result.
36. Why Does Around Advice Usually Return Object?
A generic aspect may intercept methods with different return types:
StringStudentPaymentReceiptList<Order>intbooleanvoid
Therefore, a reusable around advice normally uses:
public Object advice(...) throws Throwable
Primitive values are boxed:
int→Integerboolean→Boolean
For a void target method, the result is effectively null.
Declaring a generic around advice as void would discard any value returned by proceed() and cause the caller to observe null where a value was expected.
Practical Uses of @Around
37. Measuring Execution Time
Execution-time measurement requires state that exists both before and after the same method invocation:
This makes @Around a natural choice.
@Around("execution(* com.coderarmy.studentmanagement.service.PaymentService.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.nanoTime();
try {
return joinPoint.proceed();
} finally {
long executionTime = System.nanoTime() - startTime;
System.out.println(joinPoint.getSignature().getName() + " took " + executionTime + " nanoseconds");
}
}
Why use finally?
This version is incomplete:
long startTime = System.nanoTime();
Object result = joinPoint.proceed();
long executionTime = System.nanoTime() - startTime;
If proceed() throws an exception, execution never reaches the duration calculation. Using finally ensures that timing is recorded on both success and failure.
38. Exception Behaviour Inside @Around
@Around("...")
public Object observeExecution(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before");
Object result = joinPoint.proceed();
System.out.println("After");
return result;
}
When the target throws an exception:
"Before" is printed
proceed() throws
"After" is not printed
Exception propagates
39. Observing Success and Failure in One Advice
@Around("...")
public Object observeExecution(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Starting: " + joinPoint.getSignature().getName());
try {
Object result = joinPoint.proceed();
System.out.println("Execution successful");
return result;
} catch (Throwable exception) {
System.out.println("Execution failed: " + exception.getMessage());
throw exception;
} finally {
System.out.println("Execution completed");
}
}
This single advice can reproduce the major lifecycle positions represented by:
@Before@AfterReturning@AfterThrowing@After
That does not mean @Around should always be used. When only one lifecycle position is required, a narrower advice communicates the intention more clearly.
40. Converting an Exception into a Fallback Value
@Around("...")
public Object provideFallback(ProceedingJoinPoint joinPoint) throws Throwable {
try {
return joinPoint.proceed();
} catch (PaymentGatewayException exception) {
return new PaymentReceipt("PENDING", "Payment will be retried");
}
}
Here, the original exception does not reach the caller. The aspect converts a failed invocation into a successful return value.
This is powerful but potentially dangerous:
- Monitoring may no longer detect the original failure.
- Transaction behaviour may change.
- Callers may believe the operation succeeded.
- Business failure rules become hidden inside infrastructure code.
Fallback, retry, and circuit-breaker policies are usually safer when implemented through established resilience infrastructure rather than an informal generic aspect.
41. Modifying Method Arguments
Suppose the target method is:
public String greetStudent(String name) {
return "Hello " + name;
}
Around advice can replace the argument before continuing the invocation:
@Around("execution(* com.coderarmy.studentmanagement.service.StudentService.greetStudent(...))")
public Object normalizeName(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] arguments = joinPoint.getArgs();
String originalName = (String) arguments[0];
String normalizedName = originalName.trim().toUpperCase();
Object[] modifiedArguments = { normalizedName };
return joinPoint.proceed(modifiedArguments);
}
Calling:
studentService.greetStudent(" aditya ");
causes the target method to receive:
ADITYA
The new argument array must remain compatible with the target method's parameter types and order.
42. Modifying the Returned Value
@Around("execution(* com.coderarmy.studentmanagement.service.PaymentService.makePayment(...))")
public Object wrapResponse(ProceedingJoinPoint joinPoint) throws Throwable {
Object originalResult = joinPoint.proceed();
return new ApiResponse<>(true, "Operation completed", originalResult);
}
The caller receives the wrapper instead of the target method's direct result.
The replacement value must be compatible with the target method's declared return type. Otherwise, the caller may receive a runtime type error.
For example, a method declared to return String cannot safely be replaced with an unrelated ApiResponse object unless the original method signature is designed to return that type.
43. Calling proceed() More Than Once
Around advice may call proceed():
- Once
- More than once
- Not at all
Example retry:
@Around("...")
public Object retry(ProceedingJoinPoint joinPoint) throws Throwable {
try {
return joinPoint.proceed();
} catch (TemporaryServiceException exception) {
return joinPoint.proceed();
}
}
This may invoke the target method twice.
That is dangerous for operations with side effects:
transferMoney()sendEmail()createOrder()chargeCard()
Calling such methods twice may create duplicate operations.
Multiple calls to proceed() should be considered only when:
- The operation is safe to retry.
- Idempotency is guaranteed.
- The retry policy is clearly defined.
- Failure categories are carefully selected.
For production applications, specialized retry infrastructure is generally safer than a simplistic custom retry aspect.
Choosing the Correct Advice Type
44. Advice Comparison
45. Prefer the Narrowest Suitable Advice
The most powerful tool is not always the clearest tool.
Use:
@Beforewhen only pre-execution logic is required.@AfterReturningwhen only successful completion matters.@AfterThrowingwhen only failed completion matters.@Afterwhen cleanup must happen for every outcome.@Aroundonly when the invocation itself must be controlled.
A narrow advice makes the lifecycle position obvious and reduces the chance of accidentally changing business behaviour.
Final Mental Model
46. Complete Invocation Flow
47. Key Takeaways
- Advice is the additional behaviour applied to matched method executions.
- A pointcut expression decides which methods are matched.
- Spring normally exposes a proxy in place of the original target object.
JoinPointallows an advice to inspect the current invocation.ProceedingJoinPointallows@Aroundadvice to inspect and control the invocation.@Beforeruns before the target but cannot directly replace arguments or return an alternate result.@AfterReturningruns only after normal completion and cannot replace the returned value.@AfterThrowingruns only when an exception escapes the target and does not automatically consume it.@Afterbehaves likefinallyand runs on both success and failure.@Aroundcan modify, skip, repeat, or replace the invocation, so it must be used carefully.proceed()continues the remaining interceptor chain and eventually reaches the target method.- Prefer the narrowest advice type that correctly expresses the requirement.