Skip to content

总结:框架为什么会出现,又为什么越来越重要

学完前面的概念后,我们最后做一次完整梳理。

这一页不再专门解释某个注解或某个容器接口,而是尝试回答一个更大的问题:

明明程序只需要按照顺序执行代码,为什么后来会出现“框架”?为什么行业发展越深入,框架反而越来越复杂、越来越深入,也越来越难以从现代软件工程中移除?

要回答这个问题,不能只看一段代码是否能运行,而要观察软件如何从一个人的小程序,逐渐变成由许多人长期维护、持续变化、运行在复杂环境中的工程系统。

1. 最初,程序只是明确的步骤

一个最简单的程序往往非常容易理解:

kotlin
fun main() {
    val name = readln()
    println("Hello, $name")
}

它有一个入口、一条主要执行路径和少量数据。开发者能够在脑中完整模拟它的运行:先读输入,再输出结果。

这种程序不需要 IoC 容器,也不需要组件扫描、生命周期、事件总线和字节码增强。加入这些机制反而会使它更难理解。

所以,框架并不是因为普通代码“不够高级”才出现。框架出现的根本原因是:软件规模、协作人数、运行时间和变化频率不断增长,原本局部而明确的问题开始变成重复的系统性问题。

2. 第一次增长:代码开始重复

假设我们需要写多个网络服务。每个服务都需要:

  1. 读取配置;
  2. 打开端口;
  3. 接收请求;
  4. 解析参数;
  5. 调用业务逻辑;
  6. 捕获异常;
  7. 记录日志;
  8. 返回响应;
  9. 在程序关闭时释放资源。

如果每个项目都从头编写这些步骤,大量代码会重复。于是开发者开始提取工具函数和公共库:

kotlin
val request = HttpUtil.parse(input)
val response = JsonUtil.encode(result)

这是非常自然的第一步:把重复的局部能力做成库。

但随着项目继续发展,人们发现重复的不只是某个函数,而是整个程序骨架

每个服务都在重复相似的启动流程、请求流程、异常流程和关闭流程。真正变化的部分,可能只是:

kotlin
fun createUser(command: CreateUserCommand): User

于是新的问题出现了:能否让公共代码掌握稳定流程,业务代码只提供变化部分?

这就是框架开始出现的位置。

3. 库与框架的分界

使用库时,是应用调用库:

text
应用代码 ──调用──> 工具库

使用框架时,框架掌握主要流程,并在约定位置调用应用:

text
框架流程 ──回调──> 应用组件

例如,业务代码只声明一个事件监听器:

kotlin
@EventListener
class UserListener {
    @SubscribeEvent
    fun onCreated(event: UserCreated) = Unit
}

而框架负责:

  • 扫描这个类;
  • 识别监听器注解;
  • 创建监听器实例;
  • 注入它需要的对象;
  • 找到监听方法;
  • 将方法注册到事件总线;
  • 在事件出现时调用它;
  • 处理调用中的异常。

业务开发者只表达“当用户创建后执行这段逻辑”,没有重复实现事件基础设施。

这就是控制反转最朴素的来源:稳定流程属于框架,变化逻辑属于应用。

4. 第二次增长:对象关系开始复杂

当应用只有两三个对象时,入口可以手工组装:

kotlin
val database = Database(config)
val repository = UserRepository(database)
val service = UserService(repository)

当应用拥有数百个服务、仓库、监听器、任务和适配器后,手工组装会遇到新的系统性问题:

  • 谁负责创建每个对象?
  • 同一个对象是否应该复用?
  • 接口有多个实现时选择哪个?
  • 测试中如何替换实现?
  • 对象的创建顺序如何确定?
  • 配置值如何进入对象?
  • 插件如何接入一个编译时并不知道它的应用?

对象之间的关系逐渐形成一张很大的图:

text
Controller
└─ Service
   ├─ Repository
   │  └─ DatabasePool
   ├─ EventBus
   └─ PermissionService
      ├─ UserProvider
      └─ PolicyProvider

此时,依赖注入容器开始承担“应用级对象图组装器”的角色。

它并不是为了禁止 new,而是为了集中管理那些:

  • 生命周期较长;
  • 被多个模块共享;
  • 需要配置和注入;
  • 需要接口替换;
  • 需要框架发现;
  • 需要统一启动和停止的对象。

Rain 的 DiContextClassContextBeanCreatorBeanInjector 就是在回答这些问题。

5. 第三次增长:功能不再只来自核心团队

