6.2 运行时元编程:反射、装饰器与类创建
编译期生成器只能使用构建时知道的信息。依赖注入容器、序列化框架和测试工具还要根据运行时类型或配置选择行为,于是会使用反射、代理、装饰器或类创建钩子。它们获得了动态性,也把一部分错误推迟到了运行期。程序更灵活了,报错也可能更会挑时间。
反射:把程序结构当作数据查询
Java 反射可以检查类、字段、方法和注解,并在运行时调用成员。下面的方法片段使用 Java 17,放在普通类中,并导入 java.lang.reflect.* 和 java.util.*:
static Map<String, Object> inspectRecord(Object value)
throws ReflectiveOperationException {
Class<?> type = Objects.requireNonNull(value, "value").getClass();
if (!type.isRecord()) {
throw new IllegalArgumentException("record required");
}
Map<String, Object> result = new LinkedHashMap<>();
for (RecordComponent component : type.getRecordComponents()) {
Method accessor = component.getAccessor();
result.put(component.getName(), accessor.invoke(value));
}
return result;
}2
3
4
5
6
7
8
9
10
11
12
13
14
对当前调用方可访问的 public record User(String name, int age),传入 new User("Ada", 36),结果是 {name=Ada, age=36}。传入普通字符串会得到 IllegalArgumentException。组件顺序来自 record 声明;非公开类型、模块未导出包或自行覆盖且会抛异常的访问器,仍可能让调用失败。
这段代码能处理调用方编译时未知的 record。反射机制的一般成本还包括:
- 成员名称错误到运行时才暴露;
- 访问控制和模块边界可能阻止深层反射;
- 异常会被反射调用包装;
- 重命名和静态分析工具难以追踪字符串引用;
- 重复查找需要缓存,但缓存又要考虑类加载器卸载。
优先使用公开 API 和 MethodHandles.Lookup 支持的访问能力,不要把强行打开内部成员当成稳定扩展点。
动态代理:在调用边界插入行为
Java 的接口代理可以把方法调用交给统一处理器。下面是处理器片段,假定 target 实现被代理接口,且 metrics.record 线程安全、不会抛异常:
InvocationHandler timing = (proxy, method, args) -> {
long start = System.nanoTime();
try {
return method.invoke(target, args);
} catch (InvocationTargetException error) {
throw error.getCause();
} finally {
metrics.record(method.getName(), System.nanoTime() - start);
}
};2
3
4
5
6
7
8
9
10
Method.invoke 把目标异常包装成 InvocationTargetException;解包才能让调用方得到接口原本声明的异常或原始运行时异常。若处理器抛出接口未声明的受检异常,代理仍会转为 UndeclaredThrowableException。计时记录若在 finally 中抛错,也会覆盖原结果或异常,所以观测回调的失败策略必须明确。
事务、权限、追踪和重试框架常使用类似机制。但拦截层越多,真实调用路径越难从源码看见。尤其要明确:
- 同一对象内部自调用是否经过代理;
- 异常是否被解包或转换;
equals、hashCode、toString怎样处理;直接委托给目标的equals甚至可能使proxy.equals(proxy)为假;- 异步返回后,计时和事务边界何时结束;
- 代理是否保留注解和泛型信息。
字节码 instrumentation 可以在加载时转换 class;加载后是否支持重转换或重定义,以及允许修改什么,受 JVM 能力和规范限制,不能任意增删字段或方法。它比接口代理覆盖更广,也更容易与 JDK 版本、其他 Agent 和验证规则冲突。它适合观测和框架基础设施,不适合隐藏普通业务流程。
Python 装饰器发生在定义阶段
下面使用 Python 3.10 以上版本。装饰器在定义语句执行时接收被装饰对象并返回替代对象,包装函数里的计时则在调用时运行:
from collections.abc import Callable
from functools import wraps
from time import perf_counter
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def timed(fn: Callable[P, R]) -> Callable[P, R]:
@wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
started = perf_counter()
try:
return fn(*args, **kwargs)
finally:
print(f"{fn.__qualname__}: {perf_counter() - started:.6f}s")
return wrapper
@timed
def calculate(value: int) -> int:
return value * 22
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
可把 @timed 近似理解为类或函数定义完成后执行一次赋值:
calculate = timed(calculate)调用 calculate(5) 返回 10,同时打印一行耗时;calculate.__name__ 仍是 calculate。第二个赋值块只是解释语法的等价关系,不要再执行它把已经装饰的函数包装第二遍。
functools.wraps 保留名称、文档和 __wrapped__ 等元数据,有助于调试和工具识别,但不会让包装器自动具备异步或生成器语义。这个 timed 只计同步函数;用于 async def 时只会测到协程对象创建,异步版本需在包装器内部 await fn(...)。装饰器工厂还可以接收配置,但层层嵌套会使异常栈和类型推导变复杂。
元类控制类对象怎样创建
Python 中类本身也是对象,元类定义创建类对象的过程:
class RegistryMeta(type):
registry: dict[str, type] = {}
def __new__(mcls, name, bases, namespace, **kwargs):
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
key = namespace.get("plugin_name")
if key is not None:
if not isinstance(key, str) or not key:
raise TypeError("plugin_name must be a nonempty string")
if key in mcls.registry:
raise TypeError(f"duplicate plugin: {key}")
mcls.registry[key] = cls
return cls
class Plugin(metaclass=RegistryMeta):
pass
class CsvPlugin(Plugin):
plugin_name = "csv"2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
执行后 RegistryMeta.registry["csv"] is CsvPlugin 为真;再定义一个 plugin_name = "csv" 的类会在定义时抛出 TypeError。这里只注册本次类体显式声明的 plugin_name,未重写它的子类不会被重复注册。注册表用强引用保留类,卸载时需要显式移除;一次失败的模块导入也不会自动回滚此前成功的注册。
这适合为一族类统一执行注册或约束。若只是修改一个类,类装饰器通常更局部;若只是复用行为,普通基类或组合更容易理解。元类冲突、导入时副作用和测试隔离是采用前必须评估的成本。
动态机制要留下静态边界
即使内部依赖反射,外部也可以暴露普通类型接口:
interface Codec<T> {
byte[] encode(T value);
T decode(byte[] bytes);
}
interface CodecRegistry {
<T> Codec<T> codecFor(Class<T> type);
}2
3
4
5
6
7
8
这里定义的是接口契约,具体实现还需证明 Class<T> 与返回的 Codec<T> 对应,不能靠一次未经检查的强转就当作类型安全。把动态性封装在 registry、factory 或 adapter 后面,可以让大部分业务代码继续获得编译期检查。
运行时发现还应在启动阶段尽早验证,而不是等第一笔真实请求才发现缺少构造器或重复注册。服务启动时可以:
- 扫描并建立元数据缓存;
- 校验所有处理器签名;
- 检测冲突和循环依赖;
- 输出可查询的注册表;
- 在失败时终止启动,而非降级成随机运行错误。
性能问题先测查找路径
反射调用并不必然是系统瓶颈。常见的真正成本可能是:每次请求重复扫描 classpath、错误地创建代理、缓存持有类加载器,或动态层触发大量分配。
优化顺序应是:
- 缓存稳定的成员查找结果;
- 把扫描移到启动或构建阶段;
- 用方法句柄或生成代码减少热路径适配;
- 用 JFR 或基准测试验证,而不是只凭“反射很慢”。
安全边界
不要让不可信输入直接决定:
- 要加载的任意类名;
- 要调用的任意方法名;
- 可访问的文件或模块;
- 要反序列化并实例化的具体类型。
按插件可信程度选择允许列表、受限接口或独立进程,并限制隔离进程的资源和权限。反射仍执行访问与参数检查,但如果应用允许外部输入任选类或方法,就可能把本不该开放的能力暴露出去。
完成检查
为一个插件注册系统比较三种方案:显式 Map、注解扫描、Python 元类式自动注册。回答:
- 错误在构建、启动还是首个请求时出现;
- IDE 能否找到所有实现;
- 插件卸载后缓存是否释放类加载器;
- 重复名称怎样报告;
- 不可信插件怎样隔离。