类注入与 include

OpenModsClassTransformer.transform 的另外两个分支构成 OpenModsLib 的通用类操作能力:一条为运行时动态生成类提供字节码,一条为注解驱动的接口混入改写第三方类。两者都作用在 coremod 阶段,机制见coremod 补丁。

基本信息

属性 值
动态类管理器 openmods.injector.InjectedClassesManager
生成类名前缀 $✗$(GENERATED_CLS_PREFIX,InjectedClassesManager.java:11)
分隔符 ☞(GENERATED_CLS_SEPARATOR,InjectedClassesManager.java:13)
include 变换器 openmods.include.IncludingClassVisitor(287 行)
include 注解 @IncludeInterface、@IncludeOverride
已废弃标记接口 openmods.include.IExtendable(@Deprecated)
崩溃报告条目 InjectorSanityChecker(标签 "Class transformer null safety")

功能

动态类生成

InjectedClassesManager(src/main/java/openmods/injector/InjectedClassesManager.java:9)用「魔法类名」协议在类加载时按需合成字节码。

类名协议(InjectedClassesManager.java:11,13,32):

$<U+2697>$$<providerId><U+261E><arg>      →   实际显示为  $✗$providerId☞arg
  • 前缀 $✗$ 之后的类不要求真实存在,transform 收到 bytes == null 时才走这条路
  • providerId 标识生成者,arg 是传给生成器的参数
  • createClassName(providerId, arg)(:26)负责拼装,providerId 未注册时 Preconditions.checkState 直接失败

tryGetBytecode(:32)的解析与降级路径:

情况 行为 行
类名不以 $✗$ 开头 返回 null(交回正常加载流程) :33
按分隔符切分后不是 2 段 Log.warn("Malformed generated class: %s") 后返回 null :35-38
providerId 未注册 Log.warn("Unknown provider: %s") 后返回 null :41-44
生成器抛异常 Log.severe 后返回 null :50-53

全部失败路径都返回 null 而非抛出,让 JVM 报标准的 ClassNotFoundException —— 这是有意的降级设计,代价是真实错误原因只进日志。registerProvider(:20)对重复 providerId 直接 checkState 失败。

GENERATED_CLS_PREFIX 与 GENERATED_CLS_SEPARATOR 在源码中以 "\u2697" / "\u261E" 转义形式书写(:11,13),而非字面字符。

include:注解驱动的接口混入

这是 OpenModsLib 最有特色的字节码能力:让任何类(第三方 mod 的类也一样)通过一个注解就自动实现指定接口并生成全部转发方法。

注解(src/main/java/openmods/include/):

注解 目标 值 作用
@IncludeInterface FIELD 或 METHOD Class<?>,默认 Object.class 被注解的字段类型 / 方法返回类型需实现的接口
@IncludeOverride 仅 METHOD 无 声明本方法已手动实现,用于消解冲突

变换流程(IncludingClassVisitor,src/main/java/openmods/include/IncludingClassVisitor.java:28):

  1. visit(:239)记录类元信息;若目标是接口则立即 throw new StopTransforming()(:240)—— 接口不被处理
  2. visitField(:195)与 visitMethod(:201)各返回一个 FieldVisitor / MethodVisitor 包装器,在 visitAnnotation 中识别注解描述符(:45,79,80)
  3. 注解值经 IncludeAnnotationVisitor.visit(:124)读出接口类型,addInterfaceImplementations(:274)把接口加入类的接口列表,并用 getInterfaceMethods(:253)反射取其全部方法
  4. MethodAdder.addMethod(:145)为每个方法合成字节码:ACC_PUBLIC | ACC_SYNTHETIC 方法 → ALOAD 0 → 取接口引用 → CHECKCAST → 逐参数 ILOAD → INVOKEINTERFACE → IRETURN(:147-172)
  5. visitEnd(:210)做三重一致性校验后再写回类

三重校验(IncludingClassVisitor.java:211-227)是这套机制的安全阀:

校验 失败信息 行
已有方法与待加方法冲突、且冲突方法未标 @IncludeOverride "%s implements interface methods %s, but they are not marked with @IncludeOverride" :212-217
标了 @IncludeOverride 但没有接口提供该方法 "%s marks methods %s with @IncludeOverride, but no interface implements it" :219-224
同一方法被两个接口同时提供 "Included method '%s' conflict, interfaces = %s,%s"(addInterfaceImplementations 内) :281-287

三类失败均为 Preconditions.checkState,即直接抛异常终止类变换,不会静默生成错误字节码。

visitEnd 末尾的 super.visit 注释写明 // risky, but should work, since we are only replacing interfaces(:237)—— 它整表替换接口数组而非增量追加。

生效范围

OpenModsClassTransformer.shouldTryIncluding(src/main/java/openmods/core/OpenModsClassTransformer.java:239)决定哪些类进入 include 流程:

  • injectAsmData 已执行(正常游戏流程):只处理 FML 在 ASMDataTable 中扫描到的带 @IncludeInterface / @IncludeOverride 的类(白名单,OpenModsClassTransformer.java:230-235 收集)
  • 尚未执行:走 IGNORED_PREFIXES 黑名单(8 个前缀,见coremod 补丁)

这意味着正常游戏中该变换几乎不改动任何类,只处理显式标注的类。

变换器安全检查

InjectorSanityChecker(src/main/java/openmods/injector/InjectorSanityChecker.java:16)作为 ICrashCallable 注册进崩溃报告(OpenModsCore.java:54),用于检测其它 mod 的 coremod 是否遵守 IClassTransformer.transform 的契约。

检查逻辑(findUnsafeTransformers,:38)遍历 LaunchWrapper 的全部变换器,对每个变换器传入一个随机假类名并 bytes = null:

变换器行为 判定 记录内容 行
变换器本身为 null 不安全 <null> :41
对 bytes == null 返回非 null 字节 不安全 returned non-null result: <长度> :45-52
抛异常 不安全 crashed with <异常类>(<消息>) :54-62
全部安全 安全 "all safe" :31

transform 契约要求传入 null 字节时必须返回 null(表示「无变换」)。不遵守的第三方变换器会破坏 InjectedClassesManager 的动态类生成 —— 这就是该检查器存在的原因。

数值

数值名 值
injector 包类数 3(IClassBytesProvider、InjectedClassesManager、InjectorSanityChecker)
include 包类数 4(IExtendable、IncludeInterface、IncludeOverride、IncludingClassVisitor)
include 注解数 2
生成类名中的特殊字符数 2(前缀 $✗$、分隔符 ☞)
tryGetBytecode 的降级返回路径数 4(:33,35,41,50)
visitEnd 的一致性校验数 3
被 include 变换直接跳过的目标 全部接口类(IncludingClassVisitor.java:240)
崩溃报告中检查的变换器来源 LaunchWrapper 全局列表(InjectorSanityChecker.java:40)

交互

无玩家可见交互。两处诊断入口均在崩溃报告中:"Class transformer null safety"(本条目)和 "OpenModsLib class transformers"(见coremod 补丁)。

applyIncludes(OpenModsClassTransformer.java:254)在 include 失败时 Log.severe 后重新抛出(:259),即 include 失败会让游戏启动崩溃而非降级 —— 这与 InjectedClassesManager 的静默降级策略形成对比,前者是硬失败,后者是软失败。

已废弃 API

IExtendable(src/main/java/openmods/include/IExtendable.java:7)是一个空接口,带 @Deprecated 与注释:

/**
 * @deprecated No longer needed, annotations are sufficient
 */

它已无任何实际作用,IncludingClassVisitor 中不含对它的引用。检索 include 机制时不应把它当作有效入口。

相关条目