0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
本文是 调试实战系列 5 篇 中的第 2 篇。
- 各语言调试指南
- 各框架调试实战 ← 当前文章
- 各 IDE 调试完全整理
- 语言×框架故障案例集
- 远程调试实战指南
框架调试的核心
比语言更该先看的,是 框架的执行流程。
- 请求入口(Controller/View/Route)
- 中间件/过滤器
- 数据访问层
- 外部 API 调用
- 渲染/水合
调试就是找出数据在这条流程的哪一步被破坏的工作。由于每个框架的流程结构和惯例都不同,调试的切入点也随之不同。
各框架调试切入点速查
| 项目 | Spring Boot | Django | FastAPI | React | Next.js |
|---|---|---|---|---|---|
| 请求入口 | @Controller/@RestController | View 函数/CBV | Router 函数 | Component render | Server Component / Route Handler |
| 中间件/过滤器 | Filter, Interceptor | Middleware 类 | Middleware (Starlette) | - | middleware.ts |
| 数据访问 | JPA Repository | Django ORM | SQLAlchemy/Tortoise | - | Prisma/Drizzle(服务端) |
| 错误处理 | @ExceptionHandler | Middleware / DRF exception | exception_handler 装饰器 | ErrorBoundary | error.tsx / not-found.tsx |
| 性能分析工具 | JFR, Micrometer | Django Debug Toolbar, py-spy | py-spy, OpenTelemetry | React DevTools Profiler | Next.js build analyzer |
| 调试日志配置 | application.yml logging.level | LOGGING dict in settings.py | logging.basicConfig() | React DevTools | next.config.js logging |
各框架常见的故障类型与工具
| 框架 | 常见故障类型 | 诊断工具 | 核心解决模式 |
|---|---|---|---|
| Spring Boot | N+1 查询、事务边界错误、Bean 循环依赖 | Hibernate SQL 日志, Micrometer, JFR | @EntityGraph、调整 @Transactional 范围 |
| Django | N+1 查询、序列化器校验错误、迁移冲突 | django-debug-toolbar, Silk, assertNumQueries | select_related/prefetch_related、迁移 squash |
| FastAPI | Pydantic 校验错误、异步与同步混用导致阻塞、依赖注入顺序 | py-spy, OpenTelemetry, logging | run_in_executor、Depends 链式设计 |
| React | 无谓重渲染、状态管理复杂度、内存泄漏(漏掉取消订阅) | React DevTools Profiler, why-did-you-render | memo/useMemo、useEffect cleanup |
| Next.js | Hydration mismatch、服务端与客户端边界混淆、缓存策略误解 | 浏览器控制台, Next.js 日志, build analyzer | 明确声明 'use client'、设置 revalidate |
1) Spring Boot
运行与启动调试
# Gradle
./gradlew bootRun
# Maven
./mvnw spring-boot:run
# 以特定 profile 运行
./gradlew bootRun --args='--spring.profiles.active=dev'
远程调试:
JAVA_TOOL_OPTIONS='-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005' ./gradlew bootRun
推荐的断点位置
- Controller 入口:确认请求参数与请求头
- Service 业务分支:确认核心逻辑与条件分支
- Repository 查询之前:确认查询参数与结果
- ExceptionHandler:确认异常原因与响应映射
- Filter/Interceptor:确认认证与权限检查的流程
中间件与拦截器调试
// 用 HandlerInterceptor 追踪请求流程
@Component
public class RequestTracingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 在这里打断点 — 所有请求的第一个入口
String requestId = request.getHeader("X-Request-Id");
MDC.put("requestId", requestId);
log.info("Incoming: {} {} requestId={}", request.getMethod(),
request.getRequestURI(), requestId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
// 在这里打断点 — 响应之前确认异常
if (ex != null) {
log.error("Request failed: requestId={}", MDC.get("requestId"), ex);
}
MDC.clear();
}
}
数据库查询调试(SQL 日志)
# application.yml — 开发与调试时的 SQL 日志配置
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
# 连实际执行的 SQL 参数也一并确认
generate_statistics: true
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE # 输出绑定参数
org.hibernate.stat: DEBUG # 查询统计
org.springframework.transaction: TRACE # 确认事务边界
// N+1 检测:查询次数校验测试
@Test
void shouldNotCauseNPlusOne() {
// given
long queryCountBefore = getQueryCount();
// when
List<Order> orders = orderService.findAllWithItems();
// then
long queryCountAfter = getQueryCount();
assertThat(queryCountAfter - queryCountBefore)
.as("确认是否发生 N+1 查询")
.isLessThanOrEqualTo(2); // SELECT orders + SELECT items
}
性能分析
- Micrometer + Prometheus + Grafana:观测请求、错误与延迟
- JFR/async-profiler:CPU 与 alloc 瓶颈
- SQL 日志 + 执行计划:确认 N+1 与慢查询
2) Django
运行
python manage.py runserver
# 以特定端口运行
python manage.py runserver 0.0.0.0:8080
# 用 shell_plus 做交互式调试
python manage.py shell_plus --print-sql
断点
def create_order(request):
breakpoint() # 在 View 入口确认 request 对象
serializer = OrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# ...
推荐位置:
- View 入口:确认 request 数据
- Serializer 校验:确认 validated_data
- ORM 查询前后:确认 QuerySet 与结果
- Middleware 认证与会话处理:追踪认证流程
中间件调试
# 用自定义中间件追踪请求与响应
class RequestDebugMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 请求进入 — 在这里打断点
import time
start = time.time()
response = self.get_response(request)
# 响应之前 — 检测慢请求
duration = time.time() - start
if duration > 1.0: # 超过 1 秒则告警
import logging
logger = logging.getLogger(__name__)
logger.warning(
f"Slow request: {request.method} {request.path} "
f"took {duration:.2f}s"
)
return response
数据库查询调试
# settings.py — SQL 日志配置
LOGGING = {
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG', # 输出所有 SQL 查询
'handlers': ['console'],
},
},
}
# 在测试中校验查询次数 — 防止 N+1
from django.test.utils import override_settings
class OrderQueryTest(TestCase):
def test_no_n_plus_one(self):
# 创建 10 个订单
create_test_orders(10)
# 校验查询次数上限
with self.assertNumQueries(2): # 只允许 orders + items 两次查询
orders = Order.objects.select_related('user') \
.prefetch_related('items').all()
# 必须对结果求值,查询才会真正执行
list(orders)
N+1 的应对:
# 坏例子:产生 N+1
orders = Order.objects.all()
for order in orders:
print(order.user.name) # 每个订单都触发一次 user 查询
print(order.items.count()) # 每个订单都触发一次 items 查询
# 好例子:用两次查询解决
orders = Order.objects.select_related('user').prefetch_related('items').all()
for order in orders:
print(order.user.name) # 从缓存读取
print(order.items.count()) # 从缓存读取
性能分析
- Django Debug Toolbar:查询次数、查询耗时、模板渲染、信号
- Silk:按请求做性能分析,慢视图排行
- py-spy:以 gunicorn worker 为单位做 CPU 采样
3) FastAPI
运行
# 开发模式(自动重载)
uvicorn app.main:app --reload --log-level debug
# 生产模式
uvicorn app.main:app --workers 4 --host 0.0.0.0 --port 8000
断点推荐
- Router 函数:确认请求参数与依赖注入结果
- Dependency injection 函数:确认认证、DB 会话等公共依赖
- 外部 API 调用函数:确认请求与响应的负载
- 异常处理器:确认错误响应的映射
中间件调试
from starlette.middleware.base import BaseHTTPMiddleware
import time
import logging
logger = logging.getLogger(__name__)
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# 在这里打断点 — 请求入口
start = time.time()
request_id = request.headers.get("X-Request-Id", "unknown")
try:
response = await call_next(request)
except Exception as exc:
# 追踪中间件里没有被捕获的异常
logger.error(f"Unhandled error: {request_id} {exc}", exc_info=True)
raise
duration = time.time() - start
response.headers["X-Response-Time"] = f"{duration:.3f}s"
if duration > 1.0:
logger.warning(
f"Slow: {request.method} {request.url.path} "
f"{duration:.2f}s requestId={request_id}"
)
return response
异步与同步混用问题的调试
# 坏例子:在 async 函数里调用同步 I/O → 阻塞事件循环
@app.get("/users/{user_id}")
async def get_user(user_id: int):
# requests.get 是同步调用 → 阻塞整个服务
response = requests.get(f"http://external-api/users/{user_id}")
return response.json()
# 好例子 1:使用 httpx 异步客户端
@app.get("/users/{user_id}")
async def get_user(user_id: int):
async with httpx.AsyncClient() as client:
response = await client.get(f"http://external-api/users/{user_id}")
return response.json()
# 好例子 2:把同步代码用 run_in_executor 包起来
import asyncio
@app.get("/report")
async def generate_report():
loop = asyncio.get_event_loop()
# 在线程池中执行 CPU-bound 任务
result = await loop.run_in_executor(None, heavy_computation)
return {"result": result}
数据库查询调试
# 启用 SQLAlchemy 的 SQL 日志
import logging
logging.getLogger('sqlalchemy.engine').setLevel(logging.DEBUG)
# 或者在 create_engine 里直接设置
from sqlalchemy import create_engine
engine = create_engine(DATABASE_URL, echo=True) # echo=True → 输出 SQL
性能分析
# 用 py-spy 对 PID 采样
py-spy top --pid <PID>
# 生成 flamegraph
py-spy record -o profile.svg --pid <PID>
- OpenTelemetry:串联 endpoint 与外部调用的 trace
- pydantic validation 成本 检查:在大 payload 场景下确认 Pydantic V2 的性能改进
4) React
运行
npm run dev
# 或者
npx vite # Vite 项目
断点策略
- 事件处理器(onClick/onSubmit):确认用户交互发生的时点
- 状态更新之前(setState/useState):追踪状态变化的原因
- useEffect 依赖边界:确认无限循环与遗漏的依赖
- API 响应解析处:确认服务端数据的形态
重渲染调试
// 用 why-did-you-render 检测无谓重渲染
// wdyr.ts(在应用入口之前 import)
import React from 'react'
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React, {
trackAllPureComponents: true,
logOnDifferentValues: true,
})
}
// 在要监视的组件上做标记
const OrderList: React.FC<Props> = ({ orders }) => {
return (
<ul>
{orders.map((order) => (
<OrderItem key={order.id} order={order} />
))}
</ul>
)
}
OrderList.whyDidYouRender = true
// useEffect 无限循环调试
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null)
// 坏例子:每次渲染都有新对象进入依赖数组
// const options = { include: ['orders'] } // 每次渲染都是新引用
// useEffect(() => { fetchUser(userId, options) }, [userId, options])
// 好例子:用 useMemo 稳定引用
const options = useMemo(() => ({ include: ['orders'] }), [])
useEffect(() => {
fetchUser(userId, options).then(setUser)
}, [userId, options]) // options 稳定,因此避免了无限循环
}
性能分析
- React DevTools Profiler:可视化每个组件的渲染次数与耗时
- why-did-you-render:把重渲染的原因输出到控制台
- Web Vitals(LCP/INP/CLS)追踪:监控用户体感性能
检查要点:
- key 的稳定性:用唯一 ID 代替数组 index
- memo/useMemo/useCallback:不要滥用,先测量再用在真正需要的地方
- context 过度传播:context value 一变,所有订阅者都会重渲染
5) Next.js
运行
# 开发模式
npm run dev
# 生产构建 + 运行
npm run build && npm run start
# 用 Turbopack 启动更快的开发服务器
npx next dev --turbopack
经常出问题的点
- 服务端与客户端渲染不一致(hydration mismatch)
- API route 吞掉错误:没有 try/catch 就返回 500
- Edge 与 Node.js runtime 的差异:用了 Edge 不支持的 API
- 图片与缓存策略的误解:ISR revalidate 配置失误
App Router 专用调试
// Server Component 调试 — console.log 输出到服务端终端
// app/orders/page.tsx
export default async function OrdersPage() {
// 只在服务端执行 — 浏览器控制台看不到
console.log('[SERVER] Fetching orders...')
const orders = await db.order.findMany({
include: { user: true, items: true },
})
console.log(`[SERVER] Found ${orders.length} orders`)
return <OrderList orders={orders} />
}
// 漏写 'use client' 就会报错 — 服务端组件里不能使用 hook
// "useState is not a function" 错误的根源
'use client' // 必须写在文件最顶部
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
// Route Handler (app/api/orders/route.ts)
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const status = searchParams.get('status')
console.log('[API] GET /api/orders status=', status)
const orders = await db.order.findMany({
where: status ? { status } : undefined,
})
return NextResponse.json(orders)
} catch (error) {
// 防止错误被吞掉 — 一定要留下日志
console.error('[API] GET /api/orders failed:', error)
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
}
}
Hydration Mismatch 调试
// 常见原因 1:服务端与客户端渲染出不同的值
// 坏例子
function Greeting() {
// Date.now() 在服务端和客户端是不同的值
return <p>Timestamp: {Date.now()}</p>
}
// 好例子:用 suppressHydrationWarning 或 useEffect 处理
function Greeting() {
const [timestamp, setTimestamp] = useState<number | null>(null)
useEffect(() => {
setTimestamp(Date.now())
}, [])
return <p>Timestamp: {timestamp ?? 'Loading...'}</p>
}
断点位置
- App Router 的 server component 数据 fetch:用服务端 console.log 确认
- Route handler(
app/api/.../route.ts):接入 VS Code 调试器 - Client component 的事件处理器:用浏览器 DevTools 打断点
- middleware.ts:运行在 Edge runtime,与 Node.js 调试器是分开的
性能分析
# 分析产物体积
ANALYZE=true npm run build
# 需要在 next.config.js 中配置 @next/bundle-analyzer
- Next build output:确认每个页面与路由的体积(First Load JS)
- Chrome Performance:测量 hydration 的成本
- 服务端日志 + tracing:分离 API 延迟的原因
框架通用调试手册
- 强制请求 ID:从前端到后端再到 DB,用同一个 ID 追踪。在分布式系统里这是必需的。
- 在边界处校验:输入 DTO、外部响应、DB 写入之前。去找数据被破坏的那个点。
- 环境隔离测试:消灭只在 dev 出现的 bug。检查环境变量、密钥、网络的差异。
- 观测优先:先接指标与链路追踪,再谈日志。日志很难检索。
- 沉淀为回归测试:抓到的 bug 用测试固定下来。为了不把同一个 bug 经历两次。
综合调试检查清单
故障初期应对
- 是否准确读过错误信息与堆栈跟踪?
- 是否确认过与最近发布或变更的关联?
- 用同样的输入能否在本地复现?
- 是否用请求 ID 追踪过从前端到后端再到 DB 的完整路径?
断点布置
- 是否在请求入口(Controller/View/Router)设置了断点?
- 是否确认过请求能正常通过中间件、过滤器、拦截器?
- 是否在数据访问层(Repository/ORM)确认过查询结果?
- 是否确认过外部 API 调用的前后?
数据库调试
- 是否启用了 SQL 日志?
- 是否确认过没有发生 N+1 查询?
- 是否确认过慢查询的执行计划(EXPLAIN)?
- 是否确认过事务边界是正确的?
前端(React/Next.js)
- 是否用 React DevTools Profiler 确认过没有无谓重渲染?
- 是否确认过没有 Hydration mismatch 警告?
- 'use client' 与 'use server' 的边界是否正确?
- 是否确认过产物体积并移除了不必要的库?
解决之后的收尾
- 是否把根本原因(root cause)文档化了?
- 是否编写了回归测试?
- 是否补充了监控与告警规则?
- 是否向团队共享了 postmortem?
API 响应时间异常时 — 各框架的头 3 分钟动作
故障发生后的头 3 分钟要集中在原因分类上。下表整理了各框架最该先执行的命令与确认点。
| 框架 | 1 分钟:确认日志 | 2 分钟:确认指标 | 3 分钟:开始性能分析 |
|---|---|---|---|
| Spring Boot | 确认 logging.level.org.hibernate.SQL=DEBUG,以及 Tomcat access log | 确认 /actuator/metrics/http.server.requests 的 p99 | jcmd <PID> JFR.start duration=60s |
| Django | django-debug-toolbar 的 SQL 面板、runserver 控制台日志 | Silk 的按请求响应时间排行 | py-spy top --pid <PID> |
| FastAPI | uvicorn --log-level debug 的输出、slow callback 告警 | 确认 OpenTelemetry trace | py-spy record -o flamegraph.svg --pid <PID> |
| React | 浏览器 Console 错误、Network 面板的响应时间 | React DevTools Profiler 的渲染次数 | Lighthouse Performance 分数 |
| Next.js | 服务端终端的 console.log、浏览器 Console | next build output(First Load JS 体积) | ANALYZE=true next build |
调试工具安装一行命令
# Spring Boot — SQL 日志 + 指标(在 build.gradle 中添加)
# implementation 'io.micrometer:micrometer-registry-prometheus'
# implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
# Django — 一次性安装调试工具
pip install django-debug-toolbar django-silk nplusone
# FastAPI — 性能分析 + 链路追踪
pip install py-spy opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-fastapi
# React — 重渲染分析
npm install -D @welldone-software/why-did-you-render
# Next.js — 产物体积分析
npm install -D @next/bundle-analyzer
结论
框架调试的胜负,取决于“把断点打在哪里”。 只要理解流程并抓住边界点,再复杂的故障也能快速拆解。
问题不是因为框架复杂才产生的,而是因为看不见执行流程才被放大的。
只要能在脑中画出框架的请求生命周期,一个断点就能把 30 分钟的瞎折腾缩短到 3 分钟。
下一篇文章讲各 IDE 调试完全整理。同样的故障,为什么会因为 IDE 配置不同而在诊断速度上相差 10 倍以上,那篇做了整理。 如果想看实战故障案例,可以参考语言×框架故障案例集。
현재 단락 (1/338)
比语言更该先看的,是 **框架的执行流程**。
작성 글자: 0자원문 글자: 12,991자작성 단락: 0/338