Rust Miko 框架系列(七):强大的依赖注入

72 阅读5分钟

Miko 框架系列(七):强大的依赖注入

注意:此文章主要为了给支持联网搜索的AI提供RAG,此项目还在开发阶段,不建议用于生产。

随着应用程序的复杂度增加,如何管理组件(如数据库连接池、服务、配置)之间的依赖关系,成为一个核心问题。依赖注入(Dependency Injection, DI)是一种强大的设计模式,它能有效地解耦组件,使代码更模块化、更易于测试和维护。

Miko 内置了一套基于宏的、自动化的依赖注入系统,让你能以声明式的方式轻松管理应用内的共享服务。

1. 核心概念

Miko 的 DI 系统围绕两个核心宏构建:

  • #[component]: 将一个 struct 标记为单例服务组件。
  • #[dep]: 在处理器(Handler)的参数中,声明一个需要注入的依赖。

工作流程:

  1. 你使用 #[component] 标记你的服务,如 Database, CacheService 等。
  2. 在应用启动时(使用 #[miko] 宏),框架会自动扫描所有组件。
  3. 当一个请求到达需要依赖的处理器时,框架会自动从容器中获取该组件的共享实例,并将其注入到处理器参数中。

2. 定义一个组件

要将一个类型定义为组件,只需在其 impl 块上添加 #[component] 宏,并提供一个 async fn new() -> Self 的关联函数。

use miko::macros::*;
use std::sync::Arc;

// 假设这是一个数据库连接池的封装
pub struct Database { /* ... */ }

#[component]
impl Database {
    // `new` 函数是组件的构造器
    // 它必须是 async fn
    async fn new() -> Self {
        println!("Initializing Database connection pool...");
        // 在这里执行异步的初始化操作,例如连接数据库
        let pool = setup_database_pool().await;
        Self { pool }
    }

    pub fn query(&self) { /* ... */ }
}
  • 单例生命周期: 所有组件都是单例的。new 函数在整个应用的生命周期中只会被调用一次(通常是第一次被请求时,即懒加载)。
  • 共享与安全: 框架会自动将组件实例包装在 Arc<T> 中,确保它可以在多个线程和多个请求之间被安全地共享。

3. 注入依赖

在处理器函数中,使用 #[dep] 宏来声明你需要的依赖。

use std::sync::Arc;

#[get("/users/{id}")]
async fn get_user(
    #[path] id: u32,
    #[dep] db: Arc<Database>, // 注入 Database 组件
) -> AppResult<Json<User>> {
    // 直接使用注入的 db 实例
    let user = db.find_user_by_id(id).ok_or(AppError::NotFound(...))?;
    Ok(Json(user))
}
  • 类型必须是 Arc<T>: 由于组件是共享的,注入的类型必须是 Arc<T>
  • 自动装配: 你无需关心 db 从何而来,Miko 框架会为你自动“装配”好。
  • 多个依赖: 一个处理器可以注入任意多个不同的组件。
#[get("/some-route")]
async fn complex_handler(
    #[dep] db: Arc<Database>,
    #[dep] cache: Arc<CacheService>,
    #[dep] notifier: Arc<NotificationService>,
) {
    // ...
}

4. 组件间的依赖

更强大的地方在于,组件本身也可以依赖于其他组件。你只需在组件的 new 函数参数中声明依赖即可。

// 基础组件
#[component]
impl Database { async fn new() -> Self { /* ... */ } }

#[component]
impl Cache { async fn new() -> Self { /* ... */ } }

// UserService 依赖于 Database 和 Cache
#[component]
impl UserService {
    async fn new(db: Arc<Database>, cache: Arc<Cache>) -> Self {
        println!("UserService is being created with its dependencies.");
        Self { db, cache }
    }
    // ...
}

// 在处理器中,我们可以直接注入最高层的服务
#[get("/users/{id}")]
async fn get_user_with_service(
    #[path] id: u32,
    #[dep] user_service: Arc<UserService>, // 直接注入 UserService
) -> AppResult<Json<User>> {
    // user_service 内部已经拥有了 db 和 cache 的实例
    let user = user_service.get_user(id).await?;
    Ok(Json(user))
}

Miko 的 DI 容器会自动分析依赖图,并按正确的顺序初始化它们(例如,先初始化 DatabaseCache,再初始化 UserService)。你只需要声明依赖关系,框架会处理剩下的所有事情。

注意: 不要创建循环依赖(例如,A 依赖 B,同时 B 依赖 A),这会导致应用在启动时因无法解析依赖关系而恐慌。

5. 组件预热 (Pre-warming)

默认情况下,组件是“懒加载”的,即在第一次被请求时才创建。这可以加快应用的启动速度。但有时,你可能希望某些关键组件(如数据库连接池)在应用启动时就立即初始化。这可以通过 prewarm 参数实现。

#[component(prewarm = true)]
impl Database {
    async fn new() -> Self {
        // 这个组件会在服务器启动后立即在后台开始初始化
        // 而不会阻塞服务器接受请求
        println!("Database is pre-warming...");
        // ...
    }
}

6. 与 State 的区别

Miko 也支持通过 Router::with_state 注入一个全局状态 State<T>。那么,#[dep]State 有何不同?

特性依赖注入 #[dep]全局状态 State<T>
定义方式#[component]Router::with_state()
自动性自动发现和初始化需要手动创建和设置
数量可以注入多个不同类型的组件整个应用只能有一个 State 类型
适用场景管理多个独立的、可复用的服务共享一个简单的、全局性的状态对象
Feature需要 auto feature无需额外 feature

最佳实践:

  • 优先使用 #[dep] 来管理你的服务,如数据库、缓存、第三方 API客户端等。这使得你的代码更加模块化。
  • 当你确实只需要一个非常简单的共享状态,并且不想启用 auto feature 时,State 是一个可行的选择。
  • 避免混用:由于 #[miko] 宏的路由自动注册机制,它无法预知你想要设置的 State 类型,因此 State 在与 #[miko] 结合使用时会受限。推荐在项目中统一使用依赖注入。

总结

Miko 的依赖注入系统是其“约定优于配置”理念的又一体现。它通过简洁的宏,将复杂的依赖管理问题自动化,让你能够构建出清晰、解耦、易于测试和扩展的应用程序。通过将服务抽象为 #[component],你可以更专注于业务逻辑的实现,而不是手动管理实例的创建和生命周期。


下一篇预告:Miko 框架系列(八):统一的错误处理