tx-di-core 0.1.6

DI framwork,依赖自动注入框架,运行时使用拓扑排序,排序依赖关系
Documentation

tx_di

基于 proc_macro + linkme 的依赖注入框架,编译期收集元数据,运行期执行注入

核心特性:Singleton / Prototype 作用域、#[tx_cst(expr)] 自定义值注入、自动依赖拓扑排序。

github: https://github.com/txtxtxtx/tx_di.git gitee: https://gitee.com/tian_xiong/tx_di.git

快速上手

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tx_di_core::{app, tx_comp, tx_cst};

// 使用插件只需要引入即可,会自动注入和配置
use tx_di_log;

// ── 单例组件(默认)──────────────────────────────────────
#[derive(Clone, Debug)]
#[tx_comp]
pub struct DbPool {
    // 无字段组件自动构建
}

// ── 带自定义注入值的单例 ─────────────────────────────────
#[derive(Clone, Debug)]
#[tx_comp]
pub struct AppConfig {
    #[tx_cst("my-app".to_string())]
    pub app_name: String,

    #[tx_cst(default_port())]
    pub port: u16,
}

fn default_port() -> u16 {
    8080
}

// ── 原型组件(每次注入独立实例)──────────────────────────
#[derive(Clone, Debug)]
#[tx_comp(scope = Prototype)]
pub struct RequestLogger {
    #[tx_cst("[REQUEST]".to_string())]
    pub prefix: String,

    #[tx_cst(Arc::new(Mutex::new(0u64)))]
    count: Arc<Mutex<u64>>,
}

impl RequestLogger {
    pub fn log(&self, msg: &str) {
        let mut c = self.count.lock().unwrap();
        *c += 1;
        println!("{} [#{}] {}", self.prefix, *c, msg);
    }
}

// ── 依赖其他组件的服务 ───────────────────────────────────
#[derive(Clone, Debug)]
#[tx_comp]
pub struct UserService {
    pub db: Arc<DbPool>,
    pub config: Arc<AppConfig>,
}

// ── 聚合组件:混用单例 + 原型 + 自定义注入 ───────────────
#[derive(Debug)]
#[tx_comp]
pub struct AppServer {
    pub user_svc: Arc<UserService>,         // 单例注入
    pub logger: Arc<RequestLogger>,         // 原型注入,独占实例

    #[tx_cst(HashMap::new())]
    pub headers: HashMap<String, String>,   // 自定义值,不走 DI

    #[tx_cst("0.0.0.0:8080".to_string())]
    pub bind_addr: String,
}

// ── 声明模块,自动生成 build_app_module() ────────────────
// 不指定组件列表时,自动扫描所有 #[tx_comp] 标记的组件
app! { AppModule }

fn main() {
    let mut ctx = build_app_module();
    let server = ctx.take::<AppServer>();
    println!("Server ready at {}", server.bind_addr);
}

核心概念

Scope(作用域)

作用域 声明方式 行为
Singleton(默认) #[tx_comp] 全局共享,首次注入时构建,缓存 Arc<T>
Prototype #[tx_comp(scope = Prototype)] 每次注入调用工厂,构造新实例

字段声明方式

写法 语义
field: Arc<T> 从 DI 容器注入,框架根据 T 的 scope 自动处理
#[tx_cst(expr)] + 任意类型 不走 DI,直接用表达式赋值,不计入依赖图

字段注入示例

#[tx_comp]
pub struct MyComponent {
    // 自动注入:框架根据 DbPool 的 scope 决定是共享还是新建
    pub db: Arc<DbPool>,
    
    // 自定义值:直接调用表达式,不参与依赖图
    #[tx_cst("custom_value".to_string())]
    pub name: String,
    
    // 调用函数
    #[tx_cst(load_config())]
    pub config: Config,
    
    // 集合类型
    #[tx_cst(HashMap::new())]
    pub cache: HashMap<String, String>,
}

关键原则:scope 标记在被注入者上,消费者不需要知道依赖是单例还是原型。

组件初始化(CompInit)

组件可以实现 CompInit trait 来执行自定义的初始化逻辑(同步或异步):

use tx_di_core::{CompInit, BuildContext, BoxFuture};

#[derive(Debug)]
#[tx_comp(init)]  // 启用 init 支持
pub struct AppServer {
    pub user_svc: Arc<UserService>,
    pub logger: Arc<RequestLogger>,
}

impl CompInit for AppServer {
    /// 同步初始化:在所有依赖注入完成后调用
    fn init(ctx: &mut BuildContext) {
        println!("AppServer 同步初始化,当前组件数: {}", ctx.len());
    }

