Axum: Rust 好用的 Web 框架

Axum 是 Rust 生态中基于 Tokio 异步运行时Tower 中间件体系打造的高性能 Web 框架,以"类型安全、无宏入侵、轻量高效"为核心优势,广泛应用于云原生、微服务、API 网关等场景。它摒弃了传统 Web 框架的宏魔法,完全依赖 Rust 的类型系统实现路由匹配、请求解析、响应处理,兼顾了开发效率与运行性能。

本文将从环境搭建、核心概念、路由设计、请求处理、中间件开发到生产级实战,全方位拆解 Axum 的使用技巧,每个知识点均配套可运行的示例代码,帮助开发者从入门到精通,快速构建高性能的 Rust Web 应用。

一、环境准备与项目初始化

1.1 前置条件

  • 安装 Rust 环境:确保 rustc 版本 ≥ 1.64(Axum 对 Rust 版本有最低要求),可通过 rustup update 升级。
  • 熟悉 Tokio 异步编程:Axum 基于 Tokio 运行时,需掌握 async/await 语法。
  • 了解 Tower 中间件:Axum 底层复用 Tower 生态,中间件设计与 Tower 完全兼容。

1.2 创建 Axum 项目

首先创建一个新的 Rust 项目,并添加 Axum 及相关依赖:

bash 复制代码
cargo new axum-demo && cd axum-demo

修改 Cargo.toml,添加核心依赖:

toml 复制代码
[package]
name = "axum-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
# Axum 核心依赖
axum = "0.7"
# Tokio 异步运行时(必须启用 full 特性)
tokio = { version = "1.0", features = ["full"] }
# HTTP 请求/响应类型定义
http = "1.0"
# 日志处理
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# JSON 序列化/反序列化
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

1.3 编写第一个 Axum 应用

创建一个最简的 HTTP 服务,监听 0.0.0.0:3000,提供一个根路由 /

rust 复制代码
use axum::{routing::get, Router, Server};
use std::net::SocketAddr;

// 根路由处理函数:返回 "Hello, Axum!"
async fn root_handler() -> &'static str {
    "Hello, Axum!"
}

#[tokio::main]
async fn main() {
    // 初始化日志
    tracing_subscriber::fmt::init();

    // 构建路由:将 GET 请求 / 映射到 root_handler
    let app = Router::new().route("/", get(root_handler));

    // 定义监听地址
    let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
    println!("服务器运行在 http://{}", addr);

    // 启动服务器
    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

运行项目:

bash 复制代码
cargo run

访问 http://localhost:3000,即可看到 Hello, Axum!,标志着第一个 Axum 应用搭建成功。

二、核心概念:路由、提取器与响应

Axum 的核心设计围绕 路由(Router)提取器(Extractor)响应(Response) 三大组件展开,三者协同工作,完成从请求接收、参数解析到响应返回的全流程。

2.1 路由(Router):请求分发的核心

Router 是 Axum 的路由管理器,负责将不同 HTTP 方法和路径映射到对应的处理函数。它支持路由嵌套方法匹配路径参数等功能。

2.1.1 基本路由与 HTTP 方法

Axum 支持 getpostputdelete 等常见 HTTP 方法,通过 route 方法绑定处理函数:

rust 复制代码
use axum::{routing::{get, post}, Router};

async fn get_handler() -> &'static str {
    "这是 GET 请求"
}

async fn post_handler() -> &'static str {
    "这是 POST 请求"
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/get", get(get_handler))
        .route("/post", post(post_handler));

    // 启动服务器...
}
2.1.2 路径参数

通过 /:param 语法定义路径参数,处理函数通过提取器 获取参数值。支持 Stringu32i64 等多种类型:

rust 复制代码
use axum::{extract::Path, routing::get, Router};
use std::collections::HashMap;

// 提取单个路径参数
async fn user_handler(Path(user_id): Path<u32>) -> String {
    format!("用户 ID:{}", user_id)
}

// 提取多个路径参数
async fn article_handler(Path((user_id, article_id)): Path<(u32, String)>) -> String {
    format!("用户 {} 的文章:{}", user_id, article_id)
}

// 提取路径参数到 HashMap
async fn map_handler(Path(params): Path<HashMap<String, String>>) -> String {
    format!("路径参数:{:?}", params)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/user/:user_id", get(user_handler))
        .route("/article/:user_id/:article_id", get(article_handler))
        .route("/map/:k1/:k2", get(map_handler));

    // 启动服务器...
}

