Java(五):5.0 [元编程] 反射、注解

5.0 [元编程] 反射、注解

本章将带您深入Java的“元编程”世界。元编程是指程序在运行时能够审视并操作自身结构的能力。我们将从这一切的基石——类加载机制讲起,然后深入学习实现元编程的核心技术——反射,并最终探讨其最广泛的应用——注解Junit单元测试

5.1 [基础] 类加载机制与 ClassLoader

面试题引入

“请简述一下Java的类加载过程,以及双亲委派模型。”

类的生命周期

一个.java源文件变成可以在JVM中运行的程序,其对应的.class文件需要经历一个完整的生命周期。这个过程主要分为

加载(Loading)、**链接(Linking)初始化(Initialization)**三个阶段。

  1. 加载:JVM通过类加载器(ClassLoader)找到对应的.class文件,读取其二进制数据,并在方法区中创建一个java.lang.Class对象。
  2. **链接 **:
    • 验证:确保被加载的类文件符合JVM规范,没有安全问题。
    • 准备:为类的静态变量分配内存,并设置其类型的默认值(如int为0,Objectnull)。注意,此时并非执行程序员指定的初始值。
    • 解析:将类中的符号引用(如类名、方法名)替换为直接的内存地址引用。
  3. 初始化:这是类加载的最后一步。JVM会执行类的初始化方法<clinit>()。这个方法由编译器自动收集类中所有静态变量的赋值动作和**静态代码块(static{})**中的语句合并而成。只有到这一步,静态变量才会被赋予我们代码中指定的初始值。
类加载器 (ClassLoaders) 体系

Java通过一个层级分明的类加载器体系来完成类的加载工作。主要有三类加载器:

  1. 启动类加载器 (Bootstrap ClassLoader)

    • JVM的顶层加载器,由C++实现,是JVM自身的一部分。
    • 负责加载Java最核心的库(如rt.jar里的java.lang.*java.util.*等)。
    • 在Java代码中尝试获取它的引用会返回null
  2. 扩展类加载器 (Extension ClassLoader)

    • 负责加载Java的扩展库(位于jre/lib/ext目录下)。
    • 它的父加载器是启动类加载器。
  3. 应用程序类加载器 (Application ClassLoader)

    • 也称为系统类加载器,是我们最常打交道的加载器。
    • 负责加载用户类路径(Classpath)上我们自己编写的类和第三方库的JAR包。
    • 它的父加载器是扩展类加载器。
[核心] 双亲委派模型 (Parent-Delegation Model)

这是Java类加载器设计的核心原则,也是面试中的绝对高频考点。

  • 工作流程:当一个类加载器收到加载类的请求时,它不会自己先去尝试加载,而是会首先把这个请求委派给它的父加载器去完成。每一层的加载器都是如此。只有当父加载器在自己的搜索范围内找不到指定的类,无法完成加载请求时,子加载器才会自己去尝试加载。

image-20250714170101480

  • 为何如此设计?
    1. 避免类的重复加载:通过委派机制,一个类最终只会被一个加载器加载一次,确保了该类在JVM中的唯一性。
    2. 保证核心库的安全:这是最重要的目的。它防止了Java的核心API被恶意或无意地篡改。例如,你无法自己编写一个java.lang.String类来替代系统的String类。因为当加载请求传递到最顶层的启动类加载器时,它会找到并加载JDK自带的、真正的String类,加载过程至此结束,你编写的“假”String类将永远没有机会被加载。
代码示例:获取类加载器并查看其层级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example;

public class Main {
public static void main(String[] args) {
// 1. 获取当前自定义类的加载器
ClassLoader appClassLoader = Main.class.getClassLoader();
System.out.println("应用程序类加载器 (AppClassLoader): " + appClassLoader);

// 2. 获取其父加载器 -> 扩展类加载器
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println("扩展类加载器 (ExtClassLoader): " + extClassLoader);

// 3. 获取扩展类加载器的父加载器 -> 启动类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
// 因为启动类加载器是C++实现的,无法在Java中获取其实体,所以返回null
System.out.println("启动类加载器 (BootstrapClassLoader): " + bootstrapClassLoader);

System.out.println("--- JDK核心类的加载器 ---");

// 4. 尝试获取String类的加载器
// String类由启动类加载器加载,因此在Java层面获取不到,返回null
ClassLoader stringClassLoader = String.class.getClassLoader();
System.out.println("String.class的加载器: " + stringClassLoader);
}
}

