Java + Spring 到 Python + FastAPI (一)

Java 程序员越来越被唾弃,我看 BOSS 直聘上有的公司在要求里面明文写不要 Java 程序员,感觉自己受到了侮辱,一怒之下怒了一下,既然如此别怪我心狠手辣用 Python + FastAPI 把之前 Java + Spring 的项目重写一遍。

此文涉及 MySQL、Kafka、Redis 组件和用户、资金、订单模块,整体从 Java 迁移到 Python 所需的知识点。

语言思维转换

Java 的语法很臃肿,如果会 Java 再看其他语言简直降维打击,学起来没什么难度,顶多是底层的一些设计、架构不一样。

Python 是动态语言,比如

Python 复制代码
# 传统 Python
def add(a, b):
    return a + b

add(1, 2)       # 结果是 3
add("a", "b")   # 结果是 "ab"

没有类型校验,没有运行前编译,看起来非常不安全。于是有了 Pydantic

Python 复制代码
def add_with_hints(a: int, b: int) -> int:
    return a + b

括号里面是参数,-> int 是返回值,用了 Type Hints,相当于一种提示,让 Pydantic 读取到后进行校验和转换。

列举一个接口的例子,比如 Java

Java 复制代码
// 1. DTO 类 (POJO)
public class CreateUserDTO {
    
    // 2. Validation (javax.validation)
    @NotNull
    @Size(min = 3, max = 50)
    private String username;
    
    @Min(18)
    private Integer age;
    
    // Getters and Setters... (由 Jackson 用于序列化)
}

// 3. Controller (Jackson + Validation)
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserDTO userDTO) {
    // 到了这里,userDTO 已经被 Jackson 解析,并被 Validation 校验过了
    // ...
}

而 Python + FastAPI + Pydantic 是

Python 复制代码
from pydantic import BaseModel, Field
from typing import Optional # 对应 Java 的 Optional 或 null

# 1. Pydantic 模型 (这一个东西 = Java 的 DTO + Validation + Jackson 注解)
class CreateUserDTO(BaseModel):
    # 对应 @NotNull 和 @Size
    username: str = Field(..., min_length=3, max_length=50) 
    
    # 对应 @Min(18) 和 可选 (Integer 而非 int)
    age: Optional[int] = Field(None, ge=18) # ge = Greater than or Equal
    
    # 注意:
    # 1. ... (三个点) 表示这个字段是必填的
    # 2. Field(None, ...) 表示这个字段是可选的 (默认值为 None)


# 2. FastAPI 路由
@app.post("/users")
async def create_user(user_dto: CreateUserDTO):
    # 到了这里,Pydantic 已经自动完成了:
    # 1. 读取 JSON 请求体 (像 Jackson)
    # 2. 按照类型提示 (str, int) 校验和转换数据
    # 3. 运行校验规则 (min_length, ge) (像 javax.validation)
    # 4. 如果校验失败,自动返回一个 422 错误的 JSON 响应
    
    # 你可以直接使用这个对象
    # user_dto.username 
    
    return user_dto

Python 里面的 Optional 和 Java 里面是同一个意思,写法不一样,Python 里面把八大基本类型的包装类也用 Optional 表示。

Python 里没有 Lombok,直接.属性就行。

并发模型

这是 Java 和 Python 最大的不同,Java 是多线程,在 Spring Web 中一个请求就是一个线程(非 WebFlux),Tomcat 里面配置最大线程数就这个作用。线程内是阻塞的,比如查数据库线程就停那了,等待数据完成了继续执行。

Python 用的 asyncio 单线程事件循环。查数据库的时候,线程去执行别的,直到数据拿到了再接着执行。(具体流程后面详细分解)

比如这个接口逻辑

  • 查用户

  • 查商品

  • 写订单

  • 写订单详情

Java 是

Java 复制代码
// 线程 1
public Order createOrder(...) {
    User user = userRepo.findById(...);      // (线程 1 阻塞,等待 5ms)
    Product product = productRepo.findById(...); // (线程 1 阻塞,等待 5ms)
    
    Order order = new Order(...);
    orderRepo.save(order);                   // (线程 1 阻塞,等待 10ms)
    
    OrderDetail detail = new OrderDetail(...);
    detailRepo.save(detail);                 // (线程 1 阻塞,等待 10ms)
    
    return order; // 总耗时:5+5+10+10 = 30ms (线程 1 被独占 30ms)
}

Python 是

