Skip to main content

Build Your Own Logging Aspect Spring AOP

· 5 min read
Link Nuis
Java Developer

Introduction

In modern applications, logging is essential for debugging, monitoring, and auditing. But adding logging statements manually in every method quickly becomes repetive and messy. This is where Aspect-Oriented Programming (AOP) comes in. With Spring AOP, you can separete corss-cutting concerns like logging, security, and transactions from your core business logic.

In this blog, we'll walk through how to create your own Logging Aspect using Spring AOP.

What is Spring AOP?

Spring AOP allows you to define behaviors that should be applied across multiple points in your application without duplicating code.

Key concepts:

  • Aspect: A module that encapsulates cross-cutting concerns (e.g., logging).
  • Advice: The action taken at a particular join point (before, after, around).
  • Pointcut: Expression that defines where advice should be applied.
  • Joinpoint: Apoint in program execution (like method call).
  • Weaving: Linking aspects with other application code.

Think of AOP as a way to 'plug in' extra behavior around your existing methods without touching their code.

Setting Up Project

Add the following dependency in your pom.xml (for Maven):

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

Also, enable AOP in your Spring application (in Spring boot, it's automatically enabled):

@SpringBootApplication
@EnableAspectJAutoProxy
public class SpringAOPTest {
public static void main(String[] args) {
SpringApplication.run(SpringAOPTest.class, args);
}
}

Creating a Logging Aspect

Now let's create an aspect that logs method execution.

LoggingAspect.java
@Aspect
@Component
public class LoggingAspect {

private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

// Pointcut: all methods in service package
@Pointcut("execution(* general.create_aop.service.*.*(..))")
public void serviceMethods() {
}

// Before advice
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
logger.info("Entering method: {}", joinPoint.getSignature().getName());
}

// After advice
@After("serviceMethods()")
public void logAfter(JoinPoint joinPoint) {
logger.info("Exiting method: {}", joinPoint.getSignature().getName());
}

// Around advice
@Around("serviceMethods()")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long elapsed = System.currentTimeMillis() - start;
logger.info("Method {} executed in {} ms", joinPoint.getSignature().getName(), elapsed);
return result;
}

@AfterReturning(value = "serviceMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
logger.info("Method returned successfully: {}", joinPoint.getSignature().getName());
}

@AfterThrowing(value = "serviceMethods()", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
logger.info("Exception in method: {}", joinPoint.getSignature().getName());
logger.info("Error: {}", ex.getMessage());
}
}
info

The @Around advice wraps the entire method execution. The @After advice runs after the target method completes, but still in scope of the @Around. So in real project, Having both @Around and @After on the same pointcut may lead to unexpected behavior. Considering using only @Around if you want to control the entire execution flow.

1. @Before - logBefore() executes first
2. @Around - logExecutionTime() starts (before joinPoint.proceed())
3. Target method executes
4. @Around - logExecutionTime() completes (after joinPoint.proceed())
5. @After - logAfter() executes last

Defining Pointcuts

Pointcuts let you specify where advice applies. For example:

  • execution(* com.example.service.*.*(..)) -> all methods in service package.
  • execution(* com.example.controller.*.*(..)) -> all methods in controller package.

You can combine pointcuts for flexibility.

Testing the Logging AOP

Suppose you have a simmple api:

service/UserService.java
@Service
public class UserService {
public String getUserById(Long id) throws InterruptedException {
Thread.sleep(500);
return "User-" + id;
}
}
@SpringBootApplication
public class SpringAOPTest {
public static void main(String[] args) {
SpringApplication.run(SpringAOPTest.class, args);
}

@RestController
@RequestMapping("/api/v1")
public static class SpringAOPTestController {

private final UserService userService;

public SpringAOPTestController(UserService userService) {
this.userService = userService;
}

@GetMapping("/test/logging")
public String testLogging() throws InterruptedException {
return userService.getUserById(123L);
}
}
}

When you typing in browser url /api/v1/test/logging, the logs will show:

alt text

Notice how logging happens automatically without modifying the service code.

Best Pratices

  • Keep aspects focused on once concern (logging, securiry, monitoring, etc.).
  • Use proper logging frameworks (SLF4J, Logback).
  • Avoid logging sensitive data (like passwords).
  • Don't overuse AOP-it's powerful but can make debugging harder if misused.

Bonus example

Build a AOP that measure execution time, annotation-based is clean way to implement this.

Step 1: Create annotation

Audited.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
String action();
}

Step 2: Aspect definition

AuditAspect.java
@Aspect
@Component
@Order(100) // run after security, before tx commit
public class AuditAspect {
}

Step 3 — @Around advice

AuditAspect.java
@Aspect
@Component
@Order(100) // run after security, before tx commit
public class AuditAspect {

private static final Logger logger = LoggerFactory.getLogger(AuditAspect.class);

@Around("@annotation(audited)")
public Object audit(
ProceedingJoinPoint pjp,
Audited audited
) throws Throwable {

long start = System.currentTimeMillis();
boolean success = false;

try {
Object result = pjp.proceed();
success = true;
return result;
} catch (Throwable ex) {
throw ex;
} finally {
long duration = System.currentTimeMillis() - start;
logger.info("Audited execution time: {} ms, success: {}, audited action: {}", duration, success, audited.action());
}
}

}

Step 4: Applying the aspect

SpringAOPTest.java
@SpringBootApplication
public class SpringAOPTest {
public static void main(String[] args) {
SpringApplication.run(SpringAOPTest.class, args);
}

@RestController
@RequestMapping("/api/v1")
public static class SpringAOPTestController {

@GetMapping("/test")
@Audited(action = "execute test()")
public String test() throws InterruptedException {
Thread.sleep(1500);
return "test";
}
}
}

Result

Try to hit this url /api/v1/test, and you can see the logs show:

_alt text

Conclusion

Spring AOP makes it easy to handle cross-cutting concerns like logging without cluttering your business logic. By creating a simple logging aspect, you can monitor method calls, execution times, and flow across your application.

Try extending this to other concerns like security checks, caching, or transaction management - the possibilities are endless!