测试路径:

  • http://localhost:3000/user/100 → 输出 用户 ID:100
  • http://localhost:3000/article/100/rust-axum → 输出 用户 100 的文章:rust-axum
2.1.3 路由嵌套

通过 nest 方法实现路由嵌套,适合按业务模块划分路由(如用户模块、文章模块),提升代码可读性:

rust 复制代码
use axum::{routing::get, Router};

// 用户模块路由
fn user_routes() -> Router {
    Router::new()
        .route("/", get(|| async { "用户列表" }))
        .route("/:id", get(|Path(id): Path<u32>| async move { format!("用户详情:{}", id) }))
}

// 文章模块路由
fn article_routes() -> Router {
    Router::new()
        .route("/", get(|| async { "文章列表" }))
        .route("/:id", get(|Path(id): Path<String>| async move { format!("文章详情:{}", id) }))
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .nest("/user", user_routes())
        .nest("/article", article_routes());

    // 启动服务器...
}

测试嵌套路由:

  • http://localhost:3000/user → 输出 用户列表
  • http://localhost:3000/article/rust-axum → 输出 文章详情:rust-axum

2.2 提取器(Extractor):请求参数的解析利器

提取器是 Axum 的灵魂特性,它允许处理函数通过函数参数的形式,自动从 HTTP 请求中提取所需数据,无需手动解析请求体或查询参数。Axum 内置了多种提取器,覆盖绝大多数场景。

