注解

面向对象程序设计(Java)

掌握元数据编程,提升代码的表达力和可维护性

注解的由来:需求驱动

在理解注解之前,让我们先看看在没有注解的时代,开发者遇到了什么痛点。

😫 传统开发的痛点

1. 大量重复的样板代码

// JavaBean时代:每个属性都要写getter/setter
public class User {
    private String name;
    private String email;
    private int age;

    // 大量重复的样板代码
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    // 还要重写equals、hashCode、toString...
}

2. 配置与代码分离的烦恼

// JDBC配置时代:配置写在单独的properties文件中
// config.properties 文件内容:
/** 
 db.driver=com.mysql.jdbc.Driver
 db.url=jdbc:mysql://localhost:3306/mydb
 db.username=root
 db.password=123456
**/

// Java代码中读取配置
Properties prop = new Properties();
prop.load(new FileInputStream("config.properties"));
String driver = prop.getProperty("db.driver");
String url = prop.getProperty("db.url");

// 问题:配置和代码分离,修改配置要打开另一个文件
// 容易写错属性名,IDE没有提示,编译器也检查不出错误

3. 编译时错误难以发现

// 父类
class Animal {
    public void makeSound() { System.out.println("动物叫声"); }
}

// 子类:方法名拼写错误,编译器不会提示!
class Dog extends Animal {
    public void makesound() { // ❌ 拼写错误,没有真正重写
        System.out.println("汪汪");
    }
}

这些痛点让开发者苦不堪言,迫切需要更好的解决方案。

注解的需求与解决方案

💡 "要是能这样就好了"的需求

  • 要是能自动生成getter/setter就好了 → @Data
  • 要是配置能直接写在代码上就好了 → @Component
  • 要是编译器能帮我检查重写就好了 → @Override

注解概述

注解是Java 5引入的一种元数据机制,它为代码提供额外的信息。

注解本身不影响代码的执行逻辑,但可以被编译器、开发工具或运行时环境读取和处理。

🔍 什么是元数据?

元数据就是"描述数据的数据"。理解它需要从生活中的例子入手:

// 生活中的例子:照片
// 数据:照片本身(像素组成的图像)
// 元数据:拍摄时间、地点、相机型号、光圈大小...

// 生活中的例子:图书
// 数据:书的内容(文字、图片)
// 元数据:书名、作者、ISBN号、出版日期...
  
// Java代码中:
// 数据:变量、方法、类本身的代码
// 元数据:注解(描述这些代码的额外信息)

🔍 注解长啥样?

注解是一种特殊的语法结构,以 @ 开头,用于给代码"贴标签"。它就像给照片写备注一样,本身不改变照片内容,但提供了有用的背景信息。

@Override  // 注解:告诉编译器这个方法是重写父类的
public void makeSound() {
    System.out.println("汪汪");
}

@SuppressWarnings("unchecked")  // 注解:告诉编译器忽略类型警告
List list = new ArrayList();

核心特点:注解只是"备注",不影响代码执行,但会被编译器、框架或运行时环境读取并据此做处理。

注解是一种特殊的接口,为代码提供声明式信息。

内置注解

Java提供了一些常用的内置注解,每个都有特定的用途和意义。

🔧 @Override

  • 作用:标记方法是重写父类的方法
  • 检查:编译器会验证该方法确实重写了父类方法
  • 好处:避免因方法名拼写错误导致的"重写失败"

⚠️ @Deprecated

  • 作用:标记过时的方法或类
  • 警告:使用时编译器会产生警告
  • 建议:提示开发者使用新的替代方案

🎯 @FunctionalInterface

  • 作用:标记函数式接口(Java 8+)
  • 要求:接口只能有一个抽象方法
  • 检查:编译器会验证接口是否符合函数式接口规范

这些内置注解为开发提供了重要的编译时检查和代码提示,能够有效避免常见错误。

简单注解的使用

让我们通过具体代码来理解内置注解的使用方法和作用。

🔧 @Override 示例

class Animal {
    public void makeSound() {
        System.out.println("动物发出声音");
    }
}

class Dog extends Animal {
    @Override  // 编译器会检查是否正确重写
    public void makeSound() {
        System.out.println("汪汪汪");
    }

    // 错误示例:方法名拼写错误
    // @Override  // 编译错误!找不到父类方法
    // public void makesound() {  // 拼写错误
    //     System.out.println("汪汪汪");
    // }
}