输出结果:

1
2
3
4
5
应用程序类加载器 (AppClassLoader): sun.misc.Launcher$AppClassLoader@18b4aac2
扩展类加载器 (ExtClassLoader): sun.misc.Launcher$ExtClassLoader@1b6d3586
启动类加载器 (BootstrapClassLoader): null
--- JDK核心类的加载器 ---
String.class的加载器: null

这个输出完美地验证了类加载器的层级关系和启动类加载器的特殊性。


5.2 [核心] 反射:运行时动态操控的艺术

在了解了Java代码如何被加载到JVM中之后,我们现在来学习一个Java中非常强大、也是所有主流框架(如Spring,
MyBatis)基石的特性——反射

5.2.1 什么是反射及其应用场景

面试题引入

“什么是反射?它有哪些优缺点和应用场景?”

核心概念

反射(Reflection)是Java语言提供的一种在运行时,动态地、间接地检查、分析和操作自身结构与行为的能力。

我们可以用一个比喻来理解:

  • 常规编程:就像我们拿到一本说明书(类的代码),我们严格按照说明书上的指示(方法调用)来操作一个设备(对象)。我们在写代码的时候,就知道这个设备有什么按钮,每个按钮叫什么。
  • 反射编程:就像我们没有说明书,只有一个密封的黑盒设备。但是我们拿到了一套“万能检测和操控工具”(即反射API)。通过这套工具,我们可以在程序运行时去探测这个黑盒:它有哪些按钮(方法)?有哪些内部零件(字段)?它的型号是什么(类名)?甚至,我们可以强行按下那些没有在外部暴露的内部按钮(调用私有方法)。
优缺点
  • 优点

    • 动态性与灵活性:这是反射最大的优点。它允许我们编写非常通用的代码,可以操作在编译时完全未知的类。所有主流框架的依赖注入(DI)、AOP等核心功能,都深度依赖反射。
  • 缺点

    1. 性能开销:反射操作(如方法查找)比直接代码调用要慢得多,因为它涉及更多的查找和检查步骤,并且绕过了JIT编译器的许多优化。因此,在性能敏感的核心路径上应避免使用。
    2. 破坏封装:通过setAccessible(true)可以访问和修改类的私有成员,这违背了面向对象的封装原则。
    3. 类型不安全:编译器无法对反射代码进行类型检查,可能将潜在的ClassCastException等错误从编译期推迟到运行时。
应用场景
  • 框架开发:Spring的IoC/DI容器通过反射动态创建和注入Bean。
  • 动态代理:在运行时为一个或多个接口动态地生成实现类。
  • 注解处理:在运行时读取注解信息并执行相应逻辑。
  • 单元测试:Junit等测试框架通过反射查找并执行被@Test注解的方法。

5.2.2 反射的基石:java.lang.Class 对象

要对一个类进行反射操作,第一步永远是获取代表这个类的java.lang.Class对象。它是反射所有操作的入口。

获取Class对象的三种主要方式
  1. 通过类名获取ClassName.class
    • 最简单、最安全的方式,在编译时就会受到检查。
  2. 通过对象实例获取object.getClass()
    • 当你已经拥有一个该类的对象时使用。
  3. 通过类的全限定名获取Class.forName("com.example.MyClass")
    • 最动态的方式,可以在运行时根据一个字符串来加载任意类。常用于框架加载配置文件中指定的类。
代码示例:获取Class对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example;

class User {}

public class Main {
public static void main(String[] args) throws ClassNotFoundException {
// 方式一: .class 语法
Class<User> clazz1 = User.class;

// 方式二: object.getClass()
User user = new User();
Class<?> clazz2 = user.getClass();

// 方式三: Class.forName()
Class<?> clazz3 = Class.forName("com.example.User");

// 三种方式获取到的都是同一个Class对象实例
System.out.println("clazz1 == clazz2 : " + (clazz1 == clazz2)); // true
System.out.println("clazz1 == clazz3 : " + (clazz1 == clazz3)); // true
}
}

5.2.3 通过反射操作类的成员

获取到Class对象后,我们就可以像操作说明书一样,获取并操作它的所有部分。

