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 开发者的必备技能之一。

相关推荐
红尘散仙2 天前
想写一个像样的终端 App?试试把 React 的开发体验搬进 Rust TUI
前端·rust
vivo互联网技术2 天前
从 Web 到桌面:基于 Tauri 2.0 + Vue 3 打造 vivo 线下门店「大头贴」拍照体验系统
前端·rust
Rust研习社2 天前
这 8 个 Rust 学习资源值得每个新手收藏起来
后端·rust·编程语言
LDR0063 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术3 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript
码云数智-园园3 天前
C++20 Modules 模块详解
java·开发语言·spring
swordbob3 天前
NIO的channel中什么是 fd(File Descriptor,文件描述符)
java·开发语言·nio
源分享3 天前
Java线程同步的多种实现方法(非常详细)
java·开发语言·jvm
Luminous.3 天前
C语言--day30
c语言·开发语言
星栈3 天前
10 分钟跑起第一个 Dioxus 应用:`dx` CLI、`rsx!` 和热更新好不好用
前端·rust·前端框架