Fundamentals 12 min read

How Do Java Annotations Work Inside the JVM? From Compile‑time to Runtime Proxies

The article explains the complete lifecycle of Java annotations, covering how the compiler represents them in the AST, how retention policies affect bytecode generation, how the JVM parses annotation attributes during class loading, and how dynamic proxy objects are created and used at runtime, with concrete Spring framework examples.

Programmer1970
Programmer1970
Programmer1970
How Do Java Annotations Work Inside the JVM? From Compile‑time to Runtime Proxies

1. Compile‑time handling of annotations

When javac compiles source code, it builds an abstract syntax tree (AST) where each annotation appears as a JCTree.JCAnnotation node containing the annotation type (e.g., @Override) and its element values (e.g., value="test"). The compiler then checks the meta‑annotation @Retention to decide the retention policy ( SOURCE, CLASS, or RUNTIME). Depending on the policy, the annotation is either discarded, written to the .class file, or stored in the RuntimeVisibleAnnotations attribute.

public static class JCAnnotation extends JCTree {
    public JCTree.JCExpression annotationType; // e.g. @Override
    public List<JCTree.JCExpression> args;   // e.g. value="test"
}

For CLASS or RUNTIME retention, Attr.checkRetention serialises the annotation into the class file’s attribute table. The ClassWriter.writeAnnotations method emits the RuntimeVisibleAnnotations attribute.

private void checkRetention(Env<AttrContext> env, JCTree tree) {
    RetentionPolicy policy = getRetentionPolicy(env, tree);
    if (policy == RetentionPolicy.SOURCE) { /* drop */ }
    else if (policy == RetentionPolicy.CLASS) { /* write to class file */ }
    else { /* write to RuntimeVisibleAnnotations */ }
}

2. JVM class‑loading phase

When a class loader reads a .class file, it parses the RuntimeVisibleAnnotations attribute. The JVM component sun.reflect.annotation.AnnotationParser converts the raw bytes into internal representations of the annotation type and its element values.

public static Annotation[] parseAnnotations(byte[] code,
        ConstantPool constPool, Class<?> callerClass) { … }

For each annotation, AnnotationParser.annotationForMap creates a proxy instance that implements the annotation interface.

private static Annotation annotationForMap(ConstantPool cp,
        Class<? extends Annotation> type, Map<String,Object> values) { … }

3. Runtime dynamic proxy

If the retention is RUNTIME, the JVM generates a proxy object on demand. The proxy class is produced by sun.reflect.annotation.AnnotationProxyManager, which caches generated classes or creates new bytecode with ASM.

public Class<? extends Annotation> getAnnotationProxyClass(Class<? extends Annotation> type) {
    return proxyCache.computeIfAbsent(type, t -> {
        byte[] classBytes = generateProxyClass(t);
        return defineClass(t.getClassLoader(), classBytes);
    });
}

The proxy’s method calls are handled by AnnotationInvocationHandler, which returns stored element values or implements Object methods ( equals, hashCode, toString). If a value’s type does not match the method’s return type, AnnotationTypeMismatchException is thrown.

public Object invoke(Object proxy, Method method, Object[] args) {
    String name = method.getName();
    if (name.equals("equals")) return equalsImpl(args[0]);
    if (name.equals("hashCode")) return hashCodeImpl();
    if (name.equals("toString")) return toStringImpl();
    Object value = memberValues.get(name);
    if (value == null) throw new IncompleteAnnotationException(annotationType, name);
    if (!returnType.isInstance(value)) throw new AnnotationTypeMismatchException(null);
    return value;
}

4. Spring’s annotation processing

Spring scans for annotations such as @Autowired and @Transactional using BeanFactoryPostProcessor or BeanPostProcessor implementations. For @Autowired, AutowiredAnnotationBeanPostProcessor finds all @Autowired annotations, determines the required flag, builds InjectionMetadata, and registers injection points. Dependency resolution is performed by DefaultListableBeanFactory.resolveDependency, which selects a candidate bean and injects it.

public void postProcessMergedBeanDefinition(RootBeanDefinition bd,
        Class<?> beanType, String beanName) {
    Set<Annotation> anns = AutowiredAnnotationUtils.findAutowiredAnnotations(beanType);
    anns.forEach(a -> {
        boolean required = determineRequiredStatus(a);
        InjectionMetadata.InjectedElement element = buildAutowiredElement(a, required);
        registerInjectionPoint(beanName, element);
    });
}

For @Transactional, AnnotationTransactionAttributeSource extracts attributes (e.g., transactionManager, propagation) and builds a TransactionAttribute. AnnotationTransactionAspect weaves an AOP proxy that starts a transaction before method execution and commits or rolls back based on the outcome.

@Around("execution(* com.example..*.*(..)) && @annotation(transactional)")
public Object proceedWithTransaction(ProceedingJoinPoint pjp,
        Transactional transactional) throws Throwable {
    TransactionAttribute txAttr = parseTransactionAnnotation(transactional);
    TransactionStatus status = txManager.getTransaction(txAttr);
    try {
        Object result = pjp.proceed();
        txManager.commit(status);
        return result;
    } catch (Throwable ex) {
        txManager.rollback(status);
        throw ex;
    }
}

5. Debugging tips

Enable class‑loading tracing:

-XX:+TraceClassLoading -XX:+PrintAssembly -Dsun.reflect.DebugModuleAnnotations=true

Use Arthas to watch getAnnotation() calls:

watch com.example.TestClass getAnnotation "params,returnObj,throwExp" -x 3 -b

Generate or modify annotation bytecode with ASMify and ClassWriter to inspect the generated proxy class.

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.

JavaJVMReflectionSpringAnnotationsDynamic Proxy
Programmer1970
Written by

Programmer1970

Formerly called 'Code to 35'. Add our main WeChat ID to access a wealth of shared resources (algorithms, interview prep, tech stacks: Java, Python, Go, big data). We mainly share serious development techniques, focusing on output-driven input. Occasionally we post life snippets and gossip. Our aim is to attract precise traffic and test advertising opportunities.

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.