1. 操作构造器 (Constructor)
  • 核心APIgetConstructors(), getConstructor(...),
    getDeclaredConstructors(), getDeclaredConstructor(...),
    newInstance(...)
  • getDeclared... vs.
    get...
    :带有Declared字样的方法可以获取到所有(包括private)的成员;不带的只能获取public成员。此规则对方法和字段同样适用。
代码示例:调用不同的构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.example;

import java.lang.reflect.Constructor;

class Student {
private String name;
private int age;

public Student() {
this.name = "默认学生";
}

public Student(String name, int age) {
this.name = name;
this.age = age;
}

private Student(String name) { // 私有构造器
this.name = name;
}

@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
}

public class Main {
public static void main(String[] args) throws Exception {
Class<Student> clazz = Student.class;

// 1. 调用公共无参构造器
Constructor<Student> c1 = clazz.getConstructor();
Student s1 = c1.newInstance();
System.out.println("调用无参构造器: " + s1);

// 2. 调用公共有参构造器
Constructor<Student> c2 = clazz.getConstructor(String.class, int.class);
Student s2 = c2.newInstance("Alice", 18);
System.out.println("调用有参构造器: " + s2);

// 3. 调用私有构造器(暴力反射)
Constructor<Student> c3 = clazz.getDeclaredConstructor(String.class);
c3.setAccessible(true); // 必须设置可访问,否则会抛出IllegalAccessException
Student s3 = c3.newInstance("Bob");
System.out.println("调用私有构造器: " + s3);
}
}
2. 操作方法 (Method)
  • 核心API
  • getMethods():返回类中所有公共(public)方法的数组,包括继承的方法。
  • getMethod(…):返回类中指定的公共方法,参数为方法名和参数类型类对象数组。
  • getDeclaredMethods():返回类中声明的所有方法的数组,包括私有(private)、保护(protected)和默认(default)访问权限的方法,但不包括继承的方法。
  • getDeclaredMethod(…):返回类中声明的指定方法,参数为方法名和参数类型类对象数组。
  • invoke(…):用于调用对象的指定方法,参数为对象实例、方法对象和方法的参数数组。
代码示例:调用各种方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example;

import java.lang.reflect.Method;

class Calculator {
public int add(int a, int b) {
return a + b;
}
private void showInfo(String message) {
System.out.println("私有方法被调用: " + message);
}
public static void printVersion() {
System.out.println("Calculator Version 1.0");
}
}

public class Main {
public static void main(String[] args) throws Exception {
Class<Calculator> clazz = Calculator.class;
// 注意,我们默认没有指定构造器的修饰符,那么他就是default的,必须通过Declared来获取
Calculator calculator = clazz.getDeclaredConstructor().newInstance();

// 1. 调用公共方法
Method addMethod = clazz.getMethod("add", int.class, int.class);
Object result = addMethod.invoke(calculator, 10, 20);
System.out.println("调用add(10, 20)的结果: " + result);

// 2. 调用私有方法
Method showInfoMethod = clazz.getDeclaredMethod("showInfo", String.class);
// 调用私有方法时必须授权
showInfoMethod.setAccessible(true);
showInfoMethod.invoke(calculator, "Hello Reflection");

// 3. 调用静态方法
Method printVersionMethod = clazz.getMethod("printVersion");
// 调用静态方法时,第一个参数(对象实例)传入null即可
printVersionMethod.invoke(null);
}
}
3. 操作字段 (Field)
  • 核心API
  • getFields():返回类中所有公共(public)字段的数组,包括继承的字段。
  • getField(…):返回类中指定的公共字段,参数为字段名。
  • getDeclaredFields():返回类中声明的所有字段的数组,包括私有(private)、保护(protected)和默认(default)访问权限的字段,但不包括继承的字段。
  • getDeclaredField(…):返回类中声明的指定字段,参数为字段名。
  • get(…):用于获取对象指定字段的值,参数为对象实例和字段对象。
  • set(…):用于设置对象指定字段的值,参数为对象实例、字段对象和要设置的值。
代码示例:读写字段值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example;

import java.lang.reflect.Field;

class Person {
public String name;
private int age = 20;
}

