一个请求的完整旅程

从浏览器发出 HTTP 请求 → 到 Java 代码逐行执行 → 到数据返回浏览器,每一步都是谁在干活

进程 vs 线程 Tomcat 线程池 AOP 代理真相 JVM 内存快照
① 先搞清楚:进程、线程到底是什么
这是理解后面所有内容的基础——你的 Java 代码"跑在哪"
🏪 类比:一家银行(进程)和里面的柜员(线程)

🏢 进程 = 银行大楼:一座建筑,有大门、柜台、保险箱(内存)。操作系统分配资源的最小单位。

🧑‍💼 线程 = 柜员:楼里工作的人,每个人独立服务一个客户。CPU 调度的最小单位。

📋 一个客户 = 一个 HTTP 请求:走进银行,被分配给一个空闲柜员服务。

💾 大楼保险箱 = 堆内存(Heap):所有柜员共享的区域,存放贵重物品(对象)。

🧰 柜员自己的抽屉 = 栈(Stack):每个柜员私人区域,放自己的工作笔记(局部变量)。

操作系统 (OS) JVM 进程(你的 Java 应用) 堆内存 Heap(共享) 所有线程都能访问的对象 User Order DTO UserService、Controller 实例 Thread-1(Tomcat 工作线程) 自己的栈:局部变量、方法调用栈 Thread-2(另一个请求) 自己的栈:完全独立的执行空间 Thread-3...N(线程池) 最多200个线程同时工作
JVM 进程 = 1座银行大楼。堆=共享保险箱,线程=独立工作的柜员(各有自己的栈)

🔑 三句话记住核心关系

1. 进程是"容器"——你的 `java -jar app.jar` 启动后,操作系统给它一块内存空间,这就是 JVM 进程。

2. 线程是"执行者"——CPU 实际运行的是线程。一个进程内有多个线程,它们共享堆内存,但各自有独立的栈

3. 请求是"任务"——每个 HTTP 请求进来,Tomcat 从线程池取一个线程来处理它。处理完线程归还池子,等待下一个请求。

⚠️ 为什么要反复强调"共享 vs 独立"?

因为 99% 的并发 Bug 都来自这里:多个线程同时读+写堆内存中的同一个对象 → 数据不一致。而栈(局部变量)是线程私有的,天然安全。这就是 synchronized、volatile、ConcurrentHashMap 存在的根本原因。

② 请求从哪进来:浏览器 → 操作系统 → Tomcat
你以为请求直接到了你的 Controller?不,它先穿越了整个网络协议栈
浏览器 点击"查询订单" HTTP 请求 服务器操作系统 ① 网卡接收数据 ② TCP 协议栈 ③ 端口 8080 ④ Socket 缓冲区 Tomcat Connector (NIO) 线程池 (200个线程) T1 T2 T3 T4 T5 DispatcherServlet ↑ T3 被选中处理你的请求 等待队列 满了→ 503 拒绝连接 空闲线程 忙碌线程
请求到达网卡后,Tomcat 的 Connector 从 Socket 缓冲区读取数据,交给线程池中一个空闲线程

Tomcat 线程池 = 你的请求被分配给谁

Spring Boot 内嵌 Tomcat,默认线程池配置:

server.tomcat.threads.max=200(最多200个线程)

server.tomcat.threads.min-spare=10(最少保持10个就绪)

server.tomcat.accept-count=100(等待队列长度,超了返回503)

🏥 类比:医院门诊

🏥 Tomcat = 医院。200个诊室(线程)。

🚶 你(请求)到了大厅,护士分诊台(Connector)给你分配一个空闲诊室(线程)。

⏳ 如果200个诊室全满,你在走廊排队(等待队列,accept-count=100)。

🚫 如果排队也满了 → 医院直接挂"今日满诊"(503 Service Unavailable)。

✅ 你看完病离开,诊室消毒(线程清栈)→ 归还线程池 → 等下一个病人。

