文章目录
- [使用 Axum 构建高性能异步 Web 服务](#使用 Axum 构建高性能异步 Web 服务)
-
- [第一个 Axum 服务](#第一个 Axum 服务)
-
- 项目初始化与依赖配置
- [编写 Hello World 服务](#编写 Hello World 服务)
- 运行与测试
- [Axum 核心组件](#Axum 核心组件)
-
- 路由(Router):请求分发的核心
- 提取器(Extractor):请求参数解析的利器
-
- Path:提取路径参数
- Query:提取查询参数
- [Json:提取 JSON 请求体](#Json:提取 JSON 请求体)
- State:提取应用全局状态
- 提取器组合与常见错误
- 响应(Response):灵活构建返回结果
- 中间件(Middleware):统一拦截与处理
- 总结
使用 Axum 构建高性能异步 Web 服务
在 Rust Web 生态中,Axum 凭借其简洁的 API、强大的类型安全、完善的异步支持以及与 Tokio 生态的无缝集成,成为近年来最受欢迎的 Web 框架之一。与 Actix-web、Warp 等框架相比,Axum 以无宏入侵、模块化设计、易上手为核心优势,既适合新手快速入门,也能满足生产级项目的高性能需求。
第一个 Axum 服务
项目初始化与依赖配置
首先创建一个新的 Rust 项目,并添加 Axum 及相关依赖:
shell
cargo new axum-demo
cd axum-demo
修改 Cargo.toml 文件,添加核心依赖:
toml
[dependencies]
axum = { version = "0.8", features = ["full"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
编写 Hello World 服务
修改 src/main.rs 中的内容,实现一个简单的 HTTP 服务,监听 3000 端口,根路由返回 "Hello, Axum!":
rust
use anyhow::Result;
use axum::{Router, routing::get};
// 根路由处理函数:异步函数,返回字符串(自动实现 IntoResponse)
async fn root_handler() -> &'static str {
"Hello, Axum!"
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
// 构建路由:将 GET 请求 / 映射到 root_handler
let app = Router::new().route("/", get(root_handler));
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
运行与测试
在终端执行以下命令运行项目:
shell
cargo run
运行成功后,打开浏览器访问 http://localhost:3000,即可看到 "Hello, Axum!" 的响应。也可以使用 curl 命令测试:
shell
curl http://localhost:3000
Axum 核心组件
Axum 的核心设计围绕路由(Router)、提取器(Extractor)、响应(Response)、状态(State)、中间件(Middleware)这五大组件展开的,这五大组件协同工作,完成从请求接收、参数解析到响应返回的全流程。
路由(Router):请求分发的核心
Router 是 Axum 的路由管理器,负责将不同 HTTP 方法(GET、POST、PUT、DELETE 等)和路径映射到对应的处理函数,支持路由嵌套、路径参数、路由合并等功能。
基本路由与 HTTP 方法
Axum 支持所有常见的 HTTP 方法,通过 routing 模块中的函数(get、post、put、delete 等)绑定处理函数:
rust
use anyhow::Result;
use axum::{
Router,
routing::{delete, get, post, put},
};
// 不同 HTTP 方法的处理函数
async fn get_handler() -> &'static str {
"这是 GET 请求"
}
async fn post_handler() -> &'static str {
"这是 POST 请求"
}
async fn put_handler() -> &'static str {
"这是 PUT 请求"
}
async fn delete_handler() -> &'static str {
"这是 DELETE 请求"
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/get", get(get_handler)) // GET /get
.route("/post", post(post_handler)) // POST /post
.route("/put", put(put_handler)) // PUT /put
.route("/delete", delete(delete_handler)); // DELETE /delete
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
路径参数
通过 /:param 语法定义路径参数,处理函数通过提取器获取参数值,支持 String、u32、i64 等多种基础类型,无需手动转换:
rust
use anyhow::Result;
use axum::{Router, extract::Path, routing::get};
// 提取单个路径参数(用户 ID)
async fn user_handler(Path(user_id): Path<u32>) -> String {
format!("用户 ID:{}", user_id)
}
// 提取多个路径参数(用户 ID + 文章 ID)
async fn article_handler(Path((user_id, article_id)): Path<(u32, String)>) -> String {
format!("用户 {} 的文章:{}", user_id, article_id)
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/user/{user_id}", get(user_handler)) // GET /user/123
.route("/user/{user_id}/article/{article_id}", get(article_handler)); // GET /user/123/article/hello-axum
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
路由嵌套
对于复杂项目,可通过 nest 方法实现路由嵌套,将相关路由分组管理,提升代码可维护性:
rust
use anyhow::Result;
use axum::{Router, extract::Path, routing::get};
// 用户相关路由
async fn user_list() -> &'static str {
"所有用户列表"
}
async fn user_detail(Path(user_id): Path<u32>) -> String {
format!("用户 {} 详情", user_id)
}
// 文章相关路由
async fn article_list() -> &'static str {
"所有文章列表"
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
// 嵌套用户路由:所有 /user 开头的请求,都会匹配到 user_router
let user_router = Router::new()
.route("/", get(user_list)) // GET /user
.route("/{user_id}", get(user_detail)); // GET /user/123
// 嵌套文章路由:所有 /article 开头的请求,都会匹配到 article_router
let article_router = Router::new().route("/", get(article_list)); // GET /article
// 根路由:合并所有嵌套路由
let app = Router::new()
.nest("/user", user_router)
.nest("/article", article_router);
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
提取器(Extractor):请求参数解析的利器
提取器是 Axum 最强大的特性之一,它允许我们提取请求中的各种信息,无需手动解析请求体、请求头或参数,且所有提取器都具备类型安全特性,编译期就能校验参数类型。
Path:提取路径参数
前文已介绍,用于提取路径中的参数,支持单个、多个参数,以及复杂类型。
Query:提取查询参数
用于提取 URL 中的查询参数,比如 /users?page=1&limit=10,通常结合 serde::Deserialize 特征使用,自动将查询参数映射为结构体:
rust
use anyhow::Result;
use axum::{Router, extract::Query, routing::get};
use serde::Deserialize;
// 定义查询参数结构体
#[derive(Deserialize, Debug)]
struct Pagination {
page: u64,
limit: u64,
}
// 提取查询参数
async fn user_list(Query(pagination): Query<Pagination>) -> String {
let offset = (pagination.page - 1) * pagination.limit;
format!(
"分页查询:页码 {},每页 {} 条,偏移量 {}",
pagination.page, pagination.limit, offset
)
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let app = Router::new().route("/users", get(user_list)); // GET /users?page=1&limit=10
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
Json:提取 JSON 请求体
用于提取 JSON 格式的请求体,结合 serde::Deserialize 特征使用,自动将 JSON 字符串反序列化为结构体,同时支持自动校验 JSON 格式:
rust
use anyhow::Result;
use axum::{Json, Router, routing::post};
use serde::Deserialize;
// 定义 JSON 请求体结构体
#[derive(Deserialize, Debug)]
struct CreateUserRequest {
username: String,
email: String,
age: Option<u8>, // 可选参数
}
// 提取 JSON 请求体并处理
async fn create_user(Json(req): Json<CreateUserRequest>) -> String {
format!(
"创建用户:用户名={},邮箱={},年龄={:?}",
req.username, req.email, req.age
)
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let app = Router::new().route("/users", post(create_user)); // POST /users,请求体为 JSON
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
State:提取应用全局状态
在实际项目中,我们常常需要共享全局资源,比如数据库连接池、配置信息、缓存等,Axum 通过 State 提取器实现全局状态共享,步骤如下:
- 定义状态结构体,需要实现
Clone,因为状态会被多个请求处理函数共享; - 通过
Router::with_state将状态挂载到路由上; - 在处理函数中通过
State提取器获取状态。
rust
use anyhow::Result;
use axum::{Router, extract::State, routing::get};
use std::sync::{Arc, Mutex};
// 定义全局状态结构体
#[derive(Clone)]
struct AppState {
// Arc + Mutex 实现线程安全的可变状态
counter: Arc<Mutex<u64>>,
}
// 提取状态并修改计数器
async fn incr_counter(State(state): State<AppState>) -> String {
// 锁定计数器,修改值
let mut counter = state.counter.lock().unwrap();
*counter += 1;
format!("当前计数器值:{}", counter)
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
// 初始化全局状态
let app_state = AppState {
counter: Arc::new(Mutex::new(0)),
};
let app = Router::new()
.route("/incr", get(incr_counter))
.with_state(app_state);
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
提取器组合与常见错误
多个提取器可以组合使用,只需在处理函数参数中依次声明即可,但需注意提取器顺序:元数据提取器(State、Path、Query、Header 等)放在前面,请求体提取器(Json、Form 等)放在最后,因为请求体只能读取一次。
当提取器无法正确解析请求数据时,如 JSON 格式错误、参数类型不匹配等错误,会返回提取器拒绝(Rejection)。Axum 提供 debug_handler 宏,可帮助我们快速排查提取器相关的类型错误,但需要引入 axum-macros 这个依赖库:
rust
use axum::extract::{State, Json};
use serde::Deserialize;
#[derive(Clone)]
struct AppState {
app_name: String,
}
#[derive(Deserialize)]
struct LoginRequest {
username: String,
password: String,
}
// 添加 debug_handler 宏,排查提取器和处理函数的类型错误
#[axum_macros::debug_handler]
async fn login(
State(state): State<AppState>, // 元数据提取器(前)
Json(req): Json<LoginRequest> // 请求体提取器(后)
) -> String {
format!("{}:用户 {} 登录成功", state.app_name, req.username)
}
响应(Response):灵活构建返回结果
Axum 中,任何实现 IntoResponse 特征的类型都可以作为处理函数的返回值,无需手动构建 HTTP 响应对象,简化了响应编写流程。
基础响应类型
Axum 内置了多种基础类型的 IntoResponse 实现,可直接作为返回值:
rust
use anyhow::Result;
use axum::{Router, routing::get};
// 字符串响应
async fn str_response() -> &'static str {
"字符串响应"
}
// 字符串切片响应(带状态码)
async fn str_with_status() -> (axum::http::StatusCode, &'static str) {
(axum::http::StatusCode::OK, "成功响应")
}
// JSON 响应(serde_json::Value 或自定义结构体)
async fn json_response() -> axum::Json<serde_json::Value> {
axum::Json(serde_json::json!({
"code": 200,
"message": "success",
"data": { "username": "axum" }
}))
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/str", get(str_response))
.route("/str-status", get(str_with_status))
.route("/json", get(json_response));
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
自定义响应
对于复杂场景,比如统一响应格式、自定义响应头,可以通过实现 IntoResponse 特征自定义响应类型:
rust
use anyhow::Result;
use axum::{
Router,
http::{StatusCode, header::CONTENT_TYPE},
response::{IntoResponse, Response},
routing::get,
};
use serde::Serialize;
// 自定义统一响应结构体
#[derive(Serialize)]
struct ApiResponse<T> {
code: u16,
message: &'static str,
data: Option<T>,
}
// 实现 IntoResponse 特征
impl<T: Serialize> IntoResponse for ApiResponse<T> {
fn into_response(self) -> Response {
// 序列化 JSON
let json = serde_json::to_string(&self).unwrap();
// 构建响应:状态码 + 响应头 + 响应体
(
StatusCode::OK,
[(CONTENT_TYPE, "application/json; charset=utf-8")],
json,
)
.into_response()
}
}
// 成功响应(带数据)
async fn success_response() -> ApiResponse<String> {
ApiResponse {
code: 200,
message: "success",
data: Some("操作成功".to_string()),
}
}
// 失败响应(无数据)
async fn error_response() -> ApiResponse<()> {
ApiResponse {
code: 400,
message: "参数错误",
data: None,
}
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/success", get(success_response))
.route("/error", get(error_response));
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
中间件(Middleware):统一拦截与处理
中间件用于在请求到达处理函数之前、响应返回客户端之前,执行统一的逻辑,比如日志记录、鉴权、限流、CORS 跨域等。Axum 本身不实现中间件系统,而是直接复用 Tower 和 Tower-HTTP 生态的中间件,无需重复开发。
常用内置中间件(Tower-HTTP)
Tower-HTTP 提供了大量常用中间件,在 Cargo.toml 中添加依赖:
toml
[dependencies]
tower = "0.5"
tower-http = { version = "0.6", features = ["full"] }
http = "1.0"
以日志中间件和 CORS 中间件为例:
rust
use anyhow::Result;
use axum::{Router, routing::get};
use tower_http::{cors::CorsLayer, trace::TraceLayer};
async fn root_handler() -> &'static str {
"Hello, Axum Middleware!"
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
// 构建中间件
let cors_layer = CorsLayer::permissive(); // 允许所有跨域请求,生产环境需谨慎配置
let trace_layer = TraceLayer::new_for_http(); // 请求日志中间件
let app = Router::new()
.route("/", get(root_handler))
.layer(cors_layer) // CORS 中间件
.layer(trace_layer); // 日志中间件
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
自定义中间件
如果内置中间件无法满足需求,我们可以通过实现 Tower 的 Service 特征来自定义中间件。以下是一个简单的自定义日志中间件示例,记录请求耗时:
rust
use std::{
pin::Pin,
task::{Context, Poll},
time::Instant,
};
use anyhow::Result;
use axum::{Router, routing::get};
use tower::{Layer, Service};
// 自定义中间件服务
#[derive(Clone)]
struct TimingService<S> {
inner: S,
}
// 实现 Service 特征
impl<S, Req> Service<Req> for TimingService<S>
where
S: Service<Req>,
S::Future: Send + 'static,
Req: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Req) -> Self::Future {
let start = Instant::now();
let future = self.inner.call(req);
Box::pin(async move {
let result = future.await;
let duration = start.elapsed();
println!("请求耗时:{:?}", duration);
result
})
}
}
// 中间件层,用于挂载到路由
#[derive(Clone)]
struct TimingLayer;
impl<S> Layer<S> for TimingLayer {
type Service = TimingService<S>;
fn layer(&self, inner: S) -> Self::Service {
TimingService { inner }
}
}
async fn root_handler() -> &'static str {
"Hello, Custom Middleware!"
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", get(root_handler))
.layer(TimingLayer); // 挂载自定义中间件
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
总结
Axum 作为 Rust Web 生态的后起之秀,正在快速发展,社区活跃度不断提升。如果你想使用 Rust 构建高性能、高安全性的 Web 服务,Axum 绝对是一个值得深入学习和使用的框架。