⚠️ @Deprecated 示例

class OldAPI {
    /**
     * @deprecated 使用新方法 newMethod() 替代
     */
    @Deprecated
    public void oldMethod() {
        System.out.println("这是一个过时的方法");
    }

    public void newMethod() {
        System.out.println("这是新的方法");
    }
}

public class Main {
    public static void main(String[] args) {
        OldAPI api = new OldAPI();
        api.oldMethod();  // 编译警告:使用了过时的方法
        api.newMethod();  // 正常使用
    }
}

🎯 @FunctionalInterface 示例

@FunctionalInterface  // 只能有一个抽象方法
interface Calculator {
    int calculate(int a, int b);

    // 可以有默认方法
    default void print() {
        System.out.println("计算器接口");
    }

    // 可以有静态方法
    static void info() {
        System.out.println("这是函数式接口");
    }
}

// 使用Lambda表达式
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;

System.out.println(add.calculate(3, 5));      // 8
System.out.println(multiply.calculate(3, 5)); // 15

提问:如果我们在一个注解了 @FunctionalInterface 的接口中添加第二个抽象方法会发生什么?

自定义注解(简单版)

内置的注解肯定是远远不够的,因此我们总会想自己定义注解

📝 自定义注解的基本语法

自定义注解使用 @interface 关键字定义,语法类似于接口,但有一些特殊规则:

// 基本格式
[元注解]  // 定义注解的规则(可选但常用)
public @interface 注解名称 {
    [数据类型] 元素名称() [default 默认值];  // 注解元素(类似方法)
}

// 示例:定义一个简单的注解
public @interface MyAnnotation {
    String value();  // 元素
    int level() default 1;  // 带默认值的元素
}

🔧 注解元素的规则

规则 说明 示例
返回类型 只能是基本类型、String、Class、枚举、注解、或它们的数组 String value();
参数列表 元素声明后必须加 (),但不能有参数 int count();
默认值 可以用 default 关键字指定默认值 boolean enabled() default true;
特殊元素 元素名若为 value,使用时可省略元素名 @MyAnn("hello")

🎯 基本示例

// 定义一个简单的注解
public @interface Author {
    String name();  // 作者名称
    String version() default "1.0";  // 版本号,有默认值
}

// 使用自定义注解
@Author(name = "张三", version = "2.0")
public class Calculator {
    @Author(name = "李四")  // 使用默认版本号
    public int add(int a, int b) {
        return a + b;
    }

    @Author(name = "王五", version = "1.5")
    public int subtract(int a, int b) {
        return a - b;
    }
}

元注解简介

回顾:没有元注解的自定义注解

在上一页中,我们定义了一个简单的 @Author 注解:

public @interface Author {
    String name();
    String version() default "1.0";
}

⚠️ 没有元注解的隐患

这样定义的注解虽然能用,但功能有限,存在两个主要问题:

问题1:无法约束注解的适用范围

@Author(name = "张三")  // ✅ 合理:类需要记录作者
public class Calculator { }

@Author(name = "李四")  // ✅ 合理:方法需要记录作者
public int add(int a, int b) { return a + b; }

@Author(name = "王五")  // ❌ 不合理:成员变量只是数据,不需要单独记录作者
private int result;

public void setName(@Author(name = "赵六") String name) {
    // ❌ 不合理:参数只是输入数据,给它贴作者标签没有意义
}

public void calculate() {
    @Author(name = "孙七") int temp = 0;
    // ❌ 不合理:局部变量临时使用,不需要记录作者
}

问题2:无法控制注解的生命周期

@Author(name = "张三")
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

// 在程序运行时,我们想读取 @Author 注解的信息
Calculator calc = new Calculator();
Author author = calc.getClass().getAnnotation(Author.class);
// ❌ 返回 null!因为默认生命周期只在源码阶段,运行时已不存在

💡 核心结论

不使用元注解的注解:

  • 📍 可以用在任何地方:类、方法、变量、参数、局部变量... 甚至不恰当的地方
  • ⏱️ 只在源码阶段存在:编译后字节码中会被丢弃,运行时无法读取

因此,为了让注解更强大、更可控,我们需要使用元注解为自定义注解添加规则!


🚀 元注解的作用

Java设计者意识到需要一种方式来描述注解本身的规则,于是诞生了元注解:

  • 要是能指定注解的使用范围就好了 → @Target
  • 要是能控制注解的生命周期就好了 → @Retention
  • 要是能决定子类是否继承注解就好了 → @Inherited
  • 要是能在文档中显示注解就好了 → @Documented

