Adetayo Akinsanya unkletayo.dev

Aspect-Oriented Programming (AOP) Concepts: JoinPoints, Pointcuts, and Advices

Understanding cross-cutting concerns, AspectJ pointcut designators, and building custom auditing aspects

Part 17 in Series — Catch up on the previous article: Declarative Transaction Management: How @Transactional Works Under the Hood (Part 16) before diving into this post.

Why You Need This in Real Life

In an enterprise banking system with 150 controller and service classes, compliance mandates that every single business method must:

  1. Log the incoming user ID, method parameters, and execution timestamp.
  2. Measure and record execution latency in milliseconds to Prometheus/Micrometer.
  3. Catch any unhandled exception, attach a correlation ID, and record security audit trails.

If you write this logging, timing, and error tracking code manually inside every business method, your code becomes cluttered with duplicated infrastructure boilerplate (OWASP/Clean Code: Violating Single Responsibility Principle):

public OrderResponse placeOrder(OrderRequest request) {
    long startTime = System.currentTimeMillis();
    log.info("User {} invoking placeOrder", SecurityUtils.getCurrentUser());
    try {
        OrderResponse response = businessLogic(request);
        metrics.recordLatency("placeOrder", System.currentTimeMillis() - startTime);
        return response;
    } catch (Exception ex) {
        log.error("Failed placeOrder", ex);
        throw ex;
    }
}

When compliance requests changing the latency log format across all 150 classes, you must edit thousands of lines of code.

Aspect-Oriented Programming (AOP) solves this problem by modularizing cross-cutting concerns (logging, auditing, security, transaction management, rate limiting) into standalone classes called Aspects, leaving business code completely clean.


Part 1: Core AOP Terminology & Conceptual Mapping

To understand Spring AOP, you must master five core concepts:

+-----------------------------------------------------------------------------+
|                            Spring AOP Ecosystem                             |
|                                                                             |
|  1. Aspect: A modular module encapsulating cross-cutting logic.             |
|     (e.g., PerformanceMonitoringAspect)                                    |
|                                                                             |
|  2. JoinPoint: A specific point during execution (e.g., method call).       |
|                                                                             |
|  3. Pointcut: A predicate expression matching target JoinPoints.            |
|     (e.g., @Pointcut("execution(* com.example.service.*.*(..))"))          |
|                                                                             |
|  4. Advice: Action taken by an Aspect at a JoinPoint.                       |
|     (@Before, @AfterReturning, @AfterThrowing, @Around)                     |
|                                                                             |
|  5. Target Object: The actual business bean being proxied.                  |
+-----------------------------------------------------------------------------+

Visual Analogy

  • JoinPoint: All available electrical outlets in a house.
  • Pointcut: The specific 220V outlets in the kitchen.
  • Advice: The refrigerator plug connected to those outlets.
  • Aspect: The electrical wiring system controlling the power supply.

Part 2: Dissecting Advice Types

Spring AOP supports five types of advice executing relative to method invocation:

                      +----------------------------------+
                      |         @Around (Entry)          |
                      +----------------+-----------------+
                                       |
                                       v
                      +----------------------------------+
                      |             @Before              |
                      +----------------+-----------------+
                                       |
                                       v
                      +----------------------------------+
                      |    Target Method Execution       |
                      +----------------+-----------------+
                                       |
                   +-------------------+-------------------+
                   |                                       |
         (Method Success)                           (Method Throws Exception)
                   v                                       v
      +------------------------+              +------------------------+
      |    @AfterReturning     |              |     @AfterThrowing     |
      +------------+-----------+              +------------+-----------+
                   |                                       |
                   +-------------------+-------------------+
                                       |
                                       v
                      +----------------------------------+
                      |             @After               |
                      |   (Executes like a finally block)|
                      +----------------+-----------------+
                                       |
                                       v
                      +----------------------------------+
                      |         @Around (Exit)           |
                      +----------------------------------+

Part 3: Pointcut Designator Expressions