面试考点 为什么 Tomcat 用线程池而不是每个请求新建线程? 因为创建/销毁线程开销大(约1ms+系统调用)。线程池复用线程,避免了反复创建。但要注意:每个线程约占 1MB 栈空间,200个线程 = 200MB 仅栈开销。而且线程数不是越多越好——CPU 核心数固定,线程太多会导致频繁上下文切换(CPU 时间片在线程间跳来跳去,反而变慢)。
③ Spring 路由:请求怎么找到你的 Controller
DispatcherServlet 是所有请求的"前台总调度"
HTTP 请求 GET /api/order/123 DispatcherServlet ① doDispatch() ② 查 HandlerMapping ③ 找到目标方法 HandlerMapping(路由表) GET /api/order/{id} → getOrder() POST /api/order → createOrder() GET /api/order/{id} ✅ → 匹配! PUT /api/order/{id} → updateOrder() DELETE /api/order/{id} → deleteOrder() 返回 HandlerExecutionChain = 方法 + 拦截器链
DispatcherServlet 通过 HandlerMapping 查路由表,找到 GET /api/order/{id} 对应的 OrderController.getOrder()

这个路由表是什么时候建的?

Spring 启动时(不是请求来的时候)。Spring 扫描所有 @Controller / @RestController 类,读取 @RequestMapping / @GetMapping 等注解,构建一张 URL → Method 的路由表,存在 HandlerMapping 里。

请求来了就是查表——O(1) 级别的 Map 查找。如果查不到 → 返回 404。

💡 一个关键细节

找到的不是一个单纯的 Java 方法,而是一个 HandlerExecutionChain = 目标方法 + 这条路径上配置的所有拦截器(Interceptor)。所以拦截器在 Controller 方法执行之前就已经可以介入了。

④ AOP 代理拦截:你的代码被"中间人"接管了
这是"概念不真实"的集中爆发区——你以为你在调 Service,其实你在调代理
OrderController orderService.create(order) ⚠️ 注意! 调的不是原始对象 CGLIB 代理对象(代理层) 这个对象是你写的 Service 的"替身"(子类) Spring 启动时自动创建,Controller 注入的是它 @Transactional / @Aspect / 拦截器在这里生效 OrderServiceImpl(真实代码) 真正的业务逻辑在这里执行 @Before 执行 开启数据库事务 @After 执行 提交/回滚事务 代理对象 = "包裹"在真实对象外面的增强层 Spring 在启动时自动创建,你写代码时完全无感
Controller 调用的不是原始 Service 对象,而是 Spring 创建的 CGLIB 代理对象
📁 项目结构
📁 controller
📄 OrderController.java
📁 service
📄 OrderService.java
📄 OrderServiceImpl.java ←
📁 aspect
📄 LogAspect.java
// OrderController.java — 你写的代码 @RestController public class OrderController { // ⚠️ Spring 注入的是"代理对象",不是原始 OrderServiceImpl @Autowired private OrderService orderService; // ← 实际是 CGLIB 代理 @PostMapping("/api/order") public Result createOrder(@RequestBody OrderDTO dto) { // 这一行看起来很普通…… return orderService.create(dto); // ← 实际执行链路很长! } } // ============ 实际执行链路(你看不到,但真实发生)============ // 1. 进入代理对象的 create() 方法 // 2. 代理:检查有没有拦截器/AOP切面 → 有 LogAspect // 3. 代理:执行 @Before(记录入参日志) // 4. 代理:检查 @Transactional → 开启数据库事务(获取连接) // 5. 代理:终于调用到 OrderServiceImpl.create() ← 你写的真正代码 // 6. 执行完毕返回后 → 代理:提交事务 / 或回滚(如果有异常) // 7. 代理:执行 @After(记录返回值日志) // 8. 返回 Result 给 Controller → 返回给浏览器
🎬 类比:明星的经纪人

🌟 你想找明星拍戏 → 你不是直接联系明星本人,而是先联系经纪人

🤝 经纪人(代理)在真正转达给明星之前,会先做很多额外的事:审核合同、谈片酬、确认档期。

🎬 明星真正拍戏 → 这是你的业务逻辑执行。