2.2.1 常见内置提取器
提取器 作用 示例
Path<T> 提取路径参数 Path(user_id): Path<u32>
Query<T> 提取 URL 查询参数 Query(params): Query<HashMap<String, String>>
Json<T> 提取 JSON 请求体并反序列化为 T Json(user): Json<User>
Form<T> 提取表单请求体(application/x-www-form-urlencoded Form(form): Form<LoginForm>
HeaderMap 提取请求头 headers: HeaderMap
State<T> 提取应用全局状态 state: State<AppState>
2.2.2 提取器实战:解析 JSON 请求体

定义一个用户结构体,通过 Json 提取器接收并解析 JSON 请求体:

rust 复制代码
use axum::{extract::Json, routing::post, Router};
use serde::Deserialize;

// 定义用户结构体,派生 Deserialize 特性以支持 JSON 反序列化
#[derive(Deserialize)]
struct CreateUserRequest {
    name: String,
    age: u32,
    email: String,
}

// 处理函数:提取 JSON 请求体
async fn create_user(Json(req): Json<CreateUserRequest>) -> String {
    format!(
        "创建用户成功:姓名={}, 年龄={}, 邮箱={}",
        req.name, req.age, req.email
    )
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/user", post(create_user));

    // 启动服务器...
}

使用 curl 测试 POST 请求:

bash 复制代码
curl -X POST -H "Content-Type: application/json" -d '{"name":"张三","age":25,"email":"zhangsan@example.com"}' http://localhost:3000/user

输出结果:创建用户成功:姓名=张三, 年龄=25, 邮箱=zhangsan@example.com

2.2.3 提取器实战:解析查询参数

通过 Query 提取器解析 URL 中的查询参数,支持自动反序列化为结构体:

rust 复制代码
use axum::{extract::Query, routing::get, Router};
use serde::Deserialize;

#[derive(Deserialize)]
struct Pagination {
    page: u32,
    size: u32,
}

async fn list_users(Query(pagination): Query<Pagination>) -> String {
    format!("查询用户列表:第 {} 页,每页 {} 条", pagination.page, pagination.size)
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/users", get(list_users));

    // 启动服务器...
}

测试 URL:http://localhost:3000/users?page=1&size=10 → 输出 查询用户列表:第 1 页,每页 10 条

2.2.4 多提取器组合使用

一个处理函数可以同时使用多个提取器,Axum 会按顺序自动解析:

rust 复制代码
use axum::{extract::{Path, Query}, routing::get, Router};
use serde::Deserialize;

#[derive(Deserialize)]
struct Filter {
    keyword: String,
}

async fn search_articles(
    Path(user_id): Path<u32>,
    Query(filter): Query<Filter>,
) -> String {
    format!("用户 {} 搜索文章:关键词={}", user_id, filter.keyword)
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/user/:user_id/articles", get(search_articles));

    // 启动服务器...
}

测试 URL:http://localhost:3000/user/100/articles?keyword=rust → 输出 用户 100 搜索文章:关键词=rust

2.3 响应(Response):灵活的返回值处理

Axum 支持多种类型的返回值作为响应,无需手动构建 http::Response。常见的响应类型包括:

  • 字符串、&str → 自动转为 text/plain 响应
  • Json<T> → 自动转为 application/json 响应
  • StatusCode → 仅返回 HTTP 状态码
  • (StatusCode, Json<T>) → 返回状态码 + JSON 响应
  • 自定义响应体
2.3.1 基础响应类型
rust 复制代码
use axum::{
    extract::Path,
    http::StatusCode,
    response::Json,
    routing::get,
    Router,
};
use serde::Serialize;

#[derive(Serialize)]
struct User {
    id: u32,
    name: String,
}

async fn success_response() -> Json<User> {
    Json(User {
        id: 100,
        name: "张三".to_string(),
    })
}

async fn error_response() -> StatusCode {
    StatusCode::NOT_FOUND
}

async fn custom_response(Path(id): Path<u32>) -> (StatusCode, String) {
    if id == 100 {
        (StatusCode::OK, "请求成功".to_string())
    } else {
        (StatusCode::BAD_REQUEST, "无效 ID".to_string())
    }
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/success", get(success_response))
        .route("/error", get(error_response))
        .route("/custom/:id", get(custom_response));

    // 启动服务器...
}
2.3.2 自定义响应头

通过 ResponseBuilder 可以自定义响应头,例如设置 Content-TypeAuthorization 等:

rust 复制代码
use axum::{
    http::header,
    response::IntoResponse,
    routing::get,
    Router,
};

async fn custom_header() -> impl IntoResponse {
    (
        [
            (header::CONTENT_TYPE, "application/xml"),
            (header::X_POWERED_BY, "Axum"),
        ],
        "<user><id>100</id><name>张三</name></user>",
    )
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/xml", get(custom_header));

    // 启动服务器...
}

三、全局状态管理:State 提取器

在 Web 应用中,经常需要共享全局资源(如数据库连接池、配置信息、缓存客户端)。Axum 通过 State 提取器实现全局状态管理,状态会被所有路由共享,且支持类型安全的访问。

3.1 定义与注入全局状态

首先定义一个全局状态结构体,然后通过 Router::with_state 注入到应用中:

rust 复制代码
use axum::{extract::State, routing::get, Router, Server};
use std::net::SocketAddr;
use std::sync::Arc;

// 定义全局状态:包含应用名称和版本
#[derive(Clone)]
struct AppState {
    app_name: String,
    app_version: String,
    // 实际项目中可添加数据库连接池、Redis 客户端等
    // db_pool: sqlx::PgPool,
}

// 提取全局状态并使用
async fn get_app_info(State(state): State<Arc<AppState>>) -> String {
    format!(
        "应用名称:{},版本:{}",
        state.app_name, state.app_version
    )
}

#[tokio::main]
async fn main() {
    // 初始化全局状态,使用 Arc 实现线程安全的共享
    let app_state = Arc::new(AppState {
        app_name: "Axum Demo".to_string(),
        app_version: "1.0.0".to_string(),
    });

    // 注入状态到路由
    let app = Router::new()
        .route("/info", get(get_app_info))
        .with_state(app_state);

    let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

关键注意点

  • 状态结构体必须实现 Clone 特性(Axum 内部会克隆状态传递给处理函数)。
  • 对于重量级资源(如数据库连接池),需用 Arc 包裹,避免频繁克隆的性能开销。
  • 状态是不可变 的,若需修改状态,需配合 Mutex/RwLock 等同步原语(异步场景推荐使用 tokio::sync::Mutex)。

3.2 状态修改实战:计数器

通过 Arc + tokio::sync::Mutex 实现线程安全的可变状态:

rust 复制代码
use axum::{extract::State, routing::get, Router, Server};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::Mutex;

// 定义包含计数器的全局状态
struct AppState {
    counter: Mutex<u32>,
}

async fn increment_counter(State(state): State<Arc<AppState>>) -> String {
    let mut counter = state.counter.lock().await;
    *counter += 1;
    format!("当前计数器值:{}", counter)
}

async fn get_counter(State(state): State<Arc<AppState>>) -> String {
    let counter = state.counter.lock().await;
    format!("当前计数器值:{}", counter)
}

#[tokio::main]
async fn main() {
    let app_state = Arc::new(AppState {
        counter: Mutex::new(0),
    });

    let app = Router::new()
        .route("/increment", get(increment_counter))
        .route("/counter", get(get_counter))
        .with_state(app_state);

    let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

访问 http://localhost:3000/increment 可递增计数器,访问 http://localhost:3000/counter 可查看当前值。

四、中间件:请求/响应的拦截与处理

中间件是 Axum 处理请求生命周期的核心机制,它可以在请求到达处理函数之前拦截请求(如鉴权、日志、限流),或在响应返回客户端之前修改响应(如添加响应头、压缩响应体)。

Axum 中间件完全兼容 Tower 生态,可直接使用 Tower 提供的中间件,也可自定义中间件。

4.1 内置中间件:日志与压缩

首先演示如何使用 Tower 提供的 tower-http 中间件,实现请求日志和响应压缩。

添加依赖到 Cargo.toml

toml 复制代码
tower-http = { version = "0.5", features = ["logging", "compression"] }

使用日志和压缩中间件:

rust 复制代码
use axum::{routing::get, Router, Server};
use std::net::SocketAddr;
use tower_http::{compression::CompressionLayer, logging::LoggingLayer};

async fn root() -> &'static str {
    "Hello, Axum with Middleware!"
}

#[tokio::main]
async fn main() {
    // 初始化日志
    tracing_subscriber::fmt::init();

    let app = Router::new()
        .route("/", get(root))
        // 添加日志中间件:记录所有请求的方法、路径、状态码等
        .layer(LoggingLayer::new())
        // 添加压缩中间件:自动压缩响应体(支持 gzip、br 等)
        .layer(CompressionLayer::new());

    let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

运行项目后,控制台会输出请求日志,响应体也会被自动压缩。

4.2 自定义中间件:鉴权与限流

Axum 支持两种自定义中间件的方式:基于函数的中间件 (简单场景)和 基于 Tower Service 的中间件(复杂场景)。

4.2.1 基于函数的中间件:API 鉴权

实现一个简单的鉴权中间件,检查请求头中是否包含有效的 Authorization 令牌:

rust 复制代码
use axum::{
    body::Body,
    extract::Request,
    http::{header::AUTHORIZATION, StatusCode},
    middleware::Next,
    response::Response,
    routing::get,
    Router,
};

// 自定义鉴权中间件
async fn auth_middleware(mut req: Request<Body>, next: Next) -> Result<Response, StatusCode> {
    // 从请求头中获取 Authorization
    let auth_header = req.headers().get(AUTHORIZATION);
    if let Some(token) = auth_header {
        // 简单校验令牌是否为 "Bearer axum-token"
        if token == "Bearer axum-token" {
            // 令牌有效,继续处理请求
            return Ok(next.run(req).await);
        }
    }
    // 令牌无效,返回 401 未授权
    Err(StatusCode::UNAUTHORIZED)
}

async fn protected_route() -> &'static str {
    "这是受保护的路由,鉴权成功!"
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/protected", get(protected_route))
        // 为特定路由添加中间件
        .route_layer(axum::middleware::from_fn(auth_middleware));

    // 启动服务器...
}

测试:

  • 携带有效令牌:curl -H "Authorization: Bearer axum-token" http://localhost:3000/protected → 输出 这是受保护的路由,鉴权成功!
  • 无令牌或无效令牌:返回 401 Unauthorized
4.2.2 全局中间件与路由级中间件

Axum 支持全局中间件 (作用于所有路由)和路由级中间件(仅作用于特定路由):

rust 复制代码
use axum::{
    middleware::from_fn,
    routing::get,
    Router,
};

// 全局日志中间件
async fn log_middleware(req: Request<Body>, next: Next) -> Result<Response, StatusCode> {
    println!("请求路径:{}", req.uri().path());
    Ok(next.run(req).await)
}

// 路由级鉴权中间件
async fn auth_middleware(req: Request<Body>, next: Next) -> Result<Response, StatusCode> {
    // 鉴权逻辑...
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/public", get(|| async { "公共路由" }))
        .route("/protected", get(protected_route))
        // 路由级中间件
        .route_layer(from_fn(auth_middleware))
        // 全局中间件:作用于所有路由
        .layer(from_fn(log_middleware));

    // 启动服务器...
}

五、生产级实战:Axum + SQLx 构建 RESTful API

本节将结合 Axum 和 SQLx(Rust 生态的异步 SQL 工具),构建一个完整的用户管理 RESTful API,包含数据库连接CRUD 操作错误处理等生产级特性。

5.1 依赖准备

添加 SQLx 及 PostgreSQL 依赖到 Cargo.toml

toml 复制代码
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls", "macros", "json"] }
dotenv = "0.15"

创建 .env 文件,配置数据库连接信息:

env 复制代码
DATABASE_URL=postgres://username:password@localhost:5432/axum_demo

5.2 数据库初始化

创建 users 表,并编写 SQLx 迁移脚本:

bash 复制代码
# 创建迁移目录
mkdir -p migrations

# 创建 0001_create_users_table.sql 迁移文件
echo "
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    age INT NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
" > migrations/0001_create_users_table.up.sql

echo "
DROP TABLE users;
" > migrations/0001_create_users_table.down.sql

运行迁移脚本,创建数据库表:

bash 复制代码
sqlx migrate run --database-url postgres://username:password@localhost:5432/axum_demo

5.3 编写 RESTful API

实现用户的创建、查询、更新、删除(CRUD)操作:

rust 复制代码
use axum::{
    extract::{Json, Path, Query, State},
    http::StatusCode,
    middleware::from_fn,
    response::IntoResponse,
    routing::{delete, get, post, put},
    Router, Server,
};
use dotenv::dotenv;
use serde::{Deserialize, Serialize};
use sqlx::{postgres::PgPoolOptions, PgPool};
use std::collections::HashMap;
use std::env;
use std::net::SocketAddr;
use std::sync::Arc;

// 定义全局状态:包含数据库连接池
struct AppState {
    db_pool: PgPool,
}

// 用户结构体
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
struct User {
    id: i32,
    name: String,
    age: i32,
    email: String,
}

// 创建用户请求体
#[derive(Debug, Deserialize)]
struct CreateUserRequest {
    name: String,
    age: i32,
    email: String,
}

// 更新用户请求体
#[derive(Debug, Deserialize)]
struct UpdateUserRequest {
    name: Option<String>,
    age: Option<i32>,
    email: Option<String>,
}

// 错误响应结构体
#[derive(Debug, Serialize)]
struct ErrorResponse {
    message: String,
}

// 自定义错误类型
enum AppError {
    DatabaseError(sqlx::Error),
    NotFound,
    BadRequest(String),
}

// 实现 IntoResponse,将 AppError 转为 HTTP 响应
impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let (status, message) = match self {
            AppError::DatabaseError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
            AppError::NotFound => (StatusCode::NOT_FOUND, "资源不存在".to_string()),
            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
        };
        (status, Json(ErrorResponse { message })).into_response()
    }
}

