收益与代价
IoC 容器、运行时扫描和字节码增强不会让复杂度消失。它们会把重复组装逻辑集中起来,同时引入新的运行时规则。评估 Rain 时,应同时看到收益与代价。
先做一次复杂度搬运实验
假设原来有 100 行手工注册代码。引入 Rain 后,它们变成了 10 个注解和一行启动代码。能否得出“复杂度减少了 90%”?
不能。部分复杂度确实因为通用机制复用而消失了,另一部分则移动到了:
- 扫描规则;
- 容器创建规则;
- 注解语义;
- Loader 执行阶段;
- 运行时错误处理;
- 类加载与增强顺序。
框架的价值不是魔法般消灭复杂度,而是把重复复杂度集中管理,让业务代码只保留与业务相关的变化。
停下来想一想:什么是“好的隐藏”,什么是“坏的隐藏”?
如果开发者能通过稳定规则预测框架行为,隐藏重复细节是好事。如果规则模糊、错误只能靠猜测和调试发现,隐藏就会变成负担。
主要收益
降低对象组装成本
业务组件只声明依赖,容器递归创建对象图。新增服务时不必持续扩大中央入口或维护手写注册表。
模块之间更容易解耦
模块可以依赖 EventBus、JobManager 或自定义接口,而不是依赖具体实现。实现通过自动绑定、命名实例或集合注入接入。
扩展能力具有统一入口
@LoadBy 与 Loader 把“发现一批声明并建立基础设施”的流程抽象出来。事件、任务和其他模块可以共享同一发现机制。
Java 与 Kotlin 共享容器
Rain 使用 JVM 类型、javax.inject、Java 反射和 Kotlin 反射实现注入。Java 可以使用标准注解,Kotlin 还能利用主构造器、可空类型、默认参数与属性委托。
框架实现相对可读
DiContext -> ClassContext -> BeanCreator / BeanInjector 的主线较短。对框架开发、插件系统或需要深度定制运行时的团队来说,可以真正读懂并修改容器本身就是价值。
字节码增强突破代理限制
自定义类加载增强可以直接修改待定义类,不必永远通过代理对象访问。这对 final 类型、静态方法和某些特殊调用路径具有吸引力。
数据读取模型统一
Rain 通过 DataReader / DataReaderFactory 为普通对象、列表、映射和配置提供统一读取入口,有利于扩展新的注入数据来源。
主要代价
运行时错误多于编译期错误
缺失 Bean、名称不匹配、配置转换失败、构造器不可访问、循环依赖和类加载冲突,通常在启动或首次获取 Bean 时暴露。
类型系统能保证参数声明的类型,却不能保证运行时容器一定存在可用候选。
隐式行为增加理解成本
普通具体类只要处于扫描范围内,就可能被容器创建;一个注解可能通过 @LoadBy 间接触发 Loader;名称还可能通过配置再次解析。
这些能力减少代码量,却使阅读单个文件时不一定能看到完整行为。
扫描带来启动和边界成本
classpath 扫描需要枚举类并读取信息。扫描范围过大不仅影响启动,还会扩大容器可见类型和默认信任范围。
自定义类加载器提高调试难度
同名类由不同 ClassLoader 加载时,在 JVM 中属于不同类型。待增强类如果过早由父加载器加载,Transformer 可能无法生效。反射、测试、序列化和开发工具也可能受类加载边界影响。
字节码增强存在兼容风险
ASM 转换需要正确处理 JVM 字节码、栈帧、编译器生成结构以及不同 JDK / Kotlin 版本。增强后的异常堆栈和源码映射也可能更难理解。
容器容易退化成全局状态
如果所有代码都调用 DiContext.getBean(),容器就变成全局 Service Locator。这样虽然使用了 DI 框架,却失去了依赖显式、容易测试和模块隔离的主要好处。
生命周期协议较简单
复杂系统可能需要依赖感知的启动顺序、反向关闭、超时、健康检查、异步停止和失败回滚。Rain 当前的 ApplicationService 协议需要应用自行补充这些治理能力。
生态和现成集成有限
采用 Rain 意味着团队需要更主动维护数据库、网络、监控、配置中心、安全和部署集成。自主性提高的同时,工程责任也会提高。
什么时候不应使用容器
以下情况手工组装可能更清楚:
- 程序只有少量对象且依赖稳定;
- 一次性命令行工具启动后立即完成任务;
- 数据对象和值对象只承载数据;
- 高频创建的短生命周期对象适合普通构造器;
- 算法内部对象完全属于实现细节;
- 扫描、反射和类增强成本高于减少的样板代码。
可以用 Rain 管理应用级长期服务,同时在服务内部继续用构造器创建值对象、命令对象和临时计算对象。容器不是 new 的替代品,而是应用级对象关系的组装者。
判断标准
当某类对象满足多个条件时,更适合交给容器:
- 生命周期接近应用生命周期;
- 被多个模块共享;
- 依赖其他应用服务;
- 需要接口替换或多实现选择;
- 需要被 Loader、事件、任务或 Controller 发现;
- 需要配置、注入或框架增强。
反之,局部、短暂、纯数据或无外部依赖的对象通常保持普通创建方式更简单。
情景判断练习
尝试判断下列对象是否适合交给 Rain 管理:
| 对象 | 建议 | 原因 |
|---|---|---|
UserService | 通常适合 | 长期存在、依赖其他服务、被多个入口调用 |
DatabasePool | 通常适合 | 资源昂贵,需要统一启动和关闭 |
Order 数据对象 | 通常不需要 | 每次业务操作都会创建,生命周期短 |
Money 值对象 | 不需要 | 纯数据与计算,不依赖基础设施 |
EventListener | 适合 | 需要扫描、创建和自动注册 |
一次循环中的临时 StringBuilder | 不适合 | 完全属于局部实现细节 |
没有一个表格能替代设计判断,但可以使用三个问题快速检查:
- 它是否需要被应用中许多位置共享?
- 它是否需要框架帮助发现、注入或管理生命周期?
- 它的创建是否具有稳定、可复用的应用级规则?
多数回答为“否”时,普通构造器通常更合适。