Rust Web 初学者必看:用一个宏搞定错误处理和统一返回

引言

这篇是Rust九九八十一难第七篇,学会定义Rust过程宏。 目标是用过程宏(proc_macro_derive 或 attribute macro)封装 Response<T> 的生成逻辑和错误返回,减少 match / Json() / StatusCode 的重复书写。

当前的代码可能是这样的:

rust 复制代码
use axum::{Json, response::IntoResponse, http::StatusCode};

#[derive(serde::Serialize)]
struct User {
    name: String,
    age: u8,
}

async fn get_user() -> impl IntoResponse {
    match get_user_from_db().await {
        Ok(u) => (StatusCode::OK, Json(u)).into_response(),
        Err(e) => (StatusCode::BAD_REQUEST, Json(ErrorResponse::from(e))).into_response(),
    }
}

希望写成更优雅的:

rust 复制代码
#[json_response]
async fn get_user() -> MyResult<User, MyError> {
    get_user_from_db().await
}

然后自动生成带有统一错误处理和 JSON 包装的响应。

一、总体设置

1、设置目录结构

lua 复制代码
rust_web_resp/
├── Cargo.toml
├── axum_error_macro
│		├── src/
│   	├── lib.rs
│   	├── Cargo.toml
├── axum_macro_example
│		├── src/
│   	├── main.rs
│   	├── Cargo.toml
├── test.http

采用workspace的方式组织代码,宏定义放在axum_error_macro,main入口放在axum_macro_example。

  • 为什么用worksapce

    • proc-macro = true 的 crate 是特殊类型,不能直接包含 axum 服务器等逻辑
    • 如果不使用 workspace,你需要发布到 crates.io 或写相对路径,很麻烦
    • Workspace 让多个 crate(宏 + 应用)共用 target、依赖缓存,编译更快
    • 依赖不同,例如宏 crate 依赖 syn, quote,应用 crate 依赖 axum

    过程宏本质上是独立的编译期工具,不能与业务逻辑共存于同一个 crate,因此必须拆分。而使用 Workspace 是当前 Rust 官方推荐、最清晰、最符合工程实践的项目结构方式。

2、配置workspace Cargo依赖

rust 复制代码
[workspace]
members = [
    "axum_error_macro",
    "axum_macro_example",
]

二、宏定义

1、axum_error_macro/Cargo.toml(配置依赖包)

rust 复制代码
[package]
name = "axum_error_macro"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"

📌 [lib] 区域:proc-macro = true

  • 这个配置告诉 Cargo:当前 crate 是一个过程宏库

  • 设置 proc-macro = true 后,这个库会被编译成一种特殊的动态库,供其他 Rust 代码在编译期通过 #[derive(...)]#[attribute] 等方式调用。

  • 普通函数库(lib)是运行时使用的,而过程宏库是编译时运行的代码

📌 **[dependencies] **区域

  • syn 是用来 解析 Rust 源代码语法树(AST) 的库。

    • 在过程宏中,Rust 编译器会把传入的源码以 TokenStream 的形式交给你的宏代码,而你需要用 syn 来将它"反序列化"为结构化的 AST,方便分析和修改。
    • features = ["full"] 的含义:支持解析 Rust 中所有语法结构(函数、结构体、枚举、trait、表达式等)。过程宏通常都需要这个,否则很多语法没法处理。
  • quote 用于 把你构造的 Rust 代码转换回 TokenStream,供编译器使用。简单说,它是过程宏的"代码生成器"。

🎯 三者的关系总结

组件 作用 相当于
proc-macro = true 告诉 Cargo 这是过程宏库 编译器入口声明
syn 把输入源代码解析成 AST 读代码
quote 把 AST 生成新的代码 写代码

2、axum_error_macro/src/lib.rs(过程宏定义)

下面代码的作用是自动封装 Axum 处理函数的返回逻辑 ,避免手写重复的 Json(...)IntoResponse 等。

rust 复制代码
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn json_response(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);
    let vis = &input.vis;
    let sig = &input.sig;
    let block = &input.block;
    let name = &sig.ident;

    let expanded = quote! {
        #vis #sig {
            use axum::{response::IntoResponse, Json, http::StatusCode};
            let result = (async move #block).await;
            Json(result)
        }
        
    };

    TokenStream::from(expanded)
}
  • #[proc_macro_attribute] 表示这是一个 属性宏

    • _attr:宏调用时括号里的参数(这里没用)
    • item:被标注的整个函数源码
  • parse_macro_input!(item as ItemFn) 使用 synTokenStream 解析为 ItemFn(即一个函数定义的 AST)。

    • input.vis → 可见性(如 pub
    • input.sig → 签名(函数名、参数、返回类型等)
    • input.block→ 函数体{ ... }
  • quote! {#vis #sig ...}} : 生成新的函数实现,替换原始函数体,实现自动封装响应逻辑。这是核心逻辑

    • 自动包一层 Json(...),让 axum 能正常返回 JSON
    • 自动插入 use,不需要手写导入
    • async move #block:调用原来的函数,确保函数体逻辑在 async context 中执行(axum handler是异步的)
    • Json(result):用json封装返回结果
  • TokenStream::from(expanded): 把生成的新代码返回给编译器。

三、Web项目引用宏

1、axum_macro_example/Cargo.toml(配置依赖包)

rust 复制代码
[package]
name = "axum_macro_example"
version = "0.1.0"
edition = "2024"

[dependencies]
axum = "0.6"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror="1"
# 引用同 workspace 内的宏库
axum_error_macro = { path = "../axum_error_macro" }

2、axum_macro_example/src/main.rs

实现一个 HTTP 服务器,包含两个接口: /user和/hello,user添加了宏,hello手动返回。

rust 复制代码
use axum::{Json, Router, routing::get};
use axum_error_macro::api_response;
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Serialize)]
struct User {
    name: String,
    age: u8,
}