public class Main {
public static void main(String[] args) throws Exception {
Class<Person> clazz = Person.class;
Person person = clazz.getDeclaredConstructor().newInstance();

// 1. 操作公共字段
Field nameField = clazz.getField("name");
nameField.set(person, "张三");
System.out.println("获取公共字段name: " + nameField.get(person));

// 2. 操作私有字段
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true);
ageField.set(person, 30);
System.out.println("获取私有字段age: " + ageField.get(person));
}
}

5.2.4 [实战] 反射的应用:迷你Spring框架

场景:编写一个简单的框架,它可以根据一个app.properties配置文件,动态地创建并执行指定的对象和方法。

1. 创建 app.properties 文件 (放在srcresources目录下)

1
2
className=com.example.UserService
methodName=login

2. 创建业务类

1
2
3
4
5
6
7
8
9
10
11
package com.example;

// 这是一个被框架调用的业务类
class UserService {
public void login() {
System.out.println("用户服务:正在执行登录逻辑...");
}
public void register() {
System.out.println("用户服务:正在执行注册逻辑...");
}
}

3. 编写框架主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Properties;

// 这是一个被框架调用的业务类
class UserService {
public void login() {
System.out.println("用户服务:正在执行登录逻辑...");
}

public void register() {
System.out.println("用户服务:正在执行注册逻辑...");
}
}

public class Main {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
InputStream in = Main.class.getClassLoader().getResourceAsStream("app.properties");
props.load(in);
// 2. 从配置中获取类名和方法名
String className = props.getProperty("className");
String methodName = props.getProperty("methodName");

// 3. 使用反射动态执行
// a. 加载类
Class<?> clazz = Class.forName(className);
// b. 创建对象
Object instance = clazz.getDeclaredConstructor().newInstance();
// c. 获取方法
Method method = clazz.getMethod(methodName);
// d. 调用方法
method.invoke(instance);
}
}

这个简单的例子,就揭示了Spring等现代框架实现其强大动态能力的核心原理。


5.3 [应用] 注解:为代码嵌入元数据

注解是Java中一种强大的元编程工具,它允许我们在不改变代码本身逻辑的前提下,为类、方法、字段等程序元素添加“标签”或“元数据”。这些元数据可以被编译器或运行时环境读取,从而实现各种自动化、配置化和框架化的功能。

5.3.1 注解的核心思想

面试题引入

“注解(Annotation)是什么?它和注释(Comment)有什么本质区别?”