📝 拍完回来 → 经纪人又做收尾工作:发通稿、结算费用。

代理对象 = 经纪人。真实对象 = 明星。@Before/@After = 经纪人的额外工作。你只管找经纪人,经纪人帮你处理所有附加事务。

高频面试题 为什么 @Transactional 注解在"同一个类内部方法调用"时不生效? 因为内部调用走的是 this.method(),这是直接调用原始对象——绕过了代理。事务(以及所有 AOP 增强)只在通过代理对象调用时才生效。要解决,必须从外部注入自己(`@Autowired private OrderService self;` 然后 `self.method()`),或者使用 AopContext 获取当前代理。
⑤ Service 执行:代码逐行跑起来时,内存发生了什么
这是"概念变真实"的关键环节——每一行代码背后都有内存操作
📁 service
📄 OrderServiceImpl.java ←
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; // ← 堆内存中的单例 @Transactional public Order create(OrderDTO dto) { // 【第1行】dto 在哪?→ 在堆里,Controller 那边 @RequestBody 反序列化创建的 // 局部变量 dto 在栈上,但只是一个"引用"(指针),指向堆中的对象 Order order = new Order(); // 【第2行】new → 堆内存分配空间 order.setUserId(dto.getUserId()); // 【第3行】读写堆中 Order 对象的属性 order.setAmount(dto.getAmount()); order.setStatus("CREATED"); order.setCreateTime(LocalDateTime.now()); // now() 在堆中创建一个时间对象 // 【第4行】orderMapper.insert(order) → 进入 MyBatis(见下一节) orderMapper.insert(order); return order; // 【第5行】返回引用 → 交给代理层 → 最终序列化成 JSON 返回浏览器 } }
create() 方法执行时的 JVM 内存快照 Thread-3 的栈(线程私有) 栈帧 create() dto → 引用(指向堆0x100) order → 引用(指向堆0x200) 栈帧 insert() order → 同一个引用(0x200) 栈帧 setUserId() value = "U001" 局部变量=引用(4/8字节) 方法结束=栈帧弹出(自动回收) 堆内存 Heap(所有线程共享) OrderDTO 对象 @0x100 userId = "U001" amount = 99.00 items = List@0x300 Order 对象 @0x200 ← new 出来的 userId = "U001" amount = 99.00 status = "CREATED" createTime = 2026-06-14T... OrderServiceImpl 单例 (Spring 容器) 引用 引用
栈上只存"引用"(指针),真正的对象数据都在堆中。方法结束=栈帧弹出,但堆中的对象等 GC 回收

逐步解析每一行代码的内存行为

代码行 栈上发生了什么 堆上发生了什么
方法入口 创建栈帧,存 dto 引用 无变化(DTO 对象已存在)
new Order() order 局部变量入栈 ★ 堆中分配一块内存给 Order 对象
setUserId("U001") setUserId 栈帧创建→弹出 堆中 Order.userId 被修改
LocalDateTime.now() now() 栈帧创建→弹出 ★ 堆中又分配一个时间对象
return order create() 栈帧弹出 Order 对象仍存在(被返回引用)
💡 为什么局部变量线程安全?

因为每个线程有自己的栈。Thread-3 的 create() 栈帧里的 order 引用,和 Thread-4 的完全隔离。它们各自指向堆中不同的 Order 对象。所以局部变量永远不会出现并发冲突。

但如果两个线程的引用指向同一个堆对象(比如共享的单例 Service 里的成员变量)——并发问题就来了。

⑥ MyBatis → 数据库:SQL 是怎么执行的
你写的是 XML/注解里的 SQL,MyBatis 替你做了参数绑定和结果映射
Java 侧(JVM 内) ① MyBatis: orderMapper.insert(order) 找 XML 中 id="insert" 的 SQL 模板 ② 参数绑定 #{userId} → "U001" 用 PreparedStatement 防止SQL注入 ③ 从连接池获取 Connection HikariCP(默认)→ ThreadLocal 绑定 ④ JDBC: conn.execute(sql) 通过 TCP 发送到数据库 ⑤ 返回 ResultSet MyBatis 映射成 Java 对象 ⑥ 连接归还连接池 MySQL 数据库(独立进程) 连接管理器 → 分配一个会话线程 SQL 解析器 → 执行计划 存储引擎 InnoDB → 写入磁盘 返回受影响行数 → TCP 回传 TCP/IP MySQL协议
MyBatis 只是"翻译器":把 Java 对象的属性绑定到 SQL 参数,通过 JDBC 发给 MySQL
📮 类比:寄快递