早期程序通常由一个团队掌握全部代码。后来,应用越来越依赖模块、插件和第三方集成。

框架不可能提前知道未来会出现哪些业务类型,但它可以提前规定扩展协议:

kotlin
@LoadBy(MyFeatureLoader::class)
annotation class MyFeature

只要模块遵循协议,Rain 就可以在扫描阶段发现它,并把信息交给对应 Loader。

这改变了软件的组织方式:

text
过去:中央入口知道所有模块

现在:模块声明自己,框架根据协议发现模块

中央代码不必随着每个插件增加而修改。框架成为多个独立模块之间的共同运行规则。

这种能力越深入,框架就越不只是“帮助写业务的工具”,而逐渐成为应用的内部操作系统

  • 它发现组件;
  • 它分配对象;
  • 它连接模块;
  • 它调度任务;
  • 它传递事件;
  • 它管理启动和关闭;
  • 它规定扩展如何进入系统。

这里的“操作系统”是帮助理解的比喻,并不是说框架真的替代 JVM 或系统内核。它表达的是:框架开始管理应用内部大量公共资源和运行规则。

6. 第四次增长:非业务问题超过业务代码

一个生产应用需要处理的事情远不止业务计算:

  • 配置与多环境覆盖;
  • 日志和链路追踪;
  • 权限与认证;
  • 事务;
  • 并发和任务调度;
  • 超时、重试和熔断;
  • 监控与健康检查;
  • 启动、停止和资源回收;
  • 测试环境与生产环境差异;
  • 模块加载与版本兼容;
  • 序列化、网络协议和错误转换。

这些问题被称为“横切关注点”或“基础设施问题”:它们会穿过很多业务模块,但又不属于某一个具体业务。

如果每个业务方法都亲自处理它们,代码会变成:

kotlin
fun createOrder(command: CreateOrder): Order {
    checkPermission()
    startTrace()
    beginTransaction()

    try {
        val result = doCreateOrder(command)
        commitTransaction()
        recordMetrics()
        return result
    } catch (error: Throwable) {
        rollbackTransaction()
        recordError(error)
        throw error
    } finally {
        endTrace()
    }
}

真正的业务逻辑被大量基础设施代码包围。

框架继续深入,是因为行业不断尝试把这些稳定而重复的机制放到统一边界中。代理、拦截器、处理链、事件、生命周期和字节码增强,都是这种探索的结果。

Rain 的 Controller 过程模型、EventBus、Job、Hook 和 AppClassloader,分别尝试为不同类型的横切与运行时问题提供统一机制。

7. 为什么框架会越来越“深”

框架最初可能只是帮我们处理请求。后来它开始管理对象、配置、线程、事务、事件、类加载和测试环境。为什么会这样?

因为问题之间并不独立

例如一个任务调度器需要:

  • 从容器获得任务实例;
  • 从配置读取执行周期;
  • 在应用启动时注册;
  • 在应用停止时取消;
  • 发生异常时发布事件;
  • 在测试中使用替代环境。

任务调度已经同时依赖 DI、配置、生命周期、事件和测试基础设施。

框架能力越多,它们之间的协作就越深。一个统一框架能够让这些机制共享相同的对象模型、配置模型和生命周期,从而避免每个模块各自发明一套规则。

因为使用者要求更高的一致性

当一个应用拥有几十个模块时,“每个模块都能工作”还不够。开发者还希望:

  • 所有模块用相同方式读取配置;
  • 所有模块能注入同一批服务;
  • 所有模块遵循同一启动顺序;
  • 所有异常能进入统一处理链;
  • 所有扩展都能被测试框架发现;
  • 所有资源都能在关闭时释放。

为了提供一致性,框架必然要进入更多运行阶段。

因为工程问题需要统一治理

单个开发者可以记住自己创建了哪些对象。大型团队无法依赖每个人都记住所有隐含约定。

框架把约定编码成可重复执行的机制:

text
不是“记得注册监听器”
而是“所有 @EventListener 都由 Loader 注册”

不是“记得关闭每个服务”
而是“所有 ApplicationService 都进入统一停止流程”

框架越深入,越能够把团队经验从口头约定变成程序规则。

8. 为什么现代工程越来越难完全丢弃框架

“不可丢弃”不应该理解为任何程序都必须使用某个具体框架。一个小工具完全可以不使用 Rain、Spring 或其他应用框架。

更准确的说法是:当软件进入长期运行、多人协作、模块众多、持续变化的阶段后,它必然需要某种框架化结构。