// 转换 sqlx::Error 为 AppError
impl From<sqlx::Error> for AppError {
    fn from(err: sqlx::Error) -> Self {
        match err {
            sqlx::Error::RowNotFound => AppError::NotFound,
            _ => AppError::DatabaseError(err),
        }
    }
}

// 1. 创建用户
async fn create_user(
    State(state): State<Arc<AppState>>,
    Json(req): Json<CreateUserRequest>,
) -> Result<impl IntoResponse, AppError> {
    let user = sqlx::query_as!(
        User,
        "INSERT INTO users (name, age, email) VALUES ($1, $2, $3) RETURNING *",
        req.name,
        req.age,
        req.email
    )
    .fetch_one(&state.db_pool)
    .await?;

    Ok((StatusCode::CREATED, Json(user)))
}

// 2. 查询所有用户
async fn list_users(
    State(state): State<Arc<AppState>>,
    Query(params): Query<HashMap<String, String>>,
) -> Result<impl IntoResponse, AppError> {
    let page = params.get("page").map(|p| p.parse::<i32>().unwrap_or(1)).unwrap_or(1);
    let size = params.get("size").map(|s| s.parse::<i32>().unwrap_or(10)).unwrap_or(10);
    let offset = (page - 1) * size;

    let users = sqlx::query_as!(User, "SELECT * FROM users LIMIT $1 OFFSET $2", size, offset)
        .fetch_all(&state.db_pool)
        .await?;

    Ok(Json(users))
}

