Skip to content
CampusEduX

Production · Lesson 87 of 95

Spring AOP

Learn Spring AOP in Spring Boot: aspects, advice, pointcuts and proxies, with a bakery example that logs and audits calls without touching business code.

9 min read

Think about a busy bakery. Every time a cake leaves the counter, someone must write it in the register, someone must check the stock, and someone must note the time. If the baker did all of this by hand for each cake, there would be no time left to bake. So the shop puts a small routine at the counter, and every order passes through it. The baker only thinks about cakes.

Spring AOP works the same way. It lets you put common jobs, such as logging or checks, in one place instead of copying them into every method.

In this guide you will learn what AOP is, the words that go with it, how Spring runs your extra code around a method, and how to build a working example.

What is Spring AOP?

AOP stands for Aspect-Oriented Programming. Normal code is organised by what it does: orders, payments, stock. Some jobs cut across all of them, like logging, security checks and timing. These are called cross-cutting concerns.

Here are the words you will meet:

TermMeaningBakery picture
AspectA class holding the extra codeThe counter routine
AdviceThe extra code itself and when it runsWrite in the register
Join pointA moment where advice can run; in Spring, a method callOne cake leaving
PointcutA rule that picks the join pointsEvery cake order
TargetThe object being wrappedThe cake service
ProxyA stand-in object Spring createsThe counter clerk

Why is it used?

Without AOP, the same lines appear everywhere:

  • A log line at the start and end of fifty methods.
  • The same "is the user allowed?" check in every service.
  • Timing code wrapped around every slow call.

This copying makes code long and easy to break. Change the log format once and you must edit fifty places. With AOP, you write the job once, describe where it applies, and Spring does the rest. Your business methods stay short and focus on one task.

Spring itself uses AOP behind the scenes. @Transactional, @Cacheable and @Async all work through proxies, which is why understanding AOP explains many strange bugs later.

How it works

Spring does not change your class. When the app starts, it notices that an aspect matches the CakeService bean. It then builds a proxy, a stand-in object with the same methods, and puts the proxy in the container instead of the real bean.

text
Caller | v +---------------------+ | Proxy (made by | | Spring) | +---------------------+ | 1. run advice | 2. call real method v +---------------------+ | CakeService | | (your real bean) | +---------------------+ | v 3. run advice, return

The caller thinks it is talking to CakeService, but it talks to the proxy first. The proxy runs your advice, forwards the call to the real bean, and runs more advice on the way back. That is all the magic there is.

There are five kinds of advice:

text
@Before runs first @Around wraps the call @AfterReturning only on success @AfterThrowing only on exception @After always, last

@Around is the strongest. It receives the call itself and decides whether to continue, and it can change the result. The others are simpler and cannot stop the method.

The two most common ways to write a pointcut are:

  • execution(...) picks methods by their signature, for example every method of one class.
  • @annotation(Audited) picks every method that carries your own annotation.

Real-Life Example

A hospital reception writes down every patient file that leaves the records room: who took it, at what time, and who returned it. The doctors do not write this themselves. The records clerk does it for every file, whichever doctor asks. If the hospital wants to add a stamp to each file tomorrow, only the clerk's routine changes. The clerk is the aspect. The files are the method calls. The rule "every file leaving the room" is the pointcut.

Code Example

Let's build SweetCrumb, a bakery service. We will log every call to the cake service, print the bill, report failures, and audit methods marked with our own @Audited annotation.

File: pom.xml

xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.sweetcrumb</groupId> <artifactId>bakery</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aspectj</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

File: application.properties in src/main/resources

properties
spring.main.banner-mode=off logging.level.root=warn

File: Audited.java in package com.sweetcrumb.bakery

java
package com.sweetcrumb.bakery; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Audited { }

File: CakeService.java in package com.sweetcrumb.bakery

java
package com.sweetcrumb.bakery; import org.springframework.stereotype.Service; @Service public class CakeService { private int stock = 10; @Audited public int orderCake(String flavour, int quantity) { if (quantity < 1) { throw new IllegalArgumentException("Order at least one cake"); } stock -= quantity; return quantity * 450; } public int stockLeft() { return stock; } }

File: ShopAspect.java in package com.sweetcrumb.bakery

