You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Axum的Rust依赖注入与错误处理问题求助

问题与解决方案

问题描述

作为以C#开发为主、具备一定Rust经验的开发者,在基于Axum构建Web应用时,尝试抽象SQLx连接构建数据访问层,通过trait封装控制器对象并放入单一State实现依赖注入。此前将dyn trait包装在Arc中并要求实现Send + Sync,Axum可正常传递。

为避免原DataLayer trait遇到数据库错误直接panic,修改trait添加关联类型Error以支持自定义错误返回:

#[async_trait]
pub trait DataLayer : Send + Sync {
    type Error : std::error::Error + Send + Sync;

    async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Result<Option<UserDbModel>, Self::Error>;
    async fn get_user_by_email<'a>(&self, email: &'a str) -> Result<Option<UserDbModel>, Self::Error>;

    async fn get_refr_token_by_token<'a>(&self, token: &'a str) -> Result<Option<RefrTokenDbModel>, Self::Error>;
    async fn get_refr_token_by_id(&self, token: i32) -> Result<Option<RefrTokenDbModel>, Self::Error>;
    async fn create_refr_token(&self, refr_token: CreateRefrTokenDbModel) -> Result<u64, Self::Error>;
    async fn revoke_refr_token(&self, token: RevokeRefrTokenDbModel) -> Result<(), Self::Error>;
}

具体实现(如DbDataLayer)使用sqlx::Error作为关联类型:

pub struct DbDataLayer {
    db: MySqlPool,
    settings: TokenSettings
}

#[async_trait]
impl DataLayer for DbDataLayer {
    type Error = sqlx::Error;
    async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> sqlx::Result<Option<UserDbModel>> {
        let user = sqlx::query_as!(UserDbModel, r"
            SELECT id, email, password_hash as pwd_hash, role FROM users
            WHERE id = ?
        ", user_id).fetch_one(&self.db).await;

        match user {
            Ok(user) => Ok(Some(user)),
            Err(sqlx::Error::RowNotFound) => Ok(None)
        }
    }
// 其他方法实现省略
}

但尝试注入时使用Arc<dyn DataLayer<Error = dyn Error + Send + Sync>>触发编译错误:

the size for values of type `(dyn StdError + Send + Sync + 'static)` cannot be known at compilation time
the trait `Sized` is not implemented for `(dyn StdError + Send + Sync + 'static)`

需要解决:如何在支持Error类型泛化的同时修复编译错误?以及C#架构思路是否适配Rust?


编译错误解决方案

核心原因

Rust的trait关联类型默认要求实现Sized(编译时可知大小),而dyn Error + Send + Sync是未大小化类型(Unsized Type),无法满足关联类型的Sized约束,因此编译失败。

方案1:定义全局统一错误枚举(推荐)

创建一个包含所有可能错误类型的枚举,作为DataLayer的固定关联类型,既满足Sized要求,又能统一错误处理逻辑,符合Rust idiomatic风格。

步骤1:定义全局错误类型

#[derive(Debug)]
pub enum AppError {
    Sqlx(sqlx::Error),
    // 可扩展添加其他错误类型,如业务逻辑错误、IO错误等
}

// 实现Error trait
impl std::error::Error for AppError {}

// 实现Display trait,用于错误信息展示
impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AppError::Sqlx(e) => write!(f, "数据库错误: {}", e),
            // 其他错误类型的格式化逻辑
        }
    }
}

// 实现From转换,方便将sqlx::Error自动转为AppError
impl From<sqlx::Error> for AppError {
    fn from(e: sqlx::Error) -> Self {
        AppError::Sqlx(e)
    }
}

步骤2:修改DataLayer trait

将关联类型固定为AppError:

#[async_trait]
pub trait DataLayer : Send + Sync {
    type Error = AppError;

    async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Result<Option<UserDbModel>, Self::Error>;
    // 其他方法签名保持不变
}

步骤3:修改DbDataLayer实现

利用?操作符自动将sqlx::Error转换为AppError:

#[async_trait]
impl DataLayer for DbDataLayer {
    async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Result<Option<UserDbModel>, AppError> {
        let user = sqlx::query_as!(UserDbModel, r"
            SELECT id, email, password_hash as pwd_hash, role FROM users
            WHERE id = ?
        ", user_id).fetch_one(&self.db).await?;
        
        Ok(Some(user))
    }

    // 其他方法同理,使用?自动转换错误
}

注入使用

此时可以直接使用Arc<dyn DataLayer>,无需指定关联类型,编译正常:

let data_layer = Arc::new(DbDataLayer { db, settings });
let app = Router::new()
    .route("/user/:id", get(get_user_handler))
    .with_state(data_layer);

方案2:使用装箱错误类型

如果需要保留错误类型的灵活性,可将关联类型指定为Box<dyn Error + Send + Sync + 'static>,通过装箱将未大小化类型转为大小化类型。

修改DataLayer trait

#[async_trait]
pub trait DataLayer : Send + Sync {
    type Error = Box<dyn std::error::Error + Send + Sync + 'static>;

    async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Result<Option<UserDbModel>, Self::Error>;
    // 其他方法签名保持不变
}

修改DbDataLayer实现

将sqlx::Error装箱后返回:

#[async_trait]
impl DataLayer for DbDataLayer {
    async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Result<Option<UserDbModel>, Box<dyn std::error::Error + Send + Sync + 'static>> {
        let user = sqlx::query_as!(UserDbModel, r"
            SELECT id, email, password_hash as pwd_hash, role FROM users
            WHERE id = ?
        ", user_id).fetch_one(&self.db).await;

        match user {
            Ok(user) => Ok(Some(user)),
            Err(sqlx::Error::RowNotFound) => Ok(None),
            Err(e) => Err(Box::new(e)),
        }
    }
}

C#架构思路在Rust中的适配性分析

1. 依赖注入思路可行,但实现方式不同

Rust没有内置的DI容器(如ASP.NET Core的DI系统),Axum通过State结合Arc<dyn Trait>实现的手动DI是社区常用模式,你的思路完全适配Rust的Web开发场景。

2. 优先静态分发,必要时使用动态分发

C#依赖注入默认偏向动态分发(运行时解析),但Rust更鼓励静态分发(泛型):

  • 如果不需要运行时切换数据层实现(如仅生产用SQLx,测试用Mock),可以将CoreAuthService定义为泛型结构体:
    pub struct CoreAuthService<T: DataLayer> {
        data_layer: T,
    }
    
    这种方式编译时确定类型,避免trait对象的性能开销,更符合Rust的零成本抽象理念。
  • 若需要运行时多态(如动态切换数据源),使用Arc<dyn DataLayer>的动态分发是合理选择。

3. 错误处理需贴合Rust范式

C#使用异常处理错误,而Rust通过Result显式处理错误:

  • 统一错误枚举(方案1)是Rust的惯用做法,它能让错误类型更明确,避免动态分发的开销,同时方便上层统一处理错误。
  • 返回Box<dyn Error>(方案2)虽然灵活,但会丢失错误类型的具体信息,上层处理时只能通过downcast获取具体类型,增加复杂度。

4. trait设计需注意Rust的约束

Rust的trait关联类型有Sized默认约束,这是你遇到编译错误的核心原因,设计trait时需要考虑类型大小的要求,避免使用未大小化类型作为关联类型。


内容的提问来源于stack exchange,提问作者Christian Schmid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 13:17:57