跳到内容

6.2 运行时元编程:反射、装饰器与类创建

工坊的依赖注入容器必须等程序运行后才知道要创建哪些对象,静态生成已经覆盖不了全部情况。

编译期生成器只能使用构建时知道的信息。依赖注入容器、序列化框架和测试工具还要根据运行时类型或配置选择行为,于是会使用反射、代理、装饰器或类创建钩子。它们获得了动态性,也把一部分错误推迟到了运行期。

反射:把程序结构当作数据查询

Java 反射可以检查类、字段、方法和注解,并在运行时调用成员:

java
static Map<String, Object> inspectRecord(Object value)
        throws ReflectiveOperationException {
    Class<?> type = 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;
}

这段代码能处理编译时未知的 record,但代价包括:

  • 成员名称错误到运行时才暴露;
  • 访问控制和模块边界可能阻止深层反射;
  • 异常会被反射调用包装;
  • 重命名和静态分析工具难以追踪字符串引用;
  • 重复查找需要缓存,但缓存又要考虑类加载器卸载。

优先使用公开 API 和 MethodHandles.Lookup 支持的访问能力,不要把强行打开内部成员当成稳定扩展点。

动态代理:在调用边界插入行为

Java 的接口代理可以把方法调用交给统一处理器:

java
InvocationHandler timing = (proxy, method, args) -> {
    long start = System.nanoTime();
    try {
        return method.invoke(target, args);
    } finally {
        metrics.record(method.getName(), System.nanoTime() - start);
    }
};

事务、权限、追踪和重试框架常使用类似机制。但拦截层越多,真实调用路径越难从源码看见。尤其要明确:

  • 同一对象内部自调用是否经过代理;
  • 异常是否被解包或转换;
  • equalshashCodetoString 怎样处理;
  • 异步返回后,计时和事务边界何时结束;
  • 代理是否保留注解和泛型信息。

字节码 instrumentation 可以在加载前后转换 class,比接口代理覆盖更广,也更容易与 JDK 版本、其他 Agent 和验证规则冲突。它适合观测和框架基础设施,不适合隐藏普通业务流程。

Python 装饰器发生在定义阶段

装饰器接收被装饰对象并返回替代对象:

python
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 * 2

可把 @timed 近似理解为类或函数定义完成后执行一次赋值:

python
calculate = timed(calculate)

functools.wraps 保留名称、文档和 __wrapped__ 等元数据,有助于调试和工具识别。装饰器工厂还可以接收配置,但层层嵌套会使异常栈和类型推导变复杂。

元类控制类对象怎样创建

Python 中类本身也是对象,元类定义创建类对象的过程:

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 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"

这适合为一族类统一执行注册或约束。若只是修改一个类,类装饰器通常更局部;若只是复用行为,普通基类或组合更容易理解。元类冲突、导入时副作用和测试隔离是采用前必须评估的成本。

动态机制要留下静态边界

即使内部依赖反射,外部也可以暴露普通类型接口:

java
interface Codec<T> {
    byte[] encode(T value);
    T decode(byte[] bytes);
}

final class CodecRegistry {
    <T> Codec<T> codecFor(Class<T> type) {
        // 内部可使用反射发现,结果以有类型接口返回
    }
}

把动态性封装在 registry、factory 或 adapter 后面,可以让大部分业务代码继续获得编译期检查。

运行时发现还应在启动阶段尽早验证,而不是等第一笔真实请求才发现缺少构造器或重复注册。服务启动时可以:

  • 扫描并建立元数据缓存;
  • 校验所有处理器签名;
  • 检测冲突和循环依赖;
  • 输出可查询的注册表;
  • 在失败时终止启动,而非降级成随机运行错误。

性能问题先测查找路径

反射调用并不必然是系统瓶颈。常见的真正成本可能是:每次请求重复扫描 classpath、错误地创建代理、缓存持有类加载器,或动态层触发大量分配。

优化顺序应是:

  1. 缓存稳定的成员查找结果;
  2. 把扫描移到启动或构建阶段;
  3. 用方法句柄或生成代码减少热路径适配;
  4. 用 JFR 或基准测试验证,而不是只凭“反射很慢”。

安全边界

不要让不可信输入直接决定:

  • 要加载的任意类名;
  • 要调用的任意方法名;
  • 可访问的文件或模块;
  • 要反序列化并实例化的具体类型。

使用允许列表、能力受限的接口和独立进程隔离插件。反射绕过类型检查的便利,也可能绕过本来清晰的授权边界。

完成检查

为一个插件注册系统比较三种方案:显式 Map、注解扫描、Python 元类式自动注册。回答:

  1. 错误在构建、启动还是首个请求时出现;
  2. IDE 能否找到所有实现;
  3. 插件卸载后缓存是否释放类加载器;
  4. 重复名称怎样报告;
  5. 不可信插件怎样隔离。

参考资料

Built with VitePress | Software Systems Atlas