🎯 为什么是这四个元注解?

这四个元注解覆盖了注解最核心的四个维度:

维度 元注解 解决的问题
空间维度 @Target 注解可以用在哪些程序元素上
时间维度 @Retention 注解在哪个阶段有效
继承维度 @Inherited 注解是否可以被子类继承
文档维度 @Documented 注解是否包含在API文档中

🔧 这四个维度是必要且充分的

Java设计者经过精心考虑,选择了这四个核心维度:

  • @Target和@Retention是最基础的:定义了注解的边界和生命周期
  • @Inherited是面向对象的自然需求:继承是Java的核心特性
  • @Documented是实用性的体现:良好的文档是API设计的重要部分
  • 不多不少,恰到好处:既满足了主要需求,又不过度复杂

元注解:@Target

🎯 作用:指定注解的使用目标

@Target 决定了你的注解可以放在什么位置(类、方法、字段等)。如果未指定,默认可以用在任何地方。

常用 ElementType 枚举值

  • TYPE:类、接口、枚举
  • FIELD:字段(成员变量)
  • METHOD:方法
  • PARAMETER:参数
  • CONSTRUCTOR:构造方法
  • LOCAL_VARIABLE:局部变量

代码示例

@Target(ElementType.METHOD)  // 1. 只能用于方法
@interface MyMethodAnnotation {}

@Target({
    ElementType.TYPE,     // 2. 既可用于类
    ElementType.FIELD     //    也可用于字段
})
@interface MyMixedAnnotation {}

// ✅ 正确用法
@MyMixedAnnotation  // 允许:TYPE
public class TestClass {
    @MyMixedAnnotation  // 允许:FIELD
    private String name;

    @MyMethodAnnotation  // 允许:METHOD
    public void test() { }
}

// ❌ 错误用法(编译器会报错)
@MyMethodAnnotation  // ❌ 错误!只能用于方法,不能用于类
public class ErrorClass1 { }

public class ErrorClass2 {
    @MyMethodAnnotation  // ❌ 错误!只能用于方法,不能用于字段
    private String name;
}

就像是给注解贴上“专用标签”,防止用户把给方法用的注解错误地贴到了类头上。

元注解:@Retention

⏰ 作用:指定注解的生命周期

@Retention 决定了注解在什么阶段被丢弃。这是最重要的元注解,因为它决定了反射能否读取到注解。

策略 (RetentionPolicy) 存活时间 典型应用场景
SOURCE 源文件 编译器检查(如 @Override),编译后即丢弃
CLASS (默认) class文件 字节码处理工具(如 Lombok 编译时生成代码),JVM 加载时丢弃
RUNTIME 运行时 最常用。程序运行时可通过反射读取注解信息

代码示例:不同 RetentionPolicy 的对比

@Retention(RetentionPolicy.SOURCE)
@interface SourceAnnotation {}

@Retention(RetentionPolicy.CLASS)  // 默认值
@interface ClassAnnotation {}

@Retention(RetentionPolicy.RUNTIME)
@interface RuntimeAnnotation {}

public class TestClass {
    @SourceAnnotation
    public void method1() { }

    @ClassAnnotation
    public void method2() { }

    @RuntimeAnnotation
    public void method3() { }
}

// 运行时通过反射读取注解
public class TestReflection {
    public static void main(String[] args) throws Exception {
        Method m1 = TestClass.class.getMethod("method1");
        Method m2 = TestClass.class.getMethod("method2");
        Method m3 = TestClass.class.getMethod("method3");

        System.out.println("method1 @SourceAnnotation: " +
            (m1.getAnnotation(SourceAnnotation.class) != null));  // false
        System.out.println("method2 @ClassAnnotation: " +
            (m2.getAnnotation(ClassAnnotation.class) != null));    // false
        System.out.println("method3 @RuntimeAnnotation: " +
            (m3.getAnnotation(RuntimeAnnotation.class) != null)); // true
    }
}

// 输出结果:
// method1 @SourceAnnotation: false  (源码阶段就被丢弃)
// method2 @ClassAnnotation: false    (JVM加载时被丢弃)
// method3 @RuntimeAnnotation: true    (运行时仍然存在)

如果你希望在程序运行时通过反射获取注解,必须显式指定为 RUNTIME,否则默认是 CLASS,反射是读不到的!

