Skip to main content

5 posts tagged with "Axum"

View All Tags

本文详细介绍如何使用 Axum 框架在 Rust 中构建一个通用化的 Web 应用模板,包括:

  • 构建 RESTful API
  • 使用 SqlxPostgres 数据库实现数据库交互
  • 采用类似 Nest.js 的项目组织结构,以提升代码可维护性
  • 包含丰富的单元测试和集成测试
  • 使用 Github Actions 实现 CI/CD 流程

基础开发环境搭建

为了快速开始,可以参考我的 Rust 项目模板,点击这里获取项目代码。该模板包含基础项目结构和一些配置,帮助你迅速搭建开发环境。

类似 Nest.js 的项目组织方式

为了提升项目的可维护性和扩展性,本文中采用了类似 Nest.js 的项目组织结构:

├── docs                # 文档文件
├── fixtures # 必要文件,比如公私钥,测试 SQL 脚本
├── migrations # 数据库迁移文件,适用于 Sqlx
├── rest_client # VS Code REST Client 测试 API 文件
├── src # 源代码
│   ├── common # 公共模块,如加解密、错误处理、配置等
│   └── modules # 业务模块
│   ├── auth # 认证模块,包括: `handlers`, `services`, `dto`, `tests`, `middleware` 等
│   └── users # 用户管理模块,包括: `handlers`, `services`, `dto`, `tests`, `entity` 等

业务模块文件说明

  • mod.rs:模块的导入导出,创建当前模块的路由器
  • handlers.rs:处理用户请求的逻辑。
  • services.rs:处理业务逻辑以及与数据库的交互。
  • entity.rs:定义与数据库表对应的结构体,便于结构操作。
  • dto.rs:定义数据转换对象,用于请求和响应的序列化和反序列化。
  • tests.rs:包含单元测试和集成测试。
  • middleware.rs:定义模块的中间件,用于认证、权限控制等。
  • src/lib.rs:定义项目的路由器,加载配置文件、初始化全局状态、错误处理等。
  • src/main.rs:应用入口。

开发经验分享

在项目开发过程中,我积累了一些经验,分享如下,帮助大家更好地构建 Axum Web 应用:

1. 测试的排序和并发控制

  • 测试数量多或者测试较为复杂时,可能会遇到并发导致的错误,特别是在集成测试中。可以使用 serial_test 来对测试进行排序,防止这些问题。

2. 测试数据库的创建与销毁

  • 在测试时需要创建临时数据库,并在测试结束后销毁它。可以使用 sqlx-db-tester,并为测试初始化一个连接到测试数据库的全局状态。

3. 全局配置与数据库连接

  • 全局状态中应包含全局配置和数据库连接,这样可以统一管理数据库操作,并将所有 services.rs 中的数据库操作通过全局状态来执行,方便管理。

4. Github Actions CI 配置

  • 集成测试中,reqwest 默认依赖 OpenSSL,这在 CI 环境下可能会导致问题。因此,我们需要在 OpenSSLBoringSSL 之间做出选择。由于 jwt-simple 依赖于 BoringSSL,所以应禁用掉 reqwest 的默认 OpenSSL 依赖。

5. 公私钥生成

  • 将公私钥生成的逻辑放在 build.rs 中,这样每次执行 cargo buildcargo run 等构建操作时,都会自动生成证书文件到 fixtures 目录下,方便开发和部署。

6. 测试模块管理

  • 在 Rust 中,可以将单独文件当作一个模块(mod)。但需要注意的是,Rust 不会自动识别 integration_tests.rs,但会识别 tests.rs。因此,我将单元测试和集成测试都写在 tests.rs 中,并使用 util_testsintegration_tests 模块分别包裹,这样可以更清晰地查看测试日志。

7. 代码质量检查

  • 使用 pre-commit 执行各类代码检查工具,如 cargo-deny,确保代码规范和安全性,避免潜在的漏洞和错误。

8. Token 签名与安全性

  • 选择使用 Ed25519 作为 Token 的签名算法。公钥用于验证签名,私钥用于生成签名。这样可以将公钥公开,用于独立的服务来验证 Token,保证安全性。

9. 参数验证

  • 使用 validator crate 来验证请求参数,减少错误输入和代码量,简化开发过程。

CI/CD 集成(Github Actions)

在本项目中,使用 Github Actions 进行持续集成(CI)和持续部署(CD)。你可以通过配置 .github/workflows/ci.yml 文件来实现每次代码推送后的自动化测试和构建,确保代码的稳定性和质量。

示例配置文件

name: Rust CI

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Run tests
run: cargo test

Rust Axum Web 应用的最佳实践

为了提升代码的可维护性和扩展性,建议:

  1. 模块化设计:保持代码模块化,采用类似 Nest.js 的项目结构,有利于团队合作和项目扩展。
  2. 自动化测试:编写单元测试和集成测试,确保各个模块的功能和整体系统的稳定性。
  3. 持续集成:通过 Github Actions 等工具实现持续集成,保证代码的高质量。

链接与资源

项目展示效果

以下是单元测试和集成测试的部分运行效果展示:

Axum单元测试与集成测试

鱼雪

本文将介绍如何使用 Axum 框架实现 JWT 授权,包括从生成密钥对、创建和验证 Token 到在 Axum 中实现授权中间件,全面讲解构建安全 API 的流程。

Axum 是一个基于 Hyper 的 Rust Web 框架,它提供了高效、灵活的方式来构建现代 Web 应用。在大多数实际应用中,我们需要对 API 端点进行授权,以确保只有授权用户才能访问受保护的资源。**JWT(JSON Web Token)**是一种流行的授权机制,非常适合用于这种场景,能够实现无状态、跨平台的身份验证。

本文大体分为三个部分:

  1. 生成一个新的 Ed25519 公私钥对
  2. 生成和验证 JWT Token
  3. 在 Axum 中集成授权中间件

1. 生成 Ed25519 的公私钥

在我们的授权系统中,我们使用 Ed25519 算法来生成公私钥对。这是一种现代的、安全的数字签名算法,提供了高安全性和高性能,非常适合用在 JWT 授权中。

生成公私钥的代码示例

use anyhow::Result;
use jwt_simple::prelude::*;
use std::fs::File;

fn main() -> Result<()> {
generate_and_save_keys()?;
Ok(())
}

fn generate_and_save_keys() -> Result<()> {
let key_pair = Ed25519KeyPair::generate();

// 保存私钥
let private_key_pem = key_pair.to_pem();
let mut private_key_file = File::create("private_key.pem")?;
private_key_file.write_all(private_key_pem.as_bytes())?;

// 保存公钥
let public_key_pem = key_pair.public_key().to_pem();
let mut public_key_file = File::create("public_key.pem")?;
public_key_file.write_all(public_key_pem.as_bytes())?;

Ok(())
}

