Spring AOP & Cross-Cutting Concerns

Creating Your First Aspect

18 min Lesson 3 of 13

Creating Your First Aspect

Knowing what AOP is in theory is one thing; writing a class that Spring actually treats as an aspect is another. In this lesson you will build a real, runnable aspect from scratch inside a Spring Boot 3 application. By the end you will understand the three essential ingredients — the @Aspect annotation, the @Component annotation, and the auto-proxy infrastructure — and why each one is necessary.

The Three Ingredients

Every Spring aspect needs exactly three things to work:

  1. @Aspect — tells the AspectJ weaver (used by Spring at runtime) that this class carries advice and pointcut definitions.
  2. @Component (or any stereotype annotation) — registers the class as a Spring bean so the container can manage it. Without this, Spring never discovers the aspect.
  3. Auto-proxy enabled — the mechanism that intercepts method calls and routes them through matching advice. In Spring Boot this is on by default; in plain Spring you must add one annotation.
Why both @Aspect and @Component? They serve different purposes. @Aspect is an AspectJ marker — it tells the AOP framework how to interpret the class. @Component is a Spring IoC marker — it tells the container to create a bean. Neither implies the other. Forgetting @Component is the most common first-timer mistake; the class compiles fine, but the advice is silently ignored.

Enabling AspectJ Auto-Proxying

Spring AOP works by wrapping target beans in a proxy that intercepts calls and runs matching advice before or after delegating to the real object. The infrastructure that creates these proxies is called the auto-proxy creator, and it is activated by @EnableAspectJAutoProxy.

In a Spring Boot application you do not need to add this annotation yourself. The spring-boot-autoconfigure module includes AopAutoConfiguration, which activates auto-proxying automatically as long as spring-aop is on the classpath. Just add the starter to your build:

<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>

That single dependency pulls in both spring-aop and aspectjweaver, which is the AspectJ runtime library that Spring delegates to for pointcut matching. If you are using plain Spring (no Boot), you activate the infrastructure explicitly:

import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @EnableAspectJAutoProxy public class AppConfig { // your other bean definitions }
Spring Boot zero boilerplate. In a Spring Boot project the only setup you need is the starter dependency. If advice that looks correct is never invoked, verify that the starter is present in pom.xml or build.gradle before debugging anything else.

Writing the Aspect Class

Let us build a logging aspect that prints a message every time any method in the service layer is called. Here is the minimal skeleton:

package com.example.demo.aspect; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class); @Before("execution(* com.example.demo.service.*.*(..))") public void logBeforeServiceCall() { log.info("A service method is about to be called"); } }

Walk through each piece:

  • @Aspect — marks the class as an aspect. Spring will scan it for @Before, @After, @Around, and other advice annotations.
  • @Component — registers it as a singleton bean in the application context. You could also use @Service or @Repository, but @Component is the most honest choice because an aspect is not a service or a repository — it is cross-cutting infrastructure.
  • @Before("execution(...)") — declares a before advice with an inline pointcut expression. The expression targets every method (any return type *, any name *, any arguments (..)) inside any class in the com.example.demo.service package.

A Concrete Working Example

Here is the full picture with a real service so you can run and observe the behaviour:

// OrderService.java package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class OrderService { public String placeOrder(String item) { System.out.println("Placing order for: " + item); return "ORDER-001"; } public void cancelOrder(String orderId) { System.out.println("Cancelling order: " + orderId); } }
// LoggingAspect.java package com.example.demo.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class); @Before("execution(* com.example.demo.service.*.*(..))") public void logBeforeServiceCall(JoinPoint joinPoint) { log.info("[ASPECT] Entering: {}.{}()", joinPoint.getTarget().getClass().getSimpleName(), joinPoint.getSignature().getName()); } }

Notice the JoinPoint parameter. Spring injects this automatically whenever you declare it as the first parameter of an advice method. It gives you runtime information about the intercepted call — the target object, the method name, and the arguments. The advice signature does not need to match the target method's signature at all; Spring handles the wiring.

What Happens at Runtime

When the application context starts, the auto-proxy creator scans all bean definitions. When it finds a bean that matches at least one pointcut declared in any @Aspect bean, it wraps that bean in a dynamic proxy. From that point on, every call to the proxied bean goes through the proxy first, which checks whether advice applies to the called method and runs it if so.

For OrderService above, the proxy created by Spring intercepts both placeOrder and cancelOrder. When a controller or another service injects OrderService, it actually receives the proxy, not the raw instance. The proxy is transparent — it implements the same interface or extends the same class — so callers need no changes.

Self-invocation bypasses the proxy. If placeOrder calls cancelOrder directly (i.e. this.cancelOrder(...)), the call goes straight to the real object and skips the proxy entirely. The aspect will not fire. This is a fundamental limitation of Spring's proxy-based AOP. The solution is to inject the bean into itself (via @Lazy) or, for complex cases, use full AspectJ compile-time weaving.

Organising Aspects in a Real Project

Keep all aspect classes in a dedicated package such as com.example.demo.aspect. This makes it immediately obvious that a class is cross-cutting infrastructure and not business logic. One aspect class per concern — a LoggingAspect, a SecurityAspect, a PerformanceAspect — scales better than putting all advice into a single giant class. Each aspect should be focused and testable in isolation.

Summary

Creating an aspect in Spring requires @Aspect to identify the class as AOP infrastructure, @Component to register it as a Spring bean, and auto-proxy support (provided automatically by Spring Boot's AOP starter). Advice methods receive an optional JoinPoint parameter that exposes runtime details about the intercepted call. Self-invocation is the one hard limitation to keep in mind. With these fundamentals in place you are ready to explore the full range of advice types in the next lesson.

ES
Edrees Salih
1 hour ago

We are still cooking the magic in the way!