2024 年最新 Axum 快速上手指南:从入门到实战
在 Rust 的 Web 开发生态系统中,Axum 凭借其优雅的设计、卓越的性能和与 Tokio 生态的无缝集成,正迅速成为最受欢迎的框架之一。它由 Tokio 团队官方出品,这意味着它在设计之初就充分考虑了异步、模块化和工程化的最佳实践。
本文是一篇为 2024 年准备的全面指南,无论您是 Rust 新手还是有经验的后端开发者,都能通过本文快速掌握 Axum 的精髓,并构建出高效、健壮的 Web 服务。
为什么在 2024 年选择 Axum?
在开始之前,让我们先明确为什么 Axum 是一个值得投入时间学习的框架:
- 人体工程学设计:Axum 的 API 设计非常直观。它大量使用函数式风格,通过提取器(Extractor)和响应器(Responder)等模式,让处理 HTTP 请求和响应的代码既简洁又易于理解。
- 极致的模块化:Axum 的核心是一个
Router
,你可以像搭积木一样组合路由、中间件和服务。这一切都构建在tower
和tower-http
这两个强大的库之上,提供了丰富的即用型中间件(如日志、压缩、超时、认证等)。 - 零宏魔法:与其他一些框架不同,Axum 尽可能避免使用复杂的宏。你的处理器(Handler)就是普通的异步函数,这使得代码更容易调试、测试和理解。
- 无缝的生态集成:作为 Tokio 的一部分,Axum 与
tokio
、hyper
、tower
等业界标准库完美融合,保证了其性能和稳定性。 - 类型安全:借助 Rust 强大的类型系统,Axum 可以在编译时捕获大量潜在错误,例如请求体解析错误、路径参数类型不匹配等。
现在,让我们系好安全带,正式开启 Axum 的探索之旅!
第一部分:环境准备与 “Hello, Axum!”
万事开头难,但 Axum 的第一步异常简单。
1. 安装 Rust 环境
如果你的电脑上还没有 Rust,请通过官方工具 rustup
安装。它会同时安装 rustc
(编译器) 和 cargo
(包管理器和构建工具)。
“`bash
在 Linux 或 macOS 上
curl –proto ‘=https’ –tlsv1.2 -sSf https://sh.rustup.rs | sh
在 Windows 上,访问 https://www.rust-lang.org/tools/install 下载安装程序
“`
安装完成后,打开新的终端窗口,运行 rustc --version
确认安装成功。
2. 创建新项目
使用 Cargo 创建一个新的二进制项目:
bash
cargo new axum-quickstart
cd axum-quickstart
3. 添加依赖
打开项目根目录下的 Cargo.toml
文件,在 [dependencies]
部分添加 axum
和 tokio
。tokio
是我们运行异步代码所必需的运行时。
“`toml
[package]
name = “axum-quickstart”
version = “0.1.0”
edition = “2021”
[dependencies]
axum = “0.7” # 使用当前最新版本
tokio = { version = “1.0”, features = [“full”] }
``
tokio的
features = [“full”]` 会启用其所有功能,包括我们需要的 TCP 监听器和多线程调度器。
4. 编写第一个 Axum 应用
现在,打开 src/main.rs
,将其内容替换为以下代码:
“`rust
use axum::{
routing::get,
Router,
};
use std::net::SocketAddr;
[tokio::main]
async fn main() {
// 1. 创建我们的应用路由器
// Router 是 Axum 的核心,用于定义 API 的所有路由
let app = Router::new()
.route(“/”, get(say_hello)); // 定义一个 GET / 的路由,由 say_hello
函数处理
// 2. 定义服务器监听的地址
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("🚀 Server listening on {}", addr);
// 3. 创建一个 TCP 监听器并绑定地址
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
// 4. 运行服务器,`axum::serve` 会接收监听器和应用路由器
axum::serve(listener, app).await.unwrap();
}
// 这是一个处理器 (Handler) 函数
// 它接收 HTTP 请求并返回一个响应
// Axum 要求处理器是异步函数 (async fn)
async fn say_hello() -> &’static str {
“Hello, Axum!”
}
“`
代码解析:
#[tokio::main]
:这是一个宏,它会将main
函数转换为一个异步的main
函数,并启动 Tokio 运行时。Router::new()
:创建一个新的路由器实例。.route("/", get(say_hello))
:这是 Axum 定义路由的方式,非常直观。"/"
是路由路径。get(...)
指定这个路由只接受 HTTP GET 请求。axum::routing
模块还提供了post
,put
,delete
等函数。say_hello
是我们的处理器 (Handler),当匹配到GET /
请求时,Axum 会调用这个函数。
tokio::net::TcpListener::bind(...)
:使用 Tokio 创建一个异步的 TCP 监听器。axum::serve(listener, app)
:这是启动 Axum 服务的标准方式。它接收一个监听器和一个实现了Service
trait 的东西(Router
就实现了)。
5. 运行!
在终端中运行:
bash
cargo run
你会看到输出:
🚀 Server listening on 127.0.0.1:3000
现在,打开浏览器访问 http://127.0.0.1:3000
,或者使用 curl
:
bash
curl http://127.0.0.1:3000
你会收到 Hello, Axum!
的响应。恭喜你,你已经成功运行了第一个 Axum 应用!
第二部分:核心概念:路由、处理器与提取器
我们已经见识了最简单的路由和处理器。现在,让我们深入了解 Axum 最强大的特性之一:提取器 (Extractor)。
提取器是一种类型,它可以从请求的各个部分(如路径、查询参数、请求头、请求体等)“提取”数据。你只需在处理器函数的参数中声明你想要的提取器类型,Axum 就会自动帮你完成数据解析和验证。
1. 路径参数 (Path
)
通常我们需要动态的路径,比如 /users/1
。Path
提取器可以帮我们获取这些动态部分。
“`rust
// 在 main.rs 中
use axum::{
extract::Path, // 引入 Path 提取器
routing::get,
Router,
};
// … main 函数 …
async fn main() {
let app = Router::new()
.route(“/”, get(say_hello))
.route(“/users/:id”, get(get_user_by_id)); // 新增路由
// ... 启动服务器的代码 ...
}
// … say_hello 函数 …
async fn get_user_by_id(Path(user_id): Path
format!(“Fetching user with ID: {}”, user_id)
}
“`
- 我们定义了一个新路由
/users/:id
,其中:id
是一个占位符。 - 处理器
get_user_by_id
的参数是Path(user_id): Path<u64>
。Path<u64>
告诉 Axum 我们希望将路径中名为id
的部分解析为一个u64
类型的数字。Path(user_id)
是解构语法,直接将解析出的值赋给user_id
变量。
- 如果用户访问
/users/abc
,由于abc
无法解析为u64
,Axum 会自动返回一个400 Bad Request
错误。这就是类型安全的好处!
重启服务 (cargo run
),然后访问 http://127.0.0.1:3000/users/123
,你会看到 Fetching user with ID: 123
。
2. 查询参数 (Query
)
查询参数是 URL 中 ?
后面的部分,如 /search?q=rust&lang=en
。通常我们使用 serde
库来定义一个结构体来接收它们。
首先,在 Cargo.toml
中添加 serde
:
toml
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] } # 添加 serde
然后修改 main.rs
:
“`rust
// 在 main.rs 的顶部
use axum::extract::Query;
use serde::Deserialize;
// … Router 定义 …
async fn main() {
let app = Router::new()
// … 其他路由 …
.route(“/search”, get(search)); // 新增 search 路由
// ... 启动服务器 ...
}
// 定义一个结构体来接收查询参数
// 字段名需要和查询参数的 key 匹配
[derive(Deserialize, Debug)]
struct SearchParams {
q: String,
lang: Option
}
async fn search(Query(params): Query
format!(“Searching for ‘{}’ in language ‘{}'”,
params.q,
params.lang.unwrap_or_else(|| “default”.to_string())
)
}
``
SearchParams
* 我们定义了结构体,并使用
#[derive(Deserialize)]让
serde能够从数据中反序列化它。
search
* 处理器的参数是
Query,Axum 会自动解析 URL 查询字符串,并填充到
SearchParams实例中。
lang
*字段是
Option
重启服务,访问 http://127.0.0.1:3000/search?q=axum&lang=cn
,你会得到:Searching for 'axum' in language 'cn'
。
如果访问 http://127.0.0.1:3000/search?q=tokio
,你会得到:Searching for 'tokio' in language 'default'
。
3. JSON 请求体 (Json
)
现代 API 最常见的交互方式是 JSON。Json
提取器可以自动将请求体中的 JSON 反序列化为你的 Rust 结构体。
“`rust
// 在 main.rs 顶部
use axum::Json;
use serde::{Deserialize, Serialize};
// … Router 定义 …
async fn main() {
let app = Router::new()
// … 其他路由 …
.route(“/users”, post(create_user)); // 使用 post 方法
// ... 启动服务器 ...
}
[derive(Deserialize, Serialize, Debug)]
struct User {
id: u64,
username: String,
}
[derive(Deserialize, Debug)]
struct CreateUserPayload {
username: String,
}
// 这个处理器接收 JSON 数据…
async fn create_user(Json(payload): Json
// 实际应用中,这里会和数据库交互,生成 ID
let user = User {
id: 1337,
username: payload.username,
};
// ...并返回 JSON 数据
Json(user)
}
``
CreateUserPayload
* 我们定义了用于接收请求,
User用于表示创建后的用户实体。
create_user
* 处理器的参数是
Json。Axum 会检查
Content-Type头是否为
application/json,然后尝试解析请求体。如果失败,会自动返回
400 Bad Request或
415 Unsupported Media Type。
Json
* **返回 JSON**:注意看返回值。任何实现了
serde::Serialize的类型,只要包在
Json()里,Axum 就会自动将其序列化为 JSON 字符串,并设置正确的
Content-Type: application/json` 响应头。这就是响应器 (Responder) 的概念。
重启服务,使用 curl
发送一个 POST 请求:
bash
curl -X POST \
http://127.0.0.1:3000/users \
-H 'Content-Type: application/json' \
-d '{"username": "alice"}'
你会收到响应:
json
{"id":1337,"username":"alice"}
第三部分:状态共享与错误处理
真实的 Web 应用需要在不同的处理器之间共享状态(如数据库连接池、配置信息等),并且需要有一套统一的错误处理机制。
1. 共享状态 (State
)
State
提取器允许我们将一个状态注入到应用中,并在所有处理器中访问它。这个状态必须是 Clone
-able 和 Send + Sync
的,所以我们通常使用 Arc
(原子引用计数指针)。
让我们创建一个简单的应用状态,包含一个应用名称。
“`rust
// 在 main.rs 顶部
use axum::extract::State;
use std::sync::Arc;
// 1. 定义我们的应用状态
[derive(Clone)] // 必须可以 Clone
struct AppState {
app_name: String,
}
async fn main() {
// 2. 创建状态实例
// 使用 Arc 来安全地在多线程间共享
let shared_state = Arc::new(AppState {
app_name: “My Axum App”.to_string(),
});
let app = Router::new()
.route("/", get(root_handler))
// 3. 使用 with_state 将状态注入到 Router
.with_state(shared_state);
// ... 启动服务器 ...
}
// 4. 在处理器中使用 State
async fn root_handler(State(state): State
format!(“Welcome to {}!”, state.app_name)
}
“`
#[derive(Clone)]
是必要的,因为Router
在内部可能会克隆它。- 我们使用
Arc<AppState>
来包裹状态,Arc
实现了Clone
,它只会增加引用计数,而不会深拷贝内部数据,非常高效。 router.with_state(shared_state)
将状态附加到路由器上。之后所有在这个路由器(及其子路由)上定义的处理器都可以访问这个状态。- 处理器通过
State<Arc<AppState>>
提取状态。注意,类型必须与with_state
中传入的完全匹配。
2. 优雅的错误处理
到目前为止,我们的处理器要么返回 String
,要么返回 Json
。但如果发生错误(比如数据库查询失败),我们该怎么办?直接 panic!
会导致服务器崩溃。
Axum 的错误处理模式非常强大。核心思想是:
1. 创建一个自定义的错误类型。
2. 为这个错误类型实现 IntoResponse
trait。
3. 让处理器返回 Result<T, YourError>
,其中 T
是成功时返回的类型。
IntoResponse
trait 告诉 Axum 如何将你的类型转换成一个 HTTP 响应。
让我们来实践一下。
“`rust
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
// 1. 创建我们的自定义错误类型
enum AppError {
UserNotFound,
InternalServerError,
InvalidData(String),
}
// 2. 为 AppError 实现 IntoResponse
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AppError::UserNotFound => (StatusCode::NOT_FOUND, “User not found”),
AppError::InternalServerError => (
StatusCode::INTERNAL_SERVER_ERROR,
“Internal server error”,
),
AppError::InvalidData(msg) => (StatusCode::BAD_REQUEST, &msg),
};
let body = Json(json!({
"error": error_message,
}));
(status, body).into_response()
}
}
// 3. 修改处理器,让它返回 Result
// 假设这是一个可能失败的操作
async fn find_user_by_name(Path(name): Path
if name == “bob” {
let user = User { id: 42, username: “bob”.to_string() };
Ok(Json(user))
} else if name == “invalid” {
// 返回一个具体的错误
Err(AppError::InvalidData(“Username ‘invalid’ is not allowed”.to_string()))
}
else {
// 用户不存在
Err(AppError::UserNotFound)
}
}
// 在 main 函数中添加新路由
// .route(“/users/by_name/:name”, get(find_user_by_name))
“`
发生了什么?
- 我们定义了
AppError
枚举,代表了应用中可能发生的几种错误。 impl IntoResponse for AppError
是关键。在这个实现中,我们将每种错误变体映射到一个 HTTP 状态码和相应的 JSON 错误消息。- 现在我们的处理器
find_user_by_name
返回Result<Json<User>, AppError>
。- 如果成功,它返回
Ok(Json(user))
。Axum 知道如何处理Json<User>
。 - 如果失败,它返回
Err(AppError::UserNotFound)
。Axum 会调用我们为AppError
实现的into_response
方法,生成一个漂亮的404 Not Found
JSON 响应。
- 如果成功,它返回
这种模式极其强大,因为它将业务逻辑(find_user_by_name
)和表现层(如何将错误呈现给客户端)完全解耦。你的处理器代码可以保持干净,只关注业务逻辑和返回相应的错误类型。
第四部分:实战演练 – 构建一个简单的 Todo API
现在,我们把前面学到的所有知识点串联起来,构建一个完整的、虽然简单但功能齐全的待办事项(Todo)CRUD API。
我们将使用一个内存中的 DashMap
作为我们的“数据库”,以专注于 Axum 本身。DashMap
是一个并发的哈希图,非常适合在多线程环境中安全地共享和修改数据。
1. 添加依赖
在 Cargo.toml
中添加 serde
、uuid
(用于生成唯一 ID) 和 dashmap
。
toml
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.7", features = ["v4", "serde"] } # 用于ID
dashmap = "5.5" # 并发 HashMap
2. 完整代码 (src/main.rs
)
“`rust
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::{delete, get, post, put},
Json, Router,
};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::sync::Arc;
use uuid::Uuid;
[tokio::main]
async fn main() {
// 创建共享状态,这里是我们的内存数据库
let db = Arc::new(TodoDb::new());
// 构建应用路由器
let app = Router::new()
.route("/todos", get(get_all_todos))
.route("/todos", post(create_todo))
.route("/todos/:id", get(get_todo_by_id))
.route("/todos/:id", put(update_todo))
.route("/todos/:id", delete(delete_todo))
.with_state(db);
// 启动服务器
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("🚀 Server listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
// — 数据模型 —
[derive(Debug, Serialize, Deserialize, Clone)]
struct Todo {
id: Uuid,
text: String,
completed: bool,
}
[derive(Debug, Deserialize)]
struct CreateTodoPayload {
text: String,
}
[derive(Debug, Deserialize)]
struct UpdateTodoPayload {
text: Option
completed: Option
}
// — 数据库 (内存) —
type TodoDb = DashMap
// — 错误处理 —
enum ApiError {
NotFound,
InternalError,
}
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
let (status, message) = match self {
ApiError::NotFound => (StatusCode::NOT_FOUND, “Todo not found”),
ApiError::InternalError => (StatusCode::INTERNAL_SERVER_ERROR, “Internal server error”),
};
(status, message).into_response()
}
}
// — 处理器 —
// POST /todos
async fn create_todo(
State(db): State
Json(payload): Json
) -> impl IntoResponse {
let todo = Todo {
id: Uuid::new_v4(),
text: payload.text,
completed: false,
};
db.insert(todo.id, todo.clone());
(StatusCode::CREATED, Json(todo))
}
// GET /todos
async fn get_all_todos(State(db): State
let todos = db.iter().map(|entry| entry.value().clone()).collect();
Json(todos)
}
// GET /todos/:id
async fn get_todo_by_id(
State(db): State
Path(id): Path
) -> Result
db.get(&id)
.map(|todo| Json(todo.clone()))
.ok_or(ApiError::NotFound)
}
// PUT /todos/:id
async fn update_todo(
State(db): State
Path(id): Path
Json(payload): Json
) -> Result
let mut todo = db.get_mut(&id).ok_or(ApiError::NotFound)?;
if let Some(text) = payload.text {
todo.text = text;
}
if let Some(completed) = payload.completed {
todo.completed = completed;
}
Ok(Json(todo.clone()))
}
// DELETE /todos/:id
async fn delete_todo(
State(db): State
Path(id): Path
) -> Result
db.remove(&id).ok_or(ApiError::NotFound)?;
Ok(StatusCode::NO_CONTENT)
}
“`
3. 测试 API
运行 cargo run
,然后使用 curl
或 Postman 等工具来测试你的 API:
-
创建 Todo:
bash
curl -X POST -H "Content-Type: application/json" -d '{"text": "Learn Axum"}' http://127.0.0.1:3000/todos -
获取所有 Todo:
bash
curl http://127.0.0.1:3000/todos -
获取单个 Todo (将
<id>
替换为上一步返回的 ID):
bash
curl http://127.0.0.1:3000/todos/<id> -
更新 Todo:
bash
curl -X PUT -H "Content-Type: application/json" -d '{"completed": true}' http://127.0.0.1:3000/todos/<id> -
删除 Todo:
bash
curl -X DELETE http://127.0.0.1:3000/todos/<id>
这个例子展示了如何将路由、状态管理、JSON 序列化/反序列化和错误处理结合起来,构建一个结构清晰、易于维护的 Web API。
总结与展望
恭喜你!通过本指南,你已经掌握了使用 Axum 构建 Web 服务所需的核心知识:
- 项目设置 和 基本路由。
- 使用 提取器 (
Path
,Query
,Json
,State
) 从请求中获取数据。 - 构建不同类型的 响应,特别是 JSON 响应。
- 通过实现
IntoResponse
trait 进行 优雅的错误处理。 - 通过
State
和Arc
在处理器之间 共享状态。
这仅仅是 Axum 世界的开始。接下来,你可以探索更多高级主题:
- 中间件 (Middleware):使用
tower-http
添加日志、压缩、CORS、认证等功能。 - 数据库集成:用
sqlx
或diesel
替换内存数据库,连接到 PostgreSQL 或 MySQL。 - 测试:学习如何为你的 Axum 处理器和路由编写单元测试和集成测试。
- WebSockets:Axum 对 WebSockets 提供了优秀的一等支持。
- 部署:将你的 Axum 应用打包到 Docker 容器中,并部署到云服务。
Axum 以其现代化的设计和强大的功能,为 Rust Web 开发带来了前所未有的乐趣和效率。希望这篇 2024 年的快速上手指南能为你打开一扇通往高性能、高可靠性 Web 服务开发的大门。现在,去构建一些了不起的东西吧!