元注解:@Documented

📖 作用:将注解包含在 JavaDoc 中

默认情况下,生成的 JavaDoc 文档是不包含注解信息的。如果你希望你的注解像 @Deprecated 一样出现在 API 文档中,就需要加上它。

拓展:什么是 JavaDoc?

JavaDoc 是 Java 官方提供的文档生成工具。它会读取代码中的注释,自动生成 HTML 格式的 API 文档,方便开发者查阅。

使用方法:

// 1. 在代码中编写 JavaDoc 注释
/**
 * 用户服务类,提供用户注册、登录等功能
 * 
 * @author 张三
 * @version 1.0
 */
public class UserService {
    
    /**
     * 注册新用户
     * 
     * @param username 用户名,长度为 3-20 个字符
     * @param password 密码,长度为 6-20 个字符
     * @return 注册成功返回 true,失败返回 false
     * @throws IllegalArgumentException 如果用户名或密码不符合要求
     */
    public boolean register(String username, String password) {
        return true;
    }
}

2. 生成 JavaDoc:

方式一:命令行生成

# 进入有UserService.java 的目录,命令行生成 JavaDoc
javadoc -d docs -encoding UTF-8 -charset UTF-8 UserService.java

# 生成后会创建 docs 目录,里面是 HTML 文件
# 打开 docs/index.html 即可查看生成的文档

方式二:使用 Maven 生成

<!-- 在 pom.xml 中添加 maven-javadoc-plugin 插件 -->
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-javadoc-plugin</artifactId>
            <version>3.6.0</version>
            <configuration>
                <!-- 指定文档编码 -->
                <encoding>UTF-8</encoding>
                <!-- 指定字符集 -->
                <charset>UTF-8</charset>
                <!-- 指定文档输出目录 -->
                <outputDirectory>docs</outputDirectory>
            </configuration>
        </plugin>
    </plugins>
</build>

# 运行 Maven 命令生成 JavaDoc
mvn javadoc:javadoc

# 或者在打包时同时生成 JavaDoc
mvn package javadoc:javadoc

3. 生成的文档效果:

┌─────────────────────────────────────────┐
│  UserService                          │
│  用户服务类,提供用户注册、登录等功能  │
│  作者: 张三  版本: 1.0                │
├─────────────────────────────────────────┤
│  方法摘要                              │
│  ┌─────────────────────────────────┐   │
│  │ register                        │   │
│  │ boolean register(String username,  │
│  │                 String password) │   │
│  │ 注册新用户                       │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘

在线示例:访问 Oracle 的 Java 官方文档(如 java.util.List)就是通过 JavaDoc 生成的。

不加 @Documented vs 加上 @Documented

假设你写了一个库给别人用:

  • 不加:使用者看文档时,不知道这个类/方法被标记了注解。
  • 加上:使用者在阅读文档时,能清晰地看到该元素被标记了,这对于作为公共 API 的注解非常重要。

代码示例

@Documented  // 关键!加上这个注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ApiVersion {
    String value();
}

// 使用该注解的类
@ApiVersion("1.0")
public class UserService {
    public void register(String username) { }
}

// 运行 javadoc 生成文档后:

// 不加 @Documented 时的文档效果:
// UserService
// public class UserService
// public void register(String username)

// 加上 @Documented 时的文档效果:
// @ApiVersion("1.0")
// public class UserService
// public void register(String username)
// 
// 使用者能看到这个类标记了 @ApiVersion("1.0")

元注解:@Inherited

🧬 作用:允许子类继承父类的注解

这是一个非常有意思的规则:默认情况下,父类的注解不会被子类继承。

加上 @Inherited 后,子类会自动拥有父类的这个注解。

@Inherited // 关键!
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface DatabaseConfig { ... }

@DatabaseConfig
class BaseEntity {}

// User 自动继承了 @DatabaseConfig
class User extends BaseEntity {}

⚠️ 重要限制

  • 仅限类注解:接口继承、方法重写、字段定义都无效。
  • 接口无效:实现接口的类不会继承接口上的注解。

在 Spring 等框架中,这常用于让配置类能够传递给子类。

自定义注解(有元注解版)

📝 完整示例:定义一个 API 接口注解

假设我们在开发一个 Web 框架,需要标记哪些方法是 API 接口:

