CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Rust 语言入门/课时/Web 与生态
RawBlame
课时目录12 节

Rust 语言入门

  1. 01Rust 是什么
  2. 02基础语法
  3. 03所有权与借用
  4. 04生命周期
  5. 05结构体与枚举
  6. 06错误处理
  7. 07Trait 与泛型
  8. 08集合类型
  9. 09并发与 async
  10. 10宏与元编程
  11. 11Web 与生态
  12. 12综合练习

Web 与生态

axum/actix、serde、sqlx、reqwest。

35 分钟

Web 与生态

这一课你将能做到

  1. 用 axum 搭起 HTTP 服务:路由、提取器、共享状态三件套
  2. 用 serde 完成 JSON 的编解码,理解它对比 Jackson 的性能来源
  3. 认清 Rust Web 生态的全景图,知道每个领域该用什么 crate

为什么要学

Java Web 你熟的是 Spring Boot:注解扫描、反射调用、内嵌 Tomcat。Rust 的 axum 是另一条路:函数即 handler、路由显式注册、无反射——启动是毫秒级,内存是个位数 MB,QPS 上限高一个量级。判题系统这种「高频提交 + 低延迟查询」的服务,正是这类框架的价值区。

这一课把前面所有知识串起来:enum 建模状态(第 5 课)、Result 错误处理(第 6 课)、trait 约束(第 7 课)、Arc/Mutex 共享状态(第 9 课)、derive 宏(第 10 课)——全部落到一个能跑的接口服务上。

先建立图像

Spring Boot                          axum
@RestController + @GetMapping  ↔    Router::new().route("/x", get(handler))
@RequestBody DTO               ↔    Json<SubmitReq> 提取器
@Service @Autowired            ↔    State<AppState>(编译期注入,无反射)
Jackson ObjectMapper           ↔    serde_json(编译期生成编解码器)
内嵌 Tomcat(线程池)            ↔    hyper + tokio(async,事件驱动)

跟着做

步骤 1:建项目与依赖

cargo new judge-api
cd judge-api

Cargo.toml:

[package]
name = "judge-api"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

四个依赖各司其职:axum 是 Web 框架,tokio 是它脚下的 async 运行时,serde 负责序列化,serde_json 是 JSON 后端。

步骤 2:数据模型——derive 一把梭

use serde::{Deserialize, Serialize};

#[derive(Serialize, Clone, Debug)]
struct Submission {
    id: u64,
    language: String,
    status: String,      // PENDING / RUNNING / AC / WA
}

#[derive(Deserialize)]
struct SubmitReq {
    language: String,
    code: String,
}

请求体 SubmitReq 只需 Deserialize,响应体 Submission 只需 Serialize——按需 derive,编解码器编译期生成。

步骤 3:共享状态与服务骨架

use axum::{
    extract::{Path, State},
    http::StatusCode,
    routing::{get, post},
    Json, Router,
};
use std::{collections::HashMap, sync::{Arc, Mutex}};

#[derive(Clone, Default)]
struct AppState {
    store: Arc<Mutex<HashMap<u64, Submission>>>,   // 真实系统换成数据库
}