java
package com.sweetcrumb.bakery; import java.util.Arrays; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class ShopAspect { @Before("execution(* com.sweetcrumb.bakery.CakeService.*(..))") public void logCall(JoinPoint jp) { System.out.println("[before] " + jp.getSignature().getName() + " " + Arrays.toString(jp.getArgs())); } @AfterReturning(pointcut = "execution(* com.sweetcrumb.bakery.CakeService.orderCake(..))", returning = "bill") public void logBill(Object bill) { System.out.println("[returned] bill " + bill); } @AfterThrowing(pointcut = "execution(* com.sweetcrumb.bakery.CakeService.*(..))", throwing = "ex") public void logFailure(Exception ex) { System.out.println("[failed] " + ex.getMessage()); } @Around("@annotation(Audited)") public Object audit(ProceedingJoinPoint pjp) throws Throwable { System.out.println("[audit] start"); try { return pjp.proceed(); } finally { System.out.println("[audit] end"); } } }

File: BakeryApplication.java in package com.sweetcrumb.bakery

java
package com.sweetcrumb.bakery; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class BakeryApplication { public static void main(String[] args) { SpringApplication.run(BakeryApplication.class, args); } @Bean CommandLineRunner demo(CakeService cakes) { return args -> { System.out.println("Bill: " + cakes.orderCake("Chocolate", 2)); try { cakes.orderCake("Mango", 0); } catch (IllegalArgumentException e) { System.out.println("Caught: " + e.getMessage()); } System.out.println("Stock: " + cakes.stockLeft()); }; } }

Run it with ./mvnw spring-boot:run:

Output:

text
[audit] start [before] orderCake [Chocolate, 2] [returned] bill 900 [audit] end Bill: 900 [audit] start [before] orderCake [Mango, 0] [failed] Order at least one cake [audit] end Caught: Order at least one cake [before] stockLeft [] Stock: 8

Code Explained

  • spring-boot-starter-aspectj brings Spring AOP and the AspectJ annotations. In Spring Boot 3 this starter was called spring-boot-starter-aop.
  • @Aspect marks the class as an aspect, and @Component lets Spring find it. Both are needed.
  • The execution pointcut in ShopAspect means any return type, any method of CakeService, any arguments. That is why stockLeft also prints a [before] line.
  • @Audited is our own annotation. The pointcut @annotation(Audited) matches only methods that carry it, so stockLeft is not audited.
  • In the first call the order is [audit] start, [before], the real method, [returned], then [audit] end. The @Around advice is the outer layer and the others sit inside it.
  • In the failing call @AfterReturning is skipped and @AfterThrowing runs. The finally block still prints [audit] end, and the exception then reaches the caller.
  • The business method orderCake has no logging code at all.

Common Mistakes

  • Forgetting `@Component` on the aspect. The class then exists but Spring never uses it.
  • Forgetting `proceed()` in `@Around`. The real method never runs and the caller gets null or an error.
  • Swallowing the exception in `@Around`. If you catch it and do not rethrow, the caller thinks everything worked.
  • Expecting advice on `private` or `final` methods. Proxies can only wrap public methods that can be overridden or delegated.
  • Using a pointcut that is too broad. execution(* *(..)) in the whole app slows everything and logs noise.

Interview Questions

What is AOP and why do we need it?

Ans:It moves cross-cutting jobs such as logging, security and transactions out of business code into one place, so methods stay short and the job is written once.

What are the types of advice?

Ans:@Before, @After, @AfterReturning, @AfterThrowing and @Around.

What is the difference between a join point and a pointcut?

Ans:A join point is a moment when advice could run, for example a method call. A pointcut is the rule that selects which join points actually get advice.

How does Spring AOP work internally?

Ans:It creates a proxy around the target bean, using JDK dynamic proxies or CGLIB, and the proxy runs advice around each call.

Why does self-invocation not trigger advice?

Ans:Because the call goes from this to this, never through the proxy.

Key Points to Remember

  • AOP runs shared code around methods without editing them.
  • An aspect holds advice; a pointcut chooses where the advice applies.
  • Spring AOP uses proxies, so only calls that arrive through the proxy are advised.
  • @Around can change arguments, results and exceptions; call proceed() exactly when you want the method to run.
  • In Spring Boot 4 the starter is spring-boot-starter-aspectj.
  • @Transactional, @Cacheable and @Async are built on the same idea.

Frequently Asked Questions

Is Spring AOP the same as AspectJ?

No. Spring AOP uses proxies and supports only method calls on Spring beans. Full AspectJ changes the compiled classes and can advise almost anything. Spring borrows AspectJ's annotations and pointcut language, which is why the imports start with org.aspectj.

