How Annotations Work, How to Understand Their Implementation, and a Typical Annotation-driven AOP Workflow

1. The Core Principle of Annotations: "Marker + Parser" Duo

Compare an annotation to a label on a package:

  • Annotation = label (e.g., "Fragile", "Keep Refrigerated"). It only describes a property; it performs no action.
  • Parser = courier / warehouse worker. When they see the label, they perform the corresponding action (handle with care, store in cold room).

A complete annotation workflow consists of three steps; none of them can be omitted.

Step 1: Define the annotation (create the label template)

Define the annotation using @interface and use meta‑annotations (annotations that describe other annotations) to set usage rules – for example, where the annotation can be placed (method, class, parameter) and whether it survives to runtime or only compile time.

import java.lang.annotation.*;

// Meta‑annotation: restricts the annotation to methods
@Target(ElementType.METHOD)
// Meta‑annotation: retains the annotation at runtime (essential! Only RUNTIME can be read via reflection)
@Retention(RetentionPolicy.RUNTIME)
// Custom annotation: marks methods whose execution time should be logged
public @interface ExecutionTimer {
    // Optional attribute, e.g., log severity level
    String severity() default "INFO";
}

Key meta‑annotations (must‑know for beginners):

Meta‑annotation Purpose
@Target Limits where the annotation can be used (METHOD, CLASS, PARAMETER, etc.)
@Retention Controls how long the annotation is kept (critical! RUNTIME = usable via reflection; CLASS = retained in class file but not at runtime; SOURCE = discarded after compilation)
@Documented Includes the annotation in generated Javadoc (optional)
@Inherited Allows subclasses to inherit the annotation from a parent class (optional)

Step 2: Apply the annotation (place a label on code)

Stick the annotation on the target method or class. At this point it only serves as a marker; no extra logic runs.

public class UserService {
    // Mark this method for execution‑time logging
    @ExecutionTimer(severity = "DEBUG")
    public void findUser(String userId) {
        try {
            Thread.sleep(100);   // simulate business logic
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Looking up user: " + userId);
    }
}

Step 3: Write the parser (bring the label to life)

This is the heart of making annotations work. The parser uses reflection to read the annotation information and then executes custom logic (e.g., measuring method execution time).

import java.lang.reflect.Method;

public class ExecutionTimerParser {
    public static void main(String[] args) throws Exception {
        // 1. Obtain the target class and method (core reflection)
        Class<UserService> serviceClass = UserService.class;
        Method targetMethod = serviceClass.getMethod("findUser", String.class);
        
        // 2. Check whether the method carries the @ExecutionTimer annotation
        if (targetMethod.isAnnotationPresent(ExecutionTimer.class)) {
            // 3. Retrieve the annotation and its attributes
            ExecutionTimer timerAnno = targetMethod.getAnnotation(ExecutionTimer.class);
            String logLevel = timerAnno.severity();
            
            // 4. Run the behavior associated with the annotation
            UserService instance = new UserService();
            long startNanos = System.nanoTime();
            targetMethod.invoke(instance, "2001");
            long elapsedNanos = System.nanoTime() - startNanos;
            
            System.out.printf("[%s] Execution time: %.2f ms%n",
                              logLevel, elapsedNanos / 1_000_000.0);
        }
    }
}

Output:

Looking up user: 2001
[DEBUG] Execution time: 101.23 ms

Key takeaway: Annotations are logic‑free markers. The parser (reflection code / framework logic) is what actually makes them do something.

2. How to Investigate What an Annotation Really Does

When you click on an annotation class (e.g., Spring’s @GetMapping or @Service) and browse its definition, all you see are meta‑annotations and attribute declarations – no execution logic – because the logic lives in the framework’s parser. Follow these steps to uncover the actual behavior.

Step 1: Examine the meta‑annotations (crucial clues)

  • Check @Retention: if it is SOURCE (like @Override), the annotation only matters at compile time and is consumed by the compiler. If it is RUNTIME, the annotation is read by reflection at runtime.
  • Check @Target: this tells you where the annotation can appear (method, class, parameter), which narrows down where to look for its parser.

Step 2: Find the parser (essential)

Parsers come in two forms, and you locate them differently.

Scenario 1: Custom annotations (your own code)

Search the project for code that reads the annotation via reflection. Look for keywords such as:

  • xxx.isAnnotationPresent(YourAnnotation.class)
  • xxx.getAnnotation(YourAnnotation.class)

(For example, the ExecutionTimerParser class above is the parser for @ExecutionTimer.)

Scenario 2: Framework annotations (e.g., Spring’s @GetMapping, @Transactional)

Framework annotation parsers typically appear in these forms; you can search the source code with "annotation name + handler/processor":

  1. BeanPostProcessor – handles component annotations such as @Component, @Service.
  2. HandlerMethodMapping – handles MVC annotations like @GetMapping, @PostMapping.
  3. AOP aspects – handles cross‑cutting annotations such as @Transactional, @Cacheable.

Real example: investigating @GetMapping

  1. Navigate into the @GetMapping annotation. See @Target(ElementType.METHOD) and @Retention(RetentionPolicy.RUNTIME).
  2. We now know it is a runtime‑retained method annotation, responsible for mapping HTTP GET requests.
  3. Search the Spring source for RequestMappingHandlerMapping. This class is the parser for @GetMapping. It scans methods annotated with @GetMapping, builds a map from URL patterns to handler methods, and dispatches incoming requests to the matching method.

Step 3: Verify the annotation’s effect

  • The most direct method: remove the annotation and see if the functionality disappears (e.g., drop @GetMapping and the endpoint is no longer reachable).
  • Debug the parser code (set breakpoints inside Spring’s RequestMappingHandlerMapping to watch the annotation‑parsing process).

3. Typical Annotation‑driven AOP Flow (a Common Use Case)

AOP (Aspect‑Oriented Programming) is one of the most prevalent scenarios for annotations (logging, transactions, authorization). The idea is to add common cross‑cutting logic without touching business code. We’ll use Spring AOP to implement the same @ExecutionTimer functionality and fully explain the flow.

AOP core concepts (plain‑language version)

  • Aspect – a class that bundles cross‑cutting logic (e.g., logging aspect, transaction aspect).
  • Pointcut – a predicate that specifies which methods to intercept (e.g., all methods marked with @ExecutionTimer).
  • Advice – the logic that runs upon interception (e.g., log before the method, measure time after).
  • Join point – a point during execution, such as the actual method being intercepted (e.g., findUser).

Complete annotation‑driven AOP flow (Spring Boot example)

Step 1: Add the AOP starter dependency (Maven)
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Step 2: The annotation (reuse the @ExecutionTimer defined earlier)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExecutionTimer {
    String severity() default "INFO";
}

Step 3: Write the AOP aspect (core: pointcut + advice)
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// 1. Mark as an aspect
@Aspect
// 2. Let Spring manage this bean
@Component
public class ExecutionTimerAspect {
    // 3. Define the pointcut: all methods annotated with @ExecutionTimer
    @Pointcut("@annotation(com.example.demo.ExecutionTimer)")
    public void timedMethod() {}

    // 4. Around advice (most flexible; runs before and after the target method)
    @Around("timedMethod() && @annotation(timerConfig)")
    public Object logTimedExecution(ProceedingJoinPoint joinPoint, ExecutionTimer timerConfig) throws Throwable {
        String logLevel = timerConfig.severity();
        long startNanos = System.nanoTime();
        
        // Proceed with the actual business method
        Object result = joinPoint.proceed();
        
        long elapsed = System.nanoTime() - startNanos;
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("[%s] Method [%s] execution took %.2f ms%n",
                          logLevel, methodName, elapsed / 1_000_000.0);
        return result;
    }
}

Step 4: Apply the annotation on the business method
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @ExecutionTimer(severity = "DEBUG")
    public void findUser(String userId) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Looking up user: " + userId);
    }
}

Step 5: Enable AOP and test
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
// In Spring Boot, AOP is enabled by default. Older versions may require @EnableAspectJAutoProxy.
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(DemoApplication.class, args);
        UserService service = ctx.getBean(UserService.class);
        service.findUser("2001");
    }
}

Output:

Looking up user: 2001
[DEBUG] Method [findUser] execution took 101.45 ms

Common advice types (by execution order)

Advice type Annotation When it runs Typical use case
Before advice @Before Before the target method executes Parameter validation, authorization checks
After (finally) advice @After After the target method completes (always, even if an exception occurs) Resource cleanup
After returning advice @AfterReturning After the target method returns normally Processing the return value, logging results
After throwing advice @AfterThrowing After the target method throws an exception Error handling, alerting
Around advice @Around Wraps around the target method (most flexible) Performance timing, transaction control

Tags: Java annotations Spring AOP reflection aspectj @Retention

Posted on Sun, 19 Jul 2026 16:22:03 +0000 by quickphp