Implementing Custom Annotations and AOP for Asynchronous Logging in Java

This article explains how to design a flexible logging solution in Java by creating a custom @Log annotation, using meta‑annotations such as Retention and Target, and applying Spring AOP with various advice types to capture method parameters, return values, and notes without polluting service code.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Implementing Custom Annotations and AOP for Asynchronous Logging in Java

1. Background

The author needed a complex logging mechanism that records log type, operation type, and remarks. Writing logging code directly in the service layer broke the single‑responsibility principle, so the solution switched to a custom annotation combined with an AOP aspect for asynchronous logging.

2. Technical Solution – Custom Annotation

Annotations (metadata) can be read at compile time, class loading, or runtime. Meta‑annotations are used to describe other annotations.

2.1 Annotation Overview

Annotations are special markers in code that can be processed by the compiler, class loader, or at runtime to add supplementary information without changing the original logic.

2.2 Meta‑annotations

The JDK provides four meta‑annotations:

Retention, Target, Documented, Inherited

(1) Retention

Specifies how long the annotation is retained. Options are RetentionPolicy.SOURCE (discarded by the compiler), RetentionPolicy.CLASS (present in the .class file but not at runtime), and RetentionPolicy.RUNTIME (available via reflection at runtime).
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}

public enum RetentionPolicy {
    SOURCE,
    CLASS,
    RUNTIME
}

(2) Target

Defines which program elements (class, method, field, etc.) the annotation can be applied to.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

(3) Documented

Ensures that the annotation is included in generated Javadoc.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented { }

(4) Inherited

If an annotation is marked with @Inherited, subclasses automatically inherit it.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited { }

2.3 Implementing the Custom Annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Log {
    LogType logType();          // Log category
    OperateType operate();      // Operation type
    String note() default ""; // Optional remark
}

3. Technical Solution – AOP Aspect

AOP allows adding behavior to existing code without modification. It relies on dynamic proxies (JDK or CGLIB) and consists of join points, pointcuts, advice, target objects, aspects, and weaving.

3.1 AOP Terminology

Join point : a method execution that can be enhanced.

Pointcut : a predicate that matches join points.

Advice : the code to execute at a matched join point.

Target : the original object being proxied.

Aspect : a class that contains pointcuts and advice.

Weaving : the process of linking aspects with target objects to create an advised object.

3.2 Advice Types

Before ( @Before) – runs before the method.

After ( @After) – runs after the method regardless of outcome.

AfterReturning ( @AfterReturning) – runs after successful return.

AfterThrowing ( @AfterThrowing) – runs only on exception.

Around ( @Around) – wraps the method execution.

3.3 Implementation Example (AfterReturning)

The author chose @AfterReturning to capture the method’s return value.

@Aspect
@Component
public class DaoAspect {
    /*
     * Before advice example (not used in final solution)
     */
    @Before("execution(* com.xzit.dao.Impl.UserDaoImpl.addUser(..))")
    public void methodBefore(JoinPoint joinPoint) {
        System.out.println("methodBefore invoked ...");
        Object[] args = joinPoint.getArgs();
        System.out.println(Arrays.toString(args));
    }
}

For @AfterReturning, the returning attribute must match the parameter name in the advice method.

@AfterReturning(pointcut = "@annotation(com.xxx.xxx.xxx.Log)", returning = "cId")
public void handleRdLogs(JoinPoint joinPoint, int cId) {
    // Obtain method signature
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    // Retrieve the @Log annotation
    if (!methodSignature.getMethod().isAnnotationPresent(Log.class)) {
        log.error("Failed to get @Log annotation");
        throw new Exception("Failed to get @Log annotation");
    }
    Log log = methodSignature.getMethod().getAnnotation(Log.class);
    // Output the note
    System.out.println(log.note());
}

3.4 Related Operations

• Get method signature:

MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();

• Retrieve custom annotation:

Log log = methodSignature.getMethod().getAnnotation(Log.class);

• Access annotation attributes:

System.out.println(log.note());

4. Advanced Operations

When both return values and method parameters are needed, the author uses Spring Expression Language (SpEL) to reference parameters inside the annotation value (e.g., #note).

@Log(note = "#note")
public int rdAuditReturn(String note) {
    System.out.println(note);
    // business logic ...
    return cId;
}

The aspect parses the SpEL expression, discovers parameter names, and injects them into an evaluation context.

private final ExpressionParser parser = new SpelExpressionParser();
private final EvaluationContext evaluationContext = new StandardEvaluationContext();

private void getNote(JoinPoint joinPoint, StringBuilder noteBuilder, String note) throws NoSuchMethodException {
    if (!StringUtils.isBlank(note)) {
        Class<?> targetCls = joinPoint.getTarget().getClass();
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = targetCls.getDeclaredMethod(ms.getName(), ms.getParameterTypes());
        ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
        String[] parameterNames = pnd.getParameterNames(targetMethod);
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; ++i) {
            int index = i;
            Optional.ofNullable(args[i]).ifPresent(param -> {
                String paramName = parameterNames[index];
                this.evaluationContext.setVariable(paramName, param);
            });
        }
        Optional.ofNullable(this.parser.parseExpression(note).getValue(this.evaluationContext))
                .ifPresent(k -> noteBuilder.append((String) k));
    }
}

These techniques enable fully decoupled, configurable logging without contaminating business logic.

— End of article.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

aopspringloggingannotations
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.