代码说明:

  • 使用 Ed25519KeyPair::generate() 生成密钥对,并分别保存公钥和私钥。
  • 私钥用于生成 Token,必须严格保密,而公钥用于验证 Token,可以公开发布。
  • PEM 格式是一种常用的密钥存储格式,易于管理。
tip

将公钥和私钥分开存储可以保证系统安全性。私钥通常只在生成 Token 时使用,而公钥则用于对 Token 的验证,可以公开提供给需要验证身份的服务。

2. 生成 JWT Token 和验证 Token

在这一步,我们会学习如何生成和验证 JWT Token,帮助我们在 Web 应用中实现授权控制。

生成 Token 的实现

fn sign(user: impl Into<User>) -> Result<String> {
let private_key_pem = read_to_string("private_key.pem")?;
let key_pair = Ed25519KeyPair::from_pem(&private_key_pem)?;

let user = user.into();
let claims = Claims::with_custom_claims(user, Duration::from_secs(JWT_DURATION));
let claims = claims.with_issuer(JWT_ISS).with_audience(JWT_AUD);

let token = key_pair.sign(claims)?;
Ok(token)
}

代码说明:

  1. 从文件中读取私钥,并创建 Ed25519KeyPair
  2. 创建一个包含用户信息的 claims 对象,并设置 Token 的有效期、发行者和受众。
  3. 使用私钥对 claims 进行签名,生成 Token。

验证 Token 的实现

fn verify(token: &str) -> Result<User, Box<dyn std::error::Error>> {
let public_key_pem = read_to_string("public_key.pem")?;
let public_key = Ed25519PublicKey::from_pem(&public_key_pem)?;

let options = VerificationOptions {
allowed_issuers: Some(HashSet::from_strings(&[JWT_ISS])),
allowed_audiences: Some(HashSet::from_strings(&[JWT_AUD])),
..Default::default()
};

let claims = public_key.verify_token::<User>(token, Some(options))?;
Ok(claims.custom)
}

验证步骤:

  • 使用公钥验证 Token 的签名,并检查是否符合预期的发行者受众
  • 通过验证后,返回自定义的 User 数据。

要点:

  • 生成 Token 时使用私钥,验证 Token 时使用公钥。
  • 设置有效期发行者受众,以提高安全性。

3. 在 Axum 中集成 JWT 授权中间件

为了确保只有经过身份验证的用户才能访问受保护的 API,我们需要将 JWT 的生成和验证集成到 Axum 框架中。

定义 AuthUser 结构体并实现 FromRequestParts Trait

struct AuthUser(User);

#[async_trait]
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
{
type Rejection = StatusCode;

async fn from_request_parts<'life0, 'life1>(
parts: &'life0 mut Parts,
_state: &'life1 S,
) -> Result<Self, Self::Rejection>
where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait,
{
let token = parts
.headers
.get("Authorization")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
let user = verify(token).map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(AuthUser(user))
}
}

该实现可以让我们方便地从请求中提取出认证信息。

创建授权中间件

async fn auth_middleware(
AuthUser(user): AuthUser,
req: Request<Body>,
next: Next,
) -> impl IntoResponse {
info!("Authenticated user: {}", user.username);
next.run(req).await
}

这个中间件会记录用户信息,然后继续处理请求,确保只有认证过的用户可以访问受保护的路由。

在 Axum 中使用中间件

let app = Router::new()
.route("/login", post(get_token))
.route("/protected", get(protected_route).layer(from_fn(auth_middleware)));

我们定义了两个路由:

  • /login:用于登录并获取 Token,不需要认证。
  • /protected:受保护的路由,需要通过授权中间件的认证。

登录并获取 Token 的实现

async fn get_token(
Json(payload): Json<TokenRequest>
) -> Result<Json<TokenResponse>, StatusCode> {
let user = User {
username: payload.username,
created_at: Utc::now(),
scope: vec!["read".to_string(), "write".to_string()],
};
let token = sign(user).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(TokenResponse { token }))
}

该函数用于处理登录请求,生成并返回 JWT Token。

受保护路由的实现

async fn protected_route(AuthUser(user): AuthUser) -> impl IntoResponse {
format!("Hello, {}! Your scopes are: {:?}", user.username, user.scope)
}

该路由只有在用户通过认证后才能访问,返回用户的相关信息。

要点:

  • 使用 FromRequestParts Trait 从请求中提取认证信息,便于集成 JWT 认证。
  • 中间件统一处理认证逻辑,提高了代码的模块化和可维护性。

测试 JWT 授权

使用 REST 客户端(例如 VSCode REST Client 插件)可以测试整个授权流程:

  1. 获取 Token:首先发送 POST 请求到 /login
  2. 访问受保护的路由:使用获得的 Token,设置 Authorization 请求头访问 /protected
### Get Token
POST http://localhost:3000/login
Content-Type: application/json

{
"username": "admin"
}

@token = {{signin.response.body.token}}

### Auth user
GET http://localhost:3000/protected
Authorization: Bearer {{token}}
note

在这里,我们使用了 REST Client 插件来进行测试,方便地将获取的 Token 用于后续请求,避免手动复制粘贴。

完整代码示例

Cargo.toml 依赖库

