2024 年最新 Axum 快速上手指南 (附代码示例) – wiki基地


2024 年最新 Axum 快速上手指南:从入门到实战

在 Rust 的 Web 开发生态系统中,Axum 凭借其优雅的设计、卓越的性能和与 Tokio 生态的无缝集成,正迅速成为最受欢迎的框架之一。它由 Tokio 团队官方出品,这意味着它在设计之初就充分考虑了异步、模块化和工程化的最佳实践。

本文是一篇为 2024 年准备的全面指南,无论您是 Rust 新手还是有经验的后端开发者,都能通过本文快速掌握 Axum 的精髓,并构建出高效、健壮的 Web 服务。

为什么在 2024 年选择 Axum?

在开始之前,让我们先明确为什么 Axum 是一个值得投入时间学习的框架:

  1. 人体工程学设计:Axum 的 API 设计非常直观。它大量使用函数式风格,通过提取器(Extractor)和响应器(Responder)等模式,让处理 HTTP 请求和响应的代码既简洁又易于理解。
  2. 极致的模块化:Axum 的核心是一个 Router,你可以像搭积木一样组合路由、中间件和服务。这一切都构建在 towertower-http 这两个强大的库之上,提供了丰富的即用型中间件(如日志、压缩、超时、认证等)。
  3. 零宏魔法:与其他一些框架不同,Axum 尽可能避免使用复杂的宏。你的处理器(Handler)就是普通的异步函数,这使得代码更容易调试、测试和理解。
  4. 无缝的生态集成:作为 Tokio 的一部分,Axum 与 tokiohypertower 等业界标准库完美融合,保证了其性能和稳定性。
  5. 类型安全:借助 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] 部分添加 axumtokiotokio 是我们运行异步代码所必需的运行时。

“`toml
[package]
name = “axum-quickstart”
version = “0.1.0”
edition = “2021”

[dependencies]
axum = “0.7” # 使用当前最新版本
tokio = { version = “1.0”, features = [“full”] }
``tokiofeatures = [“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/1Path 提取器可以帮我们获取这些动态部分。

“`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) -> String {
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, // 使用 Option 表示可选参数
}

async fn search(Query(params): Query) -> String {
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) -> 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 Request415 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>) -> String {
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) -> Result, AppError> {
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 中添加 serdeuuid (用于生成唯一 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>) -> Json> {
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, ApiError> {
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, ApiError> {
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 进行 优雅的错误处理
  • 通过 StateArc 在处理器之间 共享状态

这仅仅是 Axum 世界的开始。接下来,你可以探索更多高级主题:

  • 中间件 (Middleware):使用 tower-http 添加日志、压缩、CORS、认证等功能。
  • 数据库集成:用 sqlxdiesel 替换内存数据库,连接到 PostgreSQL 或 MySQL。
  • 测试:学习如何为你的 Axum 处理器和路由编写单元测试和集成测试。
  • WebSockets:Axum 对 WebSockets 提供了优秀的一等支持。
  • 部署:将你的 Axum 应用打包到 Docker 容器中,并部署到云服务。

Axum 以其现代化的设计和强大的功能,为 Rust Web 开发带来了前所未有的乐趣和效率。希望这篇 2024 年的快速上手指南能为你打开一扇通往高性能、高可靠性 Web 服务开发的大门。现在,去构建一些了不起的东西吧!

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部