
我和技术负责人又又又因为框架选型吵起来了。他逢人就安利 Spring Boot,恨不得把所有项目都塞进这个框里;而我疯狂吹爆 FastAPI,张口就是“Python 开发快到飞起”“Java 那套复杂玩意儿纯属多余”。
吵到最后,我们干脆打了个赌。
同一个业务服务,我们各写一遍:我用 FastAPI,他用 Spring Boot。两个版本同时上线跑六个月,实打实的流量、真刀真枪的问题、还有真金白银的赌注——输家请全队去市中心那家贵得离谱的牛排馆搓一顿。
当时我自信得不行,觉得稳赢。FastAPI 多时髦啊,异步非阻塞、性能又能打;Spring Boot 呢?老古董、企业级包袱、还得写啰嗦的 Java 代码,简直毫无胜算。
先剧透结局:最后是我掏的腰包。但原因,绝对和你想的不一样。
我们要开发的是电商平台的一个 REST API,核心功能如下:
技术栈明细
FastAPI 版本:
Spring Boot 版本:
数据库、Redis、AWS 基础设施、监控工具(Datadog)、CI/CD 流水线,全部保持一致。
唯一的变量,就是框架本身。
我只用两天就把 FastAPI 版本的服务跑起来了。
两天!
from fastapi import FastAPI, Depends
from pydantic import BaseModel
app = FastAPI()
class Product(BaseModel):
name: str
price: float
stock: int
@app.post("/products")
async def create_product(product: Product):
# 存入数据库
return {"id": 1, **product.dict()}
就这么几行代码。自动参数校验、自动生成 OpenAPI 文档、类型提示还贼好用。
再看我们技术负责人那边,还在跟 Maven 依赖苦苦纠缠。
“Spring Boot 搞得咋样了?”我故意贱兮兮地问,努力憋住不笑。
“依赖……一堆破依赖。”他头都没抬,咬牙切齿地回了一句。
那会儿我真觉得,选 Python 简直是英明神武。
紧接着我们开始压测,好戏这才开场。
首轮压测结果(1000 并发用户)
FastAPI:
Spring Boot:
FastAPI 完胜!我当时都想提前找他要牛排馆的预约链接了。
但当我们模拟真实生产流量压测(5000 并发用户)时,剧情彻底反转:
FastAPI:
Spring Boot:
等等,这什么情况?
高并发真实流量下,Spring Boot 居然扛住了更多请求,而且表现更稳定、更可预测。
我的自信心,第一次开始动摇。
开发体验对比
FastAPI 写起来简直是享受:
Spring Boot 则显得笨重无比:
代码行数对比
实现同一个“根据 ID 查询商品”功能:
FastAPI(8 行):
@router.get("/products/{product_id}")
async def get_product(
product_id: int,
db: Session = Depends(get_db)
) -> ProductResponse:
product = await db.get(Product, product_id)
if not product:
raise HTTPException(404, "Not found")
return product
Spring Boot(7 行,但是前期需要一堆配置):
@GetMapping("/products/{id}")
public ResponseEntity<ProductResponse> getProduct(
@PathVariable Long id
) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
当时我没意识到的是:Spring Boot 那些“前期配置”,其实是在为后续开发省大钱。
我们团队甚至因为 SQLAlchemy 和 Spring Data JPA 吵出了“教派之争”。
简单查询?两者半斤八两。
FastAPI:
async def get_products(db: Session, category: str):
return await db.execute(
select(Product).where(Product.category == category)
).scalars().all()
Spring Boot:
public List<Product> getProducts(String category) {
return productRepository.findByCategory(category);
}
但遇到多表关联的复杂查询,Spring Boot 直接拉开差距。
FastAPI 得这么写:
async def get_products_with_reviews(db: Session):
stmt = (
select(Product)
.options(selectinload(Product.reviews))
.options(selectinload(Product.inventory))
.where(Product.active == True)
)
result = await db.execute(stmt)
return result.scalars().all()
Spring Boot 只需要一个注解查询:
@Query("SELECT p FROM Product p " +
"LEFT JOIN FETCH p.reviews " +
"LEFT JOIN FETCH p.inventory " +
"WHERE p.active = true")
List<Product> findActiveProductsWithDetails();
Spring Boot 的写法更清晰,而且 SQL 语法错误在编译阶段就能被揪出来。
反观 FastAPI?只有运行起来才知道哪里写错了,堪称“运行时火葬场”。
这才是压垮我信心的最后一根稻草。
我们有个复杂的订单处理流程,步骤环环相扣:
任何一步失败,所有操作都要回滚。
Spring Boot 实现起来,只需要一个注解:
@Transactional
public Order processOrder(OrderRequest request) {
// 检查库存
checkInventory(request.getProducts());
// 创建订单
Order order = orderRepository.save(new Order(request));
// 处理支付
paymentService.charge(request.getPaymentInfo());
// 更新库存
inventoryService.decrementStock(request.getProducts());
// 任何步骤抛异常,自动回滚
return order;
}
一个 @Transactional 注解搞定一切,数据库级别的事务保障,全程自动回滚。
再看 FastAPI 的实现:
async def process_order(
request: OrderRequest,
db: AsyncSession
) -> Order:
asyncwith db.begin():
try:
# 检查库存
await check_inventory(db, request.products)
# 创建订单
order = Order(**request.dict())
db.add(order)
await db.flush()
# 处理支付
await payment_service.charge(request.payment_info)
# 更新库存
await inventory_service.decrement_stock(
db, request.products
)
await db.commit()
return order
except Exception as e:
await db.rollback()
raise HTTPException(500, str(e))
代码逻辑更直观,控制权也更在手,但出错的概率也呈指数级上升。
而我们,还真栽了两次跟头。
第一次:嵌套事务没处理好,导致库存数据对不上,亏了 3000 元才发现问题。
第二次:支付成功了,但库存更新失败,最后手动核对了 400 多笔订单才平账。
Spring Boot 的 @Transactional 从头到尾没掉过链子,稳得一批。
FastAPI 的异步特性,纸面参数看着香到不行。
但实际用起来?简直是地狱难度。
核心问题:不是所有代码都能异步化。我们对接的支付服务商 SDK 是阻塞式的,公司内部的老 API 也是阻塞式的。
在 Python 里混合异步和同步代码,简直是灾难现场:
@router.post("/orders")
async def create_order(order: OrderRequest):
# 这行是异步的,没问题
product = await db.get_product(order.product_id)
# 这行是阻塞的——直接卡死整个事件循环
payment_result = payment_sdk.charge(order.amount)
# 回到异步逻辑
await db.save_order(order)
那个阻塞的支付调用,直接让 FastAPI 的异步优势荡然无存。
所谓的“解决方案”,是用 run_in_executor 手动管理线程池:
payment_result = await loop.run_in_executor(
None,
payment_sdk.charge,
order.amount
)
这下好了,原本追求的简洁优雅全没了,还要手动维护线程池。
再看 Spring Boot?
// 直接写就行,啥都不用额外配置
PaymentResult result = paymentService.charge(order.getAmount());
简单粗暴,还不影响性能。Java 21 的虚拟线程更是把这个优势拉满。
有时候啊,“无聊”的技术反而更靠谱。
生产环境出问题是难免的,关键是能不能快速定位、快速解决。
FastAPI 报错信息
Traceback (most recent call last):
File "/app/main.py", line 234, in process_payment
result = await payment_service.charge(amount)
File "/app/services/payment.py", line 89, in charge
response = await self.client.post(url, data=data)
AttributeError: 'NoneType' object has no attribute 'post'
self.client 为啥变成 None 了?是初始化的时候没赋值?还是依赖注入出问题了?鬼才知道。
在生产环境调试异步 Python 代码?祝你好运。堆栈信息跟天书一样,完全找不到问题根源。
Spring Boot 报错信息
org.springframework.transaction.CannotCreateTransactionException:
Could not open JPA EntityManager for transaction
Caused by: java.sql.SQLException: Connection pool exhausted
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:197)
at com.myapp.service.OrderService.processOrder(OrderService.java:45)
清晰明了,直击要害:连接池耗尽了。接下来该怎么优化,一目了然。
Spring Boot 的报错信息算不上完美,但至少能帮你定位问题;FastAPI 的异步报错?堪比考古现场,挖半天都不一定能找到线索。
FastAPI 的依赖注入,一开始用着觉得清爽无比:
async def get_db():
async with AsyncSession() as session:
yield session
@app.get("/products")
async def get_products(db: Session = Depends(get_db)):
return await db.execute(select(Product)).scalars().all()
但一旦遇到嵌套依赖、多作用域管理、生命周期控制这些复杂场景,直接当场翻车。
Spring 的依赖注入虽然写起来啰嗦,但胜在稳如泰山:
@Service
publicclass ProductService {
privatefinal ProductRepository repository;
privatefinal CacheManager cacheManager;
privatefinal EventPublisher publisher;
// 构造器注入——依赖关系一目了然
public ProductService(
ProductRepository repository,
CacheManager cacheManager,
EventPublisher publisher
) {
this.repository = repository;
this.cacheManager = cacheManager;
this.publisher = publisher;
}
}
所有依赖都明明白白列出来,没有黑箱魔法,单元测试也好写,生命周期也能精准控制。
FastAPI 的依赖注入,小项目用着爽;但面对复杂的企业级应用,Spring 的方案才是真正能扛事儿的。
经过六个月的真实流量考验,最终的数据终于出炉,这些数字才是硬道理。
可用性(Uptime)
FastAPI 的宕机时间,主要来自三次死活查不到的内存泄漏,还有一次异步代码死锁。
高并发下 95 分位响应时间
内存占用
平均 CPU 使用率
部署次数
凌晨 3 点被叫醒次数(生产故障)
最后这个指标,才是最戳我心窝子的。
所有人都说 Python 开发快,前一个月我也信了。
但接下来的日子,我才懂什么叫“出来混,迟早要还”。
前期省下来的时间,后期全得加倍奉还,甚至要赔上三倍的精力。
为啥会这样?
开头的“快”,最终变成了长期的“慢”。
Spring Security 这玩意儿,既是天使也是魔鬼。
配置起来繁琐到让人崩溃,文档更是稀烂。但一旦配置完成,那安全感直接拉满。
@Configuration
@EnableWebSecurity
publicclass SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
}
一行配置,就能搞定:
再看 FastAPI?所有安全功能都得自己造轮子。
我们用 fastapi-users + python-jose 实现 JWT 认证,自己写限流逻辑,自己搭 CSRF 防护。
功能是实现了,但很多边缘场景直接被我们忽略了。比如会话固定攻击,我们直到安全审计时才知道还有这种攻击方式。
Spring Security 是经过几十年实战检验的;而我们的 FastAPI 安全方案?不过是三个毛头小子拍脑袋想出来的。
安全审计结果
这个结果,真的让我颜面尽失。
最后来算笔经济账,钱不会说谎。
基础设施成本(每月)
没想到吧?Spring Boot 反而更省钱。
开发者时间成本
按小时计算:
这个“轻量级”的 Python 框架,居然让我们多花了两倍的人力成本。
机会成本
我们花了 3 周时间调试 FastAPI 的异步 Bug,而这些时间,本来可以用来开发新功能——这部分损失,根本无法用金钱衡量。
什么时候该用 FastAPI?
什么时候该用 Spring Boot?
终极结论:没有最好的框架,只有最适合的框架。
对我们的业务场景来说,Spring Boot 就是更好的选择。不是因为它本身“更牛”,而是因为它更契合我们的需求。
FastAPI 不是垃圾,它在自己的赛道上非常能打;Spring Boot 也不是银弹,小项目用它纯属杀鸡用牛刀。
框架之争本身就是个伪命题。真正的高手,从来不会纠结于工具,而是会根据需求,选择最适合的武器。
六个月后,我们的技术栈变成了这样: