引言
这篇是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、表达式等)。过程宏通常都需要这个,否则很多语法没法处理。
- 在过程宏中,Rust 编译器会把传入的源码以 TokenStream 的形式交给你的宏代码,而你需要用
-
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) 使用
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(配置依赖包)
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 |
学习难度 | 🟧 中等(语法+编译时思维) | 🟩 低(声明式) | 🔴 高(模板地狱) | 🟨 中(工具链依赖) |
如果喜欢,请点个关注吧,本人公众号大鱼七成饱,历史文章都会在上面发布。