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

Java(五):5.0 [元编程] 反射、注解
Prorise5.0 [元编程] 反射、注解
本章将带您深入Java的“元编程”世界。元编程是指程序在运行时能够审视并操作自身结构的能力。我们将从这一切的基石——类加载机制讲起,然后深入学习实现元编程的核心技术——反射,并最终探讨其最广泛的应用——注解与Junit单元测试。
5.1 [基础] 类加载机制与 ClassLoader
面试题引入
“请简述一下Java的类加载过程,以及双亲委派模型。”
类的生命周期
一个.java
源文件变成可以在JVM中运行的程序,其对应的.class
文件需要经历一个完整的生命周期。这个过程主要分为
加载(Loading)、**链接(Linking)和初始化(Initialization)**三个阶段。
- 加载:JVM通过类加载器(ClassLoader)找到对应的
.class
文件,读取其二进制数据,并在方法区中创建一个java.lang.Class
对象。 - **链接 **:
- 验证:确保被加载的类文件符合JVM规范,没有安全问题。
- 准备:为类的静态变量分配内存,并设置其类型的默认值(如
int
为0,Object
为null
)。注意,此时并非执行程序员指定的初始值。 - 解析:将类中的符号引用(如类名、方法名)替换为直接的内存地址引用。
- 初始化:这是类加载的最后一步。JVM会执行类的初始化方法
<clinit>()
。这个方法由编译器自动收集类中所有静态变量的赋值动作和**静态代码块(static{}
)**中的语句合并而成。只有到这一步,静态变量才会被赋予我们代码中指定的初始值。
类加载器 (ClassLoaders) 体系
Java通过一个层级分明的类加载器体系来完成类的加载工作。主要有三类加载器:
启动类加载器 (Bootstrap ClassLoader):
- JVM的顶层加载器,由C++实现,是JVM自身的一部分。
- 负责加载Java最核心的库(如
rt.jar
里的java.lang.*
、java.util.*
等)。 - 在Java代码中尝试获取它的引用会返回
null
。
扩展类加载器 (Extension ClassLoader):
- 负责加载Java的扩展库(位于
jre/lib/ext
目录下)。 - 它的父加载器是启动类加载器。
- 负责加载Java的扩展库(位于
应用程序类加载器 (Application ClassLoader):
- 也称为系统类加载器,是我们最常打交道的加载器。
- 负责加载用户类路径(Classpath)上我们自己编写的类和第三方库的JAR包。
- 它的父加载器是扩展类加载器。
[核心] 双亲委派模型 (Parent-Delegation Model)
这是Java类加载器设计的核心原则,也是面试中的绝对高频考点。
- 工作流程:当一个类加载器收到加载类的请求时,它不会自己先去尝试加载,而是会首先把这个请求委派给它的父加载器去完成。每一层的加载器都是如此。只有当父加载器在自己的搜索范围内找不到指定的类,无法完成加载请求时,子加载器才会自己去尝试加载。
- 为何如此设计?
- 避免类的重复加载:通过委派机制,一个类最终只会被一个加载器加载一次,确保了该类在JVM中的唯一性。
- 保证核心库的安全:这是最重要的目的。它防止了Java的核心API被恶意或无意地篡改。例如,你无法自己编写一个
java.lang.String
类来替代系统的String
类。因为当加载请求传递到最顶层的启动类加载器时,它会找到并加载JDK自带的、真正的String
类,加载过程至此结束,你编写的“假”String
类将永远没有机会被加载。
代码示例:获取类加载器并查看其层级
1 | package com.example; |
输出结果:
1 | 应用程序类加载器 (AppClassLoader): sun.misc.Launcher$AppClassLoader@18b4aac2 |
这个输出完美地验证了类加载器的层级关系和启动类加载器的特殊性。
5.2 [核心] 反射:运行时动态操控的艺术
在了解了Java代码如何被加载到JVM中之后,我们现在来学习一个Java中非常强大、也是所有主流框架(如Spring,
MyBatis)基石的特性——反射。
5.2.1 什么是反射及其应用场景
面试题引入
“什么是反射?它有哪些优缺点和应用场景?”
核心概念
反射(Reflection)是Java语言提供的一种在运行时,动态地、间接地检查、分析和操作自身结构与行为的能力。
我们可以用一个比喻来理解:
- 常规编程:就像我们拿到一本说明书(类的代码),我们严格按照说明书上的指示(方法调用)来操作一个设备(对象)。我们在写代码的时候,就知道这个设备有什么按钮,每个按钮叫什么。
- 反射编程:就像我们没有说明书,只有一个密封的黑盒设备。但是我们拿到了一套“万能检测和操控工具”(即反射API)。通过这套工具,我们可以在程序运行时去探测这个黑盒:它有哪些按钮(方法)?有哪些内部零件(字段)?它的型号是什么(类名)?甚至,我们可以强行按下那些没有在外部暴露的内部按钮(调用私有方法)。
优缺点
优点:
- 动态性与灵活性:这是反射最大的优点。它允许我们编写非常通用的代码,可以操作在编译时完全未知的类。所有主流框架的依赖注入(DI)、AOP等核心功能,都深度依赖反射。
缺点:
- 性能开销:反射操作(如方法查找)比直接代码调用要慢得多,因为它涉及更多的查找和检查步骤,并且绕过了JIT编译器的许多优化。因此,在性能敏感的核心路径上应避免使用。
- 破坏封装:通过
setAccessible(true)
可以访问和修改类的私有成员,这违背了面向对象的封装原则。 - 类型不安全:编译器无法对反射代码进行类型检查,可能将潜在的
ClassCastException
等错误从编译期推迟到运行时。
应用场景
- 框架开发:Spring的IoC/DI容器通过反射动态创建和注入Bean。
- 动态代理:在运行时为一个或多个接口动态地生成实现类。
- 注解处理:在运行时读取注解信息并执行相应逻辑。
- 单元测试:Junit等测试框架通过反射查找并执行被
@Test
注解的方法。
5.2.2 反射的基石:java.lang.Class
对象
要对一个类进行反射操作,第一步永远是获取代表这个类的java.lang.Class
对象。它是反射所有操作的入口。
获取Class
对象的三种主要方式
- 通过类名获取:
ClassName.class
- 最简单、最安全的方式,在编译时就会受到检查。
- 通过对象实例获取:
object.getClass()
- 当你已经拥有一个该类的对象时使用。
- 通过类的全限定名获取:
Class.forName("com.example.MyClass")
- 最动态的方式,可以在运行时根据一个字符串来加载任意类。常用于框架加载配置文件中指定的类。
代码示例:获取Class
对象
1 | package com.example; |
5.2.3 通过反射操作类的成员
获取到Class
对象后,我们就可以像操作说明书一样,获取并操作它的所有部分。
1. 操作构造器 (Constructor
)
- 核心API:
getConstructors()
,getConstructor(...)
,getDeclaredConstructors()
,getDeclaredConstructor(...)
,newInstance(...)
。 getDeclared...
vs.get...
:带有Declared
字样的方法可以获取到所有(包括private
)的成员;不带的只能获取public
成员。此规则对方法和字段同样适用。
代码示例:调用不同的构造器
1 | package com.example; |
2. 操作方法 (Method
)
- 核心API:
- getMethods():返回类中所有公共(public)方法的数组,包括继承的方法。
- getMethod(…):返回类中指定的公共方法,参数为方法名和参数类型类对象数组。
- getDeclaredMethods():返回类中声明的所有方法的数组,包括私有(private)、保护(protected)和默认(default)访问权限的方法,但不包括继承的方法。
- getDeclaredMethod(…):返回类中声明的指定方法,参数为方法名和参数类型类对象数组。
- invoke(…):用于调用对象的指定方法,参数为对象实例、方法对象和方法的参数数组。
代码示例:调用各种方法
1 | package com.example; |
3. 操作字段 (Field
)
- 核心API:
- getFields():返回类中所有公共(public)字段的数组,包括继承的字段。
- getField(…):返回类中指定的公共字段,参数为字段名。
- getDeclaredFields():返回类中声明的所有字段的数组,包括私有(private)、保护(protected)和默认(default)访问权限的字段,但不包括继承的字段。
- getDeclaredField(…):返回类中声明的指定字段,参数为字段名。
- get(…):用于获取对象指定字段的值,参数为对象实例和字段对象。
- set(…):用于设置对象指定字段的值,参数为对象实例、字段对象和要设置的值。
代码示例:读写字段值
1 | package com.example; |
5.2.4 [实战] 反射的应用:迷你Spring框架
场景:编写一个简单的框架,它可以根据一个
app.properties
配置文件,动态地创建并执行指定的对象和方法。
1. 创建 app.properties
文件 (放在src
或resources
目录下)
1 | className=com.example.UserService |
2. 创建业务类
1 | package com.example; |
3. 编写框架主类
1 | package com.example; |
这个简单的例子,就揭示了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 | package com.example.custom; |
使用注解与属性赋值
- 基本语法:
@注解名(属性名1=值1, 属性名2=值2, ...)
value
属性简写:如果一个注解只有一个名为value
的属性,在使用时可以省略value=
。- 数组属性简写:如果数组属性只有一个元素,可以省略花括号
{}
。
1 | package com.example; |
5.3.4 元注解:注解的“配置”
元注解是“用于注解的注解”,它们定义了我们自定义的注解将如何工作。
@Target
: 决定注解能用在哪里(类、方法、字段等)。@Retention
:
决定注解的生命周期。RetentionPolicy.RUNTIME
是关键,它让注解在运行时能被反射读取。- SOURCE:注解只在源代码级别保留,编译后不会包含在字节码文件中。
- CLASS:注解在源代码级别和编译后的字节码文件中保留,但在运行时不会保留。
- RUNTIME:注解在源代码级别、编译后的字节码文件中保留,并且在运行时也可以通过反射读取。
@Inherited
: 允许子类继承父类上的注解。@Documented
: 让注解信息能被javadoc
工具提取到API文档中。@Repeatable
(Java 8+): 允许同一个注解在同一个位置上重复使用。
例如我们熟知的Override注解
1 | // 用在方法上 |
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
15package com.example.annotation;
import java.lang.annotation.*;
value();} ApiDoc {String
source() default "config.properties";} InjectValue {String
Loggable {}步骤二:在一个类中使用这些注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package com.example.model;
import com.example.annotation.ApiDoc;
import com.example.annotation.InjectValue;
import com.example.annotation.Loggable;
public class UserProfile {
public String username;
private int age;
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 | --- 1. 解析类上的注解 --- |
5.3.6 [终极实战] 结合反射构建迷你ORM框架
这是注解与反射最经典的结合应用,它模拟了MyBatis等ORM框架的核心原理。
目标:编写一个程序,能够扫描指定包下的所有类,并为那些被
@Table
和@Column
注解标记的类,自动生成SQL的CREATE TABLE
语句。
步骤一:定义自定义注解 (@Table
和 @Column
)
1 | package com.example.annotation; |
1 | package com.example.orm.annotations; |
步骤二:创建被注解的实体类 (POJO)
1 | package com.example.orm.entities; |
步骤三:编写注解处理器(核心逻辑)
1 | package com.example; |
输出结果:
1 | --- 自动生成的User表SQL --- |
这个综合案例完美展示了如何通过“注解定义元数据 +
反射读取元数据”的模式,来构建强大、灵活的自动化框架。