Spring AOP uses AspectJ pointcut expression syntax to match target methods:

1. execution(): Method Signature Matching

The most common pointcut designator matches method execution signatures:

execution(modifiers-pattern? return-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
  • Example: execution(public * com.example.service.*Service.*(..))
    • public: Only public methods.
    • *: Any return type.
    • com.example.service.*Service: Any class ending with Service in that package.
    • .*(..): Any method name with zero or more arguments.

2. @annotation(): Annotation Matching

Matches any method annotated with a custom annotation:

@Pointcut("@annotation(com.example.annotation.LogExecutionTime)")
public void logTimePointcut() {}

3. within(): Type Matching

Limits matching to methods within specific packages or classes:

@Pointcut("within(com.example.controller..*)")
public void controllerPackagePointcut() {}

Part 4: Hands-On: Building a Custom Performance & Audit Logging Aspect

Let’s build a production-grade custom auditing aspect that logs execution time and parameters for any method annotated with @Audited.

Step 1: Create the Custom Annotation

package com.example.annotation;

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 {
    String action() default "DEFAULT_ACTION";
}

Step 2: Implement the @Aspect Class

package com.example.aspect;

import com.example.annotation.Audited;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;

@Aspect
@Component
public class AuditAspect {

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

    @Around("@annotation(auditedAnnotation)")
    public Object auditMethodExecution(ProceedingJoinPoint joinPoint, Audited auditedAnnotation) throws Throwable {
        long startTime = System.currentTimeMillis();

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String className = signature.getDeclaringType().getSimpleName();
        String methodName = method.getName();
        Object[] args = joinPoint.getArgs();

        log.info("[AUDIT-START] Action: {} | Method: {}.{}() | Args: {}", 
                auditedAnnotation.action(), className, methodName, Arrays.toString(args));

        Object result;
        try {
            // Proceed with actual target method invocation
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            log.error("[AUDIT-FAILURE] Action: {} | Method: {}.{}() | Error: {}", 
                    auditedAnnotation.action(), className, methodName, throwable.getMessage());
            throw throwable; // Re-throw to caller
        }

        long executionTime = System.currentTimeMillis() - startTime;
        log.info("[AUDIT-SUCCESS] Action: {} | Method: {}.{}() | Duration: {}ms", 
                auditedAnnotation.action(), className, methodName, executionTime);

        return result;
    }
}

Part 5: Production Gotchas & Performance Edge Cases

Gotcha 1: Performance Overhead of @Around Advice

@Around advice wraps method invocation in a ProceedingJoinPoint object and creates parameter arrays (joinPoint.getArgs()). In high-frequency methods executed 50,000 times per second (such as utility parsers), object allocations inside @Around advice can trigger GC pressure.

  • Solution: Use fine-grained @Before or @AfterReturning advice when modifying return values or timing is not strictly required.

Gotcha 2: Aspect Execution Ordering

When multiple aspects intercept the exact same method (e.g., AuditAspect and SecurityAspect), the execution precedence is non-deterministic unless @Order(int) is declared on the @Aspect classes:

@Aspect
@Component
@Order(1) // High precedence: Executes FIRST on entry, LAST on exit
public class SecurityAspect { ... }

@Aspect
@Component
@Order(2) // Lower precedence: Executes inside SecurityAspect
public class AuditAspect { ... }

Next Steps

Now that we understand AOP concepts, we will examine how Spring generates proxy objects at runtime: dissecting the differences between JDK Dynamic Proxies and CGLIB Bytecode Generation.

References & Further Reading

  1. Micrometer.io. Micrometer Application Monitoring & Prometheus Format Specification. Micrometer Docs.
  2. Spring.io. Spring Boot Reference Guide — Production-ready Features (Actuator). Spring Docs.
  3. CNCF Prometheus Project. Prometheus Metric Types & Query Language (PromQL). Prometheus Docs.

Up Next in Series →

Part 18: Dynamic Proxies in Spring: JDK Dynamic Proxies vs CGLIB Bytecode Generation

Continue to Part 18 →