#[tokio::main]
async fn main() {
    let state = AppState::default();

    let app = Router::new()
        .route("/submissions", post(submit))          // 提交判题
        .route("/submissions/:id", get(query))        // 查询结果
        .with_state(state);                           // 状态注入所有 handler

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("listening on http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

对照 Spring:Router 替代 @RestController 的路径注册,with_state 替代依赖注入,且全程无反射、无启动扫描。AppState 实现 Clone 是因为 axum 每个 handler 调用都会克隆一份外壳(内部是 Arc,实际共享同一份数据)。

步骤 4:两个 handler——提取器的世界

// POST /submissions:接收 JSON,返回 201 + 创建的资源
async fn submit(
    State(state): State<AppState>,       // 提取器 1:共享状态,必须是第一个参数
    Json(req): Json<SubmitReq>,          // 提取器 2:JSON 体自动反序列化,非法 JSON 自动 400
) -> (StatusCode, Json<Submission>) {
    let mut store = state.store.lock().unwrap();
    let id = store.len() as u64 + 1;
    let sub = Submission { id, language: req.language, status: "PENDING".into() };
    store.insert(id, sub.clone());
    (StatusCode::CREATED, Json(sub))
}

// GET /submissions/:id:路径参数,查询失败返回 404
async fn query(
    State(state): State<AppState>,
    Path(id): Path<u64>,                 // 路径段自动解析成 u64,非数字自动 400
) -> Result<Json<Submission>, StatusCode> {
    state.store.lock().unwrap().get(&id).cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)    // Result 直接映射为 HTTP 状态码
}

提取器是 axum 的灵魂:参数声明即文档,类型不匹配由框架统一拒绝,handler 里不再有手工解析。Result<Json<T>, StatusCode> 可直接作为返回值,Ok 走 200,Err 走声明的状态码——第 6 课的错误处理思想直接映射到 HTTP。

步骤 5:跑起来验证

cargo run

另开终端:

curl -X POST http://localhost:3000/submissions \
  -H "Content-Type: application/json" \
  -d '{"language":"rust","code":"fn main(){print!(42)}"}'
# {"id":1,"language":"rust","status":"PENDING"}

curl http://localhost:3000/submissions/1
# {"id":1,"language":"rust","status":"PENDING"}

curl http://localhost:3000/submissions/999 -i
# HTTP/1.1 404 Not Found

再提交一个非法 JSON 看框架自动返回 400——全程没写一行解析和校验代码。

讲清楚要点

  1. axum 的分层:tokio(运行时)→ hyper(HTTP 协议)→ axum(路由与提取器);每层都是独立 crate,性能是手写级别。
  2. 提取器顺序有约束:State 必须是第一个参数;Json 是消费 body 的提取器,一个 handler 只能有一个;路径/查询参数用 Path/Query。
  3. 状态共享三板斧:AppState 用 #[derive(Clone)] + 内部 Arc;可变部分用 Mutex;async 里跨 await 的锁用 tokio::sync::Mutex(第 9 课的坑在这里同样生效)。
  4. 错误即响应:handler 返回 Result<T, E>,E 实现 IntoResponse 即可;进阶用自定义 AppError enum 统一映射业务错误到状态码。
  5. serde 是生态公约数:axum 的 JSON、reqwest 的请求体、sqlx 的行映射全部走 serde derive——一套建模,全栈通用。

你可能会懵的地方

  • 没有 Spring 的分层注解,项目大了怎么组织? 按 Router 拆模块:每个领域一个 fn router() -> Router<AppState>,主入口 merge。依赖注入的需求用 trait + 泛型参数解决(第 7 课),编译期即验证,无容器魔法。

  • 中间件怎么加? tower 生态:.layer(TraceLayer::new())、超时、限流、压缩都是 layer。对标 Servlet Filter/拦截器,但类型安全且可组合。

  • 为什么启动这么快、内存这么小? 无类路径扫描、无反射元数据、无 JIT 预热;路由表和序列化代码全部编译期确定。冷启动毫秒级意味着容器/Serverless 场景的天然优势。

  • 数据库怎么接? sqlx(编译期校验 SQL 的 async 驱动)或 sea-orm(ActiveRecord 风格)。sqlx 的 query! 宏在编译期连库检查 SQL 与类型——Java 里 MyBatis 的字段对不上要跑到运行时才发现。

  • 怎么部署? cargo build --release 出单个静态可执行文件,扔进 scratch 容器即可运行,镜像 10MB 量级;没有 JVM、没有 fat jar、没有运行时依赖。

错法对照

错法问题正解
main 没有 #[tokio::main]axum::serve 无法 awaitmain 标 #[tokio::main] async fn
State 不是 handler 第一个参数error[E0277] 提取器顺序State 放最前,Body 类提取器放最后
handler 里持锁跨 await阻塞 runtime,甚至死锁临界区缩短到无 await,或换 tokio Mutex
提交两个 body 类提取器error:Json 只能消费一次用一次 Json,其余信息走 Path/Query
字段名风格对不上(problemId vs problem_id)反序列化缺字段 400#[serde(rename_all = "camelCase")] 统一映射

卡点急救

  1. error[E0277] State 提取失败:检查三处——handler 第一参数是 State(state): State<AppState>、Router 调用了 with_state、AppState 实现了 Clone。
  2. POST 返回 415 Unsupported Media Type:curl 忘了 -H "Content-Type: application/json";axum 按类型严格匹配。
  3. E0382 moved value(handler 里 clone 报错):返回体和 store 都要持有 Submission 时,clone 一份再 insert;Json 序列化后所有权即可释放,别在 handler 里留长期引用。

面试常问

  • axum 与 Spring Boot 的架构差异? axum 是编译期模型:显式路由、提取器类型化解析、无反射无扫描,运行时是 tokio 事件驱动,冷启动毫秒级、内存 MB 级;Spring 是运行时模型:注解扫描 + 代理 + 反射,换来的是生态集成与开发效率。判题网关这类延迟敏感场景 axum 占优,复杂企业集成 Spring 占优。

  • serde 为什么比 Jackson 快? 编译期为每个类型生成专用序列化器,无反射查找、无中间 Map 分配、可被编译器内联;Jackson 靠运行时反射与内省缓存。基准差距通常 2-5 倍,且 serde 无预热问题。

  • Rust Web 服务高并发的来源是什么? tokio 的事件驱动调度让 IO 等待不占线程(对标虚拟线程但无 GC 抖动),加上每请求无 GC 停顿、无 JIT 热身,尾延迟稳定;内存占用低使单机可跑更多实例。

自测三问

  1. 提取器 State、Path、Json 各从哪里取数据?顺序上有什么约束?
  2. Result<Json<Submission>, StatusCode> 的 Ok/Err 分别如何映射为 HTTP 响应?
  3. AppState 为什么要 derive Clone?clone 的是数据还是壳?

小结一句话

axum + serde 把 Spring Boot 的运行时魔法全部前移到编译期:路由显式、状态类型化、JSON 零反射——这就是 Rust 高频接口性能优势的完整拼图。

延伸

  • std::net:标准库网络模块
  • 下一课:综合练习——亲手拼出一个带并发判题内核的迷你判题服务。
加载本节练习…
← 上一课宏与元编程下一课 →综合练习