📦 你的 Java 对象 = 快递物品(各种属性的货物)。

📝 MyBatis = 快递员,负责打包:把对象属性填到运单上(SQL 模板的 #{} 位置)。

🚚 JDBC = 运输公司,通过公路(TCP连接)送到目的地(MySQL)。

🏪 MySQL = 收件仓库,拆包→入库(写表)→签收回执。

重要:数据库是另一个进程!Java 和 MySQL 之间的通信是网络请求。所以数据库连接很宝贵——用连接池复用,而不是每次新建。

⚠️ 连接池和线程池是两回事

线程池(Tomcat):200个线程,每个处理一个 HTTP 请求。

连接池(HikariCP):10个数据库连接(默认),被线程借用

当 200 个线程同时查数据库,只有 10 个能拿到连接,其余 190 个排队等待。这就是为什么慢查询会拖垮整个应用——连接被长时间占用,后面的请求全部卡住。

⑦ JVM 内存全景:一张图看懂所有区域
把零散的概念串起来——方法区、堆、栈、程序计数器,每个区域存什么

📦 堆 Heap(共享)

  • 存什么:所有 new 出来的对象、数组
  • 谁访问:所有线程都能访问
  • 回收:由 GC 自动回收
  • 类比:银行保险箱,大家共用
  • 并发风险:★ 最高!多线程同时修改
  • 调参:-Xmx2g(最大堆大小)

🧰 栈 Stack(线程私有)

  • 存什么:局部变量、方法参数、返回地址
  • 谁访问:只有当前线程
  • 回收:方法结束自动弹出
  • 类比:柜员私人抽屉
  • 并发风险:无!天然线程安全
  • 异常:StackOverflowError(递归太深)

📚 方法区 Method Area(共享)

  • 存什么:类信息、常量池、静态变量
  • 谁访问:所有线程共享
  • 回收:类卸载时(很少发生)
  • 类比:银行的规章制度手册
  • JDK8+:实现为 Metaspace(使用物理内存)
  • 注意:static 变量也在这里!并发要小心

📍 程序计数器 PC Register(线程私有)

  • 存什么:当前线程执行到哪行字节码
  • 谁访问:只有当前线程
  • 类比:柜员看到书签——读到第几行了
  • 作用:CPU 时间片用完→线程被挂起→回来后从这行继续
  • 并发风险:无!天然线程安全
  • 大小:极小,几乎不占空间
核心认知 判断并发安全只需要问一个问题:这个数据存在哪个区域?如果是栈/PC → 绝对安全(线程私有)。如果是堆/方法区 → 可能有并发问题(线程共享)。所以局部变量永远安全,而 static 变量单例对象的成员变量共享集合 都需要考虑线程安全。
⑧ 返回链路:数据怎么变成 JSON 回到浏览器
响应不是一步到位的,它沿着来时的路"逆向"走回去
Service return
Order 对象
AOP @After
执行后置通知
事务提交
commit
Controller
包装 Result
HttpMessageConverter
Jackson 序列化
Servlet 容器
写入 Socket
浏览器
渲染 JSON

序列化:Java 对象 → JSON 字符串

Spring MVC 用 Jackson(HttpMessageConverter)把 Java 对象序列化成 JSON。这个过程是递归反射——读取对象所有 getter 方法,拼成 JSON 字符串。

如果你的对象有循环引用(A 引用 B,B 又引用 A)→ StackOverflowError。这就是为什么 @JsonIgnore / @JsonBackReference 存在。

💡 线程归还

响应写完后,Tomcat 的工作线程清空自己的栈(栈帧全部弹出),归还线程池。注意:线程对象本身不销毁,只是回到"空闲"状态等下一个请求。但堆中的对象不会立即回收——要等 GC 发现没人引用了才清理。

⑨ 完整时间线:一个请求的毫秒级旅程
把所有环节串成一条时间轴,感受真实的时间消耗
0ms — 浏览器发出请求

用户点击按钮,浏览器构建 HTTP 请求(DNS 解析→TCP 三次握手→发送数据)

耗时:10-50ms(网络延迟,取决于距离)

50ms — 到达服务器网卡

操作系统网卡接收数据 → TCP 协议栈重组 → 放入 8080 端口的 Socket 缓冲区

耗时:<1ms

51ms — Tomcat Connector 读取

NIO Selector 检测到数据可读 → 分配给 Thread-3 → 解析成 HttpServletRequest

耗时:1-2ms

53ms — DispatcherServlet 路由

查 HandlerMapping → 找到 OrderController.createOrder → 查到拦截器链

耗时:<1ms(Map 查找)

54ms — 拦截器 preHandle

执行 Interceptor.preHandle() → 鉴权、日志、限流检查

耗时:1-5ms(取决于逻辑)

59ms — Controller 方法执行

调用 orderService.create(dto) → 进入代理对象

耗时:<1ms

60ms — AOP @Before + 事务开启

LogAspect 记录入参 → @Transactional 从连接池获取 Connection → setAutoCommit(false)

耗时:1-3ms(连接池获取连接)

63ms — Service 业务逻辑

new Order() → set 属性 → 各种业务校验 → 调用 Mapper

耗时:1-5ms

68ms — MyBatis 执行 SQL

参数绑定 → JDBC execute → MySQL 接收 → 解析 → InnoDB 执行 → 返回结果

耗时:5-50ms(★ 通常是最大耗时点!索引差可能 100ms+)

118ms — 事务提交 + AOP @After

commit → 连接归还连接池 → LogAspect 记录返回值

耗时:2-5ms

123ms — 序列化 + 返回

Jackson 把 Result 对象序列化成 JSON → 写入 Socket → 操作系统发回浏览器

耗时:1-5ms(大对象序列化慢)

128ms — 浏览器渲染

浏览器收到 JSON → 前端框架(Vue/React)更新 DOM → 用户看到结果

耗时:10-30ms

~150ms — 线程归还

Thread-3 栈帧弹出 → 线程归还 Tomcat 线程池 → 等待下一个请求

整个过程线程被占用约 75ms

📊 一个请求约 150ms,其中数据库占 50ms(33%)

这就是为什么SQL 优化是性能调优最有效的手段——它能砍掉最大的一块耗时。其次是减少网络往返(批量查询 vs 循环查询)。

200 个线程 × 每个请求 75ms = 理论最大 QPS ≈ 2666。如果慢查询把 75ms 变成 500ms → QPS 暴跌到 400。

⑩ 高频疑问:你可能还困惑的问题
把最容易被"填鸭"的概念彻底讲清楚
Q1:Controller、Service、Mapper 这些 Bean 是什么时候创建的?每个请求会 new 吗?

启动时创建一次,之后永远复用。Spring 在应用启动时扫描 @Component/@Service/@Repository 等注解,对每个类 new 一个实例(默认单例),放进 IoC 容器。之后所有请求共用这一个实例。

所以 Controller 和 Service 是线程不安全的——200 个线程同时访问同一个 Service 实例。这也是为什么 Service 里不能有成员变量存请求数据(除非用 ThreadLocal)。正确做法:请求数据用方法参数传递(局部变量,在栈上,天然安全)。

🏦 银行柜台类比 — 一看就懂

Service 单例 = 银行柜员:整个银行只有这 1 个柜员(Spring 启动时就上岗了,不会每个顾客来都 new 一个柜员)。

成员变量 = 柜员的私人笔记本:如果柜员把顾客 A 的信息写在笔记本上,顾客 B 来了又写在同一页 → 💥 数据串了!

局部变量 / new 对象 = 顾客的纸条 + 表单:每个顾客自带纸条(方法参数),柜员给每张表单独填写(new Order()),填完这张表跟下一张完全无关。

200 个并发请求 = 200 个人排队:都找同一个柜员,但每人拿自己的纸条和表单,互不影响——前提是柜员没有用那个共享笔记本!

图:一次请求执行时的 JVM 内存全景 — "单例在哪?new的对象在哪?"
🧠 JVM 内存全景:Thread-3 正在执行 createOrder() 方法 📚 栈区域(Stack)— 每个线程独立一份 方法调用 → 压入栈帧 → 方法结束 → 弹出销毁 Thread-1 栈帧 ├─ dto (OrderDTO) │ 引用→堆上 Order-A ├─ order (Order) │ 引用→堆上 Order-A └─ vo (OrderVO) 引用→堆上 VO-A ← 已在执行中 ✅ 局部变量:线程隔离! Thread-2 碰不到这些! Thread-2 栈帧 ├─ dto (OrderDTO) │ 引用→堆上 Order-B ├─ order (Order) │ 引用→堆上 Order-B └─ vo (OrderVO) 引用→堆上 VO-B ✅ 也是独立的! 🎯 Thread-3 栈帧(当前正在执行) ① Controller.createOrder() httpRequest → Jackson反序列化 → dto 参数dto = OrderDTO{userId:"u-003", ...} 💬 栈上的引用 → 指向堆上的 DTO-3对象 ② Service.createOrder(dto) 参数dto ← 从Controller传进来(栈上) Order order = new Order() ← 新建! 局部变量order = 引用 → 堆上新建的 Order-3 ③ Mapper.insert(order) 用的是 自己的order(Order-3) Mapper也是单例但无状态 → 安全✅ 📦 堆区域(Heap)— 所有线程共享 对象都在这里!单例只有1份,new出来的每次新1份 🔴 单例 OrderService(唯一!) Spring启动时创建,全局只有1个实例 orderMapper (成员变量) 类型: OrderMapper | 引用→Mapper代理对象 ✅ 无状态安全 ❌ currentUserId (危险!) 如果写了这里 → 所有线程共享! 3个线程共用这个! 🟢单例 OrderMapper(MyBatis代理) SqlSession + JDBC模板 | 也只有1个 内部从 DataSource 借连接 → 用完归还 ✅ 无状态 → 线程安全 🟡 每次 new 创建的新对象(每个请求各一份) Thread-1 的对象 DTO-1 {userId:"u-001"} Order-1 {id:null} VO-1 {} ✅ 只有Thread-1能访问 Thread-2 的对象 DTO-2 {userId:"u-002"} Order-2 {id:null} VO-2 {} ✅ 只有Thread-2能访问 🎯 Thread-3 的对象(当前) DTO-3 {userId:"u-003"} Order-3 {id:null, userId:"u-003"} VO-3 {orderId:null} ⭐ 这次请求专属! 请求结束→变垃圾→GC回收 new Order()在这里!
东西 在哪里? 几个? 谁碰得到? 安全?
Service 实例 📦 堆 只有 1 个 所有 200 个线程 ⚠️ 成员变量=危险!
new Order() 对象 📦 堆 每次请求 new 1 个 只有当前线程(通过栈上引用) ✅ 安全
方法参数/局部变量 📚 栈 每个线程各自 1 份 只有当前线程 ✅ 天然安全
Mapper 实例 📦 堆 只有 1 个(单例) 所有线程 ✅ 无状态所以安全
❌ 错误:Service 有成员变量存请求数据
@Service // ← 单例!全局只有1个实例 public class BadOrderService { @Autowired private OrderMapper orderMapper; // ❌ 致命:成员变量在堆上唯一的实例里! private String currentUserId; #1 所有线程共享这一个变量! private OrderDTO currentDto; #2 Thread-A设了 → Thread-B覆盖! public void step1_setUser(String userId) { this.currentUserId = userId; // Thread-A写"user-001" } public void step3_create() { orderMapper.insert(this.currentUserId); 💥 可能拿到别人的userId! } }
✅ 正确:无状态单例 + 方法参数传递
@Service // ← 还是单例,还是只有1个实例 public class GoodOrderService { @Autowired private OrderMapper orderMapper; // ✅ 没有任何成员变量存请求数据! // 请求数据全部通过方法参数传递(在栈上,线程隔离) public OrderVO createOrder(OrderDTO dto) { #1 dto在当前线程的栈上 Order order = new Order(); #2 每次请求new一个新对象(堆上) order.setUserId(dto.getUserId()); orderMapper.insert(order); #3 用的是自己的order OrderVO vo = new OrderVO(); BeanUtils.copyProperties(order, vo); return vo; #4 方法结束→栈帧销毁→order/vo变垃圾→GC回收 } } // 关键区别: // Bad: this.currentUserId → 写在唯一的堆对象上 → 多线程共享 → 危险! // Good: dto / order / vo → 在每个线程的栈帧上 → 各自独立 → 安全!
PRO TIP 判断口诀:看到 this.xxx = 在 Service 里存请求数据 → 🔴 危险(共享成员变量)。看到 new Xxx() 或方法参数 → 🟢 安全(每次新建/栈上隔离)。单例本身不是问题,单例 + 可变成员变量才是问题。
Q2:"转发"和"重定向"到底发生了什么?

转发(Forward):服务器内部的事。请求到了 Controller A,A 说"这事归 B 管",直接在同一个线程内调用 B 的方法。浏览器完全不知道,URL 不变。整个过程是一次 HTTP 请求

重定向(Redirect):服务器告诉浏览器"你去另一个地址"。浏览器收到 302 状态码 → 发起新的 HTTP 请求 → URL 变了。这是两次 HTTP 请求

类比:转发=你去柜台 A,A 的柜员直接帮你拿到 B 柜台的东西。重定向=柜台 A 告诉你"请去 B 窗口重新排队"。

Q3:方法参数是怎么传进来的?@RequestBody 和 @RequestParam 有什么区别?

在 Controller 方法被调用之前,Spring 的 HandlerAdapter 做了参数解析:

@RequestParam → 从 URL query string 或 form-data 中取值 → 基本类型/String

@RequestBody → 从 HTTP body 中读取 JSON → Jackson 反序列化成 Java 对象(new 一个对象放堆里)→ 把引用传给方法参数

@PathVariable → 从 URL 路径模板中提取(如 /api/order/{id} → id=123)

这些参数都在栈上(如果是对象则栈上存引用,指向堆中的对象)。因为是局部变量,天然线程安全。

Q4:Tomcat 线程和数据库连接是什么关系?

它们是独立的池,通过"借用"关联。

一个 Tomcat 线程(Thread-3)在执行到 `orderMapper.insert()` 时,从 HikariCP 连接池借一个 Connection。用完后归还。在线程等待数据库返回期间,这个线程是阻塞的(BLOCKED 状态),CPU 会去执行其他线程。

如果连接池只有 10 个连接,而 200 个线程同时查库 → 190 个线程排队等连接。这就是为什么 HikariCP.maximumPoolSize 的设置很关键——太小会卡,太大会压垮数据库。

Q5:异常抛出后发生了什么?为什么 @Transactional 能自动回滚?

当 Service 方法抛出 RuntimeException:

1. 异常从 Service 方法弹出 → 到达代理对象的 invoke 方法

2. 代理对象检测到有 @Transactional → catch 住异常 → 执行 connection.rollback()

3. 代理对象把异常重新抛出 → 到 Controller → 如果有 @ExceptionHandler 就捕获处理 → 返回错误 JSON

4. 如果没人处理 → DispatcherServlet → 最终返回 500

关键认知:事务回滚不是你的代码做的,是代理对象在 try-catch 中帮你做的。这也是为什么"类内部调用"不生效——内部调用走原始对象,没有代理拦截异常。

⚠️ @Transactional 失效的 5 种常见情况(面试必考)
❌ 失效1:this 内部调用(绕过代理)
@Service public class OrderService { @Transactional public void createOrder() { saveOrder(); // ❌ this.saveOrder() 走的是原始对象! } // 没经过代理 → @Transactional不生效! @Transactional public void saveOrder() { orderMapper.insert(...); 异常时不回滚!因为没走代理! } }
✅ 修复1:注入自己 / AopContext
@Service public class OrderService { @Autowired private OrderService self; #1 注入的是代理对象(Spring会特殊处理) @Transactional public void createOrder() { self.saveOrder(); #2 通过代理调用 → 事务生效! } @Transactional public void saveOrder() { ... } } // 或用: AopContext.currentProxy() → 强转成当前类型再调用
❌ 失效2:private方法(CGLIB无法代理private)
@Transactional ⚠️ 无效! private方法CGLIB代理不了! private void doSomethingPrivate() { ... } @Transactional ⚠️ 无效! final方法无法被子类重写! public final void doFinal() { ... } @Transactional ⚠️ 无效! static不属于实例方法! public static void doStatic() { ... }
❌ 失效3:异常被catch吃掉(默认只回滚RuntimeException)
@Transactional public void badCatch() { try { orderMapper.insert(order); throw new RuntimeException("oops"); } catch (Exception e) { log.error("出错了", e); 💥 异常被吃掉 → 代理收不到 → 不回滚! throw new BizException("操作失败", e); #1 如果BizException extends RuntimeException → 还能回滚 #2 如果是 checked exception → 默认不回滚(需加rollbackFor) } } @Transactional(rollbackFor = Exception.class) #3 加上这个才对checked异常也回滚 public void goodCatch() { try { ... } catch (Exception e) { throw e; #4 重新抛出 → 代理能收到 → 回滚! } }
❌ 失效4:新线程中调用(事务绑定在原线程的Connection上)
@Transactional public void asyncProblem() { CompletableFuture.runAsync(() -> { orderMapper.insert(order); 💥 新线程没有Connection事务上下文! 即使有@Transactional也不生效! 因为TransactionSynchronizationManager按ThreadLocal存 }); } // ✅ 正确:用 @Async + 各自的事务(编程式事务) @Autowired private PlatformTransactionManager txManager; public void correctAsync() { TransactionDefinition def = new DefaultTransactionDefinition(); TransactionStatus status = txManager.getTransaction(def); CompletableFuture.runAsync(() -> { try { orderMapper.insert(order); txManager.commit(status); } catch (Exception e) { txManager.rollback(status); } }); }
Q6:同步代码和异步代码(@Async)执行时有什么区别?

同步:Thread-3 从 Controller → Service → Mapper 一路执行,遇到 I/O(数据库、HTTP 调用)就阻塞等待,直到结果返回才继续。线程被"钉"在这里,什么都做不了。

异步(@Async):Thread-3 调用 @Async 方法时,代理对象把任务交给另一个线程池(AsyncTaskExecutor),立即返回一个 CompletableFuture。Thread-3 不等待,继续执行后续代码。

类比:同步=你自己去快递柜取件,走一趟。异步=你叫快递员帮你取,你继续干别的。

:@Async 同样需要通过代理调用才生效!类内部直接调 this.asyncMethod() 不走代理 → 不异步。

🎯 一句话总结

一个请求 = 一个线程一个进程内跑一段代码。它走过的每个环节——Tomcat 路由、AOP 代理、Service 逻辑、MyBatis 查库——都是这个线程在工作。理解了"谁在执行"和"数据在哪个内存区域",你就拥有了 Java 后端运行时的心智模型

📁 运行原理图解 / 01-Java请求全链路 — 配套阅读:② JVM 内存模型 · ③ Spring 运行真相
核心三原则速记

请求全链路三原则:① 一次请求经过 网卡→线程池→Filter→Controller→Service→DAO→DB 全链路 → ② 每一层都可能丢请求,超时和异常要逐层传递 → ③ ThreadLocal 和 MDC 是跨层传 traceId 的关键