找回密码
立即注册
搜索
发回帖 发新帖

6619

积分

0

好友

831

主题
发表于 昨天 23:05 | 查看: 7| 回复: 0

平时闲暇的时候,我们程序员会写一些小项目:网页、桌面应用、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 应用框架,主要有以下几个特点:

  1. 使用无宏的 API 实现路由(router)功能
  2. 使用提取器(extractor)对请求进行声明式解析
  3. 简单且可预测的错误处理模式
  4. 用最少的模板生成响应
  5. 充分利用 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。

简单日志记录

  1. 编写 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。

  1. 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

  1. 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 上下文中执行一些工作...
  1. 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 等),非常灵活强大。

  1. 添加依赖
config = { version = "0.15.11", features = ["yaml"] } # 配置
anyhow = "1.0.98" # 错误处理
  1. 新建一个 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)
    }
}
  1. 编写 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 并输出提示。这样就能在程序任何地方方便地访问全局配置,而且只会加载一次,线程安全。

Doge 拿奶茶表情包

这里也可以使用 axum 状态共享来实现,但需要给每个 Handler 显式传递,比全局静态变量复杂一些。配置这一块的原则是:读配置文件,如果没有就取默认值。

  1. 测试。在 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

这个时候,有同学可能会问:为什么我这里报了这个错?

Windows PowerShell 执行 APP_ENV=prod 报错截图

别急,前面说的是 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 来实现需求。

  1. 添加依赖
dotenv = "0.15.0" # 读取 .env 文件
  1. 在根目录下创建 .env 文件
BOOK="四大名著"
  1. 使用测试

在 main 函数中,需要检查一下:

use dotenv::dotenv;
use std::env;

#[tokio::main]
async fn main() {
    // ...
    dotenv().ok(); // 访问 .env 文件前,需要检查是否正常,防止 panic

    axum::serve(listener, app).await.unwrap();
}
  1. 读取测试
// 读取 .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 开发感兴趣,也欢迎到云栈社区互相交流学习。




上一篇:Cortex-M 程序内存布局:数据段、链接脚本与启动代码详解
下一篇:Rust + Axum 高性能 Web 框架入门:从 Hello World 到中间件实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-22 08:52 , Processed in 0.123076 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表