即使团队宣布“我们不用框架”,通常也会逐渐自己写出:

  • 全局对象注册表;
  • 配置加载器;
  • 模块初始化接口;
  • 事件分发器;
  • 请求处理链;
  • 插件扫描器;
  • 测试启动器;
  • 关闭钩子。

这些东西组合起来,就是一个内部框架,只是它可能没有被称为框架,也未必经过充分设计和验证。

框架保存了行业经验

成熟框架不仅保存代码,还保存大量失败经验:

  • 什么阶段创建对象更安全;
  • 启动失败如何报告;
  • 多实现如何消除歧义;
  • 资源怎样关闭;
  • 扩展怎样排序;
  • 测试怎样复用运行环境;
  • 如何避免每个模块互相直接依赖。

完全丢弃框架,往往意味着团队需要重新经历这些问题。

生态建立在共同协议上

数据库、网络、监控、测试和部署工具之所以能够组合,是因为它们共享框架协议。

一个模块只要知道如何成为 Bean、如何读取配置、如何注册生命周期,就能接入许多应用。框架提供共同语言,使生态组件不必针对每个项目单独适配。

人员协作需要可预期结构

新成员进入项目时,如果知道框架约定,就能预测:

  • 服务通常在哪里创建;
  • 配置从哪里进入;
  • 监听器如何注册;
  • 应用如何启动;
  • 测试怎样获得依赖。

没有共同结构时,每个项目、每个模块甚至每个开发者都可能采用不同方式。

因此,框架的重要性不仅来自运行时能力,也来自它提供的团队沟通结构。

9. 但框架不是免费的

框架越深入,它带来的代价也越大:

  • 程序行为更依赖运行时约定;
  • 错误可能从编译期移动到启动期;
  • 调用关系不再全部显式可见;
  • 调试需要理解容器、扫描和生命周期;
  • 自定义类加载与增强会增加工具链复杂度;
  • 团队可能为了“符合框架”而写出不自然的业务模型;
  • 版本升级可能影响大量模块。

所以,框架的目标不应是接管一切,而应是接管那些稳定、重复、需要统一治理的部分

下面是一个有用的判断:

问题更适合框架统一处理更适合业务显式处理
对象图组装应用级共享服务方法内部临时对象
生命周期数据库池、服务器、任务中心普通值对象
扩展发现监听器、任务、Controller单次明确调用
横切能力日志、权限、统一异常流程独特业务步骤
配置环境相关外部输入业务计算产生的局部值

框架与普通代码不是二选一。一个健康应用通常同时存在:

  • 由框架管理的应用级组件;
  • 由业务对象直接创建的局部对象;
  • 由构造器显式表达的依赖;
  • 由框架自动完成的重复组装。

10. Rain 在这条演进路线中的位置

可以把 Rain 的主要模块放回前面的历史问题中理解:

行业逐渐遇到的问题Rain 的回答
应用如何统一启动和停止ApplicationApplicationService
对象如何创建和连接DiContextClassContext、DI
配置如何进入对象ConfigManager@Config
接口如何找到实现@AutoBind、命名绑定
模块如何声明扩展@LoadByLoader
模块如何低耦合通信EventBus
定时工作如何统一调度Job 模块
请求式流程如何抽象Controller 模块
横切行为如何进入方法Hook 与类加载增强
测试如何运行在相同环境rain-test

这些模块不是互不相关的工具集合。它们共享应用扫描、DI 容器、生命周期和类加载环境,共同组成 Rain 的应用模型。

11. 用一条请求理解完整框架

假设一个外部请求最终调用 UserController.create()。表面上只是一个方法调用,背后可能经历:

text
应用启动

配置系统确定扫描包和运行模式

AppLoader 扫描 UserController、UserService 等类型

DiContext 建立类型上下文和接口绑定

ControllerLoader 发现并注册 Action

ApplicationService 启动服务器

请求进入 Router

Before 过程执行权限检查

ActionInvoker 调用 UserController.create()

UserController 使用注入的 UserService

UserService 使用 Repository 保存数据

EventBus 发布 UserCreated

监听器执行审计或通知

After 过程整理结果

响应返回

框架的意义,是让这些阶段拥有共同规则。业务开发者不必在每个 Action 中重新编写扫描、注入、权限、事件和异常处理机制。

进阶开发者则需要理解这些阶段,因为定制框架、排查启动错误和设计新模块时,必须知道自己的代码进入了哪一层。

