找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4559

积分

0

好友

593

主题
发表于 1 小时前 | 查看: 7| 回复: 0

前阵子后台收到一条私信,写得很认真:

大佬你好,我 Java 后端写了五年,Spring 全家桶很熟。最近在学 Rust,最不习惯的不是语法,是依赖注入。没有 @Autowired、没有 @Service、没有容器扫描,我的 Service 和 Repository 到底怎么互相引用?总不能每个地方都 new 吧?每次 new 一个,测试还怎么 mock?

这个问题几乎是每个 Java 转 Rust 的人都会遇到的,包括当年的我。今天就把它讲透。

先说结论:依赖注入没消失,消失的是容器

先把依赖注入拆开看,它其实只有两件事:依赖抽象(面向接口编程)和 组装(谁把实现传给谁)。

Spring 用容器把这两件事自动化了:扫描注解、反射创建、按类型注入、管理生命周期。你只写 @Autowired,剩下全交给魔法。

Rust 没这个容器,但抽象和组装这两件事一样都没少,只是换成了编译期可见的代码。你不再写 @Autowired,改在入口处手动把依赖树组装一遍,往下传。这个过程有个名字,叫 组合根(composition root)

很多人转过来第一反应是“这退化了”,恰恰相反。Spring 的注入是运行时魔法,依赖配错了要启动时才炸;Rust 的组装是编译期代码,依赖对不上编译器当场报错。用魔法换检查,这笔交易不亏。

Spring 的容器到底帮你干了啥

先回忆一下你过去依赖它的四件事,后面逐一对应:

  1. 扫描:启动时扫 @Service / @Repository,收集 bean
  2. 组装:按构造参数类型,自动找 bean 塞进去
  3. 生命周期:单例还是每次新建,容器管
  4. 可替换:测试时换一个 mock 实现,不碰业务代码

Rust 没有运行时反射,这四件事没法自动做。但每一件都有对应的显式做法,下面一个一个来。

替代一:组合根,main 里组装一次

这是 Rust 生态的主流做法,也是我的基本盘。所有依赖在 main(或一个 build_app() 函数)里组装,往下一层一层传。

// main.rs:组合根,依赖树只在这组装
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let pool = PgPool::connect(&cfg.database_url).await?;

    let repo = UserRepoPg::new(pool.clone());
    let mailer = MailerSmtp::new(cfg.smtp);
    let svc = UserService::new(Arc::new(repo), Arc::new(mailer));

    let app = Router::new()
        .route("/users", get(handlers::list_users))
        .route("/users", post(handlers::create_user))
        .with_state(Arc::new(svc));

    let listener = TcpListener::bind(&cfg.addr).await?;
    axum::serve(listener, app).await?;
    Ok(())
}

看着是不是很像你没用 Spring 之前手写的 main?对,本质就是那套,只是 Rust 社区把它扶正成了标准做法,还给起了个名字。

好处是显式的:整棵依赖树在一个函数里,谁依赖谁一目了然,不用猜 bean 是从哪来的。 你担心的“每个地方都 new”,不会发生,业务代码里你拿到的都是组装好的实例,只有组合根这一个地方在做组装。

生命周期也是显式的:需要共享的依赖,用 Arc 包一层传下去,clone 一个 Arc 只是引用计数加一,成本几乎为零。这就是 Rust 版“单例”,不需要容器管。

替代二:trait 抽象,把 mock 变成编译期的事

Spring 测试时换 mock,靠的是接口加容器重配。Rust 里更直接:业务代码面向 trait 编程,测试传一个假实现进去。

// 面向 trait,不面向实现
trait UserRepo: Send + Sync {
    async fn find(&self, id: i64) -> Result<User, Error>;
}

struct UserService {
    repo: Arc<dyn UserRepo>,
    mailer: Arc<dyn Mailer>,
}

测试时传假实现:

struct FakeRepo;

#[async_trait]
impl UserRepo for FakeRepo {
    async fn find(&self, id: i64) -> Result<User, Error> {
        Ok(User { id, name: "test".into() })
    }
}

#[tokio::test]
async fn test_find_user() {
    let svc = UserService::new(Arc::new(FakeRepo), Arc::new(FakeMailer));
    let user = svc.get_user(1).await.unwrap();
    assert_eq!(user.name, "test");
}

