10-Java工程师的Python第十课-实战项目

6 阅读8分钟

用Python重写Java项目:学以致用的综合实战

摘要:本文通过一个具体的Spring Boot用户管理API项目,用Python(FastAPI)重写,对比两种实现的异同。


写在前面

前面九篇文章我们学习了Java和Python的语法差异、数据结构、函数特性、OOP、异常处理、模块管理、常用库、Web框架和并发编程。

这一课,我们用一个完整的项目来综合运用。

我们将构建一个用户管理系统API,包含:

  • 用户CRUD操作
  • 分页查询
  • 数据验证
  • 异常处理
  • 单元测试

一、项目需求

1.1 Java Spring Boot版本

// User.java - 实体类
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer age;

    private Boolean active = true;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    // getters, setters, constructors
}

// UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    List<User> findByAgeGreaterThan(Integer age);
    Page<User> findByActiveTrue(Pageable pageable);
}

// UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;

    public User create(UserRequest request) {
        if (userRepository.findByEmail(request.getEmail()).isPresent()) {
            throw new BusinessException("EMAIL_EXISTS", "邮箱已存在");
        }
        User user = new User();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        user.setAge(request.getAge());
        user.setActive(true);
        user.setCreatedAt(LocalDateTime.now());
        return userRepository.save(user);
    }

    public User getById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new BusinessException("USER_NOT_FOUND", "用户不存在"));
    }

    public Page<User> list(int page, int size) {
        return userRepository.findByActiveTrue(PageRequest.of(page, size));
    }

    public User update(Long id, UserRequest request) {
        User user = getById(id);
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        user.setAge(request.getAge());
        user.setUpdatedAt(LocalDateTime.now());
        return userRepository.save(user);
    }

    public void delete(Long id) {
        User user = getById(id);
        user.setActive(false);
        userRepository.save(user);
    }
}

// UserController.java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @PostMapping
    public ResponseEntity<User> create(@Valid @RequestBody UserRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(request));
    }

    @GetMapping("/{id}")
    public User get(@PathVariable Long id) {
        return userService.getById(id);
    }

    @GetMapping
    public Page<User> list(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        return userService.list(page, size);
    }

    @PutMapping("/{id}")
    public User update(@PathVariable Long id, @Valid @RequestBody UserRequest request) {
        return userService.update(id, request);
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        userService.delete(id);
    }
}

// BusinessException.java
public class BusinessException extends RuntimeException {
    private final String code;
    private final String message;

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }
}

// GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public Result handleBusiness(BusinessException e) {
        return Result.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidation(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors()
            .stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.joining(", "));
        return Result.error("VALIDATION_ERROR", message);
    }
}

二、Python FastAPI版本

2.1 项目结构

python_user_api/
├── main.py              # 应用入口
├── models.py            # Pydantic模型
├── database.py          # 数据库配置
├── schemas.py           # SQLAlchemy模型
├── crud.py              # CRUD操作
├── routers/
│   ├── __init__.py
│   └── users.py         # 用户路由
├── exceptions.py        # 自定义异常
├── dependencies.py      # 依赖注入
└── tests/
    ├── __init__.py
    └── test_users.py    # 单元测试

2.2 安装依赖

pip install fastapi uvicorn sqlalchemy pydantic pytest httpx

2.3 核心代码

# exceptions.py - 自定义异常
class BusinessException(Exception):
    def __init__(self, code: str, message: str):
        self.code = code
        self.message = message

class UserNotFoundException(BusinessException):
    def __init__(self):
        super().__init__("USER_NOT_FOUND", "用户不存在")

class EmailExistsException(BusinessException):
    def __init__(self):
        super().__init__("EMAIL_EXISTS", "邮箱已存在")
# models.py - Pydantic模型(请求/响应)
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime

class UserBase(BaseModel):
    email: EmailStr
    name: str = Field(..., min_length=1, max_length=100)
    age: int = Field(..., ge=0, le=150)

class UserCreate(UserBase):
    pass