[package]
name = "axum-jwt-auth"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.86"
axum = "0.7.5"
chrono = { version = "0.4.38", features = ["serde"] }
jwt-simple = "0.12.9"
serde = { version = "1.0.204", features = ["derive"] }
tokio = { version = "1.39.1", features = ["net", "rt", "rt-multi-thread"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

主要 Rust 代码示例

use anyhow::Result;
use chrono::{DateTime, Utc};
use jwt_simple::prelude::*;
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;

use tracing::{info, level_filters::LevelFilter};
use tracing_subscriber::{
fmt::Layer, layer::SubscriberExt,
util::SubscriberInitExt,
Layer as _
};

use std::{
collections::HashSet,
fs::{read_to_string, File},
io::Write,
net::SocketAddr,
};

use axum::{
async_trait, body::Body,
extract::FromRequestParts,
http::{request::Parts, Request,StatusCode},
middleware::{from_fn, Next},
response::IntoResponse,
routing::{get, post},
Json, Router
};

// 省略部分代码,可参考上文详细内容

结论

通过这篇文章,我们了解了如何使用 Axum 框架在 Rust 中实现基于 JWT 的授权系统。具体内容包括:

  • 使用 Ed25519 算法生成密钥对,确保私钥的安全性。
  • 生成和验证 JWT Token,以实现无状态的授权。
  • 将 Token 签发和验证集成到 Axum 中,确保对 API 端点进行保护。

这种方法为我们提供了一个灵活、安全的 API 保护机制。在实际应用中,你还可以引入更多的安全措施,如 Token 刷新权限管理密钥轮换等,以进一步提升安全性。

参考链接

鱼雪

在Axum中,中间件是处理请求和响应的强大工具。

它们可以在请求到达实际处理程序之前或响应发回客户端之前对其进行处理

Axum提供了多种方式来实现中间件。

本文将介绍两种常用的方法:使用from_fntower::Layer

方式一:from_fn

from_fn是一种简便的方法,可以快速创建中间件。它适用于那些不需要复杂逻辑的中间件。

示例代码: request_id

以下是一个通过from_fn实现请求ID中间件的示例:

// request_id.rs
use super::REQUEST_ID_HEADER;
use axum::{extract::Request, http::HeaderValue, middleware::Next, response::Response};
use tracing::warn;

pub async fn set_request_id(mut req: Request, next: Next) -> Response {
let id = match req.headers().get(REQUEST_ID_HEADER) {
Some(v) => Some(v.clone()),
None => {
let request_id = uuid::Uuid::new_v4().to_string();
match HeaderValue::from_str(&request_id) {
Ok(v) => {
req.headers_mut().insert(REQUEST_ID_HEADER, v.clone());
Some(v)
}
Err(e) => {
warn!("parse generated request id failed: {}", e);
None
}
}
}
};

let mut res = next.run(req).await;

if let Some(id) = id {
res.headers_mut().insert(REQUEST_ID_HEADER, id);
}
res
}

在这个示例中,中间件会检查请求头中是否包含请求ID。 如果没有,则生成一个新的UUID并将其添加到请求头中。 然后,继续处理请求并在响应头中添加相同的请求ID。

Token验证中间件

这里再提供一个示例,展示如何实现一个验证Token的中间件:

// auth.rs
use crate::AppState;
use axum::{
extract::{FromRequestParts, Request, State},
http::StatusCode,
middleware::Next,
response::{IntoResponse, Response},
};
use axum_extra::{
headers::{authorization::Bearer, Authorization},
TypedHeader;
use tracing::warn;

pub async fn verify_token(State(state): State<AppState>, req: Request, next: Next) -> Response {
let (mut parts, body) = req.into_parts();
let req = match TypedHeader::<Authorization<Bearer>>::from_request_parts(&mut parts, &state).await {
Ok(TypedHeader(Authorization(bearer))) => {
let token = bearer.token();
match state.dk.verify(token) {
Ok(user) => {
let mut req = Request::from_parts(parts, body);
req.extensions_mut().insert(user);
req
}
Err(e) => {
let msg = format!("verify token failed: {}", e);
warn!(msg);
return (StatusCode::FORBIDDEN, msg).into_response();
}
}
}
Err(e) => {
let msg = format!("parse Authorization header failed: {}", e);
warn!(msg);
return (StatusCode::UNAUTHORIZED, msg).into_response();
}
};
next.run(req).await
}

在这个示例中,中间件会检查请求头中的Authorization Bearer token,并验证其有效性。 如果验证失败,将返回相应的错误响应。

方式二: tower::Layer

tower::Layer是创建复杂中间件的推荐方式。

它提供了更多的灵活性和控制,可以处理更复杂的逻辑。

示例代码

以下是一个通过tower::Layer实现服务器时间中间件的示例:

// server_time.rs
use super::{REQUEST_ID_HEADER, SERVER_TIME_HEADER};
use axum::{extract::Request, response::Response};
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tokio::time::Instant;
use tower::{Layer, Service};
use tracing::warn;

#[derive(Clone)]
pub struct ServerTimeLayer;

impl<S> Layer<S> for ServerTimeLayer {
type Service = ServerTimeMiddleware<S>;

fn layer(&self, inner: S) -> Self::Service {
ServerTimeMiddleware { inner }
}
}

#[derive(Clone)]
pub struct ServerTimeMiddleware<S> {
inner: S,
}

impl<S> Service<Request> for ServerTimeMiddleware<S>
where
S: Service<Request, Response = Response> + Send + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;

fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

fn call(&mut self, req: Request) -> Self::Future {
let start = Instant::now();
let future = self.inner.call(req);
Box::pin(async move {
let mut res: Response = future.await?;
let elapsed = format!("{}us", start.elapsed().as_micros());
match elapsed.parse() {
Ok(v) => {
res.headers_mut().insert(SERVER_TIME_HEADER, v);
}
Err(e) => {
warn!(
"Parse elapsed time failed: {} for request {:?}",
e,
res.headers().get(REQUEST_ID_HEADER)
);
}
}
Ok(res)
})
}
}

在这个示例中,中间件会记录请求处理的时间,并将其添加到响应头中。 它使用tower::Layer来创建一个自定义的中间件结构体ServerTimeMiddleware

通过这些示例,可以看到在Axum中实现中间件的灵活性和强大功能。 根据需求选择合适的方法,可以帮助你更好地管理和处理HTTP请求和响应。

总结

  1. 中间件概述
  • 中间件是处理请求和响应的工具,可以在请求到达处理程序之前或响应发回客户端之前对其进行处理。
  • Axum提供了多种实现中间件的方法,主要有from_fntower::Layer两种方式。
  1. 方式一:from_fn

    • from_fn方法适用于快速创建简单的中间件。
  • 示例代码展示了如何通过from_fn实现请求ID中间件:

    • 检查请求头是否包含请求ID,如果没有则生成一个新的UUID并添加到请求头中。
    • 继续处理请求,并在响应头中添加相同的请求ID。
  1. 方式二:tower::Layer

    • tower::Layer方法适用于创建复杂的中间件,提供更多灵活性和控制。
  • 示例代码展示了如何通过tower::Layer实现服务器时间中间件:

    • 记录请求处理时间,并将其添加到响应头中。
    • 创建一个自定义的中间件结构体ServerTimeMiddleware
  1. Token验证中间件

    • 示例代码展示了如何实现一个验证Token的中间件:

      • 检查请求头中的Authorization Bearer token,并验证其有效性。
    • 如果验证失败,返回相应的错误响应。

鱼雪

axum::Router结构体

pub struct Router<S = ()> { /* private fields */}

用于组合处理程序和服务的路由器类型。

实现

impl<S> Router<S>
where
S: Clone + Send + Sync + 'static,

新建路由器

pub fn new() -> Self

创建一个新的路由器,除非您添加额外的路由,否则将对所有请求响应404未找到。

添加另一个路由到路由器

pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self
  • path: 是由/分割的路径段字符串。每个段可能是静态的、捕获的或者是通配符。
  • method_router: 是一个MethodRouter,它将请求方法映射到处理程序。 method_router通常会是类似于get的方法路由器中的处理程序。

静态路径

例如:

  • /
  • /foo
  • /foo/bar

如果传入的请求路径完全匹配,则将调用相应的服务。

捕获

例如:

  • /:key
  • /foo/:key
  • /users/:id/tweets

路径可以包含类似于/:key的段,它匹配任何单个段,并将存储在key处捕获的值。 捕获的值可以是零长度,除了无效路径//

捕获可以使用Path进行提取。

MatchedPath可以用于提取匹配路径,而不是实际路径。

通配符

路径可以以/*key结尾,匹配所有段并捕获的段存储在key中。

例如:

  • /*key
  • /users/*path
  • /:id/:repo/*tree

请注意,/*key 不匹配空段。因此:

  • /*key 不匹配 /,但匹配 /a/a/ 等。
  • /x/*key 不匹配 /x/x/,但匹配 /x/a/x/a/ 等。

还可以使用 Path 来提取通配符捕获。 请注意,不包括前导斜杠,即对于路由 /foo/*rest 和路径 /foo/bar/bazrest 的值将是 bar/baz

接受多种方法

要接受同一路由的多个方法,您可以同时添加所有处理程序。

use axum::{Router, routing::{get, delete}, extract::Path};

let app = Router::new().route(
"/",
get(get_root).post(post_root).delete(delete_root),
);

async fn get_root() {}
async fn post_root() {}
async fn delete_root() {}

或者你也可以一一添加:

let app = Router::new()
.route("/", get(get_root))
.route("/", post(post_root))
.route("/", delete(delete_root));

更多例子

use axum::{Router, routing::{get, delete}, extract::Path};

let app = Router::new()
.route("/", get(root))
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(show_user))
.route("/api/:version/users/:id/action", delete(do_users_action))
.route("/assets/*path", get(serve_asset));

async fn root() {}

async fn list_users() {}

async fn create_user() {}

async fn show_user(Path(id): Path<u64>) {}

async fn do_users_action(Path((version, id)): Path<(String, u64)>) {}

async fn serve_asset(Path(path): Path<String>) {}

Panics

如果路径与另一个路由重叠,则会发生panic

use axum::{routing::get, Router};

let app = Router::new()
.route("/", get(|| async {}))
.route("/", get(|| async {}));

静态路由 /foo 和动态路由 /:key 不被视为重叠,并且 /foo 将优先。 如果路径为空,也会引发 panic。

路由服务

添加另一个路由到路由器调用一个服务

pub fn route_service<T>(self, path: &str, service: T) -> Self
where
T: Service<Request, Error=Infallible> + Clone + Send + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,

示例:

use axum::{
Router,
body::Body,
routing::{any_service, get_service},
extract::Request,
http::StatusCode,
error_handling::HandleErrorLayer,
};
use tower_http::services::ServeFile;
use http::Response;
use std::{convert::Infallible, io};
use tower::service_fn;

let app = Router::new()
.route(
"/",
any_service(service_fn(|_: Request| async {
let res = Response::new(Body::from("Hi from `GET /`"));
}))
)
.route_service(
"/foo",
service_fn(|req: Request| async move {
let body = Body::from(format!("Hi from `{}` /foo", req.method()))
let res = Response::new(body);
Ok::<_, Infallible>(res)
})
)
.route_service(
"/static/Cargo.toml",
ServeFile::new("Cargo.toml"),
);

以这种方式路由到任意服务会对背压(Service::poll_ready)产生复杂性。 有关更多详细信息,请参阅服务路由和背压模块。

由于相同的原因而出现panic,或者尝试将路由到Router时也会发生panic。

use axum::{routing::get, Router};

let app = Router::new().route_service(
"/",
Router::new().route("/foo", get(|| async {})),
);

使用Router::nest替换

在某个路径上嵌套一个路由器。

这样可以将应用程序分解成更小的部分,并将它们组合在一起。

pub fn nest(self, path: &str, router: Router<S>) -> Self

示例:

use axum::{
routing::{get, post},
Router,
};

let user_routes = Router::new().route("/:id", get(|| async {}));
let team_routes = Router::new().route("/", post(|| async {}));

let api_routes = Router::new()
.nest("/users", user_routes)
.nest("/teams", team_routes);

let app = Router::new().nest("/api", api_routes);

// Our app now accepts
// - GET /api/users/:id
// - POST /api/teams

URI如何变化

请注意,嵌套路由将无法看到原始请求URI,而是会剥去匹配的前缀。 这对于像静态文件服务之类的服务工作是必要的。 如果需要原始请求URI,请使·OriginalUri

外部路由的捕获

在使用嵌套动态路由时要小心,因为嵌套还会从外部路由中捕获:

use axum::{
extract::Path,
routing::get,
Router,
};
use std::collections::HashMap;

async fn users_get(Path(params): Path<HashMap<String, String>>) {
// Both `version` and `id` were captured even though `users_api` only
// explicitly captures `id`.
let version = params.get("version");
let id = params.get("id");
}

let users_api = Router::new().route("/users/:id", get(users_get));

let app = Router::new().nest("/:version/api", users_api);

与通配符路由的区别

嵌套路由类似于通配符路由。 不同之处在于通配符路由仍然可以看到整个 URI,而嵌套路由将会去掉前缀:

use axum::{routing::get, http::Uri, Router};

let nested_router = Router::new()
.route("/", get(|uri: Uri| async {
// `uri` will _not_ contain `/bar`
}));

let app = Router::new()
.route("/foo/*rest", get(|uri: Uri| async {
// `uri` will contain `/foo`
}))
.nest("/bar", nested_router);

后备方案

如果嵌套路由器没有自己的回退,则将从外部路由器继承回退:

use axum::{routing::get, http::StatusCode, handler::Handler, Router};

async fn fallback() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "Not Found")
}

let api_routes = Router::new().route("/users", get(|| async {}));

let app = Router::new()
.nest("/api", api_routes)
.fallback(fallback);

在这里,像 GET /api/not-found 这样的请求将进入 api_routes, 但由于它没有匹配的路由,也没有自己的回退,它将调用外部路由器的回退,即回退功能。 如果嵌套路由器有自己的回退,则外部回退将不会被继承:

use axum::{
routing::get,
http::StatusCode,
handler::Handler,
Json,
Router,
};

async fn fallback() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "Not Found")
}

async fn api_fallback() -> (StatusCode, Json<serde_json::Value>) {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "status": "Not Found" })),
)
}

let api_routes = Router::new()
.route("/users", get(|| async {}))
.fallback(api_fallback);

let app = Router::new()
.nest("/api", api_routes)
.fallback(fallback);

在这里,像 GET /api/not-found 这样的请求将转到 api_fallback

用状态嵌套路由器

当使用此方法将Router组合时,每个Router必须具有相同类型的状态。 如果您的路由器具有不同类型,您可以使用Router::with_state来提供状态并使类型匹配:

use axum::{
Router,
routing::get,
extract::State,
};

#[derive(Clone)]
struct InnerState {}

#[derive(Clone)]
struct OuterState {}

async fn inner_handler(state: State<InnerState>) {}

let inner_router = Router::new()
.route("/bar", get(inner_handler))
.with_state(InnerState {});

async fn outer_handler(state: State<OuterState>) {}

let app = Router::new()
.route("/", get(outer_handler))
.nest("/foo", inner_router)
.with_state(OuterState {});

请注意,内部路由器仍将继承外部路由器的后备机制。

恐慌

  • 如果路由与另一个路由重叠。有关详细信息,请参阅Router::route
  • 如果路由包含通配符(*)。
  • 如果path为空。

nest类似,但接受任何服务

pub fn nest_service<T>(self, path: &str, service: T) -> Self
where
T: Service<Request, Error=Infallible> + Clone + Send + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,

将两个路由器的路径(path)和回退(fallbacks)合并到一个路由器

pub fn merge<R>(self, other: R) -> Self
where
R: Into<Router<S>>,

这对于将应用程序分成更小的部分并将它们组合成一个非常有用。

use axum::{
routing::get,
Router,
};

let user_routes = Router::new()
.route("/users", get(users_list))
.route("/users/:id", get(users_show));

let team_routes = Router::new()
.route("/teams", get(teams_list));

let app = Router::new()
.merge(user_routes)
.merge(team_routes);

// 也可以执行 `user_routes.merge(team_routes)`

// 我们的应用程序现在接受
// - GET /users
// - GET /users/:id
// - GET /teams

合并路由器的状态

使用此方法合并 Router 时,每个 Router 必须具有相同类型的状态。 如果您的 routers 具有不同类型,可以使用 Router::with_state 来提供状态并使类型匹配:

use axum::{
Router,
routing::get,
extract::State,
};

#[derive(Clone)]
struct InnerState {}

#[derive(Clone)]
struct OuterState {}

async fn inner_handler(state: State<InnerState>) {}

let inner_router = Router::new()
.route("/bar", get(inner_handler))
.with_state(InnerState {});

async fn outer_handler(state: State<OuterState>) {}

let app = Router::new()
.route("/", get(outer_handler))
.merge(inner_router)
.with_state(OuterState {});

合并具有回退的路由器

使用此方法合并 Router 时,后备(fallbacks)也会合并。但是只能有一个路由器有后备

tower::Layer 应用于路由器中的所有路由。

pub fn layer<L>(self, layer: L) -> Router<S>
where
L: Layer<Route> + Clone + Send + 'static,
L::Service: Service<Request> + Clone + Send + 'static,
<L::Service as Service<Request>>::Response: IntoResponse + 'static,
<L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
<L::Service as Service<Request>>::Future: Send + 'static,

这可以用于为一组路由的请求添加额外的处理。

注意,中间件只应用于现有路由。

因此,您必须首先添加您的路由(和/或回退(fallbacks)),然后调用层(layer)。 在调用层之后添加的额外路由将不会添加中间件。

如果要将中间件添加到单个处理程序,可以使用 MethodRouter::layerHandler::layer

示例

添加tower_http::trace::TraceLayer:

use axum::{
routing::get,
Router,
};
use tower_http::trace::TraceLayer;

let app = Router::new()
.route("/foo", get(|| async {}))
.route("/bar", get(|| async {}))
.layer(TraceLayer::new_for_http());

如果您需要编写自己的中间件,请参阅“编写中间件”以获取不同的选项。

如果您只想在某些路由上使用中间件,可以使用Router::merge

use axum::{
routing::get,
Router,
};
use tower_http::{trace::TraceLayer, compression::CompressionLayer};

let with_tracing = Router::new()
.route("/foo", get(|| async {}))
.layer(TraceLayer::new_for_http());

let with_compression = Router::new()
.route("/bar", get(|| async {}))
.layer(CompressionLayer::new());

let app = Router::new()
.merge(with_tracing)
.merge(with_compression);

多中间件

当应用多个中间件时,建议使用tower::ServiceBuilder。有关更多详细信息,请参阅中间件。

路由之后运行

使用此方法添加的中间件将在路由之后运行,因此无法用于重写请求URI。 有关更多详细信息和解决方法,请参见“在中间件中重写请求URI”。

错误处理

请参阅有关错误处理影响中间件的详细信息。

向路由器应用一个 tower::Layer,只有当请求匹配路由时才会运行

pub fn route_layer<L>(self, layer: L) -> Self
where
L: Layer<Route> + Clone + Send + 'static,
L::Service: Service<Request> + Clone + Send + 'static,
<L::Service as Service<Request>>::Response: IntoResponse + 'static,
<L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
<L::Service as Service<Request>>::Future: Send + 'static,

请注意,中间件仅应用于现有路由。 因此,您必须首先添加您的路由(和/或回调),然后在之后调用 route_layer。 调用 route_layer 后添加的额外路由将不会添加中间件。

这与 Router::layer 类似,不同之处在于只有当请求匹配路由时中间件才会运行。 这对于提前返回的中间件非常有用(例如授权), 否则可能会将 404 Not Found 转换为 401 Unauthorized。

示例

use axum::{
routing::get,
Router,
};
use tower_http::validate_request::ValidateRequestHeaderLayer;

let app = Router::new()
.route("/foo", get(|| async {}))
.route_layer(ValidateRequestHeaderLayer::bearer("password"));

// `GET /foo` 使用有效令牌将接收 `200 OK`
// `GET /foo` 使用无效令牌将接收 `401 未经授权`
// `GET /not-found` 使用无效令牌将接收 `404 未找到`

向路由器添加一个回退处理程序

pub fn fallback<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static

如果没有任何路由匹配传入的请求,将调用此服务。

use axum::{
routing::get,
Router,
handler::Handler,
response::IntoResponse,
http::{StatusCode, Uri},
};

let app = Router::new()
.route("/foo", get(|| async { "foo" }))
.fallback(fallback);

async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
}

仅在路由器中没有匹配任何内容的路由时才适用回退。 如果处理程序被请求匹配但返回 404,则不会调用回退。

处理所有没有其他路由的请求

如果没有其他路由,使用Router::new().fallback(...)来接受所有请求, 无论路径或方法如何,这并不是最佳选择

use axum::Router;

async fn handler() {}

let app = Router::new().fallback(handler);

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();

直接运行处理程序会更快,因为它避免了路由的开销:

use axum::handler::HandlerWithoutStateExt;

async fn handler() {}

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, handler.into_make_service()).await.unwrap();

向路由器添加一个回退服务

pub fn fallback_service<T>(self, service: T) -> Self
where
T: Service<Request, Error=Infallible> + Clone + Send + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,

查看Router::fallback以获取更多详细信息。

为路由器提供状态

pub fn with_state<S2>(self, state: S) -> Router<S2>
use axum::{Router, routing::get, extract::State};

#[derive(Clone)]
struct AppState {}

let routes = Router::new()
.route("/", get(|State(state): State<AppState>| async {
// use state
}))
.with_state(AppState {});

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();

从函数中返回带状态的路由器

在从函数返回 Router 时,通常建议不直接设置状态

use axum::{Router, routing::get, extract::State};

#[derive(Clone)]
struct AppState {}

// 不要在这里调用 `Router::with_state`
fn routes() -> Router<AppState> {
Router::new()
.route("/", get(|_: State<AppState>| async {}))
}

// 在运行服务器之前执行
let routes = routes().with_state(AppState {});

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();

如果确实需要提供状态,并且您没有将路由嵌套/合并到另一个路由器中,则返回不带任何类型参数的 Router:

// 不要返回 `Router<AppState>`
fn routes(state: AppState) -> Router {
Router::new()
.route("/", get(|_: State<AppState>| async {}))
.with_state(state)
}

let routes = routes(AppState {});

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();

这是因为我们只能在 Router<()> 上调用 Router::into_make_service, 而不能在 Router<AppState> 上调用。有关原因的更多详细信息,请参见下文。

请注意,状态默认为(),所以RouterRouter<()>是一样的。

如果您需要嵌套/合并路由器,建议在结果路由器上使用通用状态类型:

fn routes<S>(state: AppState) -> Router<S> {
Router::new()
.route("/", get(|_: State<AppState>| async {}))
.with_state(state)
}

let routes = Router::new().nest("/api", routes(AppState {}));

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();

状态是路由器内的全局状态

传递给此方法的状态将用于该路由器接收的所有请求。 这意味着它不适合保存从请求中派生的状态,比如在中间件中提取的授权数据。 请改用 Extension 来存储此类数据。

Router<S>中的S代表什么

Router<S>表示一个缺少类型为S的状态以处理请求的路由器。 它并不意味着具有类型为S的状态的路由器。

例如:

// 需要`AppState`来处理请求的路由器
let router: Router<AppState> = Router::new()
.route("/", get(|_: State<AppState>| async {}));

// 一旦我们调用 `Router::with_state` 方法,路由器就不再缺少状态了,因为我们刚刚提供了它
//
// 因此,路由器类型变为`Router<()>`,即一个不缺少任何状态的路由器。
let router: Router<()> = router.with_state(AppState {});

// 只有 `Router<()>` 具有 `into_make_service` 方法。
//
// 因为它仍然缺少 `AppState`,所以不能在 `Router<AppState>` 上调用 `into_make_service`。
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, router).await.unwrap();

或许有点反直觉,Router::with_state 并不总是返回一个 Router<()>。 相反,您可以选择新的缺失状态类型是什么:

let router: Router<AppState> = Router::new()
.route("/", get(|_: State<AppState>| async {}));

// 当我们调用`with_state`时,我们可以选择下一个丢失的状态类型是什么。在这里我们选择`String`
let string_router: Router<String> = router.with_state(AppState {});

// 这允许我们添加使用`String`作为状态类型的新路由
let string_router = string_router
.route("/needs-string", get(|_: State<String>| async {}));

// 提供`String`,并选择 `()` 作为新的缺失状态。
let final_router: Router<()> = string_router.with_state("foo".to_owned());

// 既然我们有一个`Router<()>`,我们可以运行它。
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, final_router).await.unwrap();

为什么在调用with_state后返回Router<AppState>不起作用?

// 这不会起作用,因为我们正在返回 `Router<AppState>`
// 即,我们在说我们仍然缺少一个 `AppState`
fn routes(state: AppState) -> Router<AppState> {
Router::new()
.route("/", get(|_: State<AppState>| async {}))
.with_state(state)
}

let app = routes(AppState {});

// 我们只能在 `Router<()>` 上调用 `Router::into_make_service` 方法,
// 而 `app` 是 `Router<AppState>`
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();

由于我们提供了所需的所有状态,因此请返回 Router<()>

// 我们已经提供了所有必要的状态,因此返回 `Router<()>`。
fn routes(state: AppState) -> Router<()> {
Router::new()
.route("/", get(|_: State<AppState>| async {}))
.with_state(state)
}

let app = routes(AppState {});

// 我们现在可以调用 `Router::into_make_service`。
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();

关于性能的说明

如果您需要一个实现 Service 但不需要任何状态的 Router(也许您正在制作一个在内部使用 axum 的库), 那么建议在开始提供请求之前调用该方法:

use axum::{Router, routing::get};

let app = Router::new()
.route("/", get(|| async { /* ... */ }))
// 即使我们不需要任何状态,也要调用`with_state(())`。
.with_state(());

