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 @Around executes 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 used
  • within
    Specify by class or package scope
  • @annotation
    Narrow down to only methods with a given annotation
  • bean
    Specify by Bean name. Wildcards such as bean(*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 messages
  • getArgs()
    Receives the arguments at call time as an array
  • proceed(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 on final classes 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.