Capstone Project: Migrate a Spring Boot User Service
综合项目:迁移一个 Spring Boot 用户服务
What you’ll learn: How to migrate a small Spring Boot user service into a Rust web service step by step, preserving the HTTP contract while changing the implementation model from container-driven Java to explicit Rust composition.
本章将学习: 如何一步一步把一个小型 Spring Boot 用户服务迁移成 Rust Web 服务,在保持 HTTP 契约稳定的前提下,把实现方式从 Java 容器驱动切换成 Rust 的显式组合。Difficulty: 🔴 Advanced
难度: 🔴 高级
This capstone is intentionally shaped like real Java backend work.
这个综合项目故意做成贴近真实 Java 后端工作的形态,而不是玩具示例。
The source service contains:
原始服务包含下面这些内容:
GET /users/{id}
查询用户接口。POST /users
创建用户接口。- request validation
请求校验。 - repository layer
repository 层。 - JSON request and response DTOs
JSON 请求和响应 DTO。
Source Shape in Spring Boot
Spring Boot 版本的结构
controller -> service -> repository -> database
Typical pieces include @RestController, @Service, @Repository, request DTOs, and response DTOs.
典型组成就是 @RestController、@Service、@Repository、请求 DTO、响应 DTO 这些老熟人。
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable UUID id) {
return userService.getUser(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse createUser(@RequestBody CreateUserRequest request) {
return userService.createUser(request);
}
}
Target Shape in Rust
Rust 版本的目标结构
router -> handler -> service -> repository -> database
Suggested crates:
建议使用的 crate 组合:
[dependencies]
axum = "0.8"
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid"] }
tokio = { version = "1", features = ["full"] }
thiserror = "2"
uuid = { version = "1", features = ["serde", "v4"] }
Step 1: Freeze the Contract
第一步:先冻结接口契约
Before changing implementation details, lock down:
在动实现之前,先把这些东西钉死:
- route paths
路由路径。 - JSON payload shapes
JSON 载荷结构。 - status codes
状态码。 - validation rules
校验规则。
If the contract changes during migration, debugging becomes muddy immediately.
如果迁移过程中连契约都在乱变,排查问题时马上就会变得一团糟。
Step 2: Design the Rust Layout
第二步:先设计 Rust 模块布局
src/
main.rs
config.rs
error.rs
http/
handlers.rs
domain/
user.rs
repository/
user_repository.rs
service/
user_service.rs
This keeps the familiar layered feeling without copying Spring stereotypes one by one.
这样既保留了 Java 团队熟悉的分层感,又不会逐个复制 Spring stereotype。
Step 3: Separate DTOs and Domain Types
第三步:把 DTO 和领域对象分开
#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
pub email: String,
pub display_name: String,
}
#[derive(Debug, Serialize)]
pub struct UserResponse {
pub id: Uuid,
pub email: String,
pub display_name: String,
}
#[derive(Debug, Clone)]
pub struct User {
pub id: Uuid,
pub email: String,
pub display_name: String,
}
}
The request and response shapes belong to the HTTP boundary, while the domain type belongs to business logic.
请求和响应结构属于 HTTP 边界,领域对象属于业务逻辑内部,这两者就应该分开。
Step 4: Validate During Conversion
第四步:在转换阶段显式校验
#![allow(unused)]
fn main() {
pub struct NewUser {
pub email: String,
pub display_name: String,
}
impl TryFrom<CreateUserRequest> for NewUser {
type Error = AppError;
fn try_from(value: CreateUserRequest) -> Result<Self, Self::Error> {
let email = value.email.trim().to_ascii_lowercase();
let display_name = value.display_name.trim().to_string();
if !email.contains('@') {
return Err(AppError::Validation {
message: "email must contain @".into(),
});
}
if display_name.is_empty() {
return Err(AppError::Validation {
message: "display_name cannot be blank".into(),
});
}
Ok(Self { email, display_name })
}
}
}
This replaces a lot of annotation-driven validation magic with plain, visible rules.
这一步会把很多依赖注解的校验魔法,换成朴素而可见的规则代码。
Step 5: Keep SQL Visible
第五步:让 SQL 保持可见
#![allow(unused)]
fn main() {
pub struct UserRepository {
pool: sqlx::PgPool,
}
impl UserRepository {
pub async fn find_by_id(&self, id: uuid::Uuid) -> Result<Option<User>, sqlx::Error> {
sqlx::query_as!(
User,
"select id, email, display_name from users where id = $1",
id
)
.fetch_optional(&self.pool)
.await
}
}
}
For migration work, this is often easier to reason about than jumping straight back into another layer of ORM magic.
在迁移工作里,这通常比又一头扎进另一层 ORM 魔法更容易掌控。
Step 6: Move Business Logic into a Service
第六步:把业务逻辑搬进 Service
#![allow(unused)]
fn main() {
pub struct UserService {
repo: UserRepository,
}
impl UserService {
pub async fn get_user(&self, id: uuid::Uuid) -> AppResult<User> {
self.repo
.find_by_id(id)
.await?
.ok_or_else(|| AppError::NotFound {
entity: "user".into(),
id: id.to_string(),
})
}
}
}
This still feels familiar to Java developers, but errors are now explicit and typed.
这一步对 Java 开发者来说依然很熟悉,只不过失败路径现在已经是显式且有类型的了。
Step 7: Wire Handlers and Shared State
第七步:装配 Handler 和共享状态
#![allow(unused)]
fn main() {
#[derive(Clone)]
pub struct AppState {
pub user_service: std::sync::Arc<UserService>,
}
async fn get_user(
State(state): State<AppState>,
Path(id): Path<uuid::Uuid>,
) -> AppResult<Json<UserResponse>> {
let user = state.user_service.get_user(id).await?;
Ok(Json(UserResponse {
id: user.id,
email: user.email,
display_name: user.display_name,
}))
}
}
This is the Rust replacement for controller method binding plus dependency injection.
这就是 Rust 对“controller 方法绑定加依赖注入”的替代方案。
Step 8: Build an Error Boundary
第八步:建立错误边界
Use IntoResponse to play the role that @ControllerAdvice often plays in Spring Boot.
可以用 IntoResponse 去承担 Spring Boot 里 @ControllerAdvice 常承担的职责。
Centralized error mapping keeps HTTP behavior stable while the internals change.
即使内部实现正在迁移,集中式错误映射也能让外部 HTTP 行为保持稳定。
Step 9: Test the Contract
第九步:围绕契约做测试
The most valuable migration tests are black-box contract tests:
迁移阶段最值钱的测试,其实是黑盒契约测试:
- same status codes
状态码一致。 - same response shape
响应结构一致。 - same validation behavior
校验行为一致。
Step 10: Roll Out Carefully
第十步:谨慎发布
- mirror traffic if possible
能镜像流量就尽量镜像。 - migrate one tenant or region first
先迁一个租户或一个区域。 - compare latency, memory, and error rate
重点对比延迟、内存和错误率。
Why This Capstone Matters
为什么这个综合项目很重要
This one project forces practice with nearly every major Java-to-Rust transition:
这个项目几乎会把 Java 迁到 Rust 时最关键的转换都练一遍:
- DTO to domain conversion
DTO 到领域对象转换。 - explicit dependency wiring
显式依赖装配。 Resultinstead of exceptions
用Result替代异常流。- handler/service/repository separation
handler、service、repository 分层。 - explicit SQL and HTTP contract preservation
显式 SQL 与 HTTP 契约保持稳定。
Real-World Java-to-Rust References
真实世界的 Java 到 Rust 迁移参考
All links in this section were verified as reachable on March 26, 2026.
本节所有链接都已在 2026 年 3 月 26 日核验可访问。
-
Datadog: static analyzer migration
Datadog:静态分析器迁移案例。 Datadog migrated a production static analyzer from Java to Rust, used feature-parity tests to keep behavior stable, learned enough Rust to map the codebase in about 10 days, completed the overall migration within a month, and reported about 3x faster execution with roughly 10x lower memory use. This is one of the clearest public examples of a disciplined Java-to-Rust migration in a real product. How we migrated our static analyzer from Java to Rust
Datadog 把生产环境里的静态分析器从 Java 迁到 Rust,用特性对齐测试保证行为一致;团队大约 10 天摸清 Rust 的关键概念,1 个月完成整体迁移,并报告约 3 倍执行速度与约 10 倍内存下降。这是公开资料里非常扎实、也非常像真实工程迁移过程的一个案例。 -
CIMB Niaga: banking microservice migration
CIMB Niaga:银行微服务迁移案例。 CIMB Niaga migrated a critical internal authentication microservice from Java to Rust with a phased rollout that ran beside the Java service. Their public numbers are unusually concrete: startup time fell from about 31.9 seconds to under 1 second, CPU use dropped from 3 cores to 0.25 cores, and memory use fell from 3.8 GB to 8 MB. They also explicitly describe the learning curve as steep and mention knowledge sharing plus peer mentoring as part of the migration strategy. Delivering Superior Banking Experiences
CIMB Niaga 把一个关键的内部认证微服务从 Java 迁到 Rust,而且是与原有 Java 服务并行逐步发布。它给出的公开数据很硬:启动时间从约 31.9 秒降到 1 秒以内,CPU 使用从 3 个核心降到 0.25 个核心,内存从 3.8 GB 降到 8 MB。同时他们也明确提到学习曲线比较陡,因此配套采用了知识分享和结对辅导。 -
WebGraph and Software Heritage: large-scale graph processing rewrite
WebGraph 与 Software Heritage:超大规模图处理框架重写。 The WebGraph team rewrote a long-standing Java graph-processing framework in Rust because JVM memory and memory-mapping limits became a bottleneck at Software Heritage scale. Their paper reports about 1.4x to 3.18x speedups on representative workloads and explains how Rust’s type system and compilation model enabled a cleaner, faster implementation for huge immutable datasets. WebGraph: The Next Generation (Is in Rust)
WebGraph 团队之所以把一个存在多年的 Java 图处理框架改写成 Rust,核心原因是 Software Heritage 这种级别的数据规模下,JVM 的内存与内存映射限制已经成了瓶颈。论文里给出的代表性工作负载加速大约在 1.4 倍到 3.18 倍之间,也解释了 Rust 的类型系统和编译模型为什么更适合这类巨大而不可变的数据集。 -
Mike Bursell: a Java developer’s transition notes
Mike Bursell:Java 开发者迁到 Rust 的一手体验。 Mike Bursell describes taking one of his own Java projects and reimplementing it in Rust. The valuable part is the tone: enough of Rust felt familiar to keep going, ownership became understandable with practice, and Cargo plus compiler feedback made the language feel learnable rather than mystical. It is a good first-person account of what the transition feels like after years of Java. Why I switched from Java to Rust
Mike Bursell 讲的是把自己的一个 Java 项目改用 Rust 重写后的真实感受。这个文章有价值的地方在于它很克制:Rust 里有不少地方能让 Java 开发者保持熟悉感,所有权一开始确实拧巴,但通过练习会逐步理解,而 Cargo 加上编译器反馈会让学习过程变得非常具体。 -
Kasun Sameera: practical trade-offs before moving from Java
Kasun Sameera:迁移前必须正视的现实权衡。 Kasun Sameera compares Rust web development with Spring Boot from a Java developer’s perspective. The useful takeaway is the trade-off analysis: Rust web frameworks could outperform the same Spring Boot service, but the initial setup effort, library maturity, and convenience story still favored Java for many business applications. That balance is exactly what engineering teams need to judge honestly before migrating. Before moving to Rust from Java
Kasun Sameera 从 Java 开发者视角把 Rust Web 开发和 Spring Boot 做了一个比较。真正值得看的是他的权衡分析:Rust Web 框架确实可能比同类 Spring Boot 服务更快,但初始化成本、类库成熟度和业务开发便利性,在很多场景里依然还是 Java 更占优。迁移前把这件事想明白,比一腔热血冲过去靠谱得多。
When Java Teams Should Migrate to Rust
什么条件下适合从 Java 迁到 Rust
Rust becomes a strong choice when most of the following are true:
如果下面这些条件大部分都成立,那么 Rust 会是很强的选择:
- predictable latency, low memory usage, or fast startup materially affect user experience or operating cost
稳定延迟、低内存占用或快速启动,会直接影响用户体验或者运行成本。 - the service does parser work, protocol handling, security scanning, gateways, agents, stream processing, or other infrastructure-heavy work where control over performance matters
服务主要承担解析器、协议处理、安全扫描、网关、Agent、流处理这类基础设施型工作,而且性能控制真的很重要。 - the migration target can be isolated behind a clear HTTP, gRPC, queue, or library boundary
迁移目标可以被清晰地隔离在 HTTP、gRPC、消息队列或者库接口边界之后。 - the team is willing to invest in ownership, borrowing, explicit error handling, and stronger test discipline
团队愿意投入时间掌握所有权、借用、显式错误处理,以及更严格的测试纪律。 - success can be measured with concrete metrics instead of general excitement about a new language
迁移成效可以用明确指标来衡量,而不是只靠对新语言的兴奋感。
Java should usually remain the default when most of the following are true:
如果下面这些情况更贴近现实,那么继续留在 Java 往往更合适:
- the main bottleneck is product complexity or delivery throughput rather than runtime performance
主要瓶颈是业务复杂度或者交付速度,而不是运行时性能。 - Spring Boot, JPA, and the existing JVM platform are still the main reason the team ships quickly
Spring Boot、JPA 和现有 JVM 平台,依然是团队快速交付的主要原因。 - the team has no room for training, design reviews, or a slower first migration
团队当前没有余力做培训、设计评审,或者承受第一次迁移带来的节奏变慢。 - the proposal is a full rewrite with weak contract tests and no shadow rollout or rollback plan
方案是整块重写,但契约测试很弱,也没有影子发布和回滚预案。
A practical recommendation for Java teams is to migrate in this order:
比较务实的迁移顺序可以是这样:
- start with one bounded service, parser, background worker, or performance-critical library
先挑一个边界清楚的服务、解析器、后台任务,或者性能敏感的库开始。 - preserve the external contract first and improve internals second
先保证外部契约稳定,再谈内部实现优化。 - run the Java and Rust implementations side by side during validation
验证阶段让 Java 与 Rust 两套实现并行运行。 - measure latency, memory, startup time, and operational simplicity
重点测量延迟、内存、启动时间和运维复杂度。 - expand only after the first migration clearly pays for itself
等第一批迁移确实证明有价值之后,再继续扩大范围。
For most teams, Rust works best as a selective addition to the architecture, not as a blanket replacement for every Java service.
对大多数团队来说,Rust 更适合作为架构里的选择性补强,而不是把所有 Java 服务一股脑全换掉。