Mastering Java Annotations: From Basic Usage to Custom Annotation Processors

This article explains the fundamentals of Java annotations, their core purposes, built‑in annotations, meta‑annotations, and provides step‑by‑step practical examples for creating a runtime @Sensitive annotation and a compile‑time @AutoGetter processor, while also dissecting how Spring leverages annotations for component scanning, dependency injection, and request mapping.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Mastering Java Annotations: From Basic Usage to Custom Annotation Processors

Java annotations are a form of metadata. Each annotation implicitly implements the special interface java.lang.annotation.Annotation. When the compiler encounters an annotation, it generates a class that implements this interface, allowing the annotation to be accessed via reflection at runtime.

Core Purposes of Annotations

Compile‑time validation : e.g., @Override lets the compiler verify that a method truly overrides a superclass method.

Replace XML configuration : Spring’s @Component and @RequestMapping simplify configuration compared with legacy XML.

Runtime enhancement : Annotations can be read via reflection to add business logic such as field desensitization, permission checks, or logging.

Compile‑time code generation : Tools like Lombok’s @Data generate boilerplate code during compilation, incurring no runtime overhead.

Common Built‑in Annotations

@Override

Marks a method as overriding a superclass or interface method, enabling the compiler to catch spelling or signature mismatches. It can only be applied to methods.

public class Parent {
    public void sayHello() { System.out.println("父类方法"); }
}

public class Child extends Parent {
    @Override
    public void sayHello() { System.out.println("子类方法"); }
}

@Deprecated

Indicates that a class, method, field, or constructor is outdated. Optional attributes since (the version from which it is deprecated) and forRemoval (whether it will be removed in a future version) can be supplied.

@Deprecated(since = "1.0", forRemoval = true)
public static void oldMethod() { /* ... */ }

@SuppressWarnings

Suppresses specific compiler warnings. The warning type must be specified, e.g., "unchecked" for raw generic usage, "deprecation" for deprecated APIs, or "all" to silence all warnings (not recommended).

@SuppressWarnings("unchecked")
public void test() { List list = new ArrayList(); }

Meta‑Annotations (Annotations that Annotate Annotations)

@Target

Specifies the Java elements an annotation can be placed on (e.g., TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, ANNOTATION_TYPE, LOCAL_VARIABLE).

@Retention

Controls how long the annotation is retained:

RetentionPolicy.SOURCE : Discarded after compilation; useful for compile‑time checks or code generation.

RetentionPolicy.CLASS : Kept in the .class file but not available at runtime; used for byte‑code manipulation.

RetentionPolicy.RUNTIME : Available at runtime via reflection; enables runtime enhancements.

@Documented

Ensures the annotation appears in generated Javadoc.

@Inherited

Allows a subclass to inherit an annotation present on its superclass (only for class‑level annotations).

@Repeatable

Introduced in Java 8, permits the same annotation to be applied multiple times on a single element, requiring a container annotation.

Practical Example 1 – Runtime Custom Annotation for Field Desensitization

Goal: Create @Sensitive to mask sensitive fields (name, phone, ID card, email) automatically via reflection.

Step 1 – Define the Annotation

import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Sensitive {
    SensitiveType value() default SensitiveType.NAME;
    int prefix() default 1; // characters to keep at the start
    int suffix() default 1; // characters to keep at the end
    enum SensitiveType { NAME, PHONE, ID_CARD, EMAIL }
}

Step 2 – Write the Desensitization Utility

import java.lang.reflect.Field;

public class SensitiveUtil {
    public static <T> void desensitize(T obj) throws IllegalAccessException {
        if (obj == null) return;
        Class<?> clazz = obj.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Sensitive.class)) {
                Sensitive ann = field.getAnnotation(Sensitive.class);
                field.setAccessible(true);
                Object val = field.get(obj);
                if (val instanceof String) {
                    String masked = doDesensitize((String) val, ann.value(), ann.prefix(), ann.suffix());
                    field.set(obj, masked);
                }
            }
        }
    }
    private static String doDesensitize(String v, Sensitive.SensitiveType t, int p, int s) {
        if (v.length() <= p + s) return v;
        switch (t) {
            case NAME:   return mask(v, p, s);
            case PHONE:  return mask(v, 3, 4);
            case ID_CARD:return mask(v, 6, 4);
            case EMAIL:
                int at = v.indexOf('@');
                if (at <= p) return v;
                return mask(v.substring(0, at), p, 0) + v.substring(at);
            default:     return v;
        }
    }
    private static String mask(String str, int p, int s) {
        String start = str.substring(0, p);
        String end   = str.substring(str.length() - s);
        return start + "****" + end;
    }
}

Step 3 – Test the Annotation

public class User {
    @Sensitive(Sensitive.SensitiveType.NAME) private String username;
    @Sensitive(value = Sensitive.SensitiveType.PHONE, prefix = 3, suffix = 4) private String phone;
    @Sensitive(Sensitive.SensitiveType.ID_CARD) private String idCard;
    @Sensitive(value = Sensitive.SensitiveType.EMAIL, prefix = 1, suffix = 0) private String email;
    private Integer age; // not annotated
    // getters/setters omitted for brevity
}