// 3. 查询单个用户
async fn get_user(
    State(state): State<Arc<AppState>>,
    Path(user_id): Path<i32>,
) -> Result<impl IntoResponse, AppError> {
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
        .fetch_one(&state.db_pool)
        .await?;

    Ok(Json(user))
}

// 4. 更新用户
async fn update_user(
    State(state): State<Arc<AppState>>,
    Path(user_id): Path<i32>,
    Json(req): Json<UpdateUserRequest>,
) -> Result<impl IntoResponse, AppError> {
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
        .fetch_one(&state.db_pool)
        .await?;

    let name = req.name.unwrap_or(user.name);
    let age = req.age.unwrap_or(user.age);
    let email = req.email.unwrap_or(user.email);

    let updated_user = sqlx::query_as!(
        User,
        "UPDATE users SET name = $1, age = $2, email = $3 WHERE id = $4 RETURNING *",
        name,
        age,
        email,
        user_id
    )
    .fetch_one(&state.db_pool)
    .await?;

    Ok(Json(updated_user))
}

// 5. 删除用户
async fn delete_user(
    State(state): State<Arc<AppState>>,
    Path(user_id): Path<i32>,
) -> Result<impl IntoResponse, AppError> {
    let result = sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
        .execute(&state.db_pool)
        .await?;

    if result.rows_affected() == 0 {
        return Err(AppError::NotFound);
    }

    Ok(StatusCode::NO_CONTENT)
}

