Spring AOP is a mechanism that lets you centralize cross-cutting concerns — such as logging and authorization checks — separately from your business logic. This article explains the concepts behind Spring AOP in an approachable way, and walks through how to set it up in Spring Boot with practical examples.
Spring AOP in a Nutshell
Spring AOP is a mechanism for injecting additional processing before and after methods execute.
The following kinds of concerns tend to be needed across many features:
- Logging when and which method was called
- Measuring execution time
- Allowing only authorized users to proceed
- Handling errors in a common way when failures occur
Writing this logic in every method leads to scattered, repetitive code. Spring AOP lets you centralize cross-cutting concerns in one place and apply them only where needed.
Key Terms to Know First
There are several terms to learn, but their meanings are straightforward.
- Aspect
A class that groups common processing. It acts as the “hub” for concerns like logging and measurement. - Pointcut
The condition that defines where the Aspect applies — for example, “only this method in this package.” - Advice
The actual processing to inject. There are different types: “run before,” “run after,” and so on.
A helpful mental model: Spring AOP injects processing at the moment a method is called.
Setting Up Spring AOP in Spring Boot
With Spring Boot, adding a single dependency is essentially all the setup you need.
Adding the Dependency
For Gradle:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
For Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Measuring Execution Time and Logging It
In this example, we apply timing measurement only to the specific methods we want to measure.
Creating a Marker Annotation
Only methods annotated with this annotation will be measured.
package com.example.demo.aop;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Timed {
}
Writing the Aspect
Annotating a class with @Aspect tells Spring it is an AOP class. @Around is commonly used when you want to inject processing both before and after a method.
package com.example.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TimingAspect {
private static final Logger log = LoggerFactory.getLogger(TimingAspect.class);
@Around("@annotation(com.example.demo.aop.Timed)")
public Object measure(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
// Execute the original method here
return pjp.proceed();
} finally {
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
log.info("method={} elapsedMs={}", pjp.getSignature().toShortString(), elapsedMs);
}
}
}
Key points:
- Calling
pjp.proceed()inside@Aroundexecutes the original method. - Code written before
proceed()runs as “before” logic; code written after runs as “after” logic.
Applying the Annotation to a Method
package com.example.demo.service;
import com.example.demo.aop.Timed;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
@Timed
public String hello(String name) {
return "Hello " + name;
}
}
Every time hello is called, its execution time is logged. The benefit is that no measurement code needs to be written inside the business logic itself.
Types of Advice
You only need to know the most commonly used ones.
@Before
Runs before the method.@AfterReturning
Runs after the method returns normally.@AfterThrowing
Runs after the method exits with an exception.@After
Runs at the end regardless of success or failure.@Around
Handles both before and after in a single block.
When in doubt, @Around is the most common choice — it covers both sides and allows you to return the original return value as-is.
Logging Only on Exception
If you want to log only on failure, @AfterThrowing is the more readable option.
@AfterThrowing(
pointcut = "execution(* com.example.demo..*Service.*(..))",
throwing = "ex"
)
public void logError(Exception ex) {
log.warn("service error", ex);
}
How to Specify Where to Apply
There are two main approaches:
- Specify the target using an expression pattern
- Specify the target using an annotation
Using an Expression Pattern
To target all methods in classes whose names end with Service, write:
@Around("execution(* com.example.demo..*Service.*(..))")
public Object aroundServices(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
However, an overly broad scope can cause the Aspect to apply in unexpected places. It is recommended to start with a narrow scope and expand gradually.
Using an Annotation
In practice, you often want the Aspect to apply only to explicitly marked locations, making annotation-based targeting very convenient.
@Around("@annotation(com.example.demo.aop.Timed)")
public Object timedOnly(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
Common Pitfalls
Spring AOP is useful, but there are a few gotchas due to how it works under the hood.
Internal Method Calls May Not Be Intercepted
When one method calls another within the same class, AOP may not take effect.
public void a() {
this.b(); // AOP may not apply to calls made this way
}
Common solutions include:
- Extracting the method into a separate class and calling it as a Bean-to-Bean invocation.
- Redesigning to avoid relying on AOP for this scenario.
Remembering that “AOP takes effect through Spring-managed Bean invocations” makes it easier to diagnose the root cause.
Applying AOP to Private Methods May Not Work as Expected
It is safest to think of AOP as something that applies to methods called from outside the class. Annotating public methods that serve as service entry points is the reliable approach.
Transactions Work Similarly
@Transactional operates on the same underlying principles. Swallowing or suppressing exceptions mid-flow can lead to unexpected behavior.
When logging exceptions via AOP, being explicit about whether to rethrow the exception will prevent incidents.
When to Use AOP — and When Not To
AOP is a powerful mechanism, but routing everything through it can make code harder to read.
In practice, the following guidelines help avoid mistakes.
When to Use AOP
- Cross-cutting concerns like logging, measurement, and auditing
- Security checks and common exception handling
- Cases where multiple modules share the same pre- or post-processing
When to Avoid AOP
- Business rules themselves (e.g., pricing calculations, inventory allocation)
- Processing with complex execution order where explicit calls are more readable
- Core domain logic where the impact must be traced precisely
Keeping domain logic in regular code and cross-cutting concerns in AOP leads to a clean separation of responsibilities.
Notes for Production Use
- Do not make Pointcuts too broad from the start (prevents unintended side effects).
- Do not log PII (personally identifiable information) in log output.
- Do not swallow exceptions inside
@Around. - Control log volume for performance measurement purposes (e.g., sampling).
In particular, a Pointcut that applies to all Services at once is convenient but makes side effects harder to see — it is recommended to expand the scope incrementally.
Summary
Spring AOP is a mechanism for separating cross-cutting concerns such as logging and measurement from business logic. In Spring Boot, you can get started simply by adding the starter dependency.
A good starting point is to create a custom annotation and apply the Aspect only to annotated methods. As you grow more comfortable, gradually expanding the scope of application is the recommended approach.