    /// 异步初始化:用于需要 async/await 的场景
    /// 注意:返回的 Future 必须是 'static,不能持有 ctx 引用
    fn async_init(ctx: &mut BuildContext) -> BoxFuture<'static, ()> {
        let len = ctx.len();  // 先提取数据
        Box::pin(async move {
            println!("AppServer 异步初始化,组件数: {}", len);
            // 可以执行异步操作,如连接数据库、加载远程配置等
        })
    }

    /// 初始化顺序:数值越小越先初始化,默认 10000
    fn init_sort() -> i32 {
        1000  // 较晚初始化,确保依赖都已就绪
    }
}

重要提示

  • async_init 返回的 BoxFuture<'static, ()> 要求生命周期为 'static
  • 不能在 async 块中直接借用 ctx,需要先提取所需数据再移动进 async 块
  • init()async_init() 会在所有依赖构建完成后按 init_sort() 排序依次调用

配置文件加载

tx_di 支持从 TOML 配置文件加载组件配置。使用 #[tx_comp(conf)] 标记的组件会自动从配置文件中读取对应的配置段。

配置文件示例

# local/di-config.toml

[app_config]

app_name = "my-app"

port = 8080



[database]

host = "localhost"

port = 5432

name = "mydb"

配置组件定义

use serde::Deserialize;

/// 应用配置(从配置文件加载)
#[derive(Clone, Debug, Deserialize, Default)]
#[tx_comp(conf)]  // 自动从配置文件的 [app_config] 段加载
pub struct AppConfig {
    pub app_name: String,
    pub port: u16,
}

使用配置文件

// 方式 1:从配置文件加载
let mut ctx = BuildContext::new(Some("local/di-config.toml"));

// 方式 2:自动扫描(不使用配置文件)
let mut ctx = BuildContext::new::<PathBuf>(None);

ctx.run().await;

// 注入配置组件
let config = ctx.inject::<AppConfig>();
println!("App: {}, Port: {}", config.app_name, config.port);

访问全局配置对象

框架会自动创建 AppAllConfig 单例,可以直接访问原始 TOML 数据:

let global_config = ctx.inject::<AppAllConfig>();

// 获取配置值
if let Some(app_name) = global_config.get::<String>("app_config.app_name") {
    println!("App name: {}", app_name);
}

// 带默认值的获取
let port = global_config.get_or_default("app_config.port", 8080);

// 访问嵌套配置
let db_host = global_config.get::<String>("database.host");

#[tx_cst(expr)]

用于标记字段使用自定义表达式初始化,而不是从 DI 容器注入。

#[component]
pub struct Config {
    // 任意 Rust 表达式
    #[inject(std::env::var("APP_NAME").unwrap_or("default".to_string()))]
    pub name: String,

    // 函数调用
    #[inject(load_tls_config())]
    pub tls: TlsConfig,

    // 字面量
    #[inject(42u32)]
    pub timeout_secs: u32,

    // HashMap、Vec 等集合
    #[inject(HashMap::new())]
    pub cache: HashMap<String, String>,

    // 正常 DI 注入(无 #[inject])
    pub db: Singleton<DbPool>,
}

#[tx_cst(expr)] 字段不计入 DEP_IDS,不参与依赖图拓扑排序。

架构三层

用户代码
  #[tx_comp(scope = Prototype)]  struct Logger { #[tx_cst(...)] prefix: String }
  #[tx_comp]  struct AppServer { logger: Arc<Logger>, db: Arc<DbPool> }
  app! { AppModule }  // 自动扫描所有组件
         │
         │ proc_macro 展开
         ▼
tx-di-macros
  1. 解析 scope 参数 → Scope::Singleton / Scope::Prototype
  2. 解析字段:Arc<T>(DI 注入) / #[tx_cst(expr)](自定义值)
  3. 生成 ComponentDescriptor impl(含 DEP_IDS + SCOPE + build())
  4. 生成 linkme distributed_slice 注册条目
  5. app!{} 生成 build_app_module(),自动拓扑排序并注册
         │
         │ 链接器合并 link section
         ▼
tx-di-core
  - BuildContext:TypeId → CompRef 映射,支持并发访问(DashMap)
  - CompRef:内部类型擦除(Cached(Arc<dyn Any>) / Factory(fn))
  - COMPONENT_REGISTRY:全局组件元数据切片(linkme 收集)
  - topo_sort:自动拓扑排序,检测循环依赖

BuildContext API

let mut ctx = build_app_module();