#[tokio::main]
async fn main() {
    // 加载 .env 文件
    dotenv().ok();

    // 初始化日志
    tracing_subscriber::fmt::init();

    // 从环境变量获取数据库连接 URL
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL 未设置");

    // 创建数据库连接池
    let db_pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&database_url)
        .await
        .expect("无法连接到数据库");

    // 初始化全局状态
    let app_state = Arc::new(AppState { db_pool });

    // 构建路由
    let app = Router::new()
        .route("/users", post(create_user))
        .route("/users", get(list_users))
        .route("/users/:id", get(get_user))
        .route("/users/:id", put(update_user))
        .route("/users/:id", delete(delete_user))
        .with_state(app_state);

    // 定义监听地址
    let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
    println!("服务器运行在 http://{}", addr);

    // 启动服务器
    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

5.4 测试 API

使用 curl 或 Postman 测试 CRUD 接口:

  1. 创建用户
bash 复制代码
curl -X POST -H "Content-Type: application/json" -d '{"name":"张三","age":25,"email":"zhangsan@example.com"}' http://localhost:3000/users
  1. 查询所有用户
bash 复制代码
curl http://localhost:3000/users?page=1&size=10
  1. 查询单个用户
bash 复制代码
curl http://localhost:3000/users/1
  1. 更新用户
bash 复制代码
curl -X PUT -H "Content-Type: application/json" -d '{"age":26}' http://localhost:3000/users/1
  1. 删除用户
bash 复制代码
curl -X DELETE http://localhost:3000/users/1

六、进阶拓展:Axum 生态与性能优化

6.1 Axum 生态周边工具

  • 身份认证 :使用 axum-login 实现会话管理,jsonwebtoken 实现 JWT 鉴权。
  • OpenAPI 文档 :使用 utoipa + utoipa-swagger-ui 自动生成 Swagger 文档。
  • 缓存 :使用 tower-http::caching 实现 HTTP 缓存,或 redis 客户端实现分布式缓存。
  • 限流 :使用 tower-http::limit 实现请求限流,防止服务过载。

6.2 性能优化技巧

  1. 使用 tokio-uring 提升 I/O 性能 :对于高并发 I/O 场景,启用 Tokio 的 uring 特性,利用 Linux io_uring 机制减少系统调用开销。
  2. 合理配置连接池:数据库连接池大小不宜过大(建议等于 CPU 核心数),避免连接竞争。
  3. 启用响应压缩 :使用 tower-http::compression 中间件压缩响应体,减少网络传输量。
  4. 避免阻塞异步任务 :所有阻塞操作(如同步文件读写、CPU 密集计算)必须放入 tokio::task::spawn_blocking
  5. 使用 Release 模式编译 :生产环境编译时添加 --release 标志,启用 Rust 编译器的优化。

七、Acxum 对比 Actix-web

Acxum 对比 Actix-web 最直观的区别:无宏入侵

一、"无宏入侵"的核心含义

"无宏入侵"是指 Axum 实现 Web 开发核心能力(路由定义、请求参数解析、响应处理等)时,完全基于 Rust 原生的类型系统、函数参数和 Trait 设计,不依赖自定义宏作为核心实现手段;开发者编写的代码贴近原生 Rust 语法,无"隐藏的宏魔法",逻辑直观、可调试性强。