public class SensitiveTest {
    public static void main(String[] args) throws IllegalAccessException {
        User u = new User();
        u.setUsername("张三");
        u.setPhone("13812345678");
        u.setIdCard("110101199001011234");
        u.setEmail("[email protected]");
        u.setAge(28);
        SensitiveUtil.desensitize(u);
        System.out.println("姓名:" + u.getUsername());   // 张****三
        System.out.println("手机号:" + u.getPhone());   // 138****5678
        System.out.println("身份证:" + u.getIdCard()); // 110101****1234
        System.out.println("邮箱:" + u.getEmail());    // z****@163.com
        System.out.println("年龄:" + u.getAge());      // 28 (unchanged)
    }
}

The result shows that the annotation can be reused on any entity field, eliminating repetitive masking logic.

Practical Example 2 – Compile‑time Annotation Processor for Automatic Getter Generation

Goal: Implement @AutoGetter that generates getter methods during compilation, similar to Lombok’s @Getter.

Step 1 – Define the Source‑Level Annotation

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface AutoGetter { }

Step 2 – Implement the Annotation Processor

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.util.Elements;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;

@SupportedAnnotationTypes("com.example.anno.AutoGetter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AutoGetterProcessor extends AbstractProcessor {
    private Elements elementUtils;
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        this.elementUtils = env.getElementUtils();
        this.filer = env.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        for (Element e : env.getElementsAnnotatedWith(AutoGetter.class)) {
            if (!(e instanceof TypeElement)) continue;
            TypeElement type = (TypeElement) e;
            String className = type.getSimpleName().toString();
            String pkg = elementUtils.getPackageOf(type).getQualifiedName().toString();
            String code = generateGetterCode(pkg, className, type);
            writeGetterFile(pkg, className, code);
        }
        return true;
    }

    private String generateGetterCode(String pkg, String cls, TypeElement type) {
        StringBuilder sb = new StringBuilder();
        sb.append("package ").append(pkg).append(";

");
        sb.append("public class ").append(cls).append("Getter extends ").append(cls).append(" {

");
        for (Element el : type.getEnclosedElements()) {
            if (!(el instanceof VariableElement)) continue;
            VariableElement field = (VariableElement) el;
            String name = field.getSimpleName().toString();
            String typeStr = field.asType().toString();
            String getter = "get" + name.substring(0,1).toUpperCase() + name.substring(1);
            sb.append("    public ").append(typeStr).append(' ').append(getter).append("() {
");
            sb.append("        return this.").append(name).append(";
");
            sb.append("    }

");
        }
        sb.append("}");
        return sb.toString();
    }

    private void writeGetterFile(String pkg, String cls, String code) {
        try (Writer w = filer.createSourceFile(pkg + "." + cls + "Getter").openWriter()) {
            w.write(code);
        } catch (IOException ex) {
            throw new RuntimeException("Failed to generate getter: " + ex.getMessage(), ex);
        }
    }
}

Step 3 – Register the Processor

Either create META-INF/services/javax.annotation.processing.Processor containing the fully‑qualified processor class name, or use Google’s @AutoService(Processor.class) (recommended) to generate the registration file automatically.

Step 4 – Use the Annotation

package com.example.entity;
import com.example.anno.AutoGetter;

@AutoGetter
public class User {
    private String username;
    private Integer age;
    private String phone;
}

After compilation (e.g., mvn compile), the processor creates UserGetter.java in the generated sources directory:

package com.example.entity;
public class UserGetter extends User {
    public String getUsername() { return this.username; }
    public Integer getAge() { return this.age; }
    public String getPhone() { return this.phone; }
}

Clients can instantiate UserGetter and call the generated getters without writing any boilerplate code.

Spring Framework Annotation Internals

Spring’s most used annotations ( @Component, @Autowired, @RequestMapping) are built on the same meta‑annotation concepts described above.

@Component

Meta‑annotations: @Target(TYPE), @Retention(RUNTIME).

During container startup, ClassPathBeanDefinitionScanner scans the classpath, uses reflection to detect @Component (or its specializations @Service, @Controller), and registers the class as a Spring bean.

@Autowired

Meta‑annotations: @Target({FIELD,METHOD,CONSTRUCTOR}), @Retention(RUNTIME).

When a bean is instantiated, Spring reflects over its fields and methods; if @Autowired is present, Spring looks up a matching bean in the context and injects it via reflection.

@RequestMapping

Meta‑annotations: @Target({TYPE,METHOD}), @Retention(RUNTIME).

Spring MVC scans @Controller classes, reads the @RequestMapping values on classes and methods, builds a URL‑to‑handler map, and dispatches incoming HTTP requests to the appropriate method.

In all three cases, the annotation merely marks a Java element; the framework’s runtime or compile‑time machinery reads the mark and performs the actual work, exactly as demonstrated in the custom examples.

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.

JavaReflectionSpringAnnotationsAnnotation ProcessorMeta‑annotations
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

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.