// 注入组件(根据组件自身的 scope 自动处理)
let db: Arc<DbPool> = ctx.inject::<DbPool>();           // 单例:返回缓存的 Arc
let logger: Arc<RequestLogger> = ctx.inject::<RequestLogger>();  // 原型:构造新实例

// 取走所有权(仅用于单例,会移除缓存)
let owned: AppServer = ctx.take::<AppServer>();

// 调试:打印所有注册的组件及其依赖
BuildContext::debug_registry();

// 获取组件数量
println!("已注册组件数: {}", ctx.len());
println!("是否为空: {}", ctx.is_empty());

关键设计决策

1. Scope 标记在被注入者上

组件自己声明是 Singleton 还是 Prototype,消费者只需要写 Arc<T>,框架自动处理。

2. 自动拓扑排序

app!{} 不指定组件列表时,会自动从 COMPONENT_REGISTRY 收集所有组件,进行拓扑排序后按依赖顺序注册。

3. 原型不预构建

Scope::Prototype 组件在初始化时只注册工厂函数,不立即构建实例,保证每次 inject() 都是全新实例。

4. #[tx_cst(expr)] 字段不进依赖图

宏解析字段时,有 #[tx_cst] 的字段不加入 DEP_IDS,不影响拓扑排序,也不要求对应类型在 ctx 中存在。

5. 并发安全

使用 DashMap 存储组件实例,支持多线程环境下的并发注入。

约束

约束 原因
组件需 T: Send + Sync + 'static 存入 Arc<dyn Any + Send + Sync>,支持并发
组件需 Clone(推荐) 便于在多个地方共享
无字段组件自动构建为 Self {} 需要 struct 可默认构造
take() 只能用于单例 原型组件没有缓存,无法 take
避免循环依赖 框架会在运行时检测并 panic
async_init 返回 'static Future 异步任务不能持有 ctx 引用,需先提取数据

测试

cargo test

测试覆盖范围

项目包含 30+ 个测试用例,全面覆盖框架的核心功能:

单例测试 (3个)

  • test_singleton_shared - 验证单例在不同组件间共享
  • test_singleton_multiple_injects_same_instance - 多次注入返回相同实例
  • test_singleton_arc_clone_shares_data - Arc clone 只增加引用计数

原型测试 (3个)

  • test_prototype_independent - 验证原型实例相互独立
  • test_prototype_each_inject_creates_new_instance - 每次注入创建新实例
  • test_prototype_with_custom_values - 验证原型的自定义值注入

自定义值注入测试 (3个)

  • test_inject_custom_values - 验证 HashMap 和 String 注入
  • test_app_config_inject - 验证函数调用和字面量注入
  • test_custom_value_expression_evaluated_once - 验证表达式只求值一次

依赖关系测试 (2个)

  • test_dependency_injection_chain - 验证依赖注入链正确性
  • test_user_service_functionality - 验证服务功能正常

注册表测试 (2个)

  • test_registry - 打印所有组件及依赖名称
  • test_scope_on_component - 验证 scope 标记在组件自身

BuildContext API 测试 (3个)

  • test_build_context_len_and_empty - 验证初始状态
  • test_build_context_after_initialization - 验证初始化后状态
  • test_take_removes_from_context - 验证 take 移除组件

边界情况测试 (4个)

  • test_singleton_thread_safety - 多线程环境下的单例安全性
  • test_prototype_state_isolation - 原型实例状态隔离
  • test_component_with_no_dependencies - 无依赖组件注入
  • test_component_with_multiple_dependencies - 多依赖组件注入

调试功能测试 (1个)

  • test_debug_registry_output - 验证调试输出不 panic

配置文件加载测试 (9个)

  • test_load_from_config_file - 测试从配置文件加载组件
  • test_config_file_values_loaded_to_app_config - 验证配置文件中的值被正确加载到 AppConfig
  • test_global_config_access_raw_toml - 测试全局配置对象 AppAllConfig 可以访问原始 TOML 数据
  • test_missing_config_file_uses_defaults - 测试配置文件不存在时使用默认值
  • test_config_get_or_default - 测试 get_or_default 方法
  • test_complex_config_nested_access - 测试复杂配置文件的多层级访问
  • test_config_value_type_conversion - 测试配置值的类型转换
  • test_app_all_config_is_singleton - 测试 AppAllConfig 在上下文中是单例
  • test_auto_scan_mode - 测试自动扫描模式

运行示例

# 运行所有测试

cargo test


# 运行特定测试

cargo test test_singleton_shared


# 运行测试并显示输出

cargo test -- --nocapture


# 运行测试并显示时间

cargo test -- --show-output