与之相对,"宏入侵"是指框架将核心逻辑封装在自定义宏中,开发者必须通过编写宏(如 #[get]#[post]#[web::query] 等)才能使用框架核心功能------宏会改变代码的原生书写方式,甚至隐藏底层类型和逻辑,增加代码的"魔法感"和学习/调试成本。

是 Rust 生态中另一款主流 Web 框架,其核心功能(路由、参数提取、处理器定义)大量依赖宏实现,是理解"宏入侵"的典型案例。下面通过路由定义请求参数提取两个核心场景,对比 Axum(无宏)和 actix-web(宏依赖)的代码差异。

二、场景1:路由定义的对比

1. Axum:无宏,纯函数调用定义路由

Axum 的路由通过 Router 结构体的普通方法(routenest 等)定义,完全是原生 Rust 函数调用,无任何宏:

rust 复制代码
// Axum 路由定义(无宏)
use axum::{routing::{get, post}, Router};
use std::net::SocketAddr;

// 普通异步函数,无任何宏标注
async fn get_user() -> &'static str {
    "获取用户信息"
}

async fn create_user() -> &'static str {
    "创建用户"
}

#[tokio::main]
async fn main() {
    // 纯函数调用构建路由,逻辑清晰
    let app = Router::new()
        .route("/user", get(get_user))  // GET 路由:函数调用
        .route("/user", post(create_user)); // POST 路由:函数调用

    let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}
2. actix-web:依赖宏定义路由

actix-web 必须通过 #[get]#[post] 等宏标注处理函数,路由注册也依赖宏或宏生成的逻辑:

rust 复制代码
// actix-web 路由定义(强依赖宏)
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};

// 核心逻辑依赖 #[get] 宏标注
#[get("/user")]  // 宏:绑定 GET 方法和路径
async fn get_user() -> impl Responder {
    HttpResponse::Ok().body("获取用户信息")
}

// 核心逻辑依赖 #[post] 宏标注
#[post("/user")] // 宏:绑定 POST 方法和路径
async fn create_user() -> impl Responder {
    HttpResponse::Ok().body("创建用户")
}

#[actix_web::main] // 宏:替代 tokio::main,初始化 actix 运行时
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(get_user)  // 注册宏标注的函数
            .service(create_user)
    })
    .bind(("0.0.0.0", 3000))?
    .run()
    .await
}
差异分析:
  • Axum:路由是"数据结构 + 函数调用",符合 Rust 原生编程习惯,可通过普通代码逻辑动态调整路由(比如根据配置条件添加路由);
  • actix-web:路由逻辑被封装在 #[get]/#[post] 宏中,开发者无法直接通过原生 Rust 逻辑修改路由规则,宏的"魔法"隐藏了底层绑定逻辑(比如宏会自动生成路由注册的代码)。

三、场景2:请求参数提取的对比

请求参数解析是 Web 框架的核心能力,Axum 靠"提取器(Extractor)"(原生函数参数)实现,actix-web 则依赖宏 + 上下文对象提取。

1. Axum:无宏,通过函数参数(提取器)解析参数

Axum 直接将参数解析逻辑体现在函数参数类型中,无任何宏,类型安全且直观:

rust 复制代码
// Axum 参数提取(无宏,纯类型系统)
use axum::{extract::{Path, Json}, routing::post, Router};
use serde::Deserialize;

// 普通结构体,仅派生 Deserialize(通用序列化 trait,非框架宏)
#[derive(Deserialize)]
struct User {
    name: String,
    age: u32,
}

// 函数参数直接声明要提取的参数类型:Path(路径参数) + Json(请求体)
// 无任何宏,参数类型即解析规则
async fn update_user(
    Path(user_id): Path<u32>,       // 提取路径参数 /user/:user_id
    Json(user_info): Json<User>     // 提取 JSON 请求体
) -> String {
    format!("更新用户 {}:姓名={},年龄={}", user_id, user_info.name, user_info.age)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/user/:user_id", post(update_user)); // 纯函数调用绑定路由

    axum::Server::bind(&([0,0,0,0], 3000).into())
        .serve(app.into_make_service())
        .await
        .unwrap();
}
2. actix-web:依赖宏 + 上下文对象提取参数

actix-web 需通过 web::Path/web::Json 结合宏(或上下文)提取参数,核心逻辑依赖宏封装:

rust 复制代码
// actix-web 参数提取(依赖宏 + 上下文)
use actix_web::{post, web, App, HttpResponse, HttpServer, Responder};
use serde::Deserialize;

#[derive(Deserialize)]
struct User {
    name: String,
    age: u32,
}

