Spring AOP is a mechanism that lets you write “cross-cutting processing you want everywhere,” such as logging or authorization checks, in one place, separate from your business logic. This article explains the ideas behind Spring AOP in plain terms, then shows how to set it up in Spring Boot and how to write the basics, with samples.
Spring AOP in One Sentence
Spring AOP is a mechanism that lets you insert “extra processing” before and after a method runs.
For example, the following kinds of processing tend to be needed by every feature.
- Log when and which method was called
- Measure how long execution took
- Let only authorized users through
- Run common error handling when something fails
If you write this kind of processing in every method, your code becomes cluttered. With Spring AOP, you can gather the common processing in one place and apply it only where it is needed.
Terms to Learn First
There seem to be a lot of terms at first, but the meanings are simple.
- Aspect
A class that bundles common processing. It is the “coordinator” for things like logging and measurement. - Pointcut
The condition that decides where to apply it. It narrows things down, such as “only this method in this package.” - Advice
The actual processing that gets inserted. There are several kinds, such as “run before” and “run after.”
With Spring AOP, it is easiest to understand if you remember that processing is essentially inserted “at the moment a method is called.”
Enabling It in Spring Boot
This article assumes Spring Boot 3.x (Spring Framework 6.x, Java 17 or later).
With Spring Boot, adding the dependency is almost 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>
Let’s Measure Execution Time and Log It
Here, we will attach timing only to the places where we say “I want to measure this method.”
Create a Marker Annotation
Only methods with this annotation will be measured.
package com.example.demo.aop;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Timed {
}
Write the Aspect
Adding @Aspect tells Spring “this is a class for AOP.” @Around is commonly used when you want to insert processing both before and after.
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 {
// ここで本来のメソッドを実行します
return pjp.proceed();
} finally {
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
log.info("method={} elapsedMs={}", pjp.getSignature().toShortString(), elapsedMs);
}
}
}
The key points are these.
- Calling
pjp.proceed()inside@Aroundexecutes the original method - Code written before
proceed()becomes the “before” processing, and code after it becomes the “after” processing
Attach It to the Method You Want
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;
}
}
Now, every time hello is called, the execution time is logged. The nice part is that you do not have to write any “code for measurement” on the business logic side.
Types of Insertion Timing
You only need to remember the commonly used ones.
@Before
Runs before the method@AfterReturning
Runs after the method completes normally@AfterThrowing
Runs after the method ends with an exception@After
Runs at the end, whether it succeeded or failed@Around
Lets you write both before and after together
When in doubt, @Around is often the choice. You can write both the before and after parts, and you can return the return value as-is.
Logging Only on Exceptions
If you “only want to log when something fails,” @AfterThrowing is easier to read.
@AfterThrowing(
pointcut = "execution(* com.example.demo..*Service.*(..))",
throwing = "ex"
)
public void logError(Exception ex) {
log.warn("service error", ex);
}
However, if you also want to shape the error response returned to clients in a REST API, it is more natural to lean on @RestControllerAdvice than on AOP. An implementation that covers stack trace logging and attaching a trace ID is explained in Implementing a Production-Ready GlobalExceptionHandler in Spring Boot.
How to Specify Where to Apply
There are broadly two approaches.
- Specify the location with a text-based rule
- Specify the location with an annotation
Specifying with a Text-Based Rule
For example, if you want to target “all methods of classes whose name ends in Service,” you can write it like this.
@Around("execution(* com.example.demo..*Service.*(..))")
public Object aroundServices(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
However, if the scope is too broad, it can “take effect in places you did not expect.” It is recommended to start with a narrow scope.
Specifying with an Annotation
In practice, you often want it “to take effect only where I attached it,” so annotation-based specification is very convenient.
@Around("@annotation(com.example.demo.aop.Timed)")
public Object timedOnly(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
Combining Pointcuts to Narrow Down
Conditions can be joined with && or ||. For commonly used designators, remembering these four is enough.
execution
Specify by method signature. The most frequently usedwithin
Specify by class or package scope@annotation
Narrow down to only methods with a given annotationbean
Specify by Bean name. Wildcards such asbean(*Service)are also supported
For example, “only methods inside the service package that also have @Timed” can be written like this.
@Around("within(com.example.demo.service..*) && @annotation(com.example.demo.aop.Timed)")
If you reuse the same condition across multiple pieces of Advice, giving it a name with @Pointcut makes it easier to read.
@Aspect
@Component
public class ServiceAspect {
private static final Logger log = LoggerFactory.getLogger(ServiceAspect.class);
@Pointcut("execution(* com.example.demo..*Service.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void logStart(JoinPoint jp) {
log.info("start {}", jp.getSignature().toShortString());
}
@AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
public void logError(Exception ex) {
log.warn("service error", ex);
}
}
If you keep the condition in one place, then when you want to widen or narrow the target, you only need to fix the contents of @Pointcut.
Handling Arguments and Return Values with ProceedingJoinPoint
The ProceedingJoinPoint passed to @Around has uses beyond “executing the original method.”
getSignature()
Extracts the class name and method name. Used when building log messagesgetArgs()
Receives the arguments at call time as an arrayproceed(Object[] args)
Executes the original method with replaced arguments
For example, you can strip leading and trailing whitespace from string arguments before passing them to the original processing.
@Around("@annotation(com.example.demo.aop.Trimmed)")
public Object trimArgs(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String s) {
args[i] = s.trim();
}
}
return pjp.proceed(args);
}
Return values can also be modified before being returned, using the result of proceed(). That said, Advice that rewrites arguments or return values easily becomes “invisible magic from the caller’s perspective,” so share it with your team when you use it.
Spring AOP Runs on Proxies
Now that you have the syntax down, knowing just one thing about the mechanism will make the pitfalls in the next section click.
Spring AOP does not register the Bean targeted by an Aspect as-is. Instead, it registers a “proxy” that wraps that Bean. When another Bean calls it, the proxy receives the call first, runs the Advice, and then calls the real method inside. In other words, Advice only runs for “calls that go through the proxy.”
There are two ways proxies are created.
- JDK Dynamic Proxy
Creates the proxy based on an interface. It can only be received as the interface type - CGLIB
Creates the proxy as a subclass that extends the class. It works without an interface, but has no effect onfinalclasses or methods
In Spring Boot, the default for spring.aop.proxy-target-class is true, so CGLIB is used unless you configure otherwise. This is why “AOP works even on Services that do not have an interface.”
Differences from AspectJ
“Spring AOP” and “AspectJ” are easily confused, but they are different things.
- Spring AOP
Creates proxies at runtime and inserts processing. Targets only method calls on Beans managed by Spring. Usable without additional configuration - AspectJ
Rewrites bytecode at compile time or class load time (weaving). Also works on non-Bean classes, field access, and constructors. Requires a dedicated compiler or agent configuration
Spring AOP only borrows the AspectJ annotations such as @Aspect and @Around and the Pointcut syntax. Its operating principle is proxies. Adding spring-boot-starter-aop also pulls in aspectjweaver, but it is used only to interpret annotations and Pointcut expressions. It does not enable weaving.
For an ordinary web application, Spring AOP is enough. Only consider AspectJ once a requirement such as “I want it to work on non-Bean classes too” comes up.
Common Pitfalls
Spring AOP is convenient, but there are points that are easy to trip over because of how the mechanism works.
It May Not Take Effect for Calls Within the Same Class
When you call another method within the same class, AOP may not take effect.
public void a() {
this.b(); // この呼び方だと、AOPが効かないことがある
}
As a countermeasure, one of the following is commonly used.
- Extract the method into a separate class so that Beans call each other
- Use a design that does not rely on AOP
If you remember that “AOP takes effect via calls to Beans managed by Spring,” the cause is easier to find.
Attaching It to Private Methods Does Not Work as Expected
It is safe to think of AOP as something that basically applies to “methods called from the outside.” Attaching it to the public methods that serve as the entry points of a service is the safe choice.
Transactions Use a Similar Mechanism
@Transactional also works internally on a similar idea. If you swallow exceptions or absorb them partway through, you may get results different from what you expected.
When logging exceptions with AOP, keeping in mind “whether the exception is rethrown as-is” reduces accidents.
When to Use AOP and When Not To
It is a convenient mechanism, but pushing everything into AOP actually makes code harder to read.
In practice, separating cases by the following criteria makes failure less likely.
When to Use It
- Cross-cutting concerns such as logging, measurement, and auditing
- Security checks and common exception handling
- When the same pre-processing/post-processing is needed across multiple modules
When Not to Use It
- Business rules themselves (e.g., price calculation, inventory allocation)
- Processing with complex execution order where explicit calls are easier to read
- Core domain processing where you want to track the impact scope precisely
Keeping domain logic in regular code and moving cross-cutting processing into AOP gives you a clean separation of responsibilities.
Connections between business flows, such as “send an email after registration completes,” are also easier to trace later when made explicit as events rather than hidden away in AOP. This design is explained in How to Loosely Couple Modules with ApplicationEvent in Spring Boot.
Notes for Production Operation
- Do not widen Pointcuts too much from the start (to prevent unintended hits)
- Do not output PII (personally identifiable information) in logs
- Do not swallow exceptions in
@Around - Control log volume for load measurement use cases (sampling, etc.)
In particular, a Pointcut that “applies to all Services at once” is convenient, but side effects become hard to see, so it is recommended to widen the scope gradually.
Summary
Spring AOP is a mechanism that lets you write common processing such as logging and measurement separately from your business logic. With Spring Boot, you can get started just by adding the starter.
It is easiest to understand if you first create a custom annotation and try the form where it “takes effect only where attached.” Once you get used to it, it is recommended to widen the scope little by little.