平时闲暇的时候,我们程序员会写一些小项目:网页、桌面应用、App、小程序……写完总想着对外展示,这就有了对外服务的需求。
对外服务一般怎么做?通常是把应用的后端核心部分部署到服务器上。但这里有个很现实的问题:国内的服务器资源属实不便宜。笔者的习惯是用 Java + SpringBoot 快速写业务,但 Java 本身比较占内存,再加上 SpringBoot 这个"内存大户",哪怕是非常简单的 demo,跑起来也要吃掉不少资源。
对企业来说可能无所谓,但对个人开发者来说就很头疼了。笔者的服务器都是每年 618、双十一各大云服务商搞活动时捡的"破烂"(每年还得打包搬家~~)。

配置基本就是 1核1G 这种水平,一台服务器上跑不了几个小 demo。

所以笔者一直在尝试用其他语言开发,比如 Go、Rust,最后选择了 Rust。Rust 这几年热度不低,它是一种 速度极快、高性能、注重内存安全 的静态编译型语言。
而 Axum 是 Rust 语言中一个专注于性能和简单性的 Web框架,实际用下来感觉不错,很符合笔者的需求。在官方文档里踩了不少坑之后,顺手写了一系列教程,分享给大家。
那接下来就从本文开始,正式进入 Rust + Axum 的 Web 开发之旅。
什么是 Axum?
Axum 利用了 hyper 库的能力来增强 Web 应用程序的速度和并发性。同时,Axum 通过与 Tokio 库集成,将 Rust 的 async/await 能力推到了前台,让开发者可以构建高性能的异步 API 和 Web 应用程序。
Axum 是一个专注于人体工程学和模块化的 Web 应用框架,主要有以下几个特点:
- 使用无宏的 API 实现路由(router)功能
- 使用提取器(extractor)对请求进行声明式解析
- 简单且可预测的错误处理模式
- 用最少的模板生成响应
- 充分利用
tower 和 tower-http 的中间件、服务和工具生态
Axum 的基本功能建立在 Tokio runtime 之上,这赋予了 Rust 管理非阻塞、事件驱动活动的能力,对平稳处理多个并发进程至关重要。
Axum 与现有框架的一个显著区别在于:它没有自己的中间件系统,而是直接使用 tower::Service。这意味着 Axum 可以零成本获得超时、跟踪、压缩、授权等功能,还能与使用 hyper 或 tonic 编写的应用共享中间件。
此外,Axum 是基于 Rust 强大的类型系统和 所有权规则 构建的,这些规则能在编译期防止数据竞争和内存泄漏等常见的 Web 开发陷阱。再加上 Axum 的模块化设计理念,开发者只需添加必要的组件,就能构建出轻量级、专注的应用程序。
创建一个项目
首先创建一个 Rust 项目。这里默认你已经安装好 Rust 开发环境,具体安装步骤直接上官网查看即可。
cargo new rust_axum_web_guide
编写第一个 hello world
添加依赖:
[package]
name = "rust_axum_web_guide"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7.9"
tokio = { version = "1.0", features = ["full"] }
axum 版本选最新版,当前最新版本为 0.7.9;再引入 Tokio,以便充分利用它的生态系统。
我们采用官网的 hello world 例子,改写 main.rs:
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
// 构建一个只有单一路由的应用
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
// 用 hyper 运行应用,监听 3000 端口
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
其中,tokio::main 属性宏表明 Axum 本质上就是一个 Tokio 应用。tokio 是 Rust 非常优秀的 异步运行时框架,它提供了编写异步网络服务所需的几乎所有功能。
Router::new().route 用于创建路由,tokio::net::TcpListener::bind 负责监听服务器地址,然后把生成的 listener 传给 axum::serve() 使用,启动 axum 服务。
启动项目:
cargo run
该命令会编译并运行当前项目,同时自动下载依赖。
如果一切顺利,终端会输出 "Running on http://localhost:3000"。在浏览器中访问 http://localhost:3000 或使用 curl 工具,就能看到 "Hello, World!"。
路由与处理器
Router 用于设置哪些路径指向哪些服务,它是一个用于组合处理程序和服务的结构体。路由机制负责把传入的 HTTP 请求定向到指定的 handler,而 handler 正是业务逻辑所在的位置。
来看一个例子:
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
// 构建应用并设置多组路由
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/foo", get(get_foo).post(post_foo))
.route("/foo/bar", get(foo_bar));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn get_foo() -> String {
String::from("get请求foo方法")
}
async fn post_foo() -> String {
String::from("post请求foo方法")
}
async fn foo_bar() -> String {
String::from("get请求->foo_bar")
}
路由和 handler 绑定,handler 类似于 Java Spring 中的 controller。处理器 handler 是一个异步函数或异步代码块,它接受零个或多个 extractors 作为参数,并返回可以转换为 IntoResponse 的内容。
也可以把一个路由和多个 handler 绑定,比如 .route("/foo", get(get_foo).post(post_foo)),同时绑定了 GET 和 POST 方法。当然还可以指定 PUT、DELETE 等 HTTP 方法。
处理器更详细的信息,可见 axum::handler 官方文档。
用 Postman 访问 /foo,分别发送 GET 或 POST 请求,会看到不同的结果。
如果定义了多个完全相同的路由,程序会直接 panic:
use axum::{routing::get, Router};
let app = Router::new()
.route("/", get(|| async {}))
.route("/", get(|| async {}));
路由更多详情,可见 Router in axum 官方文档。
提取器
提取器(Extractors)的作用是:从传入请求中分离出处理程序所需的部分,比如解析异步函数的参数。
捕获动态 URL 值以及查询参数
先来看看如何捕获动态 URL 值和查询参数,主要使用 path 和 query 提取器。
use axum::{
routing::get,
Router,
extract::{Path, Query}
};
use std::collections::HashMap;
use serde::Deserialize;
#[derive(Deserialize)]
struct Page {
number: u32,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/item/:id", get(print_item))
.route("/item2/:name/:age", get(print_item_handler2))
.route("/item3/:fullpath", get(print_item_handler3))
.route("/query/:id", get(print_query));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn print_item(Path(_id): Path<u32>) -> String {
format!("print_item: {}", _id)
}
async fn print_item_handler2(Path((name, age)): Path<(String, i64)>) -> String {
format!("name: {name}, age: {age}")
}
async fn print_item_handler3(Path(fullpath): Path<HashMap<String, String>>) -> String {
format!("path: /{fullpath:?}")
}
async fn print_query(Path(id): Path<u32>, Query(page): Query<Page>) -> String {
format!("Query {} on page {}", id, page.number)
}
还需要添加序列化支持的依赖:
serde = { version = "1.0.216", features = ["derive"] }
测试结果:
/item/321 # 结果:print_item: 321
/item2/zj/18 # 结果:name: zj, age: 18
/item3/{name: zzjj} # 结果:path: /{"fullpath": "{name: zzjj}"}
/query/10?number=31 # 结果:Query 10 on page 31
我们还能把多个参数写入 HashMap 中:
use axum::{
routing::get,
Router,
extract::Query
};
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize, Debug)]
struct Info {
name: String,
age: u8,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/query_1", get(print_map_handler))
.route("/query_2", get(print_map2_handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn print_map_handler(query: Query<Info>) -> String {
let info: Info = query.0;
format!("query1: {info:?}")
}
async fn print_map2_handler(query: Query<HashMap<String, String>>) -> String {
format!("query2: {query:?}")
}
测试结果:
/query_1?name=zj&age=18 # 结果:query1: Info { name: "zj", age: 18 }
/query_2?name=zj&age=18 # 结果:query2: Query({"age": "18", "name": "zj"})
path 和 query 提取器一般适用于 GET 请求,那 POST 请求的参数怎么处理?
捕获请求体的参数
常见的请求体参数一般有两种格式:json 和 form。下面举个例子:
use axum::{
routing::{get, post},
Router,
extract::{Json, Form}
};
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct CreateUser {
username: String,
age: u8,
}
#[derive(Deserialize, Debug)]
struct LoginFormData {
field_name: String,
field_pwd: String,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/addUser", post(create_user_handler))
.route("/submit", post(submit_form_handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn create_user_handler(Json(payload): Json<CreateUser>) -> String {
format!("Added user: {payload:?}")
}
async fn submit_form_handler(Form(data): Form<LoginFormData>) -> String {
format!("submit form: {data:?}")
}
测试结果:
post /addUser # 结果:Added user: CreateUser { username: "zsan", age: 18 }
post /submit # 结果:submit form: LoginFormData { field_name: "lis", field_pwd: "123" }
捕获请求头参数
一般通过 HeaderMap 来获取请求头参数,它会提取所有标头。如果要获取指定标头,需要再做额外处理。
use axum::{
routing::get,
Router,
http::header::HeaderMap
};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/header", get(get_header_handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn get_header_handler(headers: HeaderMap) -> String {
// format!("{:?}", headers) // 获取所有请求头
headers
.get(axum::http::header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.map(|v| v.to_string())
.unwrap()
}
测试结果:
(get) /header # 结果:PostmanRuntime/7.43.0
除此之外,axum 还提供了很多有用的提取器,比如 Bytes、String、Body 和 BodyStream 用于获取请求正文。你也可以通过实现 FromRequest 来定义自己的提取器。
响应处理
任何实现了 IntoResponse 的东西都可以被处理程序返回,它会被自动转换为响应。看个例子:
use axum::{
body::Body,
routing::get,
response::Json,
Router,
};
use serde_json::{Value, json};
// &'static str 会被转换为 200 OK,content-type: text/plain; charset=utf-8
async fn plain_text() -> &'static str {
"foo"
}
// Json 提供 content-type: application/json,且适用于任何实现了 serde::Serialize 的类型
async fn json() -> Json<Value> {
Json(json!({ "data": 42 }))
}
let app = Router::new()
.route("/plain_text", get(plain_text))
.route("/json", get(json));
更多详细示例请参考官方文档。
错误处理
Axum 的目标是拥有一个简单且可预测的错误处理模型。它意味着把错误转换为响应很简单,并且能保证所有错误都被处理。
Axum 基于 tower::Service,通过其关联的错误类型工作。如果某个 Service 产生错误,并且一直传到 hyper,连接就会在未发送响应的情况下终止。这显然不是我们想要的,所以 Axum 要求所有服务都把 Infallible 作为错误类型。Infallible 是永远不会发生错误的错误类型。
这段引用自官方文档,翻译过来比较绕。说白了就是:发生错误不能让 HTTP 连接中断而导致响应无法正常返回,而是要保证响应能正常返回,同时携带错误类型和错误信息。Axum 能很轻易地做到这一点。
所以一般 Axum 的处理器方法都这样定义:
use axum::http::StatusCode;
async fn handler() -> Result<String, StatusCode> {
// ...
}
虽然看起来可能会因为 StatusCode 而失败,但实际上这并不算一个"错误"。如果此处理程序返回 Err(some_status_code),它仍然会被转换为响应并发送回客户端。这是通过 StatusCode 的 IntoResponse 实现完成的。
无论返回 Err(StatusCode::NOT_FOUND) 还是 Err(StatusCode::INTERNAL_SERVER_ERROR),在 Axum 中这些都不被视为错误。
不建议直接使用 StatusCode,更好的做法是使用中间的错误类型,最终可以转换为响应,这样就能在处理程序中使用 ? 运算符。
看一个例子:
use axum::{
routing::get,
Router,
body::Body,
http::{Response, StatusCode},
error_handling::HandleError,
};
use tower;
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.merge(router_fallible_service()) // 使用 Service 的错误处理
.route("/foo", get(get_foo));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
fn router_fallible_service() -> Router {
// 这个 service 可能出现任何错误
let some_fallible_service = tower::service_fn(|_req| async {
thing_that_might_fail().await?;
Ok::<_, anyhow::Error>(Response::new(Body::empty()))
});
Router::new().route_service(
"/test_error",
// Service 适配器通过将错误转换为响应来处理错误
HandleError::new(some_fallible_service, handle_anyhow_error),
)
}
async fn thing_that_might_fail() -> Result<(), anyhow::Error> {
// 模拟报错
anyhow::bail!("thing_that_might_fail")
}
// 把错误转化为 IntoResponse
async fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)
}
async fn get_foo() -> Result<String, StatusCode> {
Ok(String::from("get请求foo方法"))
}
大家可以自行运行一下,看看返回的响应。要正常运行上述示例,还需要添加依赖:
anyhow = "1.0.95"
tower = { version = "0.5.2", features = ["full"] }
更多用法请参考 axum::error_handling 官方文档。
中间件
Axum 的独特之处在于它没有自己的定制中间件系统,而是与 Tower 集成。这意味着 tower 和 tower-http 的中间件都可以跟 Axum 一起使用。axum 也可以把请求路由到任何 tower 服务——可以是你用 service_fn 编写的服务,也可以是其他 crate 中的现成组件,从而充分复用和利用不同应用的生态,潜力巨大。
中间件允许你在请求到达处理程序之前或之后添加自定义逻辑,这为实现认证、日志记录、性能监视等通用功能提供了强大的手段。
看一个简单的例子:
use axum::{
body::Body,
http::Request,
middleware::{self, Next},
response::Response,
routing::get,
Router
};
async fn logging_middleware(req: Request<Body>, next: Next) -> Response {
println!("Received a request to {}", req.uri());
next.run(req).await
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, world!" }))
.layer(middleware::from_fn(logging_middleware));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
运行后,用户访问任意请求都会被终端记录下来:
Received a request to /foo
Received a request to /
Received a request to /test
更多例子请参考 axum::middleware 官方文档。
集成 Tracing 来实现日志
Tracing 是什么?
我们使用 Tracing 这个库来实现日志记录。它是一个用于检测 Rust 程序以收集结构化、基于事件的诊断信息的框架。它允许开发者跟踪异步操作的执行流,从而更好地理解、监控和调试应用。对 Web 项目来说,日志监控非常重要。
在像 Tokio 这样的异步系统中,由于异步编程模型较为复杂,解释传统日志消息通常很有挑战性(效率低下且难以掌握执行流程)。Tracing 扩展了日志样式的诊断方式,允许库和应用记录结构化事件,可以按区间 span 记录日志,并提供时间性和因果关系的附加信息,收集关键上下文,极大地提高了应用的可观测性。
添加依赖
这里选用 tracing 库的当前最新版本:
tracing = { version = "0.1.41", features = ["async-await"] } # 日志追踪诊断信息,同时开启异步支持
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "chrono"] } # 日志订阅器,更好地处理日志
这里把 axum 也升到最新版本,笔者发文时的最新版本是 0.8.4。
简单日志记录
- 编写 logger.rs
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
pub fn init() {
// 注册一个全局日志记录器
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) // 设置日志级别
.with(
tracing_subscriber::fmt::layer()
.with_file(true) // 打印文件名
.with_line_number(true) // 打印行号
.with_thread_ids(true) // 打印线程 ID
.with_thread_names(true)// 打印线程名称
.with_target(false) // 不打印 target
)
.init();
}
如果从环境变量 RUST_LOG 未获取到日志等级,就设置默认等级为 info。
- main.rs
tracing 有 TRACE、DEBUG、INFO、WARN、ERROR 共 5 个日志级别,其中 TRACE 是最详细的级别。
mod logger;
#[tokio::main]
async fn main() {
logger::init(); // 初始化日志
let app = Router::new()
.route("/test", get(handle_test)); // 测试接口
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
tracing::info!("rust_demo listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
#[debug_handler]
async fn handle_test() -> Json<Value> {
tracing::trace!("trace");
tracing::debug!("debug");
tracing::info!("info,test方法打印");
tracing::warn!("warn");
tracing::error!("error");
Json(json!({
"code": 0,
"msg": "请求成功",
"data": "手动构造返回请求体"
}))
}
logger::init() 初始化日志后,就可以通过 tracing::debug! 这样的宏来记录日志,非常方便。上面设置的日志等级是 INFO,因此 TRACE 和 DEBUG 级别的日志不会显示。
Spans 与 Events
Spans(区间):Span 表示具有开始和结束的时间段及元数据。当程序在上下文中执行一个工作单元时,就会进入该上下文的 span;停止执行时会退出 span。线程当前正在执行的 span 称为该线程的当前 span。
use tracing::{span, Level};
let span = span!(Level::TRACE, "my_span");
// enter 返回一个 RAII guard,drop 时退出 span
let _enter = span.enter();
// 在 my_span 上下文中执行一些工作...
Events(事件):表示某个时刻发生的事情,与非结构化日志中的记录类似。但与典型的日志行不同,Event 可以发生在 span 的上下文中。
use tracing::{event, span, Level};
// 在任意 span 上下文之外记录事件
event!(Level::INFO, "something happened");
let span = span!(Level::INFO, "my_span");
let _guard = span.enter();
// 在 my_span 中记录事件
event!(Level::DEBUG, "something happened inside my_span");
读取多种格式的配置文件
项目里通常需要读取 yaml、json、toml、env 等格式的配置文件,或者从环境变量中读取配置。
这里选用 Rust 中常用的配置读取库 config,它支持多种格式(YAML、JSON、TOML、INI 等),非常灵活强大。
- 添加依赖
config = { version = "0.15.11", features = ["yaml"] } # 配置
anyhow = "1.0.98" # 错误处理
- 新建一个
config 子模块,不然逻辑都写在 main.rs 里就太臃肿了。
创建 config/server.rs 文件,用于定义服务器配置结构体和对应方法:
use serde::Deserialize;
/// 服务器配置结构体
#[derive(Debug, Deserialize)]
pub struct ServerConfig {
pub port: Option<u16>, // 端口
// pub host: String,
// pub log_level: String,
// pub log_file: String,
// pub log_max_size: u64,
// pub log_max_backups: u64,
// pub log_max_age: u64,
// pub log_compress: bool,
// pub log_local_time: bool,
// pub log_utc_time: bool,
// pub log_format: String,
}
impl ServerConfig {
pub fn port(&self) -> u16 {
self.port.unwrap_or(3000)
}
}
- 编写
config/mod.rs。先定义应用总配置结构体 AppConfig,用来整合服务器配置、数据库配置等,再通过 load 方法实现读取配置文件的核心逻辑。这里格式选择 yaml。
mod server;
use std::sync::LazyLock;
pub use server::ServerConfig;
use config::{Config, FileFormat};
use serde::Deserialize;
use anyhow::Context;
#[derive(Debug, Deserialize)]
pub struct AppConfig {
pub server: ServerConfig,
}
static CONFIG: LazyLock<AppConfig> =
LazyLock::new(|| AppConfig::load().expect("初始化配置文件失败"));
impl AppConfig {
pub fn load() -> anyhow::Result<Self> {
Config::builder()
// 加载默认配置文件(必须存在)
.add_source(
config::File::with_name("application")
.format(FileFormat::Yaml)
.required(true)
)
// 加载环境变量
.add_source(
config::Environment::with_prefix("APP")
.try_parsing(true)
.separator("_") // 变量前缀分隔符
.list_separator(",") // 嵌套字段分隔符
)
.build()
.with_context(|| anyhow::anyhow!("Failed to load config"))?
.try_deserialize() // 反序列化为 AppConfig 结构
.with_context(|| anyhow::anyhow!("Failed to deserialize config"))
}
pub fn server(&self) -> &ServerConfig {
&self.server
}
}
pub fn get() -> &'static AppConfig {
&CONFIG
}
这里主要通过 Config::builder() 构建配置加载器,依次加载 yaml 配置文件和环境变量。注意加载多个配置时,后加载者优先级更高,会覆盖先加载者。
多环境配置文件读取,可以像 Java 的 Spring 框架一样引入:
// 获取当前运行环境,默认为 dev
let env = std::env::var("APP_ENV").unwrap_or_else(|_| "dev".into());
tracing::info!("APP_ENV:{}", env);
// 加载环境特定配置文件(可选)
.add_source(config::File::with_name(&format!("application-{}", env))
.format(FileFormat::Yaml)
.required(false))
另外 static CONFIG: LazyLock<AppConfig> = LazyLock::new(|| AppConfig::load().expect("初始化配置文件失败"));
这行代码定义并初始化了一个全局静态配置对象。它在第一次被访问时(懒加载 LazyLock)会尝试自动加载配置文件并初始化,如果失败则直接 panic 并输出提示。这样就能在程序任何地方方便地访问全局配置,而且只会加载一次,线程安全。

这里也可以使用 axum 状态共享来实现,但需要给每个 Handler 显式传递,比全局静态变量复杂一些。配置这一块的原则是:读配置文件,如果没有就取默认值。
- 测试。在
main.rs 中调用。
当然,首先要创建 src/application.yaml 配置文件:
server:
port: 9999
use axum::{
routing::get,
debug_handler,
Json, Router
};
use serde_json::{Value, json};
mod logger;
mod config; // 别忘了声明模块
#[tokio::main]
async fn main() {
let _guard = logger::init();
let app = Router::new()
.route("/testConfig", get(handle_config_test)); // 配置文件测试
let port = config::get().server().port();
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
#[debug_handler]
async fn handle_config_test() -> Json<Value> {
// 读取 yaml 配置文件
let config = config::get();
tracing::info!("server port:{}", config.server().port());
Json(json!({
"code": 0,
"msg": "请求成功",
"data": null
}))
}
请求 /testConfig 接口,控制台会打印:
2025-06-26T02:35:17.445796Z INFO server port:9999
at src\main.rs:28 on tokio-runtime-worker ThreadId(10)
需要注意,笔者发现 Rust 打包出的可执行文件并不会包含配置文件,所以配置文件后续得传到生产环境,建议放在与可执行文件同级的目录下。
如果使用多环境配置文件读取,需要执行下面的命令注入环境变量:
APP_ENV=prod cargo run
这个时候,有同学可能会问:为什么我这里报了这个错?

别急,前面说的是 Linux 下的写法。在 Windows 下应该这样写:
$env:APP_ENV="prod"; cargo run

读取 Cargo.toml 文件
除了上面这些格式的文件,如果想读取项目自带的 Cargo.toml 配置,该怎么办?不需要引入新的第三方库,直接这样:
use std::env;
// 读取项目默认 Cargo.toml 中的元信息
let name = env!("CARGO_PKG_NAME");
let version = env!("CARGO_PKG_VERSION");
let author = env!("CARGO_PKG_AUTHORS");
tracing::info!("project info:{} {} {}", name, version, author);
日志打印结果:
2025-06-26T02:39:44.920518Z INFO project info:rust_litchi_admin 0.1.0 zhangjunhttps://xiaoniuhululu.com
at src\main.rs:35 on tokio-runtime-worker ThreadId(17)
读取 .env 文件
常见的配置文件还有 .env 格式,这里使用 dotenv 来实现需求。
- 添加依赖
dotenv = "0.15.0" # 读取 .env 文件
- 在根目录下创建
.env 文件
BOOK="四大名著"
- 使用测试
在 main 函数中,需要检查一下:
use dotenv::dotenv;
use std::env;
#[tokio::main]
async fn main() {
// ...
dotenv().ok(); // 访问 .env 文件前,需要检查是否正常,防止 panic
axum::serve(listener, app).await.unwrap();
}
- 读取测试
// 读取 .env
let book = env::var("BOOK").expect("BOOK 没有在 .env 文件里设置");
tracing::info!(" book: {}", book);
控制台打印结果:
2025-06-26T05:33:46.091872Z INFO book: 四大名著
at src\main.rs:40 on tokio-runtime-worker ThreadId(13)
小结
本文覆盖了一个 Web 框架需要解决的核心问题:路由、处理器、提取器、响应处理、错误处理、中间件、日志追踪以及配置文件读取。后续还会继续探索 Axum 更多有意思的特性。如果你对 Rust 或 Web 开发感兴趣,也欢迎到云栈社区互相交流学习。