没有 mock 框架,没有运行时替换,假实现就是普通代码。你连 mockito 都不用装。

再补一个选择:如果不需要运行时多态,用泛型比 trait object 更好,编译期就确定实现,零开销:

struct UserService<R: UserRepo> {
    repo: R,
}

什么时候用泛型、什么时候用 Arc<dyn Trait>,判断标准一句话:一个进程里只有一个实现,用泛型;配置驱动、插件化、需要运行时换,用 trait object。

替代三:框架自带的“注入”,axum 的 State

Web 框架层面,axum 有自己的注入机制:State 提取器。你在组合根把依赖塞进 Router,handler 里直接要:

async fn list_users(
    State(svc): State<Arc<UserService>>,
) -> Result<Json<Vec<User>>, ApiError> {
    let users = svc.list().await?;
    Ok(Json(users))
}

这其实就是 Spring @Autowired 在 Rust 里的样子,只是不靠反射,靠类型。handler 要求 State<Arc<UserService>>,框架在请求进来时把它取出来递给你,类型对不上编译期就报错。

那 Rust 的 DI 框架呢?为什么不是主流

确实有人做了。shaku 是编译期 DI,用宏模拟 Spring 的组件模型,单例、瞬态都有;国内还有位老哥辞职四个月用 Rust 复刻了个 Spring Boot(predawn + rudi),启动服务一行代码,注解、扫描、注入全套都有。

但这些都不是主流,原因有三:

  1. 学习成本不值。宏魔法写起来像 Java,出了问题排查起来比 Java 还难,而手写组合根就那么几十行。
  2. 违背显式哲学。Rust 社区的核心价值观是“能编译期检查的绝不放运行时”,DI 容器的运行时解析恰恰是反的。
  3. 生态没长起来。shaku 的 axum 集成 2025 年初才跟上 axum 0.8,容器跟着框架版本追,手写组合根永远不用追。

我的建议是别碰,组合根加 trait 够你用 99% 的场景。 真到那 1%(比如你要做插件系统),先想清楚是不是设计问题,而不是缺个容器。

全局变量:最后的选项,也是最大的坑

从 Java 过来的人容易想:没有容器,我把 Service 做成全局单例不就行了?

Rust 里全局可变状态是编译器都不允许的,static mut 读写要 unsafe,这是有原因的,数据竞争是未定义行为。用 OnceLock 可以安全地初始化全局共享数据:

static DB: OnceLock<PgPool> = OnceLock::new();

但这是最后的选项,不是第一选择。全局变量的代价是测试时没法换:测试想用假数据库,全局池子还指着真的。宁可多传几层参数,也别图省事挂全局。

守住三条底线,离开容器也不慌

从 Spring 切过来,只要守住这三条,依赖这件事不会成为你的麻烦:

  1. 依赖走构造参数显式传。 别搞全局可变状态,别在业务代码里直接 new 依赖,谁创建谁负责。
  2. 面向 trait 编程。 Service 依赖接口不依赖实现,测试才能换假实现,这是可测试性的来源。
  3. 组合根只留一个。 main 或 build_app() 里组装一次,别在代码里到处组装,组装点越多越乱。

值得反复研读的项目

想知道 Rust 生态怎么处理依赖,看这几个:

项目 为什么值得看
axum 看 State / Extension 怎么做框架层的“注入”,不靠反射靠类型
shaku 模拟 Spring 组件模型的编译期 DI,看它为了像 Java 做了哪些妥协
predawn + rudi 中文社区有人用 Rust 复刻 Spring Boot,一行代码启动,看看代价是什么
ripgrep 经典单体,看它怎么在 main 里手动组装,极简范例

最后说两句

Java 老手转 Rust,最难受的从来不是语法,是把“容器帮我搞定”的思维换成“我自己组装”。这个转换期大概一两个项目就适应了,适应之后你会开始嫌弃 Spring 的启动日志里那堆 bean 初始化提示,太吵了。




上一篇:未授权访问自动化扫描Skill:基于Chrome DevTools MCP的Web全量API探测实战
下一篇:DeepSeek Harness 必装的10个插件实测推荐
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-28 06:46 , Processed in 1.058805 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表