Skip to content

Rain 与 Spring

Rain 吸收了不少 Spring 已经验证过的设计思想,但 Rain 不是 Spring 的兼容实现,也不是简单缩小后的 Spring。比较的目的,是借助熟悉概念理解 Rain,而不是以 Spring 的功能清单要求 Rain。

没用过 Spring 也可以读这一页

这一页中的 Spring 可以先理解为一个发展时间很长、功能非常完整的 JVM 应用框架。它解决的很多基础问题与 Rain 相似:创建对象、连接依赖、读取配置、驱动生命周期和提供扩展机制。

我们引入 Spring,不是为了要求你先学 Spring,而是为了展示:面对相似问题,框架可能做出不同设计选择。

阅读比较时重点观察三件事:

  1. 两者想解决的问题是否相同?
  2. 两者把规则放在了哪里?
  3. 更简单的规则减少了什么,也失去了什么?

比较框架的方法

不要只比较“有没有某个注解”。更有价值的是比较对象如何被发现、元数据保存在哪里、扩展何时执行、错误何时暴露,以及开发者能否预测运行过程。

共同的设计方向

容器负责对象组装

两者都鼓励业务对象声明依赖,让容器创建并连接对象图。构造器注入、字段注入、命名候选和集合注入都体现了这一点。

应用由组件与扩展点组成

应用不再是不断扩大的 main 函数,而由服务、监听器、任务、控制器和基础设施扩展组成。框架发现组件,并把它们放入统一运行流程。

面向接口与可替换实现

Rain 的 @AutoBind、命名实例和集合注入,与 Spring 的按类型注入、限定符和多实现注入有相似目标:调用方依赖能力,运行时决定实现。

声明式编程

@EventListener@SubscribeEvent@CronTab@Cron@LoadBy 允许开发者声明“这是什么”或“何时执行”,框架负责重复的注册与调度机制。

这延续了 Spring 生态中非常成功的原则:稳定机制放入框架,变化策略留给应用。

Bean 发现哲学不同

Spring 通常先建立明确的 Bean 元数据,来源包括组件注解、@Bean 方法、XML、自动配置、Import 和编程式 BeanDefinition 注册。

Rain 当前更直接:扫描范围内只要不是接口或抽象类,就属于可实例化类型。

可以概括为两种不同默认值:

  • Spring 默认不管理,声明或配置后管理;
  • Rain 在扫描边界内默认具备被容器创建的能力。

Rain 少写组件标记,但更依赖严格的包扫描边界。Spring 的注册更显式,元数据也更丰富。

Rain 没有复制 BeanDefinition 体系

Spring 容器围绕 BeanDefinition、BeanFactory、ApplicationContext、后处理器和生命周期协议建立了庞大而成熟的体系。

Rain 使用更直接的 ClassContextBeanCreatorBeanInjectorDataReader 模型。目前没有与 Spring 同等丰富的:

  • Bean 定义继承与合并;
  • 条件化注册体系;
  • 完整作用域 SPI;
  • 大量 BeanPostProcessor 扩展;
  • FactoryBean 语义与成熟工厂生态;
  • 统一而细致的初始化和销毁协议。

Rain 的模型更小、更容易阅读和修改;Spring 的模型更成熟、更适合大量第三方基础设施在同一容器中协作。

自动绑定与候选解析不同

Rain 的接口需要 @AutoBind 才会自动收集实现,绑定名称来自实现类的 @Named

Spring 则在已注册 BeanDefinition 中按类型寻找候选,并通过 @Qualifier@Primary、条件和优先级等规则消除歧义。

两者都解决“接口如何找到实现”,但 Rain 是较小的扫描绑定模型,Spring 是建立在完整 Bean 元数据上的候选解析模型。

增强技术路线不同

Spring 常见 AOP、事务和安全拦截大量依赖运行时代理,包括 JDK 动态代理和子类代理。代理对象包裹目标对象,在方法边界插入行为。

Rain 的突出路线是自定义 AppClassloader 加 ASM ClassTransformer,在类定义前修改字节码。Hook 也建立在类加载增强之上。

方式优点代价
Spring 式代理模型成熟、拦截链清晰、通常不修改原类字节码需要理解自调用、final 类型和代理类型边界
Rain 类加载增强可直接改变目标类行为,能覆盖部分代理难处理场景对类加载顺序敏感,调试和工具链更复杂

Rain README 强调 final 类、final 方法、静态和实例方法的 Hook 支持,这正体现了它没有完全沿用 Spring 的代理路线。

扩展协议不同

Spring 拥有 BeanPostProcessor、ImportSelector、Registrar、ApplicationListener、自动配置等多层扩展机制。

Rain 主要通过 @LoadBy 把类型或注解关联到 Loader

text
扫描类 -> 识别 @LoadBy -> 聚合 LoadItem -> Loader.load(items)

Event 和 Job 模块都建立在这个统一协议上。它容易理解,但复杂条件、分阶段后处理和精细排序能力目前不如 Spring 完整。

生命周期不同

Rain 的核心生命周期围绕 ApplicationService.start() / stop() 和应用状态事件。服务按优先级从小到大启动,当前实现停止时仍按相同顺序调用。

Spring 提供初始化、销毁、SmartLifecycle、阶段控制、容器事件和上下文层级。不能把 Spring 中关于回调存在性和关闭顺序的经验直接带入 Rain。

配置环境不同

Rain 使用 confconf/{runMode} 和运行目录配置进行合并,通过 rain.runMode 选择模式。

它与 Spring Profiles、PropertySource 有相似目标,但并不是 Spring Environment 的兼容实现。不要假设 Rain 支持 Spring 的配置优先级、占位符、条件注解或绑定规则。

定位和生态不同

Spring 是覆盖企业 JVM 大量场景的成熟生态,拥有广泛的第三方集成、生产监控、安全、数据访问、云原生与长期兼容实践。

Rain 更像一个可阅读、可改造、模块化的 JVM 应用框架基础。开发者能更接近容器、类加载器和扩展机制本身,也需要自行选择、实现和验证更多生产能力。

最合适的理解不是“Rain 比 Spring 少哪些功能”,而是:Rain 选择了更小、更直接、更容易改造的核心模型,并在类加载增强与 Loader 协议上形成自己的路线。

用同一个问题观察两种设计

假设我们要让容器知道 UserService 是一个应用组件。

在 Spring 常见模型中,开发者通常显式标记或配置它:

kotlin
@Service
class UserService

在 Rain 当前模型中,只要它是扫描范围内的具体类,就具备被容器创建的条件:

kotlin
class UserService

Spring 的问题是“哪些类型明确注册成 Bean”;Rain 的问题是“哪些具体类型进入了扫描边界”。

两者都能减少手工组装,但注意力不同:

  • 使用 Spring 时,要检查组件是否注册、条件是否成立;
  • 使用 Rain 时,要检查扫描包是否正确、类型是否可构造。

这就是“相同目标,不同心智模型”。

进阶思考

  1. 显式注册和扫描范围默认管理,哪一种更适合大型团队?为什么?
  2. Rain 的模型更容易阅读源码,是否意味着大型应用也一定更容易维护?
  3. Spring 使用代理、Rain 使用类加载增强,它们分别会让哪类问题更容易,哪类问题更困难?
  4. 如果未来 Rain 引入更丰富的条件化 Bean 元数据,它会获得什么,又增加什么复杂度?

基于 Apache License 2.0 发布