Python 复制代码
# 单个事件循环线程
async def create_order(...):
    # 'await':我把控制权交回事件循环
    user = await user_repo.get_by_id(...)      # (I/O 开始,任务暂停)
    
    # ... 某个未来的时刻,事件循环恢复了我的执行 ...
    
    product = await product_repo.get_by_id(...) # (I/O 开始,任务再次暂停)
    
    # ... 再次恢复 ...
    
    order = Order(...)
    await order_repo.save(order)              # (I/O 开始,任务再次暂停)
    
    # ... 再次恢复 ...
    
    detail = OrderDetail(...)
    await detail_repo.save(detail)            # (I/O 开始,任务再次暂停)
    
    # ... 再次恢复 ...
    
    return order # 总耗时:还是 30ms (I/O 总时间)

这导致的第一重大变化是 ThreadLocal 没了,之前用户登录的 token 可以用 AOP 一路带到线程里,在 Python 里面用 FastAPI 的 Depends 解决,后面想说。

第二个是 async 必须全程到底,一个方法用了,所有上下游调用都用,外部的 MySQL、Kafka 这种组件也要用 async 的版本。

那为什么 Python 是单线程的?

主要是 Python 的 RAM 内存管理用的 GLC(Global Interpreter Lock),不像 Java 有复杂的 GC 垃圾回收机制,GLC 在用到对象的引用计数 + 1,没用到 -1,为 0 则清理,为了防止多线程 RAM 内存出问题,有了 GLC。

asyncio 就是用单线程多进程(协程)的方式,提高并发。

进程、线程、协程有什么区别?

打开 Win 的任务管理器,最外层的 Micosoft Edge 就一个进程,括号里的 18 就是 18 的线程,协程一种特殊的线程,可以很快速的在不同任务之间切换,asyncio 里面这么叫。

Python + asyncio 这么搞效率岂不是很低?多核处理器怎么办?

其实 CPU 的速度是远超 IO 的,老早之前单核 CPU 就有时间切片模拟成多核,服务器大都是 IO 密集型,卡在查 MySQL、Kafka、Redis 等,这样其实更高效。

每个线程的启动都要消耗几 M 的 RAM,线程上下文切换也比较耗资源,进程之间效率高得多。

线上真实的多核处理器服务器,会用进程管理器 Gunicorn 根据服务器核心数启动多个 Python 进程,由 Gunicorn 将请求分发。

asyncio 的原理

asyncio 底层有两个组件,Ready QueueWaiting Map。Ready Queue 是可以立即执行的队列,Waiting Map 是异步等待唤醒的任务。

以为一个请求流程为例

Python 复制代码
async def create_order(...):
    # 你的代码从这里开始执行
    user_data = await request.json()  # 假设这是第一个 await (I/O)
    ...

这些代码都先当做任务放到 Ready Queue,直到遇到第一个 await 则暂停,把任务移动到 Waiting Map,然后从 Ready Queue 取下一个任务执行。直到所有的 Ready Queue 都执行完成。

一个是 Queue 一个是 Map,Queue 存的是任务,Map 中存的是任务 + 回调,这样数据回调的时候知道去执行哪个任务。

await 交给操作系统系统 OS 处理,在日常开发中比如付款场景等支付宝回调,在这里类似。

await 告诉操作系统帮我查下 MySQL,有结果了通知我。但不同的操作系统通知方式不一样,Mac UNIX 的 kqueue、Linux 的 epoll 属于Reactor 的就绪通知 ,Win 的 IOPC 属于Proactor 的回调

他俩区别是系统 OS 层的,涉及网卡、数据流那些,Proactor 帮忙把数据流复制了一份,Reactor 则自己去 read() write(),代码层面没有区别,asyncio 已经帮忙封装好了,根据操作系统自动调用。

相关推荐
Seven972 小时前
剑指offer-37、数字在升序数组中出现的次数
java
SimonKing2 小时前
还在为HTML转PDF发愁?再介绍两款工具,为你保驾护航!
java·后端·程序员
龙泉寺天下行走2 小时前
[Powershell入门教程]第4天:模块、脚本编写、错误处理与 .NET 集成
java·服务器·前端
aniden2 小时前
Swagger从入门到实战
java·开发语言·spring
泥嚎泥嚎2 小时前
【Android】给App添加启动画面——SplashScreen
android·java
Java天梯之路2 小时前
09 Java 异常处理
java·后端
玖剹2 小时前
多线程编程:从日志到单例模式全解析
java·linux·c语言·c++·ubuntu·单例模式·策略模式
一 乐2 小时前
社区养老保障|智慧养老|基于springboot+小程序社区养老保障系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·小程序
2401_841495643 小时前
【自然语言处理】基于统计基的句子边界检测算法
人工智能·python·算法·机器学习·自然语言处理·统计学习·句子边界检测算法