乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

自定义注解

本章定位:掌握使用 @interface 关键字定义自己的注解,理解注解成员的声明规则(类型限制、默认值)、value() 特殊成员的语法糖,以及标记注解的使用场景。通过飞翔科技 ORM 框架和测试框架两个实战项目融会贯通。


什么是自定义注解?

JDK 内置的五个注解只能覆盖通用场景。在真实项目中,框架开发者需要定义自己的注解来表达领域语义。

飞翔科技后端架构师白歌在设计 ORM 框架雏形时,看着满屏的 XML 映射文件摇头:"如果能用注解直接在实体类上声明表和字段的映射关系,代码会比现在清晰十倍。"于是她定义了一套 @Table/@Column 注解——这就是自定义注解的典型应用。

定义语法:@interface

自定义注解使用 @interface 关键字,本质上定义了一个继承自 java.lang.annotation.Annotation 的接口。

// 最简单的自定义注解——标记注解(无成员)
public @interface TestMethod {
}

// 带成员的自定义注解——类似定义接口中的方法
public @interface Column {
    String name();      // 成员:列名
    String type();      // 成员:数据类型
}

注解成员详解

成员声明方式

注解成员以类似无参方法的形式声明,返回值类型即为成员的类型:

public @interface MyAnnotation {
    String name();          // String 类型成员
    int order();            // int 类型成员
    boolean enabled();      // boolean 类型成员
    Class<?> target();      // Class 类型成员
}

允许的成员类型

并不是所有 Java 类型都可以作为注解成员。JDK 8 规定注解成员只能使用以下类型:

类型类别具体类型示例
基本数据类型int、long、float、double、boolean、byte、short、charint order();
Stringjava.lang.StringString name();
Classjava.lang.Class<?>Class<?> mapper();
枚举任意枚举类型ElementType target();
注解另一个注解类型Constraint constraint();
以上类型的数组一维数组String[] aliases();

特别注意:不允许使用包装类型(Integer、Boolean 等)、集合类型(List、Map)、或自定义类作为注解成员类型。这个限制源于注解数据必须能被编译器以紧凑的二进制格式写入 .class 文件的常量池。

默认值:default 关键字

注解成员可以使用 default 关键字指定默认值。使用注解时,有默认值的成员可以不赋值:

public @interface Column {
    String name();                      // 必填成员
    String type() default "VARCHAR";    // 可选成员:默认 VARCHAR
    int length() default 255;           // 可选成员:默认 255
    boolean nullable() default true;    // 可选成员:默认允许 null
}

// 使用时:必填成员 name 必须赋值,其他可选
@Column(name = "student_name")                          // 使用所有默认值
@Column(name = "email", type = "VARCHAR", length = 100) // 部分覆盖默认值
@Column(name = "age", type = "INT", length = 3, nullable = false) // 全部覆盖

value() 特殊成员

当注解中定义了一个名为 value 的成员时,如果使用注解时仅给 value 赋值且没有给其他成员赋值,可以省略成员名 value =:

public @interface Table {
    String value();        // 特殊成员:表名
    String schema() default "public";  // 普通成员
}

// ====== 三种使用方式 ======

// 方式一:完整写法——给 value 和其他成员赋值时,必须写成员名
@Table(value = "students", schema = "university")

// 方式二:value 语法糖——仅给 value 赋值,可省略成员名
@Table("students")     // 等价于 @Table(value = "students")

// 方式三:仅给其他成员赋值——此时 value 没有提供,必须显式写成员名
@Table(schema = "university")  // ✗ 编译错误!value 没有默认值时必须提供

语法糖生效条件:

  1. 成员名为 value
  2. 使用注解时只给 value 赋值(或 value 有默认值,仅其他成员被赋值时不可省略 value =)
  3. 语法糖仅对 value() 有效,其他成员名不可省略

标记注解

标记注解(Marker Annotation) 不包含任何成员,仅作为类型标记存在:

// 标记注解:没有任何成员
public @interface TestMethod {
}

// 使用:不需要加括号
@TestMethod
public void testEnrollStudent() {
    // 测试代码
}

JDK 内置的 @Override 和 @Deprecated 也都是标记注解。