12. 新人应该建立的完整知识地图

可以把目前学到的概念整理成六层。

第一层:普通对象

类通过构造器接收数据和协作者。即使没有框架,仍应尽量保持职责清晰、依赖显式。

第二层:依赖关系

对象不是孤立存在的。构造器参数表达直接依赖,接口表达可替换边界,多实现需要选择规则。

第三层:容器

容器读取依赖关系,创建并保存应用级对象,形成对象图。Rain 使用 DiContextClassContext 完成这些工作。

第四层:控制反转

框架不只创建对象,还掌握扫描、调用时机、扩展注册和生命周期。应用提供组件,框架驱动稳定流程。

第五层:模块与横切能力

事件、任务、Controller、配置和 Hook 依赖共同的容器与生命周期,相互组合成完整应用。

第六层:工程治理

框架把团队经验编码成统一规则,使模块可接入、行为可预测、测试可复用、生态可组合。

理解这六层后,再看到一个框架注解时,就不会只问“怎么写”,而会继续追问:

  • 它在哪个阶段被发现?
  • 它改变了对象创建,还是改变了方法调用?
  • 它由哪个 Loader 或服务处理?
  • 它依赖哪些容器状态?
  • 它失败时会在哪个阶段报错?
  • 如果不用它,需要手工实现哪些流程?

这组问题是从“框架使用者”走向“框架理解者”的关键。

13. 对 Rain 新手的学习顺序

建议按下面顺序实践:

  1. 先写普通 Kotlin 类:使用构造器明确传入依赖。
  2. 手工组装一次:在 main 中亲自创建完整对象图。
  3. 再让 Rain 组装:比较手工创建与容器创建的差异。
  4. 加入接口与两个实现:理解命名绑定和候选选择。
  5. 加入配置注入:观察外部环境如何进入对象。
  6. 加入事件监听器:理解扫描、Loader 和回调。
  7. 加入 ApplicationService:理解启动与停止控制权。
  8. 加入任务或 Controller:观察模块如何共享容器。
  9. 编写普通单元测试:手工传入 fake 依赖。
  10. 编写 RainTest:比较纯对象测试和完整上下文测试。
  11. 最后研究 Hook:先理解类加载,再使用字节码增强。

不要从最复杂的增强机制开始。框架越深入,越需要前面的对象设计、依赖关系和生命周期知识作为基础。

14. 最后的边界:使用框架,但不要失去普通代码

框架最危险的误用,是让所有业务逻辑都只能在框架环境中理解和运行。

一个良好的 Rain 业务类仍然应该像普通 Kotlin 类:

kotlin
class OrderService(
    private val repository: OrderRepository,
    private val eventBus: EventBus,
) {
    fun create(command: CreateOrder): Order {
        val order = Order(command)
        repository.save(order)
        eventBus.post(OrderCreated(order.id))
        return order
    }
}

Rain 负责在真实应用中提供 repositoryeventBus。测试也可以手工传入 fake 实现。

这代表一种健康关系:

text
业务对象表达业务和依赖
框架负责应用级组装与运行

而不是:

text
业务对象到处查找框架状态
没有框架便无法构造和测试

框架越来越重要,不代表业务代码应该越来越框架化。恰恰相反,框架越强,越应该帮助业务代码保持简单。

15. 最终总结

框架诞生于重复:许多程序不断重复相同的启动、组装、调用和关闭流程。

框架成长于规模:对象、模块、团队和运行环境增加后,需要统一管理依赖、配置、生命周期和扩展。

框架深入于协作:事件、任务、请求、测试和增强必须共享同一套应用规则,才能稳定组合。

框架难以丢弃,是因为现代软件最终总会需要某种框架化结构。放弃现成框架,往往不是消灭框架,而是开始编写自己的框架。

但框架也不是目的。框架真正应该实现的是:

把重复、稳定、需要统一治理的机制集中起来,让业务代码专注表达变化,并让开发者能够预测整个系统如何运行。

理解 Rain,也就是理解这条边界:

  • 容器组装对象,但不替你设计对象;
  • Loader 发现扩展,但不应隐藏普通业务流程;
  • 生命周期统一服务,但仍需要明确资源关系;
  • Hook 提供底层能力,但不应代替清晰设计;
  • 框架减少重复,但不会消灭复杂度。

当你能够看见框架替你完成的工作,也能够看见这些自动化背后的代价时,你就不再只是会使用一个框架,而是开始真正理解框架为什么存在。

基于 Apache License 2.0 发布