class UserUpdate(BaseModel):
    email: Optional[EmailStr] = None
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    age: Optional[int] = Field(None, ge=0, le=150)

class UserResponse(UserBase):
    id: int
    active: bool
    created_at: datetime
    updated_at: Optional[datetime] = None

    class Config:
        from_attributes = True

class PageResponse(BaseModel):
    items: list[UserResponse]
    total: int
    page: int
    size: int
    pages: int
# database.py - 数据库配置
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "sqlite:///./users.db"

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
# schemas.py - SQLAlchemy模型
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.sql import func
from database import Base

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, nullable=False, index=True)
    name = Column(String, nullable=False)
    age = Column(Integer, nullable=False)
    active = Column(Boolean, default=True)
    created_at = Column(DateTime, server_default=func.now())
    updated_at = Column(DateTime, onupdate=func.now())
# crud.py - CRUD操作
from sqlalchemy.orm import Session
from sqlalchemy import and_
from typing import Optional, List
import math

from schemas import User, UserCreate, UserUpdate

def get_user(db: Session, user_id: int) -> Optional[User]:
    return db.query(User).filter(
        and_(User.id == user_id, User.active == True)
    ).first()

def get_user_by_email(db: Session, email: str) -> Optional[User]:
    return db.query(User).filter(User.email == email).first()

def get_users(db: Session, page: int = 0, size: int = 10) -> tuple[List[User], int]:
    query = db.query(User).filter(User.active == True)
    total = query.count()
    users = query.offset(page * size).limit(size).all()
    return users, total

def create_user(db: Session, user: UserCreate) -> User:
    db_user = User(
        email=user.email,
        name=user.name,
        age=user.age,
        active=True
    )
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user

def update_user(db: Session, user_id: int, user: UserUpdate) -> User:
    db_user = get_user(db, user_id)
    if db_user is None:
        return None

    update_data = user.model_dump(exclude_unset=True)
    for key, value in update_data.items():
        setattr(db_user, key, value)

    db.commit()
    db.refresh(db_user)
    return db_user

def delete_user(db: Session, user_id: int) -> bool:
    db_user = get_user(db, user_id)
    if db_user is None:
        return False
    db_user.active = False
    db.commit()
    return True
# dependencies.py - 依赖注入
from sqlalchemy.orm import Session
from database import get_db

def get_user_service(db: Session = Depends(get_db)):
    return UserService(db)

class UserService:
    def __init__(self, db: Session):
        self.db = db

    def create(self, user_data: UserCreate):
        from crud import get_user_by_email, create_user
        if get_user_by_email(self.db, user_data.email):
            raise EmailExistsException()
        return create_user(self.db, user_data)

    def get_by_id(self, user_id: int):
        from crud import get_user
        user = get_user(self.db, user_id)
        if not user:
            raise UserNotFoundException()
        return user

    def list(self, page: int, size: int):
        from crud import get_users
        users, total = get_users(self.db, page, size)
        pages = math.ceil(total / size) if size > 0 else 0
        return {
            "items": users,
            "total": total,
            "page": page,
            "size": size,
            "pages": pages
        }

    def update(self, user_id: int, user_data: UserUpdate):
        from crud import update_user
        user = update_user(self.db, user_id, user_data)
        if not user:
            raise UserNotFoundException()
        return user

    def delete(self, user_id: int):
        from crud import delete_user
        if not delete_user(self.db, user_id):
            raise UserNotFoundException()
# routers/users.py - 用户路由
from fastapi import APIRouter, Depends, HTTPException, Query
from typing import List

from models import UserCreate, UserUpdate, UserResponse, PageResponse
from dependencies import UserService, get_user_service

router = APIRouter(prefix="/api/users", tags=["users"])

@router.post("", response_model=UserResponse, status_code=201)
def create_user(
    user_data: UserCreate,
    service: UserService = Depends(get_user_service)
):
    try:
        return service.create(user_data)
    except EmailExistsException as e:
        raise HTTPException(status_code=400, detail={"code": e.code, "message": e.message})