// 定义注解:使用四个元注解完善功能
@Target(ElementType.METHOD)              // 只能用在方法上
@Retention(RetentionPolicy.RUNTIME)      // 运行时需要反射读取
@Documented                              // 出现在 JavaDoc 文档中
// @Inherited                              // 这里不需要,因为@Target是METHOD
public @interface ApiEndpoint {
    
    // API 路径(必须提供)
    String path();
    
    // HTTP 请求方法(有默认值)
    String method() default "GET";
    
    // 接口描述(可选)
    String description() default "";
    
    // 需要的权限(数组类型)
    String[] permissions() default {};
}

🎯 使用注解

@ApiEndpoint(
    path = "/users",
    method = "POST",
    description = "创建新用户",
    permissions = {"user:create", "user:write"}
)
public void createUser(String username, String password) {
    // 实现
}

@ApiEndpoint(path = "/users/{id}")
public User getUserById(String id) {
    // 实现(使用默认 method = "GET")
}

⚠️ 注意事项

  • 注解元素必须有默认值,或者在使用时提供值
  • 不能有方法体,只能使用 default 关键字提供默认值
  • 数组元素赋值时使用 {} 语法
  • 如果只有一个元素且名称为 value,可以省略名称
// 简化语法:只有value元素时
@MyAnnotation("简化用法")
public void simpleMethod() {
    // 等价于:@MyAnnotation(value = "简化用法")
}

自定义注解为项目提供了强大的元数据能力,合理使用可以让代码更加清晰和易于维护。

用反射读取注解

如果只是给代码打上标记,它就像是写给程序员看的注释一样,仅仅是“锦上添花”的装饰, 并不会自动改变程序的原有行为。

但这显然不是我们的目的! 我们希望这些元数据能够发挥真正的威力。 让注解变得“强力”的关键钥匙就是 —— Java 反射机制。

通过反射,我们可以在代码中 “读取”这些标记,提取其中的配置信息(如 URL 路径、请求方法), 并据此动态地控制程序的逻辑。

现在,让我们回到刚才定义的 ApiEndpoint 注解。 它已经被“打”在了方法上,接下来,我们就编写一个 API 扫描器 (ApiScanner), 看看如何利用反射技术,把这些沉睡的标记“唤醒”!

🔍 通过反射读取注解

import java.lang.reflect.Method;

public class ApiScanner {
    public static void scan(Class clazz) {
        Method[] methods = clazz.getDeclaredMethods();
        
        for (Method method : methods) {
            ApiEndpoint endpoint = method.getAnnotation(ApiEndpoint.class);
            
            if (endpoint != null) {
                System.out.println("发现 API 接口:");
                System.out.println("  路径: " + endpoint.path());
                System.out.println("  方法: " + endpoint.method());
                System.out.println("  描述: " + endpoint.description());
                System.out.println("  权限: " + String.join(", ", endpoint.permissions()));
            }
        }
    }
    
    // 被扫描的控制器类示例
    static class UserController {
        
        @ApiEndpoint(
            path = "/users",
            method = "POST",
            description = "创建新用户",
            permissions = {"user:create", "user:write"}
        )
        public void createUser(String username, String password) {
            System.out.println("创建用户: " + username);
        }

        @ApiEndpoint(
            path = "/users/{id}",
            method = "GET",
            description = "根据ID获取用户信息"
        )
        public User getUserById(String id) {
            System.out.println("获取用户: " + id);
            return new User();
        }
        
        @ApiEndpoint(
            path = "/users/{id}",
            method = "DELETE",
            permissions = {"user:delete"}
        )
        public void deleteUser(String id) {
            System.out.println("删除用户: " + id);
        }
    }
    
    // 简单的 User 类示例
    static class User {}
    
    // 主方法:演示如何使用扫描器
    public static void main(String[] args) {
        System.out.println("=== API 接口扫描器演示 ===\n");
        
        // 扫描 UserController 类中的所有 API 接口
        ApiScanner.scan(UserController.class);
    }
}

📤 程序运行输出

=== API 接口扫描器演示 ===

发现 API 接口:
  路径: /users
  方法: POST
  描述: 创建新用户
  权限: user:create, user:write

发现 API 接口:
  路径: /users/{id}
  方法: GET
  描述: 根据ID获取用户信息
  权限: 

发现 API 接口:
  路径: /users/{id}
  方法: DELETE
  描述: 
  权限: user:delete

🔍 反射读取注解的基本方法

