Rust in Everything
基于 axum 0.8.9

axum 解决什么问题

主线位置:两条主线的外面。本页回答 axum 在一个 Web 程序里管哪一段:装配主线上它提供 Handler、Router、提取器这些积木,请求主线上它只负责从「拿到一个 Request」到「交出一个 Response」这一段。

flowchart TB
  fns["你的 async fn"] --> axum["axum:路由、提取器、响应"]
  axum --> core["axum-core:提取器与响应 trait"]
  axum --> svc["tower Service 与 Layer"]
  thttp["tower-http 中间件"] --> svc
  axum --> serve["axum::serve"]
  serve --> hutil["hyper-util auto::Builder"]
  hutil --> hyper["hyper:一条连接上的 HTTP"]
  hyper --> tokio["tokio:IO、任务、定时器"]

一句话定位

lib.rs 第 1 行:

axum is an HTTP routing and request-handling library that focuses on ergonomics and modularity.

「routing and request-handling」划出了 axum 的范围:给它一个已经解析好的 http::Request,它决定交给哪个函数、怎么把请求拆成函数的参数、怎么把返回值变成 http::Response。连接、HTTP/1 和 HTTP/2 的报文、keep-alive 都不归它管,那是 hyper 卷的内容;axum::serve 只是一个很薄的 accept 循环,把每条连接交给 hyper-util。

事情谁来做
接受连接,每条连接 spawn 一个任务axum::serve(或者你自己写)
解析和编码 HTTP 报文,一个端口同时接 HTTP/1 和 HTTP/2hyper、hyper-util
路径匹配matchit(axum 包了一层 PathRouter)
按方法分发、405、fallbackaxum 的 MethodRouter、Router
把请求拆成函数参数axum 的提取器(trait 在 axum-core)
把返回值变成响应IntoResponse(trait 在 axum-core)
超时、压缩、CORS、日志等中间件tower、tower-http

五条「High-level features」各落在哪

crate 文档和 README 开头列了五条,每条都能对上一组具体的类型:

- Route requests to handlers with a macro-free API.
- Declaratively parse requests using extractors.
- Simple and predictable error handling model.
- Generate responses with minimal boilerplate.
- Take full advantage of the [`tower`] and [`tower-http`] ecosystem of
  middleware, services, and utilities.

不用宏的路由

路由表是在 main 里用普通的方法调用一步步造出来的值,不是编译期由属性宏收集的。lib.rs 第 51 行的例子:

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

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

// which calls one of these handlers
async fn root() {}
async fn get_foo() {}
async fn post_foo() {}
async fn foo_bar() {}

root、get_foo 都是没有任何标注的 async fn。get(root) 能编译,是因为 axum 用 impl_handler! 为「最多 16 个参数、每个参数都是提取器、返回值实现 IntoResponse」的函数实现了 Handler trait。路由表既然是普通的值,就可以写在函数里返回、按配置决定加不加某条路由、用 nest 和 merge 拼起来。代价是报错:一个参数类型不对,rustc 只能说「Handler<_, _> 不满足」,于是有了可选的 #[debug_handler](见源码地图)。

声明式的提取器

handler 的参数类型就是「我要请求里的什么」:Path<u32> 要路径参数,Query<T> 要查询串,Json<T> 要 JSON body,State<S> 要应用状态。axum 在调用你的函数之前依次运行每个参数类型的提取逻辑,任何一个失败就直接回响应,函数根本不会被调用。两个 trait FromRequestParts、FromRequest 的区别和「为什么 body 只能给最后一个参数」见 FromRequestParts 与 FromRequest。

错误也是响应

src/docs/error_handling.md 解释了 axum 的错误模型从哪来:

axum is based on [`tower::Service`] which bundles errors through its associated
`Error` type. If you have a [`Service`] that produces an error and that error
makes it all the way up to hyper, the connection will be terminated _without_
sending a response. This is generally not desirable so axum makes sure you
always produce a response by relying on the type system.

axum does this by requiring all services have [`Infallible`] as their error
type. `Infallible` is the error type for errors that can never happen.

这条要求写在类型约束里。Router::layer 要求套上中间件之后的服务满足 Error: Into<Infallible>,route_service、fallback_service 直接要求 Error = Infallible;handler 的 Future 输出的是 Response,根本没有错误通道。一个会返回 BoxError 的 tower 中间件(比如 Timeout)不能直接 .layer,要先用 HandleErrorLayer 把错误转成响应,见 layer 与 route_layer。

📝NOTE
没有蠢问题

问: handler 写成 async fn h() -> Result<String, StatusCode>,返回 Err(StatusCode::NOT_FOUND) 难道不是错误吗?

答: 对 axum 来说不是。axum-core 为 Result 实现了 IntoResponse(into_response.rs 第 138 行),只要 T 和 E 都能变成响应:

impl<T, E> IntoResponse for Result<T, E>
where
    T: IntoResponse,
    E: IntoResponse,
{
    fn into_response(self) -> Response {
        match self {
            Ok(value) => value.into_response(),
            Err(err) => err.into_response(),
        }
    }
}

Err 分支和 Ok 分支一样被转成一个普通的 Response,发给客户端。错误文档的原话是「It doesn't matter whether you return Err(StatusCode::NOT_FOUND) or Err(StatusCode::INTERNAL_SERVER_ERROR). These are not considered errors in axum.」Result 在这里只是让你能在 handler 里用 ?;tower 意义上的「错误」(会让 hyper 直接断开连接的那种)在 axum 的服务树里不存在。

少写样板的响应

&'static str 变成 200 OK 加 content-type: text/plain; charset=utf-8,Json<T> 变成 application/json,(StatusCode, Json<T>) 这样的元组一次给出状态码和 body。这些都是 IntoResponse 的现成实现,见 IntoResponse 与 IntoResponseParts。

中间件就是 tower

第五条是 axum 自己最强调的一条。crate 文档紧接着写:

In particular, the last point is what sets `axum` apart from other libraries / frameworks.
`axum` doesn't have its own middleware system but instead uses
[`tower::Service`]. This means `axum` gets timeouts, tracing, compression,
authorization, and more, for free. It also enables you to share middleware with
applications written using [`hyper`] or [`tonic`].

落到代码上:Router::layer 的参数类型是 L: Layer<Route>,就是 tower 的 Layer;Route 是一个 BoxCloneSyncService<Request, Response, E>;Router<()> 自己也实现了 tower 的 Service<Request<B>>。axum 的 from_fn 只是「用一个 async 函数写 tower 中间件」的便利工具,产物仍然是一个 Layer。所以 tower-http 的中间件不需要知道 axum 的存在:tower-http 的 crate 文档说它的中间件只依赖 http 和 http-body 两个 crate,「compatible with any library or framework that also uses those crates, such as hyper, tonic, and warp」。

代价也来自 tower:中间件的错误类型必须能转成 Infallible(上一节),连续 .layer 的顺序和 ServiceBuilder 相反(见 tower 卷的层的顺序反了)。

不追求运行时无关

hyper 不绑定运行时,axum 绑定。lib.rs 第 18 行的「Compatibility」一节只有两句:

axum is designed to work with [tokio] and [hyper]. Runtime and
transport layer independence is not a goal, at least for the time being.

axum::serve 用的是 tokio 的 TcpListener 和 tokio::spawn,ConnectInfo、SSE 的 keep-alive 也都要 tokio feature。不过 Router 本身只是一个 tower Service:关掉默认 feature,axum 不依赖 tokio 和 hyper,Router 照样能用 oneshot 调用(源码地图里有依赖树的对比)。

性能方面,README 的「Performance」一节说 axum 是「a relatively thin layer on top of hyper and adds very little overhead」,因此「axum's performance is comparable to hyper」。它给了两个第三方基准的链接,没有给自己的数字,本卷也不引用。

和其他框架

axum 自己的文档、README、CHANGELOG 和 ECOSYSTEM.md 里,没有和 actix-web、Rocket、warp 的逐项比较;唯一的对比性说法就是上面那句「what sets axum apart from other libraries / frameworks」,指的是中间件直接用 tower。本卷也就不做逐项比较,只记两处能查到出处的交集:

  • Router::nest 上有一个文档别名 #[doc(alias = "scope")],注释是「Some other libs like actix-web use this term」。在 docs.rs 上搜 scope 能找到 nest。这个别名由 #2756 加入,最早出现在 axum 0.7.6。
  • tower-http 的文档把 warp 列为能直接使用它的框架之一(上一节的引文):warp 也建在 http、http-body 这套类型上。

小结

  • axum 管「Request 进来、Response 出去」之间的路由和请求处理;连接与协议在 hyper,路径匹配在 matchit,中间件在 tower / tower-http。
  • 路由不用宏:路由表是运行时造出来的普通值,handler 是普通 async fn,靠 impl_handler! 宏实现的 Handler trait 接进来;代价是报错难读。
  • 所有服务的错误类型都是 Infallible:错误一旦到了 hyper 就只能断开连接,所以 axum 用类型系统保证每个请求都有响应;Result<T, E> 只是两种都能变成响应的返回值。
  • axum 没有自己的中间件系统:.layer 收 tower 的 Layer,Route 是 BoxCloneSyncService,tower-http 的中间件直接可用。
  • axum 绑定 tokio 和 hyper,文档明说运行时无关「not a goal」;但 Router 本身只是一个 tower Service。
  • axum 的文档里没有和 actix-web、Rocket、warp 的逐项比较,本卷不补。

💬 关联讨论

发起讨论
加载中...