这不是必需的,但它让 axum 有机会更新路由器中的一些内部内容,可能会影响性能并减少分配。

note

请注意,Router::into_make_serviceRouter::into_make_service_with_connect_info 会自动执行此操作。

将路由器转换为带有固定请求体类型的借用服务,以辅助类型推断

pub fn as_service<B>(&mut self) -> RouterAsService<'_, B, S>

在某些情况下,在 Router 上调用 tower::ServiceExt 的方法时,可能会出现类似以下内容的类型推断错误

let response = router.ready().await?.call(request).await?;
^^^^^ cannot infer type for type parameter `B`

这是因为 Router 使用 impl<B> Service<Request<B>> for Router<()> 实现了 Service

例如:

use axum::{
Router,
routing::get,
http::Request,
body::Body,
};
use tower::{Service, ServiceExt};

let mut router = Router::new().route("/", get(|| async {}));
let request = Request::new(Body::empty());
let response = router.ready().await?.call(request).await?;

调用 Router::as_service 可以解决此问题。

use axum::{
Router,
routing::get,
http::Request,
body::Body,
};
use tower::{Service, ServiceExt};

let mut router = Router::new().route("/", get(|| async {}));
let request = Request::new(Body::empty());
let response = router.as_service().ready().await?.call(request).await?;