注解 vs. 注释
  • 注释 (//, /*...*/):
    是写给程序员看的,用于解释代码,提高可读性。编译器会完全忽略注释。
  • 注解 (@...):
    是写给**程序(编译器、框架、工具)**看的。它是一种元数据,程序可以根据这个元数据来决定不同的处理方式。
注解的重要性:现代框架的基石

理解现代Java框架的实现原理,有一个公认的公式:框架 = 反射 + 注解 +
设计模式
。Spring的依赖注入、MyBatis的SQL映射、Junit的单元测试,其核心都是通过反射来查找并处理开发者定义的注解,从而实现自动化配置和功能的。

5.3.2 Java 内置注解

Java预置了一些非常重要的注解,用于辅助编译器进行检查。

  • @Override:
    标记一个方法意图重写父类的方法。这是给编译器的“承诺书”,如果该方法并未正确重写(如方法名拼写错误),编译器将报错。
  • @Deprecated:
    标记一个元素(类、方法、字段)已过时,不推荐使用。调用被此注解标记的元素时,编译器会发出警告。
  • @SuppressWarnings:
    压制编译器警告。在明确知道警告无害的情况下使用,可以使代码更整洁。例如@SuppressWarnings("deprecation")
  • @FunctionalInterface (Java 8+):
    标记一个接口为“函数式接口”,即该接口有且仅有一个抽象方法。这是编译器层面的约束,确保该接口可以被Lambda表达式所使用。

5.3.3 自定义注解

我们可以使用@interface关键字来定义自己的注解。

定义注解与属性

注解的属性定义形式为 类型 属性名();。可以为属性提供default默认值。

  • 属性支持的类型
    • 所有基本数据类型 (int, double等)
    • String, Class, enum
    • 注解类型
    • 以上所有类型的一维数组形式
代码示例:定义一个复杂的数据库信息注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.custom;

// 一个用于模拟数据库信息的注解
public @interface DataBaseInfo {
String url();

String user();

String password();

// 带有默认值的属性
String driver() default "com.mysql.cj.jdbc.Driver";

int port() default 3306;

String[] characterSets() default {"UTF-8", "GBK"};
}
使用注解与属性赋值
  • 基本语法@注解名(属性名1=值1, 属性名2=值2, ...)
  • value属性简写:如果一个注解只有一个名为value的属性,在使用时可以省略value=
  • 数组属性简写:如果数组属性只有一个元素,可以省略花括号{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example;
import com.example.custom.DataBaseInfo;

public class Main {
// 使用自定义注解并为属性赋值
@DataBaseInfo(
url = "jdbc:mysql://localhost",
user = "root",
password = "password123",
characterSets = "UTF-8" // 数组属性只有一个元素,省略了花括号
)
public void connectToDatabase() {
// ...
}
}

5.3.4 元注解:注解的“配置”

元注解是“用于注解的注解”,它们定义了我们自定义的注解将如何工作。

  • @Target: 决定注解能用在哪里(类、方法、字段等)。
  • @Retention:
    决定注解的生命周期。RetentionPolicy.RUNTIME是关键,它让注解在运行时能被反射读取。
    • SOURCE:注解只在源代码级别保留,编译后不会包含在字节码文件中。
    • CLASS:注解在源代码级别和编译后的字节码文件中保留,但在运行时不会保留。
    • RUNTIME:注解在源代码级别、编译后的字节码文件中保留,并且在运行时也可以通过反射读取。
  • @Inherited: 允许子类继承父类上的注解。
  • @Documented: 让注解信息能被javadoc工具提取到API文档中。
  • @Repeatable (Java 8+): 允许同一个注解在同一个位置上重复使用。

例如我们熟知的Override注解

1
2
3
4
@Target(ElementType.METHOD) // 用在方法上
@Retention(RetentionPolicy.SOURCE) // 生命周期为源代码级别
public @interface Override {
}

5.3.5 反射解析注解:读取元数据的API

面试题引入

“如何通过反射在运行时获取到一个类、方法或字段上的注解信息?”

核心接口:AnnotatedElement

Java的反射体系中,Class, Method, Field, Constructor
等所有可以被注解的程序元素,都实现了java.lang.reflect.AnnotatedElement接口。这个接口是所有注解解析操作的入口,它提供了统一的、用于读取注解的核心方法。

核心方法速查表
方法签名功能描述
boolean isAnnotationPresent(Class annotation)判断当前元素上是否存在指定类型的注解。
<T extends Annotation> T getAnnotation(Class<T> annotation)获取当前元素上指定类型的注解对象,如果不存在则返回null
Annotation[] getAnnotations()获取当前元素上所有的注解对象数组。
Annotation[] getDeclaredAnnotations()获取直接在当前元素上声明的注解(不包括从父类继承的)。
代码示例:系统性地解析一个类上的所有注解

场景:我们定义一个UserProfile类,在其类、字段、方法上都使用自定义注解,然后编写一个解析器来读取所有这些元数据。

  • 步骤一:定义几个用于演示的注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.example.annotation;

    import java.lang.annotation.*;

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface ApiDoc {String value();}

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    @interface InjectValue {String source() default "config.properties";}

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @interface Loggable {}
  • 步骤二:在一个类中使用这些注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.example.model;

    import com.example.annotation.ApiDoc;
    import com.example.annotation.InjectValue;
    import com.example.annotation.Loggable;

    @ApiDoc("用户资料实体类")
    public class UserProfile {

    @InjectValue(source = "db.properties")
    public String username;

    @InjectValue
    private int age;

    @Loggable
    public void displayProfile() {
    System.out.println("Displaying user profile...");
    }

    public void setAge(int age) {
    this.age = age;
    }
    }
  • 步骤三:编写反射解析器

    1
    package com.example;

    import com.example.annotation.ApiDoc;
    import com.example.annotation.InjectValue;
    import com.example.annotation.Loggable;
    import com.example.model.UserProfile;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33

    import java.lang.reflect.Field; import java.lang.reflect.Method;

    public class Main {
    public static void main(String[] args) {
    // 获取UserProfile的Class对象
    Class<UserProfile> clazz = UserProfile.class;

    System.out.println("--- 1. 解析类上的注解 ---");
    if (clazz.isAnnotationPresent(ApiDoc.class)) {
    ApiDoc apiDoc = clazz.getAnnotation(ApiDoc.class);
    System.out.println("类文档注解: " + apiDoc.value());
    }

    System.out.println("\n--- 2. 解析字段上的注解 ---");
    // 遍历所有已声明的字段
    for (Field field : clazz.getDeclaredFields()) {
    if (field.isAnnotationPresent(InjectValue.class)) {
    InjectValue injectValue = field.getAnnotation(InjectValue.class);
    System.out.println("字段 '" + field.getName() + "' 需要从 '" +
    injectValue.source() + "' 注入值。");
    }
    }

    System.out.println("\n--- 3. 解析方法上的注解 ---");
    // 遍历所有已声明的方法
    for (Method method : clazz.getDeclaredMethods()) {
    if (method.isAnnotationPresent(Loggable.class)) {
    System.out.println("方法 '" + method.getName() + "' 需要被日志记录。");
    }
    }
    }
    }

输出结果:

1
2
3
4
5
6
7
8
9
--- 1. 解析类上的注解 ---
类文档注解: 用户资料实体类

--- 2. 解析字段上的注解 ---
字段 'username' 需要从 'db.properties' 注入值。
字段 'age' 需要从 'config.properties' 注入值。

--- 3. 解析方法上的注解 ---
方法 'displayProfile' 需要被日志记录。

5.3.6 [终极实战] 结合反射构建迷你ORM框架

这是注解与反射最经典的结合应用,它模拟了MyBatis等ORM框架的核心原理。

目标:编写一个程序,能够扫描指定包下的所有类,并为那些被@Table@Column注解标记的类,自动生成SQL的
CREATE TABLE 语句。

步骤一:定义自定义注解 (@Table@Column)
1
2
3
4
5
6
7
8
9
10
package com.example.annotation;

import java.lang.annotation.*;

// @Table注解,用于类上,指定表名
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value(); // 用value属性来接收表名
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.example.orm.annotations;

import java.lang.annotation.*;

// @Column注解,用于字段上,指定列名和类型
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String name();
String type() default "varchar(255)"; // 默认类型为varchar
int length() default 255;
}
步骤二:创建被注解的实体类 (POJO)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.orm.entities;

import com.example.orm.annotations.*;

@Table("t_user") // 指定该类对应的表名为 t_user
public class User {
@Column(name = "id", type = "int")
private int id;

@Column(name = "user_name")
private String username;

@Column(name = "user_age", type = "int")
private int age;

// email字段没有注解,将被忽略
private String email;
}
步骤三:编写注解处理器(核心逻辑)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.example;

import com.example.annotation.Column;
import com.example.annotation.Table;

import java.lang.reflect.Field;

public class Main {
public static String generateCreateTableSql(String className) throws Exception {
// 1. 获取Class对象
Class<?> clazz = Class.forName(className);
// 2. 检查类上是否有@Table注解
if (!clazz.isAnnotationPresent(Table.class)) {
return null; // 如果没有,则不是实体类,直接返回
}
// 3. 获取@Table注解并读取表名
Table tableAnnotation = clazz.getAnnotation(Table.class);
String tableName = tableAnnotation.value();

StringBuilder sql = new StringBuilder();
sql.append("CREATE TABLE ").append(tableName).append(" (\n");

// 4. 遍历所有字段,查找@Column注解
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Column.class)) {
// 5. 获取@Column注解并读取列名和类型
Column columnAnnotation = field.getAnnotation(Column.class);
String columnName = columnAnnotation.name();
String columnType = columnAnnotation.type();

sql.append(" ").append(columnName).append(" ").append(columnType).append(",\n");
}
}
// 移除最后一个多余的逗号和换行符
if (sql.toString().contains(",")) {
sql.delete(sql.length() - 2, sql.length());
}

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

public static void main(String[] args) throws Exception {
String userSql = generateCreateTableSql("com.example.entities.User");
System.out.println("--- 自动生成的User表SQL ---");
System.out.println(userSql);
}
}

输出结果

1
2
3
4
5
6
--- 自动生成的User表SQL ---
CREATE TABLE t_user (
id int,
user_name varchar(255),s
user_age int
);

这个综合案例完美展示了如何通过“注解定义元数据 +
反射读取元数据
”的模式,来构建强大、灵活的自动化框架。