@router.get("/{user_id}", response_model=UserResponse)
def get_user(
    user_id: int,
    service: UserService = Depends(get_user_service)
):
    try:
        return service.get_by_id(user_id)
    except UserNotFoundException as e:
        raise HTTPException(status_code=404, detail={"code": e.code, "message": e.message})

@router.get("", response_model=PageResponse)
def list_users(
    page: int = Query(default=0, ge=0),
    size: int = Query(default=10, ge=1, le=100),
    service: UserService = Depends(get_user_service)
):
    return service.list(page, size)

@router.put("/{user_id}", response_model=UserResponse)
def update_user(
    user_id: int,
    user_data: UserUpdate,
    service: UserService = Depends(get_user_service)
):
    try:
        return service.update(user_id, user_data)
    except UserNotFoundException as e:
        raise HTTPException(status_code=404, detail={"code": e.code, "message": e.message})

@router.delete("/{user_id}", status_code=204)
def delete_user(
    user_id: int,
    service: UserService = Depends(get_user_service)
):
    try:
        service.delete(user_id)
    except UserNotFoundException as e:
        raise HTTPException(status_code=404, detail={"code": e.code, "message": e.message})
# main.py - 应用入口
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError

from database import engine, Base
from routers.users import router as users_router
from exceptions import BusinessException

Base.metadata.create_all(bind=engine)

app = FastAPI(title="User Management API", version="1.0.0")

app.include_router(users_router)

@app.exception_handler(BusinessException)
async def handle_business_exception(request: Request, exc: BusinessException):
    return JSONResponse(
        status_code=400,
        content={"code": exc.code, "message": exc.message}
    )

@app.exception_handler(RequestValidationError)
async def handle_validation_exception(request: Request, exc: RequestValidationError):
    errors = exc.errors()
    message = ", ".join([f"{e['loc']}: {e['msg']}" for e in errors])
    return JSONResponse(
        status_code=422,
        content={"code": "VALIDATION_ERROR", "message": message}
    )

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

三、单元测试对比

3.1 Java JUnit测试

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void testCreateUser() {
        UserRequest request = new UserRequest("alice@example.com", "Alice", 30);

        when(userRepository.findByEmail("alice@example.com"))
            .thenReturn(Optional.empty());

        User savedUser = new User(1L, "alice@example.com", "Alice", 30, true);
        when(userRepository.save(any(User.class))).thenReturn(savedUser);

        User result = userService.create(request);

        assertNotNull(result);
        assertEquals("Alice", result.getName());
        verify(userRepository).findByEmail("alice@example.com");
        verify(userRepository).save(any(User.class));
    }

    @Test
    void testGetUserNotFound() {
        when(userRepository.findById(999L)).thenReturn(Optional.empty());

        BusinessException ex = assertThrows(
            BusinessException.class,
            () -> userService.getById(999L)
        );

        assertEquals("USER_NOT_FOUND", ex.getCode());
    }
}

3.2 Python pytest测试

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

from main import app
from database import Base, get_db
from schemas import User
from crud import create_user, get_user_by_email

# 测试数据库
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(SQLALCHEMY_DATABASE_URL,
    connect_args={"check_same_thread": False},
    poolclass=StaticPool)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def override_get_db():
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db

@pytest.fixture(autouse=True)
def setup_database():
    Base.metadata.create_all(bind=engine)
    yield
    Base.metadata.drop_all(bind=engine)

client = TestClient(app)