这主要是在测试时调用路由时使用的。当通过 Router::into_make_service 正常运行路由时,这是不必要的。

将路由器转换为具有固定请求正文类型的所有服务,以帮助类型推断

pub fn into_service<B>(self) -> RouterIntoService<B, S>

这与 Router::as_service 相同,只是它返回一个拥有的服务。有关更多详细信息,请参见该方法。

impl Router

pub fn into_make_service(self) -> IntoMakeService<Self>

将此路由器转换为一个 MakeService,它是一个响应是另一个服务的服务。

use axum::{
routing::get,
Router,
};

let app = Router::new().route("/", get(|| async { "Hi!" }));

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();

仅在tokio特性上可用

pub fn into_make_service_with_connect_info<C>(
self
) -> IntoMakeServiceWithConnectInfo<Self, C>

将这个路由器转换为一个MakeService,它将把C的相关ConnectInfo存储在一个请求扩展中, 以便ConnectInfo可以提取它。

这使得可以提取类似客户端远程地址的信息。

提取std::net::SocketAddr 是开箱即用的。

use axum::{
extract::ConnectInfo,
routing::get,
Router,
};
use std::net::SocketAddr;

let app = Router::new().route("/", get(handler));

async fn handler(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
format!("Hello {addr}")
}

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();