// 1. 获取类上的注解
Class clazz = User.class;
if (clazz.isAnnotationPresent(MyAnnotation.class)) {
    MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);
    System.out.println("类注解值: " + annotation.value());
    System.out.println("版本: " + annotation.version());
}

// 2. 获取方法上的注解
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
    if (method.isAnnotationPresent(MyAnnotation.class)) {
        MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
        System.out.println("方法 " + method.getName() + " 注解: " + annotation.value());
    }
}

// 3. 获取所有注解
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annotation : annotations) {
    System.out.println("注解类型: " + annotation.annotationType().getSimpleName());
}

🛠️ 常用的反射方法

  • isAnnotationPresent():检查是否存在指定注解
  • getAnnotation():获取指定类型的注解
  • getAnnotations():获取所有注解
  • getDeclaredAnnotations():获取直接声明的注解

反射是处理运行时注解的核心技术,需要注意的是,只有RUNTIME级别的注解才能在运行时通过反射读取。

实践:简单测试框架-1

现在我们尝试利用注解构造一个简单的测试框架,体验注解在实际开发中的强大作用。

🛠️ 第一步:定义测试注解

首先创建一个标记测试方法的注解,需要在运行时通过反射读取。

// 定义测试注解
@Retention(RetentionPolicy.RUNTIME)  // 运行时保留,允许反射读取
@Target(ElementType.METHOD)           // 只能作用于方法
@interface Test {
    String description() default "";  // 可选的测试描述
}

🏃‍♂️ 第二步:实现测试运行器

运行器通过反射扫描带有@Test注解的方法并执行它们。

public class TestRunner {
    public static void runTests(Class testClass) throws Exception {
        int passed = 0, failed = 0;

        // 获取测试类中声明的所有方法(包括私有方法)
        for (Method method : testClass.getDeclaredMethods()) {
            // 检查当前方法是否带有@Test注解
            if (method.isAnnotationPresent(Test.class)) {
                // 获取方法上的@Test注解对象,以便读取注解的属性值
                Test testAnnotation = method.getAnnotation(Test.class);
                System.out.println("运行测试: " + method.getName());
                // 如果注解中有description属性值,则输出测试描述
                if (!testAnnotation.description().isEmpty()) {
                    System.out.println("描述: " + testAnnotation.description());
                }

                try {
                    // 通过无参构造器创建测试类的实例对象
                    Object instance = testClass.getDeclaredConstructor().newInstance();
                    // 使用反射调用测试方法,instance是方法所属的对象
                    method.invoke(instance);
                    System.out.println("✅ 测试通过");
                    passed++;
                } catch (Exception e) {
                    // 捕获测试方法执行过程中的异常(如断言失败、空指针等)
                    System.out.println("❌ 测试失败: " + e.getMessage());
                    failed++;
                }
            }
        }

        System.out.println("\n测试结果: 通过 " + passed + ", 失败 " + failed);
    }
}

这个测试框架的核心思想是:通过反射扫描类中的方法,找到带有@Test注解的方法并自动执行。

实践:简单测试框架-2

📊 第三步:编写测试类

创建测试类,使用@Test注解标记需要执行的测试方法。

public class MathTests {

    @Test(description = "测试加法运算")
    public void testAddition() {
        int result = 2 + 3;
        assert result == 5 : "2 + 3 应该等于 5";
    }

    @Test(description = "测试除法运算")
    public void testDivision() {
        int result = 10 / 2;
        assert result == 5 : "10 / 2 应该等于 5";
    }

    @Test(description = "故意失败的测试")
    public void testFailure() {
        assert 1 == 2 : "这个测试应该失败";
    }
}

// 第四步:运行测试
public static void main(String[] args) throws Exception {
    TestRunner.runTests(MathTests.class);
}

通过反射处理注解,我们可以创建灵活、可扩展的框架和工具,实现代码的自动化处理,提升开发效率。

真实案例:Spring框架

Spring是Java生态中最流行的框架之一。下面用一个Web开发的例子展示注解如何简化代码。

🔴 没有Spring:处理HTTP请求需要写大量代码

public class UserServlet extends HttpServlet {
    private UserService userService;

    public void init() {
        userService = new UserService(new UserDao(new DataSource()));
    }

    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String idStr = req.getParameter("id");
        Long id = Long.parseLong(idStr);
        User user = userService.findById(id);