用途:标记注解虽然不承载数据,但反射代码可以检测其存在与否,从而做出判断。JUnit 4 中的 @Test 就是一个标记注解(JUnit 5 的 @Test 增加了成员属性)。


完整示例一:飞翔科技 ORM 框架模拟

场景描述

架构师白歌设计了一个轻量级 ORM 框架雏形,让开发人员可以通过 @Table 和 @Column 注解直接在实体类上声明数据库映射关系。后端开发小崔使用这些注解定义 Student 实体,并编写 SimpleORM 工具类通过反射解析注解生成 SQL 建表语句。

注解定义

import java.lang.annotation.*;

/**
 * 表映射注解 —— 标记一个类对应数据库中的某张表
 */
@Retention(RetentionPolicy.RUNTIME)   // 运行时保留,供反射读取
@Target(ElementType.TYPE)            // 只能用于类/接口上
public @interface Table {
    String value();                   // 表名(语法糖,可省略成员名)
}

/**
 * 列映射注解 —— 标记一个字段对应表中的某一列
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)           // 只能用于字段上
public @interface Column {
    String name();                    // 列名(必填)
    String type() default "VARCHAR";  // 列类型
    int length() default 255;         // 列长度
    boolean nullable() default true;  // 是否允许 null
    boolean primaryKey() default false; // 是否主键
}

实体类定义

/**
 * 学生实体 —— 映射到 students 表
 */
@Table("students")
public class Student {

    @Column(name = "id", type = "BIGINT", length = 20, nullable = false, primaryKey = true)
    private Long id;

    @Column(name = "name", length = 50, nullable = false)
    private String name;

    @Column(name = "age", type = "INT", length = 3)
    private Integer age;

    @Column(name = "major", length = 100)
    private String major;

    @Column(name = "email", length = 200)
    private String email;

    // 无注解字段——不会被映射(如 transient 用途)
    private String tempCache;

    // 构造方法
    public Student() {}

