柏虎资源网

专注编程学习,Python、Java、C++ 教程、案例及资源

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

引言

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

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

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(),
    }
}

希望写成更优雅的:

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

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

一、总体设置

1、设置目录结构

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依赖

[workspace]
members = [
    "axum_error_macro",
    "axum_macro_example",
]

二、宏定义

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

[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 等。

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) 使用 syn 把 TokenStream 解析为 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(配置依赖包)

[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手动返回。

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

学习难度

中等(语法+编译时思维)

低(声明式)

高(模板地狱)

中(工具链依赖)

如果喜欢,请点个关注吧, 本人公众号大鱼七成饱

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言