Axum 作为 Rust 生态中基于 Tokio 的低资源消耗、高性能 HTTP 框架,其核心优势之一是灵活的路由系统和简洁的参数提取机制。本教程将从 Axum 基础代码出发,循序渐进地讲解路由扩展、路由嵌套、路径参数、查询参数等核心功能,最终实现一个支持多场景参数处理的模块化 HTTP 服务。
一、基础回顾:Axum 服务的核心结构
在扩展路由与参数功能前,我们先回顾上一章的内容。其核心结构分为 4 部分:
- 依赖导入:Axum 核心组件、日志、网络地址、异步信号处理;
- 日志初始化:配置日志过滤规则与输出格式,便于调试;
- 服务器启动:创建 TCP 监听器、绑定地址、启动服务;
- 优雅关闭:监听 Ctrl+C/SIGTERM 信号,确保当前请求处理完成后再停止服务。
基础代码中仅包含根路由(/),处理函数返回固定的 Hello world!
。接下来我们将在此基础上逐步扩展路由与参数能力。
二、第一步:路由扩展基础 —— 添加简单路由
路由是 HTTP 服务的 “导航系统”,用于将不同 URL 路径与对应的处理函数绑定。Axum 中通过 Router::route
方法实现路由配置,语法为:
Router::new().route("路径", HTTP方法绑定(处理函数))
1.1 新增 “问候路由”(/say)
我们先添加一个无参数的 /say
路由,返回通用问候语。需要修改两部分代码:
步骤 1:导入依赖(无需新增,基础代码已包含核心依赖)
use axum::{routing::get, Router}; // 已存在于基础代码
步骤 2:新增路由处理函数
在 root
函数后添加 say_hello
函数,负责处理 /say
路径的 GET 请求:
/// 处理"/say"路径的GET请求:无参数的通用问候
async fn say_hello() -> &'static str {
"Hello guts!" // 口语化问候,返回静态字符串(编译期确定,无内存分配)
}
步骤 3:在主函数中注册路由
修改 main
函数中 app
的路由配置,新增 /say
路由:
// 基础代码中的路由配置
let app = Router::new()
.route("/", get(root)) // 原有根路由
.route("/say", get(say_hello)); // 新增/say路由
1.2 关键知识点解释
routing::get
:将异步函数绑定为 GET 请求的处理器,类似地还有post
/put
/delete
等方法,对应不同 HTTP 方法;- 处理函数返回值:此处返回
&'static str
(静态字符串引用),Axum 会自动将其封装为 HTTP 响应(状态码 200 OK,Content-Type 为 text/plain); - 路径匹配规则:
/say
仅匹配精确路径,不匹配/say/abc
这类子路径(后续将通过嵌套路由解决)。
三、第二步:路由模块化 —— 使用 nest
实现路由嵌套
当路由数量增多时,直接在主函数中注册所有路由会导致代码混乱。Axum 提供 Router::nest
方法支持路由嵌套,可按业务模块拆分路由(如 “问候模块”“用户模块”),提升代码可维护性。
3.1 实现 “问候模块” 路由组
我们将所有以 /say
为前缀的路由统一放在 say_hello_routes
函数中,形成独立的路由模块:
步骤 1:新增路由组函数
/// 创建"问候相关"的路由组:统一管理以"/say"为前缀的所有路由
fn say_hello_routes() -> Router {
Router::new()
// 子路由1:/(嵌套后完整路径为 /say)
.route("/", get(say_hello))
// 后续将在此处添加更多子路由(如带参数的/say/{name})
}
步骤 2:在主函数中嵌套路由
修改 main
函数的 app
配置,用 nest
替代直接的 route
:
let app = Router::new()
.route("/", get(root)) // 原有根路由
// 嵌套路由:所有以"/say"开头的请求,委托给say_hello_routes()的子路由处理
.nest("/say", say_hello_routes());
3.2 关键知识点解释
nest
方法:参数为 “前缀路径” 和 “子 Router”,嵌套后子路由的路径会与前缀拼接。例如:- 子路由
/
→ 完整路径/say
; - 子路由
/abc
→ 完整路径/say/abc
;
- 子路由
- 模块化优势:后续新增 “问候模块” 的路由(如
/say/search
),只需在say_hello_routes
中添加,无需修改主函数; - 路由合并:子 Router 可独立配置中间件、错误处理,实现模块内的功能隔离。
四、第三步:路径参数处理 —— 从 URL 路径提取数据
实际开发中,我们常需要从 URL 路径中获取动态数据(如 /user/123
中的用户 ID 123
)。Axum 提供 Path
提取器(axum::extract::Path
),可轻松从路径中提取参数。
4.1 单段路径参数(/{param}
)
先实现 /say/{name}
路由,根据路径中的 name
动态返回问候语(如 /say/Alice
返回 Hello Alice
)。
步骤 1:导入 Path
提取器
在 axum
依赖导入中添加 extract::Path
:
use axum::{
routing::get,
Router,
extract::Path, // 新增:导入路径参数提取器
};
步骤 2:新增带路径参数的处理函数
/// 处理"/say/{name}"路径的GET请求:带单路径参数的问候
/// Path(name):从路径中提取{name}参数,类型为String(拥有所有权,避免生命周期问题)
async fn say_hello_to(Path(name): Path<String>) -> String {
format!("Hello {}", name) // 动态生成响应(返回String而非&'static str)
}
步骤 3:在路由组中注册路由
在 say_hello_routes
函数中添加新路由:
fn say_hello_routes() -> Router {
Router::new()
.route("/{name}", get(say_hello_to)) // 新增:/say/{name}
.route("/", get(say_hello))
}
4.2 通配符路径参数(/{*param}
)
单段参数仅能匹配一个路径段(如 /say/Alice
中的 Alice
),若需匹配剩余所有路径段(如 /say/love/you/forever
中的 you/forever
),需使用通配符路径参数 {*param}
。
步骤 1:新增通配符路由与处理函数
在 say_hello_routes
中添加 /love/{*rest}
路由(注意:原代码中 /love{*rest}
缺少 /
,需修正为 /love/{*rest}
以匹配预期路径):
// 新增处理函数
/// 处理"/say/love/{*rest}"路径的GET请求:带通配符参数的表白
async fn say_love_to(Path(rest): Path<String>) -> String {
format!("I love {}", rest) // 如/rest=you → "I love you"
}
// 注册路由(在say_hello_routes中)
fn say_hello_routes() -> Router {
Router::new()
.route("/{name}", get(say_hello_to))
.route("/", get(say_hello))
.route("/love/{*rest}", get(say_love_to)) // 新增:通配符路由
}
4.3 路径参数关键知识点
-
提取器语法:处理函数的参数需写成
Path(param): Path<Type>
,其中:param
是参数名,必须与路径中的{param}
或{*param}
一致;Type
是参数类型,需实现axum::extract::FromUriParts
trait(常见类型如String
、u32
、i64
已默认实现);
-
单段 vs 通配符:
- 单段参数
{name}
:匹配一个路径段(不包含/
),如/say/Alice/Bob
中{name}
仅匹配Alice
; - 通配符参数
{*rest}
:匹配剩余所有路径段(包含/
),如/say/love/you
中{*rest}
匹配you
,/say/love/you/forever
中匹配you/forever
;
- 单段参数
-
返回值类型:当响应内容动态生成时(如
format!
拼接),需返回String
(而非&'static str
),因为静态字符串无法存储运行时生成的内容。
五、第四步:查询参数处理 —— 从 URL 查询字符串提取数据
除了路径参数,HTTP 请求还常通过查询字符串(URL 中 ?
后的部分,如 /search?query=rust&page=1
)传递参数。Axum 提供 Query
提取器(axum::extract::Query
)处理这类参数。
5.1 用 HashMap
处理动态查询参数
若查询参数的键名或数量不固定(如通用搜索接口,支持动态筛选条件),可使用 std::collections::HashMap
存储查询参数。
步骤 1:导入依赖
添加 Query
提取器和 HashMap
:
// Axum依赖中添加Query
use axum::{
routing::get,
Router,
extract::Path, Query, // 新增Query
};
// 标准库中导入HashMap
use std::collections::HashMap; // 新增:存储查询参数键值对
步骤 2:新增查询参数处理函数
实现 /say/search
路由,返回所有查询参数:
/// 处理"/say/search"路径的GET请求:用HashMap接收动态查询参数
/// Query(params):从查询字符串中提取键值对,存储为HashMap<String, String>
async fn search(Query(params): Query<HashMap<String, String>>) -> String {
format!("Search query: {:?}", params) // 格式化输出所有参数
}
步骤 3:注册路由
在 say_hello_routes
中添加路由:
fn say_hello_routes() -> Router {
Router::new()
.route("/{name}", get(say_hello_to))
.route("/", get(say_hello))
.route("/love/{*rest}", get(say_love_to))
.route("/search", get(search)) // 新增:查询参数路由
}
5.2 用自定义结构体处理固定结构参数
若查询参数的键名和类型固定(如分页接口 ?page=1&size=10
),推荐使用自定义结构体替代 HashMap
,可获得类型安全和自动校验。
例如,实现固定的分页查询参数:
// 1. 定义结构体(需派生Deserialize trait,需在Cargo.toml中添加serde依赖)
use serde::Deserialize; // 需在Cargo.toml添加serde = { version = "1.0", features = ["derive"] }
#[derive(Deserialize, Debug)]
struct PageParams {
page: u32, // 页码(必须为非负整数)
size: u32, // 每页条数
query: String,// 搜索关键词
}
// 2. 处理函数:用结构体接收查询参数
async fn paginated_search(Query(params): Query<PageParams>) -> String {
format!(
"Search: query={}, page={}, size={}",
params.query, params.page, params.size
)
}
// 3. 注册路由(在say_hello_routes中)
// .route("/paginated-search", get(paginated_search))
5.3 查询参数关键知识点
Query
提取器:原理与Path
类似,通过Query(params): Query<Type>
提取查询参数,Type
需实现serde::Deserialize
trait(HashMap
已实现,自定义结构体需派生Deserialize
);HashMap
适用场景:参数键名动态(如用户自定义筛选条件)、参数数量不固定;- 自定义结构体适用场景:参数结构固定(如分页、固定筛选),可避免键名拼写错误,且自动处理类型转换(如将字符串
page=1
转为u32
); - 依赖说明:使用自定义结构体时,需在
Cargo.toml
中添加serde
依赖(serde = { version = "1.0", features = ["derive"] }
),因为Query
提取器依赖serde
反序列化查询参数。
六、完整代码整合与解析
将上述所有功能整合后,最终代码如下(与本文开头提供的 “带路由和参数处理的代码” 一致,此处逐段解析关键部分):
// 导入必要的组件:按功能分类整理,明确各依赖的核心用途
// Axum框架核心:用于构建HTTP路由和处理请求
use axum::{
routing::get, // 导入GET请求处理器构造函数,用于将函数绑定为GET路由的处理逻辑
Router, // 路由构建器:用于定义请求路径与处理函数的映射关系,支持嵌套和合并
extract::{Path, Query}, // 导入参数提取器:
// Path:从URL路径中提取参数(如 /{name} 中的name)
// Query:从URL查询参数中提取数据(如 ?key=value 中的键值对)
};
// 标准库组件:网络地址与数据结构
use std::net::SocketAddr; // 定义网络地址格式(IP + 端口),用于指定服务器绑定的地址
use std::collections::HashMap; // 哈希表结构:用于存储查询参数的键值对(适合参数数量/键名不固定的场景)
// 日志系统:用于记录运行时信息、错误,便于调试和监控
use tracing::{info, error}; // 日志宏:info!记录普通信息,error!记录错误信息
use tracing_subscriber::{ // 日志订阅器:配置日志的过滤规则、输出格式并初始化
layer::SubscriberExt, // 为订阅器添加额外功能(如多层日志处理)的扩展 trait
util::SubscriberInitExt // 提供订阅器初始化方法的扩展 trait
};
// 异步信号处理:用于实现服务器的优雅关闭(响应系统终止信号)
use tokio::signal; // Tokio的信号处理模块:监听系统信号(如Ctrl+C、SIGTERM)
// 异步主函数:Axum基于Tokio异步运行时,必须用#[tokio::main]宏标记
// 该宏会自动初始化Tokio的多线程运行时,处理异步任务调度(如HTTP请求、IO操作)
#[tokio::main]
async fn main() {
// -------------------------- 1. 初始化日志系统 --------------------------
// 创建日志订阅器注册表:管理日志的过滤、格式化等多层处理逻辑
tracing_subscriber::registry()
// 1.1 配置日志过滤规则:控制哪些日志会被输出
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
// 优先从环境变量(如 RUST_LOG)读取过滤规则,若未设置则使用默认规则
.unwrap_or_else(|_| "axum_tutorial=debug,tower_http=debug".into())
// 默认规则说明:
// - axum_tutorial=debug:当前程序(crate名)输出debug及以上级别日志
// - tower_http=debug:axum依赖的tower_http库输出debug及以上级别日志
)
// 1.2 配置日志输出格式:添加格式化层,默认输出包含时间、日志级别、模块名、日志内容
.with(tracing_subscriber::fmt::layer())
// 1.3 初始化日志系统:将配置应用到全局,后续可通过info!、error!等宏输出日志
.init();
// 记录服务器启动的信息日志
info!("Starting axum server...");
// -------------------------- 2. 创建路由表 --------------------------
// 初始化根路由,并添加具体的路由规则
let app = Router::new()
// 根路径("/")的GET请求:由root函数处理
.route("/", get(root))
// 嵌套路由组:将所有以"/say"开头的请求,委托给say_hello_routes()返回的路由处理
// 作用:按业务模块拆分路由,提升代码可维护性(如"/say"下的所有接口都属于问候相关)
.nest("/say", say_hello_routes());
// -------------------------- 3. 绑定服务器地址 --------------------------
// 定义服务器绑定的地址:
// - [0, 0, 0, 0]:监听所有可用的网络接口(允许外部设备访问,而非仅本地)
// - 8080:绑定的端口号
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
// 记录绑定地址的信息日志
info!("Binding to address: {}", addr);
// 创建TCP监听器:绑定到指定地址,用于接收客户端的TCP连接
// 使用match处理绑定结果(异步操作,需await):
let listener = match tokio::net::TcpListener::bind(addr).await {
Ok(listener) => listener, // 绑定成功:获取监听器对象,用于后续接收请求
Err(e) => { // 绑定失败(如端口被占用):
error!("Failed to bind to address {}: {}", addr, e); // 记录错误日志
return; // 退出程序,避免后续无效执行
}
};
// -------------------------- 4. 启动服务器并处理优雅关闭 --------------------------
// 记录服务器启动成功的信息日志
info!("Server started, listening on {}", addr);
// 创建Axum服务器:
// - listener:用于接收TCP连接的监听器
// - app:定义好的路由表,用于处理接收到的HTTP请求
let server = axum::serve(listener, app);
// 启动服务器并配置优雅关闭:
// - with_graceful_shutdown:接收一个异步函数(shutdown_signal),当该函数完成时触发优雅关闭
// - 优雅关闭:等待当前正在处理的请求完成后再停止服务器,避免请求中断
if let Err(e) = server.with_graceful_shutdown(shutdown_signal()).await {
// 若服务器运行过程中出错(如端口被强制释放),记录错误日志
error!("Server error: {}", e);
}
// 服务器优雅关闭后,记录关闭完成的信息日志
info!("Server shutdown");
}
/// 根路径("/")的GET请求处理函数
/// 功能:返回静态字符串作为HTTP响应(状态码默认200 OK,Content-Type默认text/plain)
/// 返回值:&'static str(静态字符串引用,编译期确定,无需动态分配内存)
async fn root() -> &'static str {
"Hello world!" // 响应体内容:简单的问候字符串
}
/// 处理系统关闭信号的异步函数:用于触发服务器优雅关闭
/// 功能:监听系统终止信号(Windows的Ctrl+C、Unix的Ctrl+C/SIGTERM),信号触发后通知服务器关闭
async fn shutdown_signal() {
// 分支1:监听Ctrl+C信号(所有系统通用)
let ctrl_c = async {
signal::ctrl_c() // 注册Ctrl+C信号处理器
.await // 等待Ctrl+C信号触发
.expect("Failed to install Ctrl+C handler"); // 若注册失败, panic(生产环境可优化为优雅处理)
};
// 分支2:在Unix系统(如Linux、macOS)上额外监听SIGTERM信号(系统终止信号,如kill命令触发)
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate()) // 注册SIGTERM信号处理器
.expect("Failed to install SIGTERM handler") // 注册失败时panic
.recv() // 等待SIGTERM信号触发
.await;
};
// 等待任一信号触发(根据系统类型分支处理):
#[cfg(unix)]
tokio::select! {
// Unix系统:等待Ctrl+C或SIGTERM,任一信号触发则继续执行(通知服务器关闭)
_ = ctrl_c => {},
_ = terminate => {},
}
// 非Unix系统(如Windows):仅等待Ctrl+C信号
#[cfg(not(unix))]
ctrl_c.await;
// 记录信号接收日志:提示开始优雅关闭流程
info!("Received shutdown signal, starting graceful shutdown...");
}
/// 创建"问候相关"的路由组:统一管理以"/say"为前缀的所有路由
/// 返回值:Router实例,包含该模块下的所有路由规则
fn say_hello_routes()->Router{
Router::new()
// 1. 路径:"/say/{name}"(嵌套后完整路径),GET请求由say_hello_to处理
// {name}:单路径参数,匹配"/say/"后紧跟的单个路径段(如/say/Alice中的Alice)
.route("/{name}", get(say_hello_to))
// 2. 路径:"/say/"(嵌套后完整路径),GET请求由say_hello处理
// 无参数,返回固定问候语
.route("/", get(say_hello))
// 3. 路径:"/say/love{*rest}"(嵌套后完整路径),GET请求由say_love_to处理
// {*rest}:通配符参数,匹配"/say/love"后所有剩余路径段(如/say/lovesome中的some,注意:语法需补"/"为/love/{*rest}才符合预期)
.route("/love{*rest}", get(say_love_to))
// 4. 路径:"/say/search"(嵌套后完整路径),GET请求由search处理
// 用于接收查询参数(如/say/search?query=rust&page=1)
.route("/search", get(search))
}
/// 处理"/say/{name}"路径的GET请求:带单路径参数的问候
/// 参数:Path(name):通过Path提取器获取路径中的{name}参数,类型为String(拥有所有权,避免生命周期问题)
/// 返回值:格式化后的问候字符串(动态生成,类型为String)
async fn say_hello_to(Path(name): Path<String>) -> String {
format!("Hello {}", name) // 拼接问候语(如name=Alice时返回"Hello Alice")
}
/// 处理"/say/"路径的GET请求:无参数的通用问候
/// 返回值:静态字符串(无需动态分配,直接返回固定内容)
async fn say_hello() -> &'static str {
"Hello guts!" // 响应内容:口语化的"嘿,大伙儿!"(guts为口语中对同伴的称呼)
}
/// 处理"/say/love{*rest}"路径的GET请求:带通配符参数的表白
/// 参数:Path(name):通过Path提取器获取通配符{*rest}匹配的路径内容(注意:参数名需与通配符名一致,当前代码参数名name与通配符rest不匹配,运行时需修正为Path(rest))
/// 返回值:格式化后的表白字符串(动态生成,类型为String)
async fn say_love_to(Path(name): Path<String>) -> String {
format!("I love {}", name) // 拼接表白语(如rest=you时返回"I love you")
}
/// 处理"/say/search"路径的GET请求:通过HashMap接收查询参数
/// 参数:Query(params):通过Query提取器获取URL中的查询参数(如?key1=val1&key2=val2),存储为HashMap<String, String>
/// 适用场景:查询参数的数量或键名不固定(如通用搜索接口,支持动态筛选条件)
/// 返回值:格式化后的查询参数信息(展示所有键值对)
async fn search(Query(params): Query<HashMap<String, String>>)->String{
format!("Search query: {:?}", params) // 以调试格式输出所有查询参数(如{"query":"rust", "page":"1"})
}
七、功能测试与验证
运行代码前,需确保 Cargo.toml
包含以下依赖:
[package]
name = "axum-tutorial"
version = "0.1.0"
edition = "2024"
[dependencies]
# Axum 核心框架(处理路由、请求/响应等)
axum = "0.8.4"
# 异步运行时(仅保留核心特性,减少冗余)
tokio = { version = "1.47.1", features = ["rt-multi-thread", "net", "macros","signal"] }
# 日志相关(初始化日志输出)
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } # 添加env-filter特性
启动服务后,可通过 curl 或 浏览器 测试以下路由:
测试 URL | 预期响应 | 功能说明 |
---|---|---|
http://localhost:8080 | Hello world! | 根路由(无参数) |
http://localhost:8080/say | Hello guts! | 嵌套路由(无参数) |
http://localhost:8080/say/Alice | Hello Alice | 单段路径参数 |
http://localhost:8080/say/love/You | I love You | 通配符路径参数 |
http://localhost:8080/say/search?query=axum&page=1 | Search query: {"query": "axum", "page": "1"} | HashMap 处理查询参数 |
以下为Windows下crul测试的结果:
D:\>curl "http://localhost:8080"
Hello world!
D:\>curl "http://localhost:8080/say"
Hello guts!
D:\>curl "http://localhost:8080/say/Tom"
Hello Tom
D:\>curl "http://localhost:8080/say/loveTom"
I love Tom
D:\>curl "http://localhost:8080/say/search?name=Tom&age=30"
Search query: {"age": "30", "name": "Tom"}
八、总结:Axum 路由与参数处理核心要点
-
路由基础:
- 用
Router::route
绑定 “路径 + HTTP 方法 + 处理函数”; - 用
Router::nest
实现路由嵌套,按业务模块拆分代码。
- 用
-
参数提取:
- 路径参数:
Path
提取器,支持单段({param}
)和通配符({*param}
); - 查询参数:
Query
提取器,支持HashMap
(动态参数)和自定义结构体(固定参数)。
- 路径参数:
-
处理函数:
- 提取器作为函数参数,需显式标注类型(如
Path(name): Path<String>
); - 返回值支持
&'static str
(静态内容)和String
(动态内容),Axum 自动封装为 HTTP 响应。
- 提取器作为函数参数,需显式标注类型(如
-
模块化与可维护性:
- 路由嵌套是实现模块化的关键,避免主函数中路由配置臃肿;
- 固定结构参数优先用自定义结构体,兼顾类型安全与代码可读性。
通过本教程的循序渐进扩展,你已掌握 Axum 路由与参数处理的核心能力。