STAGE 31 OF 37

Stage 31: add logging advice to a service

Learning path / Stage 31 of 37

Before you start: use JDK 21+ and Maven 3.9+. Extract this stage’s project and open a terminal in its pom.xml folder. Complete the setup below, including a database connection where required, then follow the commands in this lesson’s Run this example section. Console lessons finish after printing their result; web lessons keep running until Ctrl+C. Follow the lesson’s run section for profile and database settings.

Observe extra behaviour around an existing product service without mixing it into the business operation.

Download the complete Stage 31 project · Open the project guide

Before you start

Use JDK 21 or newer and Maven 3.9 or newer. Verify with java -version and mvn -version. Extract this project into its own folder; do not merge its files with another stage.

This lesson reuses the product catalog and Todo responsibilities from Stages 8–14. Only the feature described below is new. Read the ordered explanation before opening the complete files.

Caller → Spring proxy → Advice → ProductService → Original result

What we change and why

  1. Keep business logic in the service

    ProductService.count returns the product count. The aspect contains the additional logging concern; the service does not call the aspect itself.

  2. Enable proxy-based interception

    @EnableAspectJAutoProxy installs Spring’s annotation-driven AOP support. We request class-based proxies to keep the concrete service lookup simple. AspectJ annotations and pointcut expressions are used with Spring runtime proxies, not AspectJ bytecode weaving.

  3. Name the AOP pieces

    The service method execution is the join point. The pointcut selects executions. The advice is the extra method that runs. The aspect groups these rules. In Spring AOP, interception applies to method execution through eligible managed proxies, not arbitrary source-code lines.

  4. Read before and after precisely

    @Before runs before the selected method. @After means finally-style advice: it runs after normal return or exception. @AfterReturning is different and runs only after successful completion. The result is printed after the proxied method returns.

Run this example

Open a terminal in the extracted folder containing pom.xml. Run one command at a time.

Terminal · Windows, macOS and Linux
mvn test
mvn spring-boot:run

This is a console lesson. It does not open a web port. Application.main starts a Spring context, calls the demonstration and closes the context. Read the terminal, not a browser. Spring Boot’s Maven plugin is used as a convenient launcher; these examples show Spring Framework directly.

Expected result

Expected output · ignore framework startup messages
Before: count
Service body
After (finally)
Result: 2

Object identifiers and elapsed times can vary. Compare the relationship between values rather than copying an identifier.

Understand each complete file

Create the following files at exactly these paths, or use the ZIP. Source files include their imports. The test checks the behaviour described in the lesson.

pom.xml
pom.xml · complete file
<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.edupoly</groupId><artifactId>spring-stage-31-aop-advice</artifactId><version>1.0.0</version>
  <properties><java.version>21</java.version></properties>
  <dependencies>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
    <dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId></dependency>
    <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency>
  </dependencies>
  <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
src/main/java/com/edupoly/lesson/AppConfig.java
src/main/java/com/edupoly/lesson/AppConfig.java · complete file
package com.edupoly.lesson;

import org.springframework.context.annotation.*;
@Configuration @ComponentScan("com.edupoly.lesson") @EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig { }
src/main/java/com/edupoly/lesson/ProductService.java
src/main/java/com/edupoly/lesson/ProductService.java · complete file
package com.edupoly.lesson;

import org.springframework.stereotype.Service;
@Service public class ProductService {
    public int count() { System.out.println("Service body"); return 2; }
}
src/main/java/com/edupoly/lesson/LoggingAspect.java
src/main/java/com/edupoly/lesson/LoggingAspect.java · complete file
package com.edupoly.lesson;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect @Component public class LoggingAspect {
    @Pointcut("execution(* com.edupoly.lesson.ProductService.count(..))")
    public void productOperation() { }
    @Before("productOperation()") public void before(JoinPoint point) { System.out.println("Before: " + point.getSignature().getName()); }
    @After("productOperation()") public void after() { System.out.println("After (finally)"); }
}
src/main/java/com/edupoly/lesson/Lesson.java
src/main/java/com/edupoly/lesson/Lesson.java · complete file
package com.edupoly.lesson;

import org.springframework.stereotype.Component;
@Component public class Lesson {
    private final ProductService service;
    public Lesson(ProductService service) { this.service = service; }
    public void run() { System.out.println("Result: " + service.count()); }
}
src/main/java/com/edupoly/lesson/Application.java
src/main/java/com/edupoly/lesson/Application.java · complete file
package com.edupoly.lesson;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
    public static void main(String[] args) throws Exception {
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            context.getBean(Lesson.class).run();
        }
    }
}
src/test/java/com/edupoly/lesson/LessonTest.java
src/test/java/com/edupoly/lesson/LessonTest.java · complete file
package com.edupoly.lesson;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

class LessonTest {
    @Test void advicePreservesReturnValueAndUsesProxy() {
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            var service = context.getBean(ProductService.class);
            assertTrue(org.springframework.aop.support.AopUtils.isAopProxy(service));
            assertEquals(2, service.count());
        }
    }
}

Try a failure, then correct it

Remove @EnableAspectJAutoProxy. The beans still exist but their aspect advice no longer runs. Restore it.

Words you can now explain

WordMeaning in this example
AspectA bean defining advice and the operations it applies to.
PointcutA rule selecting method executions.
AdviceAdditional behaviour before, after or around an invocation.

Practice assignment

Add another service method. Predict whether the current pointcut matches it, then run the example to check. Keep the service return value unchanged.

Read the official documentation →

Concepts used in this stage

Use the right column for a quick reminder, or open a concept above for its full explanation.

Java 21 · Spring Boot 4.1.1 · Edupoly