注解与元注解

面试官问:"Java 注解是什么?用过自定义注解吗?"

候选人小丁答:"注解是给代码添加元数据的方式。比如 @Override 就是注解。"

面试官追问:"@Override 注解是怎么工作的?为什么加上 @Override 就能检测方法是否重写?"

小丁说:"编译器会检查?"

面试官又问:"元注解有哪些?"

小丁列举了半天,没说全。

【面试官心理】 注解是 Java 最容易被忽视的特性之一。大多数候选人只知道 @Override 的用法,不知道注解的本质是"interface"、不知道元注解的作用。能说出"运行时注解处理"的候选人,说明有实际开发自定义注解的经验。

一、注解的本质 🔴

1.1 注解是特殊的 interface

// @Override 的定义(简化)
public @interface Override {
    // 没有属性
}

// 编译后生成:Override.class(接口类型)

@interface 编译后生成一个继承 java.lang.annotation.Annotation 的接口。

1.2 注解的组成部分

// 一个完整的注解
@Retention(RetentionPolicy.RUNTIME)  // 注解保留到运行时
@Target(ElementType.METHOD)         // 只能用在方法上
public @interface Author {
    String name();                    // 注解属性
    String date() default "2024-01-01"; // 带默认值
}

1.3 注解的使用

@Author(name = "John", date = "2024-06-01")
public void process() {
    // ...
}

// 如果所有属性都有默认值,可以不加括号
@Deprecated
public void oldMethod() {
    // ...
}

二、四大元注解 🔴

元注解作用参数值
@Retention保留策略SOURCE / CLASS / RUNTIME
@Target使用范围TYPE / METHOD / FIELD / PARAMETER / CONSTRUCTOR 等
@Documented是否出现在 Javadoc无参数
@Inherited是否可继承无参数

2.1 @Retention:保留策略

// SOURCE:只保留在源码中,编译后丢弃
// 典型:@Override, @SuppressWarnings
@Retention(RetentionPolicy.SOURCE)
public @interface Override { }

// CLASS:保留到编译时,但 JVM 加载类时丢弃(默认行为)
@Retention(RetentionPolicy.CLASS)
public @interface MyClassAnnotation { }

// RUNTIME:一直保留到运行期,可通过反射读取
// 典型:@Autowired, @Transactional, @Component
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRuntimeAnnotation { }
⚠️

Spring 的 @Autowired 是 RUNTIMERetention,所以可以用反射读取。如果标记为 CLASS 或 SOURCE,反射就读取不到了。这也是为什么 Spring 能通过反射做依赖注入。

2.2 @Target:使用范围

// 可以同时标记多个位置
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})

// 常用值:
ElementType.TYPE          // 类、接口、枚举
ElementType.METHOD        // 方法
ElementType.FIELD         // 字段
ElementType.PARAMETER     // 方法参数
ElementType.CONSTRUCTOR   // 构造器
ElementType.LOCAL_VARIABLE // 局部变量
ElementType.ANNOTATION_TYPE // 注解类型

2.3 @Documented 与 @Inherited

@Documented
public @interface MyDocAnnotation { }
// 使用此注解的元素在生成 Javadoc 时会显示此注解

@Inherited
public @interface MyInheritAnnotation { }
// 子类会自动继承父类的 @MyInheritAnnotation
// 只有类上的注解会继承,方法上的不会

三、自定义注解实战 🔴

3.1 日志注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecution {
    boolean logParams() default true;
    boolean logResult() default false;
}

// 切面处理
@Aspect
@Component
public class LogAspect {

    @Around("@annotation(logExecution)")
    public Object around(ProceedingJoinPoint pjp, LogExecution logExecution) throws Throwable {
        String methodName = pjp.getSignature().getName();

        if (logExecution.logParams()) {
            log.info("Executing: {} with args: {}", methodName, pjp.getArgs());
        }

        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        long elapsed = System.currentTimeMillis() - start;

        if (logExecution.logResult()) {
            log.info("{} returned: {} in {}ms", methodName, result, elapsed);
        }

        return result;
    }
}

// 使用
@LogExecution(logParams = true, logResult = true)
public User findById(Long id) {
    return userRepository.findById(id);
}

3.2 注解处理器(反射实现)

// 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NotNull {
    String message() default "Field cannot be null";
}

// 处理器
public class ValidationProcessor {
    public static void validate(Object obj) throws ValidationException {
        for (Field field : obj.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(NotNull.class)) {
                field.setAccessible(true);
                if (field.get(obj) == null) {
                    NotNull notNull = field.getAnnotation(NotNull.class);
                    throw new ValidationException(notNull.message()
                        + ": " + field.getName());
                }
            }
        }
    }
}

// 使用
public class User {
    @NotNull(message = "用户名不能为空")
    private String name;
}

// 调用
ValidationProcessor.validate(user);

四、Java 8 新增的注解 🟡

4.1 @Repeatable

// JDK 8 之前,一个元素只能使用注解一次
// JDK 8+ 可以重复使用

// 定义可重复注解
@Repeatable(Authors.class)
public @interface Author {
    String name();
}

// 定义容器注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Authors {
    Author[] value();
}

// 使用
@Author(name = "Alice")
@Author(name = "Bob")
public class Book {
    // 两个 @Author 注解同时生效
}

// 读取:
Author[] authors = Book.class.getAnnotationsByType(Author.class);

4.2 @FunctionalInterface

@FunctionalInterface
public interface Runnable {
    void run();
}
// 编译器检查:是否只有一个抽象方法
// 如果接口有多个抽象方法,编译错误
// 如果接口已经有 Object 的方法(如 toString),不算抽象方法

五、Spring 的注解驱动开发 🟡

Spring 2.5 引入的注解,极大简化了 XML 配置:

// 传统 XML 配置
<bean id="userService" class="com.example.UserService">
    <property name="userRepository" ref="userRepository"/>
</bean>

// 注解驱动配置
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository; // 自动注入
}

// @Autowired 原理:
// Spring 在初始化 Bean 时,通过反射找到所有 @Autowired 标注的字段
// 然后从容器中找到对应类型的 Bean 注入
💡

Spring Boot 的出现让注解驱动达到了巅峰:几乎所有配置都可以用注解替代。能说出 Spring 如何通过反射处理注解的候选人,说明理解 Spring 的核心原理。

六、追问升级

面试官:"注解和注释(comment)有什么区别?"

// 注释(comment):给人类看的,编译器完全忽略
// 注解(annotation):给机器看的,可以影响程序行为

/**
 * 这是一个注释
 */
@Deprecated // 这是一个注解,编译器会警告"使用了已过时的方法"
public void oldMethod() { }

【面试官心理】 问这个区别的候选人,说明对注解的理解停留在表面。但这个问题本身很简单,能主动提出来的说明有思考。