        resp.setContentType("application/json");
        resp.getWriter().write(convertToJson(user));
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String json = req.getReader().lines().collect(Collectors.joining());
        User user = parseJson(json);
        validateUser(user);
        User saved = userService.save(user);

        resp.setStatus(201);
        resp.setContentType("application/json");
        resp.getWriter().write(convertToJson(saved));
    }
}

🟢 有Spring:只需要加几个注解

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }

    @PostMapping
    public User createUser(@RequestBody User user) {
        return userService.save(user);
    }
}

💡 对比一下

任务 没有Spring 有Spring
创建对象 手动new,层层传递依赖 @Autowired 自动注入
处理GET请求 手动解析参数、转JSON @GetMapping 自动处理
处理POST请求 读请求体、解析、验证 @PostMapping + @RequestBody
设置响应 手动设置状态码、Content-Type 自动处理

🔍 @GetMapping到底是怎么工作的?

用大白话来说,Spring框架处理@GetMapping的步骤其实和我们前面写的TestRunner很像:

  1. 启动时扫描:Spring程序启动时,扫描所有类,找到带@RestController注解的控制器类
  2. 查找方法:遍历控制器中的每个方法,检查是否有@GetMapping("/api/users/{id}")这样的注解
  3. 建立映射:Spring在内存里建立一个"地址本":
    "GET /api/users/123" → UserController.getUser(123L)
  4. 接收请求:当浏览器发送一个GET请求到/api/users/123时
  5. 查找方法:Spring从"地址本"里找到对应的方法getUser
  6. 提取参数:从URL路径中提取id值(123),转换成Long类型
  7. 调用方法:用反射调用userService.findById(123L)
  8. 返回结果:将返回的User对象自动转换成JSON,发送给浏览器

两个例子做的事情完全一样,但Spring版本只需要几个注解,代码量减少80%以上。这就是注解的威力:用"标签"告诉框架"怎么做",让框架帮你完成繁琐的底层工作。

真实案例:Lombok注解

Lombok是一个Java库,通过注解自动生成样板代码,大大减少代码量。

🔴 没有Lombok:需要手写大量样板代码

public class User {
    private Long id;
    private String name;
    private String email;
    private LocalDateTime createdAt;
    private String internalCode;

    // 无参构造方法
    public User() {}

    // 全参构造方法
    public User(Long id, String name, String email, LocalDateTime createdAt, String internalCode) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.createdAt = createdAt;
        this.internalCode = internalCode;
    }

    // Getter方法
    public Long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public String getInternalCode() { return internalCode; }

    // Setter方法
    public void setId(Long id) { this.id = id; }
    public void setName(String name) { this.name = name; }
    public void setEmail(String email) { this.email = email; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
    public void setInternalCode(String internalCode) { this.internalCode = internalCode; }

    // toString方法
    @Override
    public String toString() {
        return "User{id=" + id + ", name='" + name + "', email='" + email + "'}";
    }

    // equals方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id) &&
               Objects.equals(name, user.name) &&
               Objects.equals(email, user.email);
    }

    // hashCode方法
    @Override
    public int hashCode() {
        return Objects.hash(id, name, email);
    }

    // Builder模式(如果需要)
    public static class Builder {
        private Long id;
        private String name;
        private String email;
        private LocalDateTime createdAt;
        private String internalCode;

        public Builder id(Long id) { this.id = id; return this; }
        public Builder name(String name) { this.name = name; return this; }
        public Builder email(String email) { this.email = email; return this; }
        public Builder createdAt(LocalDateTime createdAt) { this.createdAt = createdAt; return this; }
        public Builder internalCode(String internalCode) { this.internalCode = internalCode; return this; }

        public User build() { return new User(id, name, email, createdAt, internalCode); }
    }
}
// 总共约60行样板代码!

🟢 有Lombok:几个注解搞定

@Data            // 自动生成getter/setter/toString/equals/hashCode
@AllArgsConstructor // 自动生成全参构造方法
@NoArgsConstructor  // 自动生成无参构造方法
@Builder         // 自动生成建造者模式
public class User {
    private Long id;
    private String name;
    private String email;
    private LocalDateTime createdAt;
    private String internalCode;
}
// 只需要10行!代码量减少了80%+

🔍 @Data到底是怎么工作的?