Does AOP slow down my application?

Slightly. Each advised call goes through the proxy and your advice code. For logging and simple checks the cost is tiny. Keep the advice light and the pointcut narrow.

Can I have more than one aspect on a method?

Yes. All matching aspects run. Use @Order on the aspect classes to decide which one is outermost.

Should I use AOP for business rules?

No. Use it for cross-cutting jobs that apply to many methods. A rule that belongs to one feature is easier to read as normal code in that feature.

Practice Problems

Try each problem on your own first. Each one has its own pom.xml with the AspectJ starter.

Easy: Doctor Visit Log

City Care Hospital wants a log line every time a method of DoctorService is called. The service has consult(String patient) which returns "Consulted " + patient. Write an aspect with @Before advice that prints [log] consult called for Meera when the app asks for Meera's consultation. Do not put any print statement inside DoctorService.

Show answer
The aspect matches every method of DoctorService. jp.getArgs()[0] is the first argument. The service stays clean.

File: pom.xml

xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.citycare</groupId> <artifactId>clinic</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aspectj</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

File: application.properties in src/main/resources

properties
spring.main.banner-mode=off logging.level.root=warn

File: DoctorService.java in package com.citycare.clinic

java
package com.citycare.clinic; import org.springframework.stereotype.Service; @Service public class DoctorService { public String consult(String patient) { return "Consulted " + patient; } }

File: VisitLogAspect.java in package com.citycare.clinic

java
package com.citycare.clinic; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class VisitLogAspect { @Before("execution(* com.citycare.clinic.DoctorService.*(..))") public void log(JoinPoint jp) { System.out.println("[log] " + jp.getSignature().getName() + " called for " + jp.getArgs()[0]); } }

File: ClinicApplication.java in package com.citycare.clinic

java
package com.citycare.clinic; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class ClinicApplication { public static void main(String[] args) { SpringApplication.run(ClinicApplication.class, args); } @Bean CommandLineRunner demo(DoctorService doctors) { return args -> System.out.println(doctors.consult("Meera")); } }

The console prints:

text
[log] consult called for Meera Consulted Meera

Medium: Cinema Booking Guard

StarPlex Cinema allows at most 4 seats in one booking. TicketService.book(String movie, int seats) returns "Booked " + seats + " for " + movie. Write one @Around aspect that:

  • rejects a booking of more than 4 seats by throwing IllegalArgumentException("Max 4 seats") without calling the real method, and
  • counts every accepted booking so the app can print the total.
Show answer
The aspect checks the argument first. Only when it is valid does it call proceed(), then it increments the counter. The service has no rule inside it, so the limit can change in one place.

File: pom.xml

xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.starplex</groupId> <artifactId>booking</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aspectj</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

File: application.properties in src/main/resources

properties
spring.main.banner-mode=off logging.level.root=warn

File: TicketService.java in package com.starplex.booking

java
package com.starplex.booking; import org.springframework.stereotype.Service; @Service public class TicketService { public String book(String movie, int seats) { return "Booked " + seats + " for " + movie; } }

File: BookingGuardAspect.java in package com.starplex.booking

java
package com.starplex.booking; import java.util.concurrent.atomic.AtomicInteger; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class BookingGuardAspect { private final AtomicInteger accepted = new AtomicInteger(); @Around("execution(* com.starplex.booking.TicketService.book(..))") public Object guard(ProceedingJoinPoint pjp) throws Throwable { int seats = (int) pjp.getArgs()[1]; if (seats > 4) { throw new IllegalArgumentException("Max 4 seats"); } Object result = pjp.proceed(); accepted.incrementAndGet(); return result; } public int acceptedCount() { return accepted.get(); } }

File: BookingApplication.java in package com.starplex.booking

java
package com.starplex.booking; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class BookingApplication { public static void main(String[] args) { SpringApplication.run(BookingApplication.class, args); } @Bean CommandLineRunner demo(TicketService tickets, BookingGuardAspect guard) { return args -> { System.out.println(tickets.book("Leo", 3)); try { tickets.book("Leo", 6); } catch (IllegalArgumentException e) { System.out.println("Rejected: " + e.getMessage()); } System.out.println("Accepted: " + guard.acceptedCount()); }; } }

The console prints:

text
Booked 3 for Leo Rejected: Max 4 seats Accepted: 1