// 需通过 #[post] 宏绑定路由,参数需通过 web::Path/web::Json 从上下文提取
#[post("/user/{user_id}")] // 宏:绑定路径(含参数)
async fn update_user(
    // web::Path 是 actix 封装的类型,需从宏生成的上下文中提取
    user_id: web::Path<u32>,
    // web::Json 同理,依赖框架上下文,而非原生函数参数
    user_info: web::Json<User>
) -> impl Responder {
    let response = format!(
        "更新用户 {}:姓名={},年龄={}",
        user_id, user_info.name, user_info.age
    );
    HttpResponse::Ok().body(response)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(update_user) // 注册宏标注的函数
    })
    .bind(("0.0.0.0", 3000))?
    .run()
    .await
}
差异分析:
  • Axum:参数提取是"原生函数参数 + 类型系统",开发者只需声明参数类型(如 Path<u32>),框架通过 Trait 自动解析,无宏介入,类型错误在编译期直接暴露,且代码可直接跳转、调试;
  • actix-web:参数提取依赖 web::Path/web::Json 封装类型,而这些类型的解析逻辑被隐藏在 #[post] 宏生成的上下文里------开发者无法直接看到"路径参数如何映射到 web::Path",宏成为参数解析的"黑盒"。

四、"无宏入侵"的核心价值(对比 actix-web)

维度 Axum(无宏入侵) actix-web(宏依赖)
代码可读性 贴近原生 Rust,逻辑直观,无"魔法代码" 宏隐藏底层逻辑,需记忆框架专属宏规则
调试/可维护性 可直接跳转函数、查看类型,编译错误提示清晰 宏展开后代码复杂,错误提示指向宏内部
灵活性 可通过原生 Rust 逻辑动态调整路由/参数解析 宏逻辑固定,动态调整需适配框架宏规则
学习成本 只需掌握 Rust 类型系统和异步编程 需额外学习框架自定义宏的使用规则

五、总结

Axum 的"无宏入侵"本质是将框架能力完全融入 Rust 原生语法体系,而非通过宏创造"专属语法":

  1. 核心逻辑(路由、参数提取)靠 Trait、函数参数、类型系统实现,无框架专属宏;
  2. 代码风格与原生 Rust 一致,降低学习和调试成本;
  3. 类型安全由 Rust 编译器直接保障,而非框架宏的额外校验。

而 actix-web 的宏虽然简化了"书写量",但也带来了"宏入侵"的问题------代码依赖框架自定义宏,脱离框架后难以复用,且宏的"黑盒特性"增加了问题定位的难度。这也是 Axum 成为 Rust Web 开发首选框架的核心原因之一:在保持高性能的同时,兼顾了代码的原生性和可维护性。

八、总结

Axum 凭借其类型安全、无宏入侵、高性能的特性,成为 Rust Web 开发的首选框架。它的核心设计理念是"用 Rust 的类型系统解决 Web 开发的常见问题",通过提取器、响应、中间件等组件,构建了一套简洁而强大的开发范式。

本文从基础入门到生产级实战,全面覆盖了 Axum 的核心功能与使用技巧。掌握 Axum 不仅能帮助开发者构建高性能的 Web 应用,更能深入理解 Rust 异步编程、类型系统的精髓。在云原生和微服务时代,Axum 无疑是 Rust 开发者的必备技能之一。

相关推荐
HDO清风25 分钟前
CASIA-HWDB2.x 数据集DGRL文件解析(python)
开发语言·人工智能·pytorch·python·目标检测·计算机视觉·restful
2201_7569890926 分钟前
C++中的事件驱动编程
开发语言·c++·算法
多米Domi01137 分钟前
0x3f 第48天 面向实习的八股背诵第五天 + 堆一题 背了JUC的题,java.util.Concurrency
开发语言·数据结构·python·算法·leetcode·面试
2301_8223776537 分钟前
模板元编程调试方法
开发语言·c++·算法
csbysj202041 分钟前
Python 循环嵌套
开发语言
测试_AI_一辰43 分钟前
Agent & RAG 测试工程05:把 RAG 的检索过程跑清楚:chunk 是什么、怎么来的、怎么被命中的
开发语言·人工智能·功能测试·自动化·ai编程
Coding茶水间1 小时前
基于深度学习的输电电力设备检测系统演示与介绍(YOLOv12/v11/v8/v5模型+Pyqt5界面+训练代码+数据集)
开发语言·人工智能·深度学习·yolo·目标检测·机器学习
清风~徐~来1 小时前
【视频点播系统】BRpc 介绍及使用
开发语言
啟明起鸣1 小时前
【C++ 性能提升技巧】C++ 的引用、值类型、构造函数、移动语义与 noexcept 特性,可扩容的容器
开发语言·c++