Lombok处理@Data注解的步骤是这样的:

  1. 你写代码:你写了一个简洁的User类,只有@Data注解和几个字段
  2. 启动编译:当你运行javac编译器时,Lombok的注解处理器介入
  3. 扫描注解:Lombok扫描到@Data注解,知道"哦,这个类需要生成getter/setter等方法"
  4. 生成代码:Lombok自动把下面这些代码插入到类中:
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    // 还有toString、equals、hashCode...
  5. 输出.class:编译生成的User.class文件包含了完整的方法,就像你手写的一样
  6. 源码不变:你的User.java文件依然保持简洁,只有几行代码
  7. IDE显示:IDEA等IDE安装Lombok插件后,在编辑器里能自动提示这些生成的方法

Lombok通过注解处理器在编译期自动生成代码,让开发者专注于业务逻辑而非样板代码。一个User类原本需要50行代码,现在只需要5行。

真实案例:MyBatis注解

MyBatis是一个数据库操作框架。下面用一个例子展示注解如何简化数据库访问代码。

🔴 没有MyBatis:用JDBC操作数据库很繁琐

public User findById(Long id) throws SQLException {
    String sql = "SELECT * FROM users WHERE id = ?";
    Connection conn = DriverManager.getConnection(url, user, password);
    PreparedStatement stmt = conn.prepareStatement(sql);
    stmt.setLong(1, id);
    ResultSet rs = stmt.executeQuery();

    User user = null;
    if (rs.next()) {
        user = new User();
        user.setId(rs.getLong("id"));
        user.setName(rs.getString("name"));
        user.setEmail(rs.getString("email"));
    }
    rs.close();
    stmt.close();
    conn.close();
    return user;
}

🟢 有MyBatis:一个注解搞定

@Mapper  // MyBatis自动扫描这个接口
public interface UserMapper {

    @Select("SELECT * FROM users WHERE id = #{id}")
    @Results({
        @Result(property = "userName", column = "name"),
        @Result(property = "emailAddress", column = "email")
    })
    User findById(@Param("id") Long id);

    @Insert("INSERT INTO users(name, email) VALUES(#{name}, #{email})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);

    @Update("UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}")
    int update(User user);

    @Delete("DELETE FROM users WHERE id = #{id}")
    int deleteById(Long id);
}

� @Select到底是怎么工作的?

用大白话来说,MyBatis处理@Select注解的步骤是这样的:

  1. 启动时扫描:MyBatis程序启动时,扫描所有带@Mapper注解的接口
  2. 解析注解:扫描接口方法上的@Select("SELECT * FROM users WHERE id = #{id}")
  3. 创建代理对象:MyBatis使用动态代理技术,为接口创建一个实现类(不用你写!)
  4. 调用方法:当你调用userMapper.findById(123L)时
  5. 替换参数:MyBatis把SQL中的#{id}替换成123,得到SELECT * FROM users WHERE id = 123
  6. 执行SQL:通过JDBC执行SQL语句
  7. 映射结果:把数据库返回的每一行数据转换成User对象,字段自动对应
  8. 返回对象:返回完整的User对象给你

💡 对比一下

任务 没有MyBatis 有MyBatis
查询数据 写20行JDBC代码 @Select + 1行SQL
设置参数 手动setLong(1, id) 自动替换#{id}
映射结果 手动rs.getString、rs.getLong... 自动映射到User对象
释放资源 手动close rs、stmt、conn 自动管理

MyBatis通过注解将SQL语句直接写在Java接口上,实现了SQL与代码的统一。原本20行JDBC代码,现在只需要1个注解,代码量减少90%以上。这就是注解的威力:你只写SQL,框架帮你完成繁琐的JDBC操作。

注解总结

📈 注解的核心优势

优势 说明
简化配置 减少XML配置文件,配置与代码在一起
提高可读性 配置信息直观地显示在代码上
减少样板代码 自动生成重复性代码
类型安全 编译时检查,减少运行时错误

🎯 注解的发展趋势

  • 更多框架采用:新框架普遍以注解为主要配置方式
  • 标准化进程:Java标准库不断引入新的注解
  • 工具链完善:IDE对注解的支持越来越好
  • 性能优化:注解处理器的性能持续改进

🚀 学习建议

  • 理解基本概念:掌握注解的定义、分类和生命周期
  • 熟悉常用注解:熟练使用Java内置注解和主流框架注解
  • 学会自定义注解:根据项目需求创建合适的注解
  • 理解处理机制:掌握注解处理器的工作原理

注解已经成为现代Java开发的标准实践,掌握注解的使用对于开发高质量、可维护的Java应用程序至关重要。

1 / X