    public Student(Long id, String name, Integer age, String major, String email) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.major = major;
        this.email = email;
    }

    // Getter 和 Setter(省略部分)
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public String getMajor() { return major; }
    public void setMajor(String major) { this.major = major; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

ORM 工具类:注解解析器

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * 简易 ORM 工具 —— 通过读取 @Table 和 @Column 注解生成建表 SQL
 */
public class SimpleORM {

    /**
     * 根据实体类的注解生成 CREATE TABLE 语句
     * @param entityClass 实体类
     * @return SQL 建表语句
     */
    public static String generateCreateTableSQL(Class<?> entityClass) {
        // 1. 获取 @Table 注解
        Table tableAnnotation = entityClass.getAnnotation(Table.class);
        if (tableAnnotation == null) {
            throw new IllegalArgumentException(
                "类 " + entityClass.getSimpleName() + " 缺少 @Table 注解"
            );
        }
        String tableName = tableAnnotation.value();

        // 2. 遍历所有字段,收集 @Column 注解
        List<String> columnDefs = new ArrayList<>();
        List<String> primaryKeys = new ArrayList<>();

        for (Field field : entityClass.getDeclaredFields()) {
            Column columnAnnotation = field.getAnnotation(Column.class);
            if (columnAnnotation == null) {
                continue;  // 没有 @Column 注解的字段跳过
            }

            // 构建列定义
            StringBuilder colDef = new StringBuilder();
            colDef.append("    ").append(columnAnnotation.name()).append(" ");
            colDef.append(columnAnnotation.type());
            
            // 特殊处理 VARCHAR 类型加长度
            if ("VARCHAR".equalsIgnoreCase(columnAnnotation.type())) {
                colDef.append("(").append(columnAnnotation.length()).append(")");
            }

            if (!columnAnnotation.nullable()) {
                colDef.append(" NOT NULL");
            }

            columnDefs.add(colDef.toString());

            if (columnAnnotation.primaryKey()) {
                primaryKeys.add(columnAnnotation.name());
            }
        }

        if (columnDefs.isEmpty()) {
            throw new IllegalStateException(
                "类 " + entityClass.getSimpleName() + " 没有任何 @Column 字段"
            );
        }

        // 3. 组装 SQL
        StringBuilder sql = new StringBuilder();
        sql.append("CREATE TABLE ").append(tableName).append(" (\n");
        sql.append(String.join(",\n", columnDefs));

        if (!primaryKeys.isEmpty()) {
            sql.append(",\n    PRIMARY KEY (")
               .append(String.join(", ", primaryKeys))
               .append(")");
        }

        sql.append("\n);");
        return sql.toString();
    }
}

测试主类

public class ORMDemo {
    public static void main(String[] args) {
        System.out.println("========== 飞翔科技 ORM 建表SQL生成 ==========\n");

        // 生成 Student 表的建表 SQL
        String studentTableSQL = SimpleORM.generateCreateTableSQL(Student.class);
        System.out.println("【Student 表建表SQL】");
        System.out.println(studentTableSQL);

        // 反射查看 Student 类上的注解
        System.out.println("\n【反射验证——Student 类注解信息】");
        Table table = Student.class.getAnnotation(Table.class);
        System.out.println("表名: " + table.value());

        for (java.lang.reflect.Field field : Student.class.getDeclaredFields()) {
            Column col = field.getAnnotation(Column.class);
            if (col != null) {
                System.out.printf("字段:%s → 列名:%s,类型:%s,长度:%d,可空:%s,主键:%s%n",
                    field.getName(),
                    col.name(),
                    col.type(),
                    col.length(),
                    col.nullable(),
                    col.primaryKey()
                );
            }
        }
    }
}
运行输出:
========== 飞翔科技 ORM 建表SQL生成 ==========

【Student 表建表SQL】
CREATE TABLE students (
    id BIGINT NOT NULL,
    name VARCHAR(50) NOT NULL,
    age INT,
    major VARCHAR(100),
    email VARCHAR(200),
    PRIMARY KEY (id)
);

【反射验证——Student 类注解信息】
表名: students
字段:id → 列名:id,类型:BIGINT,长度:20,可空:false,主键:true
字段:name → 列名:name,类型:VARCHAR,长度:50,可空:false,主键:false
字段:age → 列名:age,类型:INT,长度:3,可空:true,主键:false
字段:major → 列名:major,类型:VARCHAR,长度:100,可空:true,主键:false
字段:email → 列名:email,类型:VARCHAR,长度:200,可空:true,主键:false

完整示例二:@TestMethod 测试框架模拟

场景描述

小崔希望编写一个简易的单元测试框架,能够自动发现和运行带有 @TestMethod 标记的方法,并统计通过/失败的测试数量。

注解定义

import java.lang.annotation.*;

/**
 * 测试方法标记注解
 * value 成员:优先级(数字越小优先级越高),默认 5
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TestMethod {
    int value() default 5;   // 优先级(1最高,9最低)
}

测试运行器

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * 简易测试运行器 —— 扫描并执行 @TestMethod 标记的方法
 */
public class TestRunner {

    private int passed = 0;
    private int failed = 0;
    private final List<String> failures = new ArrayList<>();

    /**
     * 运行指定对象的测试方法
     */
    public void runTests(Object testInstance) {
        Class<?> clazz = testInstance.getClass();
        System.out.println("=== 开始运行测试:" + clazz.getSimpleName() + " ===\n");

        // 1. 收集所有带 @TestMethod 的方法
        List<Method> testMethods = new ArrayList<>();
        for (Method method : clazz.getDeclaredMethods()) {
            TestMethod testAnnotation = method.getAnnotation(TestMethod.class);
            if (testAnnotation != null) {
                testMethods.add(method);
            }
        }

        // 2. 按优先级排序(value 越小越优先)
        testMethods.sort(Comparator.comparingInt(m -> m.getAnnotation(TestMethod.class).value()));

        // 3. 逐个执行
        for (Method method : testMethods) {
            TestMethod annotation = method.getAnnotation(TestMethod.class);
            System.out.printf("[优先级 %d] 执行测试:%s ... ", annotation.value(), method.getName());
            
            try {
                method.setAccessible(true);
                method.invoke(testInstance);
                passed++;
                System.out.println("通过");
            } catch (InvocationTargetException e) {
                failed++;
                String reason = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
                failures.add(method.getName() + " —— " + reason);
                System.out.println("失败!原因:" + reason);
            } catch (Exception e) {
                failed++;
                failures.add(method.getName() + " —— " + e.getMessage());
                System.out.println("错误:" + e.getMessage());
            }
        }

        // 4. 输出统计
        System.out.println("\n=== 测试汇总 ===");
        System.out.printf("通过:%d | 失败:%d | 总计:%d%n", passed, failed, passed + failed);
        if (!failures.isEmpty()) {
            System.out.println("\n失败详情:");
            failures.forEach(f -> System.out.println("  ✗ " + f));
        }
    }

    public int getPassed() { return passed; }
    public int getFailed() { return failed; }
}

测试用例类

/**
 * 学生服务测试用例
 */
public class StudentServiceTest {

    @TestMethod(1)  // 最高优先级
    public void testSaveStudent() {
        System.out.println("(模拟) 保存学生到数据库...");
        // 模拟成功
    }

    @TestMethod(2)
    public void testFindById() {
        System.out.println("(模拟) 按 ID 查询学生...");
        // 模拟成功
    }

    @TestMethod(3)
    public void testDeleteStudent() {
        System.out.println("(模拟) 删除学生...");
        throw new RuntimeException("删除接口尚未实现!");  // 模拟失败
    }

    @TestMethod(5)  // 默认优先级
    public void testEnrollStudent() {
        System.out.println("(模拟) 学生入学流程...");
        // 模拟成功
    }

    @TestMethod(4)
    public void testExportReport() {
        System.out.println("(模拟) 导出学生报表...");
        throw new AssertionError("报表格式不正确");  // 模拟失败
    }
}

运行入口

public class TestFrameworkDemo {
    public static void main(String[] args) {
        TestRunner runner = new TestRunner();
        runner.runTests(new StudentServiceTest());
    }
}
运行输出:
=== 开始运行测试:StudentServiceTest ===

[优先级 1] 执行测试:testSaveStudent ... (模拟) 保存学生到数据库...
通过
[优先级 2] 执行测试:testFindById ... (模拟) 按 ID 查询学生...
通过
[优先级 3] 执行测试:testDeleteStudent ... (模拟) 删除学生...
失败!原因:删除接口尚未实现!
[优先级 4] 执行测试:testExportReport ... (模拟) 导出学生报表...
失败!原因:报表格式不正确
[优先级 5] 执行测试:testEnrollStudent ... (模拟) 学生入学流程...
通过

=== 测试汇总 ===
通过:3 | 失败:2 | 总计:5

失败详情:
  ✗ testDeleteStudent —— 删除接口尚未实现!
  ✗ testExportReport —— 报表格式不正确

深度原理:注解成员的存储与动态代理

关键机制:

  1. 注解的存储:注解的使用信息(成员名-值映射)被写入 .class 文件属性表中的 RuntimeVisibleAnnotations 条目
  2. 动态代理:当调用 getAnnotation() 时,JVM 不会去加载或实例化注解接口本身,而是创建一个 java.lang.reflect.Proxy 动态代理对象,该对象实现了注解接口并持有成员值
  3. 成员访问:通过代理对象调用 value() 等成员方法时,代理返回编译时写入的值

可以用如下代码验证:

// 验证注解实例是动态代理对象
Table table = Student.class.getAnnotation(Table.class);
System.out.println(table.getClass().getName());
// 输出类似:com.sun.proxy.$Proxy1

易错场景

反例一:注解成员类型使用了不允许的类型

小崔想给 @Column 加一个"校验规则"列表,自然地想用 List<String>:

// ❌ 错误:注解成员不能用集合类型
public @interface Column {
    String name();
    String type() default "VARCHAR";
    List<String> validations();  // 编译错误!Invalid type for annotation member
}

编译报错:

错误: 注解成员类型无效; 应为 '原始类型', 'String', 'Class', 枚举, 注解
或以上类型的数组

纠正:使用一维数组替代:

// ✅ 正确:用数组代替集合
public @interface Column {
    String name();
    String type() default "VARCHAR";
    String[] validations() default {};  // 字符串数组
}

// 使用:花括号括起来的数组
@Column(name = "email", validations = {"email", "notBlank", "maxLength:200"})

反例二:value 语法糖使用不当

小崔在自定义注解中同时定义了 value 和其他有默认值的成员,但使用时企图省略 value = 却失败了:

// 注解定义
public @interface Task {
    String value();             // 任务名称
    String assignee() default "小崔";  // 负责人
}

// ❌ 错误:同时给 value 和 assignee 赋值时,不能省略 value =
@Task("学生报表", assignee = "白歌")  // 编译错误!

编译器报错原因是:当有多个成员需要赋值时,value 语法糖失效。

纠正:两种情况分别处理:

// ✅ 方式一:仅给 value 赋值,可省略
@Task("学生报表")

// ✅ 方式二:给 value 和其他成员同时赋值,value 必须写全
@Task(value = "学生报表", assignee = "白歌")

反例三:注解成员有默认值但试图使用 null

小崔将 @Column 中的 type() 默认值设为 ""(空字符串),想通过判断空字符串来代表"不指定"。但代码审查时白歌指出更好的做法:

// ❌ 不良实践:用空字符串作为"未指定"的标记
public @interface Column {
    String name();
    String type() default "";   // 语义不清
}

// ✅ 推荐:使用更明确的默认值
public @interface Column {
    String name();
    String type() default "VARCHAR";  // 清晰表达默认类型
}

设计原则:注解成员的默认值应当具有明确的业务语义,避免使用空字符串或特殊魔法值代表"未指定"。


面试考点

Q1:自定义注解和接口有什么区别?

① 关键字不同:注解用 @interface,接口用 interface;② 注解自动继承 java.lang.annotation.Annotation,接口不自动继承;③ 注解成员只能用特定的几种类型(基本类型、String、Class、枚举、注解及其数组),接口方法无此限制;④ 注解成员可以有默认值(default),接口方法在 JDK 8 之前不能,JDK 8 引入 default 方法后才支持;⑤ 注解不能被实例化或实现,接口可以被类实现。

Q2:注解的 value() 有什么特殊之处?为什么可以省略成员名?

value() 是注解中的约定俗成的特殊成员名。Java 语言规范(JLS §9.7.3)明确规定:如果在注解使用时只提供一个元素值且该值对应的成员名为 value,则可以省略 value =。这是编译器层面的语法糖,而非 JVM 级别的特性。当注解中有多个成员需要赋值时,就不能省略。

Q3:注解成员为什么不能用包装类型(如 Integer)而只能用基本类型(int)?

这主要是设计上的简洁性考量。注解数据必须紧凑地存储在 .class 文件的常量池中,基本类型和 String 都有直接对应的常量池条目(CONSTANT_Integer_info、CONSTANT_String_info 等),而包装类型的自动装箱/拆箱会增加编译器和运行时的复杂度。此外,基本类型的默认值语义清晰(0、false),而包装类型 null 可能带来更多的歧义。

Q4:标记注解(Marker Annotation)有什么实际用途?

标记注解虽然不携带数据,但可用于"打标签"——在运行时通过反射检测其存在与否来决定程序行为。例如 JUnit 4 的 @Test 就是标记注解,测试运行器扫描到带有该标记的方法即视为测试用例。Spring 的 @Repository 本身不携带数据,但 Spring 容器检测到它会自动进行数据访问异常翻译。标记注解是一种轻量级的"类型开关"。

Q5:如何在运行时判断一个方法上是否存在某个注解?

使用反射 API:Method.isAnnotationPresent(MyAnnotation.class) 判断是否存在;或 Method.getAnnotation(MyAnnotation.class) 获取注解实例(不存在返回 null)。注意,这要求注解的 @Retention 必须是 RUNTIME,否则反射无法读取。


小结

知识要点核心内容
定义语法public @interface 注解名 { 成员声明; }
成员类型基本类型、String、Class、枚举、注解、及上述类型的数组
默认值使用 default 关键字,有默认值的成员在使用时可省略
value()特殊成员,仅给 value 赋值时省略 value =
标记注解无成员注解,仅作为类型标记存在
反射读取通过 getAnnotation() / isAnnotationPresent() 读取
底层实现运行时通过动态代理(java.lang.reflect.Proxy)返回注解实例

自定义注解让开发者能够在 Java 类型系统中表达自己的领域语义。但要真正控制注解在什么位置可用、保留到哪个阶段、是否能继承——这就需要元注解的加持了。

上一页
元注解详解