#[derive(Debug, Error)]
enum MyError {
    #[error("User not found")]
    NotFound,
    #[error("DB error")]
    DbError,
}

#[derive(Serialize)]
struct ErrorResponse {
    code: u16,
    message: String,
}

impl From<MyError> for ErrorResponse {
    fn from(err: MyError) -> Self {
        match err {
            MyError::NotFound => Self {
                code: 404,
                message: err.to_string(),
            },
            MyError::DbError => Self {
                code: 500,
                message: err.to_string(),
            },
        }
    }
}
type MyResult<T> = Json<ApiResponse<T>>;
#[json_response]
async fn get_user() -> MyResult<User> {
    // 模拟数据库查询

    // 模拟数据库查询
    let user = Some(User {
        name: "Tyler".to_string(),
        age: 30,
    });

    match user {
        Some(u) => ApiResponse::success(u),
        None => ApiResponse::error(10086, "User not found"),
    }
}

#[derive(Serialize, Deserialize)]
struct ApiResponse<T> {
    code: u16,
    message: String,
    data: Option<T>,
}

impl<T> ApiResponse<T> {
    /// 创建通用的响应
    pub fn new(code: u16, message: impl Into<String>, data: Option<T>) -> Self {
        Self {
            code,
            message: message.into(),
            data,
        }
    }

    /// 成功响应,默认 code 200
    pub fn success(data: T) -> Self {
        Self {
            code: 200,
            message: "Success".to_string(),
            data: Some(data),
        }
    }

    /// 成功响应,无数据
    pub fn success_no_data() -> Self {
        Self {
            code: 200,
            message: "Success".to_string(),
            data: None,
        }
    }

    /// 失败响应,可以自定义 code 和 message
    pub fn error(code: u16, message: impl Into<String>) -> Self {
        Self {
            code,
            message: message.into(),
            data: None,
        }
    }

    /// 失败响应,默认 code 500
    pub fn internal_error(message: impl Into<String>) -> Self {
        Self {
            code: 500,
            message: message.into(),
            data: None,
        }
    }
}

async fn hello() -> Json<ApiResponse<String>> {
    let response = ApiResponse {
        message: "Hello, Axum!".to_string(),
        data: None,
        code: 200,
    };
    Json(response)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/user", get(get_user))
        .route("/hello", get(hello));

    println!("Server running on http://localhost:3000");
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

3、user handler 展开效果

  • 左侧是源代码,右侧是包含了原函数的生成代码,可以增加数据查询等操作。

4、请求效果

四、小结

本文通过一个小例子,一步一步封装过程宏,完成对json结果的封装,减少json重复代码。开发过程还是比较繁琐,一般在基础组件中使用。跟其他语言比,Rust 宏比 Java 注解更强,比 C++ 模板更安全,比 Go 的代码生成更优雅。

特性 Rust 宏(macro_rules! / proc-macro) Java 注解(Annotation + APT) C++ 模板 / 宏 Go 代码生成(go:generate / struct tag)
执行阶段 编译期(语法树级别展开) 编译前(APT 预处理器) 编译期(模板实例化) 编译前(外部代码生成器)
能否改代码结构 ✅ 可生成函数、结构体、impl ⚠️ 仅能影响编译器插件生成代码 ✅ 可生成类型、函数模板 ⚠️ 靠外部脚本生成代码
类型安全 ✅ 强(语法树级检查) ✅ 强 ⚠️ 复杂模板元编程时弱 ⚠️ 弱(字符串替换式)
语法整合度 ✅ 完全融入语言(AST 级) ⚠️ 依赖反射或编译插件 ✅ 高 ⚠️ 低,生成文件独立
典型用途 自动实现 trait、序列化、derive、DSL Lombok、Spring、ORM 实体映射 STL、CRTP、模板元函数 JSON 生成、ORM、mock
学习难度 🟧 中等(语法+编译时思维) 🟩 低(声明式) 🔴 高(模板地狱) 🟨 中(工具链依赖)

如果喜欢,请点个关注吧,本人公众号大鱼七成饱,历史文章都会在上面发布。

相关推荐
Vallelonga4 小时前
Rust 设计模式 Marker Trait + Blanket Implementation
开发语言·设计模式·rust
ftpeak7 小时前
《Cargo 参考手册》第二十一章:Cargo 包命令
开发语言·rust
Source.Liu10 小时前
【BuildFlow & 筑流】品牌命名与项目定位说明
c++·qt·rust·markdown·librecad
ftpeak12 小时前
《Cargo 参考手册》第二十二章:发布命令
开发语言·rust
JoannaJuanCV2 天前
error: can‘t find Rust compiler
开发语言·后端·rust
Kiri霧3 天前
在actix-web应用用构建集成测试
后端·rust·集成测试
muyouking113 天前
Tauri Android 开发踩坑实录:从 Gradle 版本冲突到离线构建成功
android·rust
Kiri霧3 天前
Rust开发环境搭建
开发语言·后端·rust
xuejianxinokok3 天前
什么是代数类型 ? java为什么要添加record,Sealed class 和增强switch ?
后端·rust