您可以这样实现自定义连接(Connected):

use axum::{
extract::connect_info::{ConnectInfo, Connected},
routing::get,
serve::IncomingStream,
Router,
};

let app = Router::new().route("/", get(handler));

async fn handler(
ConnectInfo(my_connect_info): ConnectInfo<MyConnectInfo>,
) -> String {
format!("Hello {my_connect_info:?}")
}

#[derive(Clone, Debug)]
struct MyConnectInfo {
// ...
}

impl Connected<IncomingStream<'_>> for MyConnectInfo {
fn connect_info(target: IncomingStream<'_>) -> Self {
MyConnectInfo {
// ...
}
}
}

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<MyConnectInfo>()).await.unwrap();

Trait实现

impl<S> Clone for Router<S>

  • fn clone(&self) -> Self: 返回一个克隆值。
  • fn clone_from(&mut self, source: &Self): 从源(source)执行复制赋值。

impl<S> Debug for Router<S>

  • fn fmt(&self, f: &mut Formatter<'_>) -> Result: 使用给定的格式器格式化值

impl<S> Default for Router<S> where S: Clone + Send + Sync + 'static

  • fn default() -> Self: 返回一个默认值。

impl Service<IncomingStream<'_>> for Router<()>

  • 仅在 tokio crate 功能和 (crate 功能 http1http2) 中可用。

type Response = Router

  • 由服务提供的响应。

type Error = Infallible

  • 由服务生成的错误

type Future = Ready<Result<<Router as Service<IncomingStream<'_>>>::Response, <Router as Service<IncomingStream<'_>>>::Error>>

  • 功能响应值

fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>

  • 当服务能够处理请求时,返回 Poll::Ready(Ok(()))

fn call(&mut self, _req: IncomingStream<'_>) -> Self::Future

  • 处理请求并异步返回响应。

impl<B> Service<Request<B>> for Router<()>
where
B: HttpBody<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>,

type Response = Response<Body>

  • 由服务提供的响应。

type Error = Infallible

  • 由服务生成的错误

type Future = RouteFuture<Infallible>

  • 功能响应值

fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>>

  • 当服务能够处理请求时,返回 Poll::Ready(Ok(()))

fn call(&mut self, req: Request<B>) -> Self::Future

  • 异步处理请求并返回响应

自动特性实现

  • impl<S> Freeze for Router<S>
  • impl<S = ()> !RefUnwindSafe for Router<S>
  • impl<S> Send for Router<S>
  • impl<S> Sync for Router<S>
  • impl<S> Unpin for Router<S>
  • impl<S = ()> !UnwindSafe for Router<S>

通用实现

impl<T> Any for T
where
T: 'static + ?Sized,

impl<T> Borrow<T> for T
where
T: ?Sized,

impl<T> BorrowMut<T> for T
where
T: ?Sized,

impl<T> From<T> for T

impl<T> FromRef<T> for T
where
T: Clone,

impl<T> Instrument for T

impl<T, U> Into<U> for T
where
U: From<T>,

impl<M, S, Target, Request> MakeService<Target, Request> for M
where
M: Service<Target, Response = S>,
S: Service<Request>,

impl<T> PolicyExt for T
where
T: ?Sized,

impl<T> Same for T

impl<S, R> ServiceExt<R> for S
where
S: Service<R>,

impl<T, Request> ServiceExt<Request> for T
where
T: Service<Request> + ?Sized,

impl<T> ToOwned for T
where
T: Clone,

impl<T, U> TryFrom<U> for T
where
U: Into<T>,

impl<T, U> TryInto<U> for T
where
U: TryFrom<T>,

impl<V, T> VZip<V> for T
where
V: MultiLane<T>,

impl<T> WithSubscriber for T
鱼雪

目录

  • 高级功能
  • 兼容性
  • Hello World
  • 路由
  • 处理程序
  • 提取器
  • 响应
  • 错误处理
  • 中间件
  • 与处理程序共享状态
  • Axum集成
  • 必需依赖
  • 示例
  • 功能标志

高级功能

  • 使用无宏(macro-free)的API路由(router)请求(request)到处理程序(handler)
  • 使用提取器声明式解析请求
  • 简单且可预测的错误处理模型
  • 生成局域最少样板的响应
  • 充分利用towertower-http生态系统的中间件、服务和使用程序

特别是,最后一点是区分axum与其它框架的地方。 axum没有自己的中间件系统,而是使用tower::Service。 这意味着axum获得超时追踪压缩授权等功能, 而且是免费的。 它还能让您与使用hypertonic编写的应用程序共享中间件。

兼容性

axum旨在与tokiohyper协同工作。 至少目前来看,并不追求运行时和传输层独立性。

Hello World

use axum::{
routing::get,
Router,
};

#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(|| async { "Hello World!" }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
note

使用#[tokio::main]需要您启用tokio的宏和rt-multi-thread功能, 或者只需启用所有功能(cargo add tokio --features macro, rt-multi-thread)

路由

路由器用于设置哪些路径指向那些服务。

use axum::{Router, routing::get};

let app = Router::new
.route("/", get(root))
.route("/foo", get(get_foo).post(post_foo))
.route("/foo/bar", get(foo_bar));

async fn root() {}
async fn get_foo() {}
async fn post_foo() {}
async fn foo_bar() {}

处理程序

axum中,处理程序是一个异步函数,它接受零个或多个提取器作为参数, 并返回可以转换为响应的东西。 处理程序是您的应用程序逻辑所在的地方,并且axum应用程序是通过在处理程序 之间进行路由而构建的。

提取器

提取器是实现了FromRequestFromRequestsParts接口的类型。 提取器是您拆解传入请求以获取处理程序所需部分的方式。

use axum::extract::{Path, Query, Json};
use std::collections::HashMap;

// `Path`会提供路径参数并对其进行反序列化
async fn path(Path(user_id): Path<u32>) {}

// `Query`为您提供查询参数并将其反序列化
async fn query(Query(params): Query<HashMap<String, String>>) {}

// 将请求正文缓冲并将其反序列化为JSON到`serde_json::Value`
// `Json`支持任何实现`serde::Deserialize`的类型
async fn json(Json(payload): Json<serde_json:Value>) {}

响应

处理程序返回的可以实现IntoResponse接口的任何内容

use axum::{
body::Body,
routing::get,
response::Json,
Router,
};
use serde_json::{Value, json};

async fn plain_text() -> &'static str {
"foo"
}

async fn json() -> Json<Value> {
Json(json!({ "data": 42 }))
}

let app = Router::new()
.route("/plain_text", get(plain_text))
.route("/json", get(json));

错误处理

axum旨在拥有一个简单且可预测的错误处理模型。 这意味着将错误转换为响应变得简单, 并且可以保证所有错误都得到处理。

参见error_handling

处理程序之间共享状态

处理程序之间共享状态是很常见的。 例如,可能需要共享数据库连接池或其他服务的客户端。 实现这一点的三种常见方式是:

  • 使用State提取器
  • 使用请求扩展
  • 使用闭包捕获
  1. 使用State提取器
use axum::{
extract::State,
routing::get,
Router,
};
use std::sync::Arc;

struct AppState {
//...
}

let shared_state = Arc::new(AppState { /* ... */ });

let app = Router.new()
.route("/", get(handler))
.with_state(shared_state);

async fn hander(
State(state): State<Arc<AppState>>,
) {
// ...
}

如果可能的话,您应该更倾向于使用State,因为它更具有类型安全性。 不足之处是,它比请求扩展更少的动态性。

  1. 使用请求扩展

在处理程序中提取状态的另一种方式是使用Extension作为中间层和提取器。

use axum::{
extract::Extension,
routing::get,
Router,
};
use std::sync::Arc;

struct AppState {
// ...
}

let shared_state = Arc::new(AppState { /* ... */ });

let app = Router::new()
.route("/", get(handler))
.layer(Extension(shared_state));

async fn hander(
Extension(state): Extension<Arc<AppState>>
) {
// ...
}

这种方法的缺点是,如果你尝试提取一个不存在的扩展, 你将会得到运行时错误(500内部服务器错误响应)

  1. 使用闭包捕获

状态也可以通过闭包捕获直接传递给处理程序。

use axum::{
Json,
extract::{Extension, Path},
routing::{get, post},
Router,
};
use std::sync::Arc;
use serde::Deserialize;

struct AppState {
// ...
}

let shared_state = Arc::new(AppState { /* ... */ });

let app = Router::new()
.route(
"/users",
post({
let shared_state = Arc::clone(&shared_state);
move |body| create_user(body, shared_state)
}),
)
.route(
"/users/:id",
get({
let shared_state = Arc::clone(&shared_state);
move |path| get_user(path, shared_state)
}),
);

async fn get_user(Path(user_id): Path<String>, state: Arc<AppState>) {
// ...
}

async fn create_user(Json(payload): Json<CreateUserPayload>, state: Arc<AppState>) {
// ...
}

#[derive(Deserialize)]
struct CreateUserPayload {
// ...
}

这种方法的缺点在于,它比使用StateExtensions要冗长一些。

为axum构建集成

系统提供FromRequest, FromRequestPartsIntoResponse实现的库 作者应该依赖于axum-core包,而不是axumaxum-core包含核心类型和特性,并且不太可能出现破坏性变化。

必需依赖

要使用axum,你还需要引入一些依赖:

[dependencies]
axum = "<latest-version>"
tokio = { version = "<latest-version>", features = ["full"] }
tower = "<latest-version>"

为了开始使用,完整功能对于 tokio 并不是必需的,但却是最简单的方法。 Tower 也不是绝对必要的,但在测试时很有帮助。 请参考存储库中的测试示例,了解有关测试 axum 应用程序的更多信息。

特性标志

axum使用一组功能标志来减少编译和可选依赖项的数量

以下可选项特性可用:

名称描述是否默认
http1启用hyper的http1特性
http2启用hyper的http2特性
json启用Json类型和一些类似的便利功能
macro启动可选工具宏
matched-path启用了对每个请求的路由路径进行捕获,并使用MatchedPath提取器
multipart启用Multipart解析multipart/form-data请求
original-uri启用捕获每个请求的原始URI和OriginalUri提取器
tokio启用tokio作为依赖和axum::serve,SSE和extract::connect_info类型
tower-log启用tower的日志特性
tracing从内置提取器记录日志
ws通过extract::ws启用Websocket支持
form启用表单提取器
query启用查询提取器

模块

  • body: HTTP请求体工具
  • error_handling: 错误处理模型和工具
  • extract: 从请求为类型和特型提取数据
  • handler: 可以用来处理请求的异步函数
  • middleware: 写中间件的工具
  • response: 生成响应的类型和特型
  • routing: 在Service和处理之间的路由
  • serve: 提供服务

结构体

  • Error: 在使用axum时可能发生的错误
  • Extension: 提取器和扩展响应
  • Form: URL编码的提取器和响应。
  • Json: JSON提取器 / 响应。
  • Router: 用于组合处理程序和服务的路由器类型。

Traits(特型)

  • RequestExt: 扩展特性,为Request添加额外方法
  • RequestPartExt: 扩展特性,为Parts添加额外的方法
  • ServiceExt: 想任何服务添加附加方法的扩展特性

函数

  • serve: tokio和(http1或http2),使用提供的监听器提供服务

属性宏

  • debug_handler: 宏在应用处理函数时生成更好的错误消息。
鱼雪