axum 是一个易于使用,但功能强大的 Web 框架,旨在充分利用 Tokio 的生态系统。
高级特性
- 使用无宏的API实现路由(router)功能
- 使用提取器(extractor)对请求进行声明式的解析
- 简单和可预测的错误处理模式。
- 用最少的模板生成响应。
- 充分利用
tower
和tower-http
的中间件、服务和工具的生态系统
axum 与现有框架不同的地方。axum 没有自己的中间件系统,而是使用tower::Service
。这意味着 axum 可以免费获得超时、跟踪、压缩、授权等功能。它还可以让你与使用hyper
或tonic
编写的应用程序共享中间件。
使用示例
先来一个Hello World的入门示例:
[dependencies]
axum="0.6.16"
tokio = { version = "1.0", features = ["full"] }
添加上面的依赖项后,就可以编码了:
use axum::{
routing::get,
Router,
};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
let app = route("/", get(handler)); // http://127.0.0.1:3000
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
// run it with hyper on localhost:3000
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// 处理器
async fn handler() -> &'static str {
"Hello, World!"
}
对GET/
的请求响应是200 OK
,其中正文是Hello, World!
。任何其他请求将导致404 Not Found
响应。
注:cargo run 启动后,浏览器里跑一下 http://127.0.0.1:3000 或者 curl -X GEThttp://127.0.0.1:3000
路由(Routers)
Router
用于设置哪些路径指向哪些服务,可以使用一个简单的 DSL 来组合多个路由。
#[tokio::main]
async fn main() {
// our router
let app = Router::new()
.route("/", get(root)) // http://127.0.0.1:3000
.route("/foo", get(get_foo).post(post_foo)) // http://127.0.0.1:3000/foo
.route("/foo/bar", get(foo_bar)); // http://127.0.0.1:3000/foo/bar
// run it with hyper on localhost:3000
axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
// which calls one of these handlers
async fn root() -> String {
String::from("hello axum")
}
async fn get_foo() -> String {
String::from("get请求的foo")
}
async fn post_foo() -> String {
String::from("post请求的foo")
}
async fn foo_bar() -> String {
String::from("foo:bar")
}
注:这里 /foo 同时绑定了GET及POST方法的路由。可以用 crul 命令工具测试一下:
curl -X GET http://127.0.0.1:3000/foo
curl -X POST http://127.0.0.1:3000/foo
细节可以查看
Router
处理器(Handlers)
在 axum 中,处理器(handler)是一个异步异步函数或者异步代码块,它接受零个或多个“ extractors “作为参数,并返回一些可以转换为一个“IntoResponse”的内容。
处理器是应用程序逻辑存在的地方,而 axum 应用程序是通过处理器之间的路由构建的。
请参见
handler
了解处理程序的更多详细信息。
提取器(Extractors)
请求可以使用 “提取器(Extractor)” 进行声明式的解析,是一个实现了FromRequest
或FromRequestParts
的类型,作用是分离传入请求以获得处理程序所需的部分(比如解析异步函数的参数),如果请求的URI匹配,就会运行。
use axum::extract::{Path, Query, Json};
use std::collections::HashMap;
// Path路径,eg. /users/<id>
async fn path(Path(user_id): Path<u32>) {}
// Query参数,eg. /users?id=123&name=jim
async fn query(Query(params): Query<HashMap<String, String>>) {}
// Json 格式参数,一般用于 POST 请求
async fn json(Json(payload): Json<serde_json::Value>) {}
例如,Json
是一个提取器,它消耗请求主体并将其解析为JSON
:
use axum::{ routing::get, Router, extract::Json};
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct CreateUser {
username: String,
}
// curl -H "Content-Type: application/json" -d '{"username":"someName"}' -X POST http://127.0.0.1:3000/users
async fn create_user(Json(payload): Json<CreateUser>) -> (StatusCode, Json<CreateUser>){
// `payload` is a `CreateUser`
// 响应内容为Json格式,状态码是201
(StatusCode::CREATED, Json(payload))
}
#[tokio::main]
async fn main() {
// our router
let app = Router::new()
.route("/users", post(create_user)); // http://127.0.0.1:3000/users
// run it with hyper on localhost:3000
axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
注:cargo run 启动后,运行 curl 命令:
curl -H “Content-Type: application/json” -d ‘{“username”:”someName”}’ -X POST http://127.0.0.1:3000/users
axum
提供了许多有用的提取器,例如:
-
Bytes
,String
,Body
, 和BodyStream
用于获取请求正文 -
Method
,HeaderMap
, 和Uri
用于获取请求的特定部分 -
Form
,Query
,UrlParams
, 和UrlParamsMap
用于更高级别的请求解析 -
Extension
用于跨处理程序共享状态的扩展 -
Request<hyper::Body>
如果你想完全控制 -
Result<T, E>
andOption<T>
使提取器成为可选
你也可以通过实现FromRequest
来定义你自己的提取器。
更多细节可以参看
extract
构建响应(IntoResponse)
处理程序可以返回任何实现了IntoResponse
的东西,它将被自动转换为响应:
use http::StatusCode;
use axum::response::{Html, Json};
use serde_json::{json, Value};
// We've already seen returning &'static str
async fn text() -> &'static str {
"Hello, World!"
}
// String works too
async fn string() -> String {
"Hello, World!".to_string()
}
// Returning a tuple of `StatusCode` and another `IntoResponse` will
// change the status code
async fn not_found() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "not found")
}
// `Html` gives a content-type of `text/html`
async fn html() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
// `Json` gives a content-type of `application/json` and works with any type
// that implements `serde::Serialize`
async fn json() -> Json<Value> {
Json(json!({ "data": 42 }))
}
这意味着在实践中,你很少需要建立你自己的响应。你也可以实现IntoResponse
来创建你自己的特定领域响应。
更多细节参看
response
错误处理(Error handling)
axum旨在提供一个简单且可预测的错误处理模型,这意味着将错误转换为响应很简单,并且可以保证所有错误都得到处理。
use std::time::Duration;
use axum::{
body::Body,
error_handling::{HandleError, HandleErrorLayer},
http::{Method, Response, StatusCode, Uri},
response::IntoResponse,
routing::get,
BoxError, Router,
};
use tower::ServiceBuilder;
#[tokio::main]
async fn main() {
let app = Router::new()
.merge(router_fallible_service()) // 模拟使用 Service的错误处理
.merge(router_fallible_middleware()) // 模拟使用中间件的错误处理
.merge(router_fallible_extractor()); // 模拟使用提取器的错误处理
let addr = "127.0.0.1:3000";
println!("listening on {}", addr);
axum::Server::bind(&addr.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
// 错误处理方式1: 模拟使用 Service的错误处理
fn router_fallible_service() -> Router {
// 这个 Service 可能出现任何错误
let some_fallible_service = tower::service_fn(|_req| async {
thing_that_might_fail().await?;
Ok::<_, anyhow::Error>(Response::new(Body::empty()))
});
Router::new().route_service(
"/",
// Service 适配器通过将错误转换为响应来处理错误。
HandleError::new(some_fallible_service, handle_anyhow_error),
)
}
// 业务处理逻辑,可能出现失败而抛出 Error
async fn thing_that_might_fail() -> Result<(), anyhow::Error> {
// 模拟一个错误
anyhow::bail!("thing_that_might_fail")
}
// 把错误转化为 IntoResponse
async fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)
}
// 处理器:模拟超时
async fn handler_timeout() -> impl IntoResponse {
println!("sleep 3 seconds");
tokio::time::sleep(Duration::from_secs(3)).await; // 休眠3秒,模拟超时
format!("Hello Error Handling !!!")
}
// 错误处理方式2 : 用中间件处理错误的路由
fn router_fallible_middleware() -> Router {
Router::new()
.route("/fallible_middleware", get(handler_timeout))
.layer(
ServiceBuilder::new()
// `timeout` will produce an error if the handler takes
// too long so we must handle those
.layer(HandleErrorLayer::new(handler_timeout_error))
.timeout(Duration::from_secs(1)),
)
}
// 用中间件处理错误
async fn handler_timeout_error(err: BoxError) -> (StatusCode, String) {
if err.is::<tower::timeout::error::Elapsed>() {
(
StatusCode::REQUEST_TIMEOUT,
"Request time too long, Timeout!!!".to_string(),
)
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", err),
)
}
}
// 错误处理方式3: 用运行时提取器处理错误的路由
fn router_fallible_extractor() -> Router {
Router::new()
.route("/fallible_extractor", get(handler_timeout))
.layer(
ServiceBuilder::new()
// `timeout` will produce an error if the handler takes
// too long so we must handle those
.layer(HandleErrorLayer::new(handler_timeout_fallible_extractor))
.timeout(Duration::from_secs(1)),
)
}
// 用运行时提取器处理错误
async fn handler_timeout_fallible_extractor(
// `Method` and `Uri` are extractors so they can be used here
method: Method,
uri: Uri,
// the last argument must be the error itself
err: BoxError,
) -> (StatusCode, String) {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("`{} {}` failed with {}", method, uri, err),
)
}
参见
error_handling
了解更多关于axum错误处理模型以及如何优雅地处理错误的详细信息。
中间件(Middleware)
axum 支持来自tower
和tower-http
的中间件。
[dependencies]
axum = "0.6.16"
tokio = { version = "1.0", features = ["full"] }
tower = { version = "0.4.13", features = ["full"] }
tower-http = { version = "0.4", features = ["fs", "trace", "compression-br"] }
添加上面的依赖项后,就可以编码了:
use std::net::SocketAddr;
use axum::{routing::get, Router};
use tower::ServiceBuilder;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
#[tokio::main]
async fn main() {
let middleware_stack = ServiceBuilder::new()
// add high level tracing of requests and responses
.layer(TraceLayer::new_for_http())
// compression responses
.layer(CompressionLayer::new())
// convert the `ServiceBuilder` into a `tower::Layer`;
.into_inner();
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.layer(middleware_stack);
// run it with hyper on localhost:3000
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
println!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
这个功能很关键,因为它允许我们只写一次中间件,并在不同的应用中分享它们。例如,axum
不需要提供自己的 tracing/logging
中间件,可以直接使用来自tower-http
的TraceLayer
。同样的中间件也可以用于用 tonic 制作的客户端或服务器路由到任何tower::Service
axum
也可以将请求路由到任何 tower 服务。可以是你用service_fn
编写的服务,也可以是来自其他 crate 的东西,比如来自tower-http
的ServeFile
:
use axum::{
body::Body, http::Request, response::Response, routing::get_service, Router,
};
use std::{convert::Infallible, net::SocketAddr};
use tower::service_fn;
use tower_http::services::ServeFile;
#[tokio::main]
async fn main() {
let app = Router::new()
// GET `/static/Cargo.toml` goes to a service from tower-http
.route(
"/static",
get_service(ServeFile::new("Cargo.toml")),
)
.route(
// Any request to `/` goes to a some `Service`
"/",
get_service(service_fn(|_: Request<Body>| async {
let res = Response::new(Body::from("Hi from `GET /`"));
Ok::<_, Infallible>(res)
})),
);
// run it with hyper on localhost:3000
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
println!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
为 axum 编写中间件有几种不同的方法。详见
中间件
与处理器共享状态(Sharing state with handlers)
在处理程序之间共享某些状态是很常见的。例如,可能需要共享到其他服务的数据库连接或客户端池。
最常见的三种方法是:
- 使用状态提取器:
State
- 使用请求扩展提取器:
Extension
- 使用闭包捕获:
Closure
use axum::{
extract::{Path, State},
response::IntoResponse,
routing::get,
Extension, Router,
};
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
// 共享状态结构体
#[derive(Debug)]
struct AppState {
// ...
state: AtomicUsize,
}
// 方法1: 使用 State 状态提取器
async fn handler_as_state_extractor(State(state): State<Arc<AppState>>) -> impl IntoResponse {
// ...
state.state.fetch_add(1, Ordering::SeqCst); //请求一次 state 的值递增1
format!("State extract shared_state: {:?}", state)
}
// 方法2: 使用 Extension 请求扩展提取器
// 这种方法的缺点是,如果尝试提取一个不存在的扩展,可能是因为忘记添加中间件,
// 或者因为提取了错误的类型,那么将得到运行时错误(特别是500 Internal Server Error 响应)。
async fn handler_as_extension_extractor(Extension(state): Extension<Arc<AppState>>) -> impl IntoResponse {
// ...
state.state.fetch_add(1, Ordering::SeqCst); //请求一次 state 的值递增1
format!("Extension extract shared_state: {:?}", state)
}
// 方法3: 使用闭包捕获(closure captures)直接传递给处理器
async fn get_user(Path(user_id): Path<String>, state: Arc<AppState>) -> impl IntoResponse {
// ...
state.state.fetch_add(1, Ordering::SeqCst); //请求一次 state 的值递增1
format!("closure captures shared_state: {:?}", state)
}
#[tokio::main]
async fn main() {
// 处理器共享状态(Sharing state with handlers)
let shared_state = Arc::new(AppState { state: 0.into()/* ... */ });
let shared_state_extension = Arc::clone(&shared_state);
let shared_state_closure = Arc::clone(&shared_state);
let app = Router::new()
.route("/state", get(handler_as_state_extractor)) // 1.使用State提取器
.with_state(shared_state)
.route("/extension", get(handler_as_extension_extractor)) // 2.使用Extension提取器
.layer(Extension(shared_state_extension))
.route(
"/users/:id",
get({
move |path| get_user(path, shared_state_closure) // 3.使用闭包捕获直接传递给处理器
}),
);
let addr = "127.0.0.1:3000";
println!("listening on {}", addr);
axum::Server::bind(&addr.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
注:可以用浏览器跑一下或者 crul 命令工具测试一下(交替请求),看下state的是否在共享基础上递增
curl -X GET http://127.0.0.1:3000/state
curl -X GET http://127.0.0.1:3000/extension
curl -X GET http://127.0.0.1:3000/users/111
总结
axum 是一个易于使用,功能强大的 Web 框架,旨在充分利用 Tokio 的生态系统,使用无宏 API 实现了路由功能,基于hyper构建的,中间件基于 tower 和 tower-http 生态实现,可利用其中中间件、服务以及实用程序。支持 WebSocket 和其他协议,以及异步 I/O。
- axum 的中间件是直接使用 tower 的抽象,这样的好处就是:
- 使用了统一 的 Service 和 Layer 抽象标准,方便大家来繁荣生态
- 复用和充分利用 tokio / hyper/ tonic 生态,潜力很大
- axum 的路由机制并没有使用像 rocket那样的属性宏,而是提供了简单的 DSL (链式调用)。路由是基于迭代和正则表达式来匹配的,所以路由性能和 actix-web 差不多。
- 也提供了方便的提取器 ,只要实现 FromRequest 就是一个提取器,实现起来也非常方便。
总之,Axum 是 Rust 在 Web 开发领域的一个里程碑,它强势带动了 tokio/tower 生态,潜力很大。
参考
- docs.rs/axum/latest…
- docs.rs/axum/latest…
- github.com/tokio-rs/ax…
- tokio.rs/blog/2021-0…
- blog.logrocket.com/rust-axum-e…