class TestUserAPI:
    def test_create_user(self):
        response = client.post("/api/users", json={
            "email": "alice@example.com",
            "name": "Alice",
            "age": 30
        })
        assert response.status_code == 201
        data = response.json()
        assert data["email"] == "alice@example.com"
        assert data["name"] == "Alice"
        assert data["active"] is True

    def test_create_user_duplicate_email(self):
        client.post("/api/users", json={
            "email": "alice@example.com",
            "name": "Alice",
            "age": 30
        })

        response = client.post("/api/users", json={
            "email": "alice@example.com",
            "name": "Bob",
            "age": 25
        })
        assert response.status_code == 400
        assert response.json()["detail"]["code"] == "EMAIL_EXISTS"

    def test_get_user_not_found(self):
        response = client.get("/api/users/999")
        assert response.status_code == 404
        assert response.json()["detail"]["code"] == "USER_NOT_FOUND"

    def test_list_users(self):
        for i in range(5):
            client.post("/api/users", json={
                "email": f"user{i}@example.com",
                "name": f"User{i}",
                "age": 20 + i
            })

        response = client.get("/api/users?page=0&size=2")
        assert response.status_code == 200
        data = response.json()
        assert len(data["items"]) == 2
        assert data["total"] == 5
        assert data["pages"] == 3

    def test_update_user(self):
        create_resp = client.post("/api/users", json={
            "email": "alice@example.com",
            "name": "Alice",
            "age": 30
        })
        user_id = create_resp.json()["id"]

        response = client.put(f"/api/users/{user_id}", json={
            "name": "Alice Updated"
        })
        assert response.status_code == 200
        assert response.json()["name"] == "Alice Updated"

    def test_delete_user(self):
        create_resp = client.post("/api/users", json={
            "email": "alice@example.com",
            "name": "Alice",
            "age": 30
        })
        user_id = create_resp.json()["id"]

        response = client.delete(f"/api/users/{user_id}")
        assert response.status_code == 204

        get_resp = client.get(f"/api/users/{user_id}")
        assert get_resp.status_code == 404

四、运行测试

4.1 运行Python测试

cd python_user_api
pytest tests/ -v

4.2 启动服务器

uvicorn main:app --reload

4.3 API文档

FastAPI自动生成API文档:


五、代码行数对比

模块Java (行)Python (行)
实体/模型~50~25 (Pydantic)
仓库/CRUD~60~80
服务层~50~40
控制器/路由~40~50
异常处理~30~15
测试~60~80
总计~290~290

Python的优势:

  • 无需getter/setter(Pydantic自动生成)
  • 无需编译
  • 测试代码更直观

六、核心差异总结

维度Java Spring BootPython FastAPI
启动方式mvn spring-boot:runuvicorn main:app
数据验证@Valid + 注解Pydantic模型
路由@RequestMapping@router.post()
依赖注入@AutowiredDepends()
ORMJPA/HibernateSQLAlchemy
序列化JacksonPydantic
文档SpringDoc自动生成
测试JUnit + MockMvcpytest + TestClient

七、延伸学习建议

7.1 下一步可以学习的

  1. 数据库迁移

    • Java: Flyway / Liquibase
    • Python: Alembic
  2. 认证授权

    • Java: Spring Security / OAuth2
    • Python: FastAPI-security / JWT
  3. API文档

    • Java: SpringDoc OpenAPI
    • Python: FastAPI内置 + openapi-schema-pydantic
  4. 性能优化

    • Java: JPA二级缓存 / Redis
    • Python: SQLAlchemy缓存 / Redis
  5. 部署

    • Java: JAR + Docker
    • Python: Gunicorn + Docker

7.2 推荐项目

  • 用Django重写(对比全功能框架)
  • 用Flask重写(对比微框架)
  • 添加Redis缓存
  • 添加单元测试覆盖率

八、系列总结

经过十篇文章的学习,你应该已经:

课程核心知识点
01基础语法对比、缩进、注释
02数据结构(list/dict/set vs ArrayList/HashMap)
03函数、lambda、装饰器、生成器
04类、继承、多继承、dataclass
05异常处理、try-except、with语句
06import机制、pip、虚拟环境
07文件IO、JSON、正则、datetime
08Flask/Django/FastAPI框架对比
09GIL、多线程、多进程、asyncio
10实战项目综合运用

恭喜你完成了"Java工程师的Python学习之路"系列!

继续加油。技术的道路永无止境,Java和Python各有优势,两者结合会更加游刃有余。