Spring/框架 —— 从 MVC/MP 分页到 Netty 线程模型与无锁化设计
10 题 · 平均 3.1/5 · low 3.33 · medium 3.25 · high 2.67 · 第 29 轮
> 最后更新:2026-10-27 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:MVC 是表现层架构模式,把"处理请求"和"展示结果"分离为 M/V/C 三块,降低耦合。
> 关键词:模型业务数据 · 视图渲染 · 控制器协调
> 边界:只解决表现层,不替代三层架构;前后端分离时 V 交给前端。
### L2 · 核心讲解
**核心考点定位**:考察是否理解 MVC 的**职责边界**(不是业务分层),以及 springMVC 的请求处理链路。面试官想排除"把 MVC 当成整个系统分层"的错误认知。
**机制链(≥3 环)**:
1. 请求到达 Controller → 调 Model 拿数据:为什么——Controller 是协调者,不写业务,业务在 Model
2. Model 返回数据 → Controller 选 View:为什么——展示与数据解耦,Controller 决定用哪个视图
3. View 渲染 → 返回响应:为什么——View 只负责展示,不含业务逻辑,可复用
**证据锚点**(≥6):
- `DispatcherServlet`:前端控制器,所有请求经它分发
- `HandlerMapping`:找 Controller,如 `RequestMappingHandlerMapping`
- `ViewResolver`:根据视图名找 View
- `Controller`:接收请求、调 Service、返回 ModelAndView/数据
- `Model`:业务数据 + 业务逻辑
- `View`:渲染展示
**对比与权衡**:
| 方案 | 正确性 | 成本 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| MVC(同步渲染) | 职责清晰 | 中 | C 臃肿、V 依赖后端模板 | 服务端渲染 |
| 前后端分离 | 前后解耦 | 高(联调) | C 只出 JSON,需约定接口 | 大型前端/多端 |
| 三层架构 | 分层清晰 | 低 | 与 MVC 混为一谈 | 系统分层(非请求模型) |
**常见误区**:
1. 错误:MVC 就是三层架构 → 正确:MVC 是表现层请求模型,三层是系统分层 → 为什么:MVC 管"一个请求如何被处理",三层管"业务/持久/表现"分层,维度不同
2. 错误:V 里写业务逻辑 → 正确:V 只渲染 → 为什么:违反分离关注点,V 含业务难测试难复用
3. 错误:Controller 做所有事 → 正确:Controller 只协调 → 为什么:C 臃肿会变成 God Object
**适用边界**:MVC 只适用"请求-响应"的 Web 场景;非 Web 系统(如消息消费、定时任务)没有 V,不套 MVC;前后端分离后 V 移交给前端,后端只剩 C+M。
### 延伸追问(≥3)
1. **springMVC 一个请求的完整处理链路?**
- 要点:DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → Service → ViewResolver → View
- 追问落点:为什么用 DispatcherServlet 做前端控制器(Facade 简化,统一入口)
2. **MVC 和三层架构什么关系?**
- 要点:MVC 是表现层模式,三层是系统分层;springMVC 的 Controller 属表现层,Service 属业务层
- 追问落点:分层后 Controller 调 Service 还是直接调 DAO?
3. **前后端分离怎么改 MVC?**
- 要点:V 变前端渲染,后端 Controller 返回 JSON 而非 ModelAndView
- 追问落点:返回 JSON 用 @ResponseBody/@RestController 怎么实现
### 面试者参考回答(400–700 字)
**【结论】** 面试官您好,MVC 是表现层的架构模式,把"处理请求"和"展示结果"分离成 M/V/C 三块,降低耦合、便于维护。
**【三点】** 第一,M 是模型——业务数据和业务逻辑,比如订单金额计算、库存扣减都在 M;V 是视图——只负责渲染,不写业务;C 是控制器——接收请求、调模型、选视图,是协调者。第二,价值是分离关注点:展示和业务解耦,V 可换、C 可测、M 可复用,多人协作互不干扰。第三,springMVC 里 DispatcherServlet 是前端控制器,所有请求先进它,经 HandlerMapping 找到对应 Controller,Controller 调 Service 处理业务,拿到数据后用 ViewResolver 找 View 渲染返回,形成完整链路。
**【边界】** 当然,MVC 只管表现层,不替代三层架构(表现/业务/持久);前后端分离时 V 移交给前端,后端 Controller 只返回 JSON,此时 MVC 里的 V 就弱化掉了。
---
> 最后更新:2026-10-27 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:MyBatis-Plus 分页本质是拦截 SQL、改写分页,走 MyBatis 拦截器插件机制,物理分页。
> 关键词:Interceptor 拦截 · LIMIT 改写 · count 优化
> 边界:深分页(offset 极大)LIMIT 也慢,需游标/索引。
### L2 · 核心讲解
**核心考点定位**:考察是否理解 MyBatis 插件机制(不是 AOP),以及分页是物理还是内存。面试官想排除"分页 = 内存分页"和"分页 = AOP"两个错误认知。
**机制链(≥3 环)**:
1. 查询执行 → `MybatisPlusInterceptor` 拦截 `Executor.query`:为什么——MyBatis 插件基于 JDK 动态代理拦截 Executor,这是 MP 挂载点
2. 拦截到 Page 参数 → 先查 count:为什么——分页需要总数,count 可智能改写优化
3. count 完成 → 改写 SQL 加 LIMIT:为什么——物理分页,数据库层只回当前页数据
**证据锚点**(≥6):
- `MybatisPlusInterceptor`:MP 提供的全局拦截器
- `PaginationInnerInterceptor`:分页核心拦截器,注册进 MybatisPlusInterceptor
- `Executor.query`:MyBatis 执行器方法,被代理拦截
- `LIMIT offset,size`:改写后 SQL,物理分页关键
- `COUNT(*)`:count 查询智能改写目标
- `Page<T>`:分页参数对象,携带 current/size
**对比与权衡**:
| 方案 | 正确性 | 成本 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| MP 物理分页 | 高 | 低(SQL 改写) | 深分页慢 | 常规列表分页 |
| PageHelper 内存分页 | 中 | 高(全量查回) | 数据量大 OOM | 小表/已全量场景 |
| 游标分页 | 高 | 中 | 不支持跳页 | 大表深分页 |
**常见误区**:
1. 错误:MP 分页是 AOP → 正确:是 MyBatis Interceptor 插件(JDK 动态代理拦截 Executor)→ 为什么:AOP 切的是 Spring Bean,MyBatis 插件切的是 Executor,机制不同
2. 错误:MP 分页是内存分页 → 正确:物理分页,改写 SQL 加 LIMIT → 为什么:内存分页全量查回,大数据灾难
3. 错误:count 查询每次全表 → 正确:MP 智能改写 count 优化 → 为什么:能改写成 COUNT(*) 就改写,减少扫描
**适用边界**:MP 分页只对"能改写 SQL 的查询"有效;自定义复杂 SQL(多表 join)count 可能不准,需指定 count 列;深分页 LIMIT 也慢,改游标。
### 延伸追问(≥3)
1. **为什么 count 要单独查?**
- 要点:分页要返回 total,count 可优化;避免每次都全表 count
- 追问落点:count(1) vs count(*) vs count(列名) 区别
2. **大表深分页 limit 1000000,10 为什么慢?**
- 要点:offset 大,数据库扫描大量行丢弃;用游标(where id>? 按序)
- 追问落点:游标分页的适用场景与局限(不支持任意跳页)
3. **多表 join 分页要注意什么?**
- 要点:count 可能不准,需指定 count 列;分页字段要明确
- 追问落点:子查询 vs 临时表优化
### 面试者参考回答(400–700 字)
**【结论】** 面试官您好,MyBatis-Plus 分页本质是拦截 SQL、改写分页,走的是 MyBatis 的拦截器插件机制,属于物理分页。
**【三点】** 第一,MyBatis 插件基于 JDK 动态代理拦截 Executor 的方法,MP 用 MybatisPlusInterceptor 注册 PaginationInnerInterceptor,这就是挂载点,不是 AOP。第二,分页做两件事——先 count 总数(能优化成 COUNT(*) 就优化),再改写原 SQL 加 LIMIT,只查当前页数据,这是物理分页。第三,相比 PageHelper 的内存分页(全量查回再 subList),MP 高效得多,大数据量下内存分页会 OOM。
**【边界】** 当然,深分页(offset 极大)即使 LIMIT 也慢,因为数据库要扫描大量行再丢弃,那时得用游标或索引定位,别指望分页插件解决一切。
---
> 最后更新:2026-10-27 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:Dubbo 是 RPC 框架(二进制长连接),Feign 是 HTTP 客户端(文本短连接),核心差异在协议。
> 关键词:TCP 长连接 · Hessian 序列化 · 声明式 HTTP
> 边界:内部高并发用 Dubbo,对外/异构用 Feign。
### L2 · 核心讲解
**核心考点定位**:考察 RPC 与 HTTP 的本质差异——二进制长连接 vs 文本短连接,以及选型依据。面试官想排除"两个都是 HTTP"的错误认知。
**机制链(≥3 环)**:
1. Dubbo 调用 → 序列化为二进制(Hessian):为什么——二进制体积小,传输快
2. Feign 调用 → JSON 文本(Jackson):为什么——HTTP 标准化,可读性好
3. Dubbo 长连接复用 → 减少握手:为什么——Netty/TCP 长连接,避免频繁建连开销
**证据锚点**(≥6):
- `Netty`:Dubbo 底层传输,TCP 长连接 NIO
- `Hessian`:Dubbo 默认序列化,二进制
- `Zookeeper/Nacos`:Dubbo 注册中心
- `@FeignClient`:声明式接口,Spring 生成代理
- `Jackson`:Feign JSON 序列化
- `LoadBalancer`:Spring Cloud 服务发现+负载均衡
**对比与权衡**:
| 方案 | 正确性 | 性能 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| Dubbo(RPC) | 强类型 | 高(长连接+二进制) | 跨语言弱、协议私有 | 内部服务调用 |
| Feign(HTTP) | 松耦合 | 中(短连接+JSON) | 连接开销、吞吐低 | 对外/异构/多语言 |
| gRPC | 强类型 | 高(HTTP/2+Protobuf) | 学习成本 | 跨语言高性能 |
**常见误区**:
1. 错误:Dubbo 也是 HTTP 调用 → 正确:Dubbo 是 TCP 长连接 RPC → 为什么:底层 Netty/TCP,非 HTTP
2. 错误:Feign 性能更高 → 正确:Dubbo 性能更高 → 为什么:二进制 + 长连接复用
3. 错误:Dubbo 和 Feign 只能二选一 → 正确:可混用 → 为什么:内部 RPC 对外 HTTP 是常见组合
**适用边界**:Dubbo 跨语言弱、私有协议,不适合多语言异构;Feign 性能低于 RPC,不适合超高吞吐;gRPC 介于两者之间。
### 延伸追问(≥3)
1. **Dubbo 为什么比 HTTP 快?**
- 要点:长连接复用 + 二进制序列化(体积小)+ TCP 而非 HTTP 文本
- 追问落点:Netty 长连接如何复用(连接池)
2. **Dubbo 和 Feign 能混用吗?**
- 要点:可以,内部高频用 Dubbo,对外/异构用 Feign
- 追问落点:同服务双协议暴露的代价
3. **gRPC 相比两者的位置?**
- 要点:HTTP/2 + Protobuf,兼具标准化与性能,跨语言强
- 追问落点:HTTP/2 多路复用对性能的提升
### 面试者参考回答(400–700 字)
**【结论】** 面试官您好,Dubbo 是 RPC 框架、Feign 是 HTTP 客户端,核心差异在"二进制长连接 vs 文本短连接"。
**【三点】** 第一,Dubbo 基于 Netty/TCP 长连接 + Hessian 二进制序列化,体积小、延迟低,靠注册中心(Zookeeper/Nacos)做服务发现和负载均衡,支持权重路由、熔断等服务治理能力。第二,Feign 是声明式 HTTP 客户端,@FeignClient 接口由 Spring 生成代理,走 HTTP/JSON,标准化、跨语言、可读性好,还和 Spring Cloud LoadBalancer、熔断组件天然整合。第三,选型上内部高并发用 Dubbo,对外或异构系统用 Feign,两者可混用,没有冲突。
**【边界】** 当然,Dubbo 跨语言弱、协议私有,Feign 性能不及 RPC——没有绝对,看调用场景;gRPC 是折中(HTTP/2+Protobuf)。
---
> 最后更新:2026-10-27 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:Netty 默认主从 Reactor 模型,bossGroup 接收连接、workerGroup 处理 IO,核心是单线程串行无锁。
> 关键词:bossGroup · workerGroup · EventLoop 单线程
> 边界:业务阻塞要丢线程池,否则阻塞该 EventLoop 所有 Channel。
### L2 · 核心讲解
**核心考点定位**:考察 Reactor 模型理解 + EventLoop 绑定机制,尤其"为什么一个 Channel 绑一个线程就不用锁"。面试官想排除"Netty 用多线程池并行处理一切"的错误认知。
**机制链(≥3 环)**:
1. accept 新连接 → bossGroup 单线程接收:为什么——接收连接很快,一个线程够,避免多线程竞争 accept 和惊群效应
2. 连接分给 worker → EventLoop.next() 轮询/取模:为什么——把连接均匀分配到多个 EventLoop,避免单点瓶颈
3. Channel 绑定 EventLoop → 所有操作该线程串行:为什么——串行化消除并发,无需加锁(无锁设计)
4. 读事件到达 → 触发 ChannelRead → 沿 Pipeline 传播:为什么——事件驱动,一个线程处理一个 Channel 的 IO 和回调
**证据锚点**(≥10):
- `NioEventLoopGroup`:线程组,bossGroup/workerGroup 都用它
- `bossGroup`:默认一个线程,负责 accept
- `workerGroup`:默认 2×CPU 线程,处理 IO 读写
- `EventLoop`:一个线程 + 一个任务队列
- `EventLoopGroup.next()`:轮询分配连接
- `NioServerSocketChannel`:服务端 channel,boss 处理
- `NioSocketChannel`:客户端 channel,worker 处理
- `ChannelPipeline`:Channel 关联的 handler 链
- `DefaultEventExecutorGroup`:业务线程池(业务阻塞时用)
- `MAX_PENDING_CONNECTIONS`:boss 接收队列上限
**对比与权衡**:
| 模型 | 正确性 | 并发度 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| 单 Reactor 单线程 | 低 | 极低 | 单线程瓶颈 | 演示/教学 |
| 单 Reactor 多线程 | 中 | 中 | 接收与 IO 争抢 | 小规模 |
| 主从 Reactor(Netty) | 高 | 高 | 线程数配置不当 | 生产默认 |
**常见误区**:
1. 错误:Netty 一个连接一个线程 → 正确:一个 EventLoop 管理多个 Channel → 为什么:线程数有限,靠事件驱动复用,连接数可以远超线程数
2. 错误:Channel 操作可在任意线程 → 正确:必须调度到绑定 EventLoop → 为什么:保证串行无锁与读写顺序,跨线程会破坏一致性
3. 错误:业务阻塞没关系 → 正确:会阻塞该 EventLoop 上所有 Channel → 为什么:单线程串行,一个阻塞全堵
4. 错误:bossGroup 线程越多越好 → 正确:一个就够,多了反而竞争 accept → 为什么:接收是轻量操作,单线程避免惊群
**适用边界**:主从 Reactor 适合连接数大、IO 密集;业务阻塞耗时操作必须丢 `DefaultEventExecutorGroup`,否则拖垮整个 worker;CPU 密集型业务不适合 Netty 默认模型。
### 延伸追问(≥3)
1. **为什么一个 Channel 的所有操作要绑定同一 EventLoop?**
- 要点:保证串行、避免锁、保证读写顺序
- 追问落点:跨线程操作 Channel 会怎样(抛异常/需调度)
2. **Channel 怎么分配到 EventLoop?**
- 要点:EventLoopGroup.next() 轮询/取模,均匀分配
- 追问落点:next() 的实现(round-robin / power-of-two)
3. **业务阻塞会怎样?**
- 要点:阻塞该 EventLoop,影响同线程所有 Channel
- 追问落点:为什么用 DefaultEventExecutorGroup 丢业务
### 面试者参考回答(400–700 字)
**【结论】** 面试官您好,Netty 默认主从 Reactor 模型,bossGroup 接收连接、workerGroup 处理 IO,核心是单线程串行无锁。
**【三点】** 第一,bossGroup 一个线程只 accept 新连接,快速分配给 workerGroup,接收与处理分离。第二,workerGroup 多个 EventLoop,每个 EventLoop 一个线程,绑定多个 Channel,处理 IO 读写。第三,同一 Channel 所有操作都调度到同一 EventLoop 串行执行,所以无需加锁——这是 Netty 高性能的关键,也是无锁设计的来源。
**【边界】** 当然,业务是阻塞操作时要主动丢到 DefaultEventExecutorGroup,否则会阻塞该 EventLoop 上所有 Channel,一个慢请求拖垮一批连接。
---
> 最后更新:2026-10-27 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:动态生成 Bean 的本质是在 BeanDefinition 阶段注册元数据,核心接口是 BeanDefinitionRegistryPostProcessor。
> 关键词:BeanDefinition 先行 · BDRegistryPostProcessor · ImportBeanDefinitionRegistrar
> 边界:注册必须发生在实例化之前(容器刷新早期)。
### L2 · 核心讲解
**核心考点定位**:考察 Bean 生命周期早期 + 注册机制。面试官想排除"动态生成 = 直接 new 对象丢给容器"的错误认知。
**机制链(≥3 环)**:
1. 容器刷新 → 调用 BeanDefinitionRegistryPostProcessor:为什么——这是容器早期扩展点,此时 BeanDefinition 还没实例化
2. 遍历配置 → BeanDefinitionBuilder 构建定义:为什么——用 builder 构建元数据,而非 new 对象
3. registerBeanDefinition → 注册进容器:为什么——注册定义后,容器后续统一实例化、依赖注入、管理生命周期
**证据锚点**(≥10):
- `BeanDefinitionRegistryPostProcessor`:批量注册入口,容器刷新早期回调
- `BeanDefinitionRegistry`:注册表,registerBeanDefinition 方法
- `BeanDefinitionBuilder`:构建 BeanDefinition 的工具
- `ImportBeanDefinitionRegistrar`:@Import 时注册
- `@Import`:显式导入类/配置
- `@Bean`:配置类方法声明 Bean
- `@ConditionalOnXxx`:条件注册(如 @ConditionalOnProperty)
- `@Profile`:按环境注册
- `BeanDefinition`:Bean 元数据(class/scope/属性)
- `FactoryBean`:单点定制创建
**对比与权衡**:
| 方式 | 正确性 | 粒度 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| BDRegistryPostProcessor | 高 | 批量 | 注册时机早 | 按配置批量注册 |
| ImportBeanDefinitionRegistrar | 高 | 模块 | 依赖 @Import | 模块化引入 |
| FactoryBean | 高 | 单点 | 定制复杂 | 第三方类封装 |
| @ConditionalOnXxx | 中 | 单点 | 条件误配 | 开关式组件 |
**常见误区**:
1. 错误:动态生成 = new 对象放入容器 → 正确:注册 BeanDefinition,容器统一实例化 → 为什么:直接 new 绕过生命周期,依赖注入失效
2. 错误:BDRegistryPostProcessor 和 BeanFactoryPostProcessor 一样 → 正确:前者能新增定义,后者只能改属性 → 为什么:Registry 是注册器,PostProcessor 是修改器
3. 错误:任意时机都能注册 → 正确:必须容器刷新早期 → 为什么:实例化后注册不进去
4. 错误:用反射直接 new 多个 → 正确:用 BeanDefinitionBuilder 构建 → 为什么:builder 支持设置属性/scope,new 做不到
**适用边界**:动态注册只适合"启动时已知"的 Bean;运行时动态增减 Bean 不是主流,用 @Lazy/工厂更合适。
### 延伸追问(≥3)
1. **BeanDefinitionRegistryPostProcessor 和 BeanFactoryPostProcessor 区别?**
- 要点:前者能新增 BeanDefinition(注册器),后者只改属性(修改器)
- 追问落点:执行顺序(Registry 先于 PostProcessor)
2. **@Import 和 @ComponentScan 区别?**
- 要点:@Import 显式导入,@ComponentScan 扫描包
- 追问落点:@Import 导入普通类与 @Configuration 的区别
3. **动态注册的 Bean 如何注入?**
- 要点:注册后正常 @Autowired,但要在容器刷新前注册完
- 追问落点:注册的 Bean 能否被 AOP 代理
### 面试者参考回答(400–700 字)
**【结论】** 面试官您好,动态生成 Bean 的本质是在 BeanDefinition 阶段注册元数据,核心接口是 BeanDefinitionRegistryPostProcessor。
**【三点】** 第一,Spring 先注册 BeanDefinition 再实例化,所以"动态生成"就是提前注册定义,不是直接 new 对象。第二,最常用 BeanDefinitionRegistryPostProcessor——在容器刷新早期遍历配置,用 BeanDefinitionBuilder 构建并 registerBeanDefinition,可批量注册。第三,单点定制用 FactoryBean,按条件生成用 @ConditionalOnXxx / @Profile,模块化引入用 ImportBeanDefinitionRegistrar。
**【边界】** 当然,动态注册必须发生在实例化之前(容器刷新早期),晚了注册不进去;且批量注册要用 BeanDefinitionBuilder 而不是直接 new。
---
> 最后更新:2026-10-27 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:BeanFactory 是 IoC 容器接口,FactoryBean 是创建 Bean 的钩子,二者通过 getBean 关联。
> 关键词:容器接口 · getObject() 产品 · & 前缀取工厂
> 边界:FactoryBean 只在"无法注解标注/需定制构建"时用。
### L2 · 核心讲解
**核心考点定位**:区分 **BeanFactory(容器接口)** 与 **FactoryBean(工厂钩子)**——名字像但职责完全不同,且通过 getBean 关联。面试官想排除"二者无关"的错误认知。
**机制链(≥3 环)**:
1. 注册 FactoryBean → 容器存的是"工厂"而非产品:为什么——FactoryBean 是实现接口的类,容器先实例化它
2. getBean("name") → 检测是 FactoryBean → 调 getObject():为什么——容器约定:遇到 FactoryBean 返回产品
3. getBean("&name") → 返回 FactoryBean 本身:为什么——& 前缀是 Spring 约定,用于拿工厂做定制
**证据锚点**(≥10):
- `BeanFactory`:容器顶层接口
- `getBean(String)`:按名取 Bean
- `FactoryBean<T>`:对象工厂接口
- `getObject()`:返回实际要暴露的 Bean
- `getObjectType()`:返回产品类型
- `isSingleton()`:是否单例产品
- `&` 前缀:getBean 取 FactoryBean 本身
- `DefaultListableBeanFactory`:BeanFactory 主要实现
- `MapperFactoryBean`:MyBatis 典型 FactoryBean
- `ProxyFactoryBean`:AOP 典型 FactoryBean
**对比与权衡**:
| 概念 | 本质 | 方法 | 典型 |
|---|---|---|---|
| BeanFactory | 容器接口 | getBean/containsBean/isSingleton | DefaultListableBeanFactory |
| FactoryBean | 对象工厂钩子 | getObject/getObjectType/isSingleton | MapperFactoryBean |
| @Bean 方法 | 配置方法 | 返回实例 | @Configuration 类 |
**常见误区**:
1. 错误:二者没什么关系 → 正确:getBean 关联(& 前缀约定)→ 为什么:容器能识别 FactoryBean 并调用 getObject
2. 错误:getBean("x") 返回 FactoryBean 实例 → 正确:返回 getObject() 的产品 → 为什么:容器自动解包
3. 错误:FactoryBean 是容器 → 正确:是接口钩子,非容器 → 为什么:容器是 BeanFactory,FactoryBean 只定制单个 Bean
4. 错误:所有类都能用 FactoryBean → 正确:仅第三方/需定制构建类 → 为什么:普通类 @Component 更简单
**适用边界**:FactoryBean 适用于"无法用注解标注的第三方类"或"需要定制构建逻辑"(如 MyBatis Mapper 代理、AOP 代理对象);普通 Bean 用 @Component/@Bean 更直接,别为用而用。另外注意 FactoryBean 返回的产品也可以注册其他 Bean,但通常单点场景更合适。
### 延伸追问(≥3)
1. **@Bean 方法返回 FactoryBean 时,getBean 拿到什么?**
- 要点:拿到 getObject() 的产品;& 前缀拿 FactoryBean
- 追问落点:容器如何区分产品与工厂(isFactoryBean 判断)
2. **FactoryBean 的 isSingleton=false 会怎样?**
- 要点:每次 getBean 都调 getObject() 返回新实例
- 追问落点:prototype FactoryBean 的用途
3. **BeanFactoryPostProcessor 和 FactoryBean 都能"动态生成 Bean",区别?**
- 要点:前者改 BeanDefinition 批量注册;后者单点定制创建
- 追问落点:两者能否叠加使用
### 面试者参考回答(400–700 字)
**【结论】** 面试官您好,BeanFactory 是 IoC 容器接口,FactoryBean 是创建 Bean 的钩子,二者通过 getBean 关联起来。
**【三点】** 第一,BeanFactory 是容器——管所有 Bean 的定义和生命周期,getBean 是它的核心方法,主要实现是 DefaultListableBeanFactory。第二,FactoryBean 是特殊接口——实现类告诉容器"我的 getObject() 返回值才是要暴露的 Bean",典型是 MyBatis 的 MapperFactoryBean 和 AOP 的 ProxyFactoryBean。第三,关联点——`getBean("name")` 拿产品,`getBean("&name")` 拿 FactoryBean 本身,这是 Spring 的 & 前缀约定。
**【边界】** 当然,FactoryBean 只在"无法用注解标注的类"或"需要定制构建逻辑"时用,普通 Bean 直接用 @Component/@Bean 就行,别为了用而用。
---
> 最后更新:2026-10-27 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:缓存预热选 ApplicationRunner/CommandLineRunner,所有 Bean 就绪后、对外服务前执行。
> 关键词:Bean 就绪 · run() 完成前 · @Order 排序
> 边界:预热要幂等容错,失败不阻断启动。
### L2 · 核心讲解
**核心考点定位**:考察 Spring 启动生命周期钩子,选对预热时机。面试官想排除"用 @PostConstruct 预热"的时机错误。
**机制链(≥3 环)**:
1. Bean 实例化 → @PostConstruct 执行:为什么——Bean 级,可能依赖未就绪,时机过早
2. 容器刷新完成 → ContextRefreshedEvent:为什么——所有 Bean 已就绪,但应用可能未对外
3. SpringApplication.run() 完成前 → Runner 执行:为什么——最安全,所有 Bean 就绪且即将对外服务
**证据锚点**(≥10):
- `ApplicationRunner`:run(ApplicationArguments) 方法
- `CommandLineRunner`:run(String[] args) 方法
- `@PostConstruct`:Bean 实例化后执行
- `InitializingBean.afterPropertiesSet()`:Bean 级初始化
- `ApplicationListener<ContextRefreshedEvent>`:容器刷新
- `ApplicationReadyEvent`:应用就绪事件
- `@Order`:控制多个 Runner 顺序
- `SmartLifecycle`:phase 控制启动顺序
- `ApplicationArguments`:解析后的命令行参数
- `SpringApplication.run()`:应用启动入口
**对比与权衡**:
| 时机 | 正确性 | 依赖就绪 | 适用 |
|---|---|---|---|
| @PostConstruct | 中 | 部分 Bean | Bean 内初始化 |
| ContextRefreshedEvent | 高 | 全 Bean | 容器级预热 |
| ApplicationRunner | 高 | 全 Bean+环境 | 缓存预热(推荐) |
| ApplicationReadyEvent | 高 | 全部就绪 | 对外后预热 |
**常见误区**:
1. 错误:用 @PostConstruct 预热缓存 → 正确:用 ApplicationRunner → 为什么:@PostConstruct 时部分 Bean 未就绪,缓存依赖可能拿不到
2. 错误:Runner 失败会静默 → 正确:抛异常会导致启动失败 → 为什么:需 try-catch 容错,记录日志
3. 错误:预热越多越好 → 正确:控制耗时 → 为什么:阻塞启动,大缓存需异步/分批
4. 错误:CommandLineRunner 和 ApplicationRunner 一样 → 正确:参数类型不同 → 为什么:前者 String[] args,后者 ApplicationArguments
**适用边界**:Runner 是启动一次性动作;运行时缓存更新靠定时任务或事件监听,不能只靠启动预热。大缓存预热要异步或分批,避免拖慢启动。若应用是集群部署,预热还要考虑多实例同时预热对下游 DB 的冲击,必要时用预热锁或错峰。
### 延伸追问(≥3)
1. **@PostConstruct 和 ApplicationRunner 哪个先?**
- 要点:@PostConstruct 更早(Bean 级),Runner 在所有 Bean 就绪后
- 追问落点:@PostConstruct 时能否依赖其他 Bean
2. **预热失败会怎样?**
- 要点:Runner 抛异常导致启动失败,需 try-catch 容错
- 追问落点:失败后如何降级(跳过预热用空缓存)
3. **多个 Runner 如何控制顺序?**
- 要点:@Order 注解,值越小越先执行
- 追问落点:SmartLifecycle 的 phase 与 @Order 区别
### 面试者参考回答(400–700 字)
**【结论】** 面试官您好,缓存预热我选 ApplicationRunner/CommandLineRunner,它们在所有 Bean 就绪后、应用对外服务前执行。
**【三点】** 第一,时机——@PostConstruct 是 Bean 级,此时其他 Bean 可能未就绪;Runner 在 SpringApplication.run() 完成前执行,所有 Bean 已就绪,最安全。第二,两者区别——CommandLineRunner 收 String[] args,ApplicationRunner 收解析后的 ApplicationArguments,可用 @Order 控制顺序。第三,实践——预热要幂等容错(失败记日志不阻断启动),大缓存异步或分批控制耗时,避免拖慢启动。
**【边界】** 当然,预热是启动时一次性动作,运行时缓存更新还得靠定时任务或监听变更事件,不能只靠启动预热。
---
> 最后更新:2026-10-27 · 基于第 2 次作答
### L1 · 30 秒速记
> 结论:Netty 常考 6 大件是单例/工厂/建造者/责任链/策略/观察者,每个要能对应 Netty 具体类。
> 关键词:ChannelPipeline 责任链 · Bootstrap 建造者 · ChannelFuture 观察者
> 边界:别背"6 大"清单,要能说"这个模式解决什么、Netty 怎么用的"。
### L2 · 核心讲解
**核心考点定位**:考察能否把设计模式与 Netty 的实际实现类对应,而非只报模式名字。面试官想排除"报 5 个模式名但说不出 Netty 哪个类体现"的空泛回答。
**机制链(≥3 环)**:
1. 请求进入 Channel → 沿 Pipeline 传播:为什么——ChannelPipeline 是责任链,Inbound 向上、Outbound 向下,每个 Handler 决定是否传递
2. 事件产生 → ChannelHandler 回调:为什么——观察者模式,EventLoop 驱动事件,解耦产生与处理
3. 构建服务 → Bootstrap 链式配置:为什么——建造者模式,链式调用最后 build() 创建
4. 异步 IO 完成 → ChannelFuture 通知:为什么——观察者,Promise/Future 承载结果,addListener 回调避免阻塞
5. 传输选择 → NioSocketChannel/NioDatagramChannel:为什么——策略模式,运行时按需选 TCP/UDP
**证据锚点**(≥12):
- `ChannelPipeline`:责任链,多 Handler 串联
- `ChannelHandler`:责任链节点,处理特定事件
- `EventLoop`:观察者,事件驱动回调
- `ChannelFuture`:观察者,异步回调(addListener)
- `Promise`:观察者,Future 的结果载体
- `Bootstrap`:建造者,链式配置服务端/客户端
- `NioSocketChannel`:策略,TCP 传输实现
- `NioDatagramChannel`:策略,UDP 传输实现
- `SimpleChannelInboundHandler`:模板方法,channelRead0 模板
- `ChannelDuplexHandler`:模板方法,inbound/outbound 两套
- `EventLoopGroup`:单例,通常服务级复用一组
- `WrappedByteBuf`:装饰器,透明包装增强
- `ChannelHandlerAdapter`:适配器,连接 Inbound/Outbound 适配
- `NioEventLoopGroup`:工厂,创建 EventLoop
**对比与权衡**:
| 模式 | Netty 体现 | 解决的问题 | 适用 |
|---|---|---|---|
| 责任链 | ChannelPipeline | 事件多级处理解耦 | 协议栈处理 |
| 观察者 | ChannelFuture/Promise | 异步结果通知 | 非阻塞回调 |
| 建造者 | Bootstrap | 复杂对象构建 | 服务启动 |
| 策略 | Channel 传输/编解码 | 运行时选实现 | TCP/UDP 切换 |
| 模板方法 | SimpleChannelInboundHandler | 固定流程+子类细节 | Handler 编写 |
| 装饰器 | WrappedByteBuf | 透明增强 | ByteBuf 包装 |
**常见误区**:
1. 错误:把 Netty 说成 NGINX → 正确:Netty 是 Java NIO 框架 → 为什么:完全不同的框架,NGINX 是 C 语言 Web 服务器
2. 错误:只报模式名 → 正确:说清 Netty 哪个类体现 → 为什么:报名字是敲门砖,对应类才是深度
3. 错误:漏观察者 → 正确:ChannelFuture/Promise 是异步回调观察者 → 为什么:Netty 非阻塞核心靠它通知结果
4. 错误:装饰器和适配器不算 → 正确:WrappedByteBuf 是装饰器,ChannelHandlerAdapter 是适配器 → 为什么:Netty 实际用了,属加分项
5. 错误:Pipeline 是单向 → 正确:Pipeline 是双向责任链 → 为什么:Inbound 向上、Outbound 向下,与 Servlet Filter 单向不同
**适用边界**:设计模式不是越多越好,Netty 在恰当时机用恰当模式;面试重点在"模式解决什么问题 + Netty 哪里用了",不在背清单。单例在 Netty 里是"服务级复用"而非严格单例,别硬套;装饰器/适配器是加分项,但责任链/观察者才是 Netty 的灵魂,先说清这两个。
### 延伸追问(≥3)
1. **Netty 的 Pipeline 和 Servlet 的 FilterChain 有什么区别?**
- 要点:Pipeline 双向(Inbound/Outbound),FilterChain 单向;Pipeline 支持动态增删 Handler
- 追问落点:动态增删 Handler 的场景
2. **Netty 的 EventLoop 模型和 Reactor 模型什么关系?**
- 要点:EventLoop 是 Reactor 具体实现;BossGroup 接收、WorkerGroup 处理;每个 EventLoop 单线程无锁
- 追问落点:为什么单线程无锁
3. **Netty 如何扩展自定义 Handler?**
- 要点:继承 ChannelInboundHandlerAdapter/ChannelOutboundHandlerAdapter,pipeline.addLast() 添加
- 追问落点:Handler 的线程模型(绑定 EventLoop)
### 面试者参考回答(500–800 字)
**【结论】** 面试官您好,Netty 是设计模式的应用典范,我按创建/结构/行为三类讲,每个都对应到具体类。
**【三点】** 第一,创建型——单例:EventLoopGroup 通常服务级复用一组;建造者:Bootstrap 链式配置最后 build();工厂:NioEventLoopGroup 创建 EventLoop。第二,结构型——责任链:ChannelPipeline 由多个 ChannelHandler 组成,Inbound 向上、Outbound 向下,支持动态增删;装饰器:WrappedByteBuf 透明包装;适配器:ChannelHandlerAdapter。第三,行为型——观察者:ChannelFuture/Promise 异步回调(addListener),非阻塞核心;策略:NioSocketChannel/NioDatagramChannel 运行时选传输;模板方法:SimpleChannelInboundHandler 定义 channelRead0 模板。
**【边界】** 当然,别背"6 大"固定清单——面试官更想看"这个模式解决什么、Netty 怎么用的",报名字只是敲门砖;Netty 与 NGINX 是两个完全不同的框架,别混淆。
---
> 最后更新:2026-10-27 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:Spring 常考单例/工厂/代理/模板/观察者/责任链/策略,每个要能对应具体实现类。
> 关键词:单例默认 scope · AOP 代理 · JdbcTemplate 模板
> 边界:别背清单,要能说"模式解决什么、Spring 哪里用"。
### L2 · 核心讲解
**核心考点定位**:考察设计模式与 Spring 组件的对应,是否理解模式解决什么问题。面试官想排除"报一堆名字但说不出具体类"的空泛回答。
**机制链(≥3 环)**:
1. 请求进入 → BeanFactory 创建单例:为什么——单例模式,默认 scope 容器缓存,避免反复实例化
2. AOP 增强 → JDK 动态代理/CGLIB 代理:为什么——代理模式,拦截方法做增强,解耦横切
3. 模板执行 → JdbcTemplate 固定流程:为什么——模板方法,流程骨架固定,子类/回调定制细节
4. 事件发布 → ApplicationListener 响应:为什么——观察者模式,解耦发布与订阅
5. 请求拦截 → HandlerInterceptor 链:为什么——责任链,多级拦截按序处理,可中断
**证据锚点**(≥12):
- `DefaultSingletonBeanRegistry`:单例缓存(singletonObjects Map)
- `BeanFactory`:工厂模式,顶层容器
- `FactoryBean`:对象工厂钩子
- `JdkDynamicAopProxy`:JDK 动态代理(接口)
- `CglibAopProxy`:CGLIB 代理(类)
- `JdbcTemplate`:模板方法,流程骨架
- `RestTemplate`:模板方法,HTTP 流程
- `RedisTemplate`:模板方法,Redis 操作
- `ApplicationEvent`:观察者,事件
- `ApplicationListener`:观察者,监听器
- `HandlerInterceptor`:责任链,MVC 拦截器
- `Resource`:策略,资源抽象
- `BeanDefinitionBuilder`:建造者,构建 BeanDefinition
- `RequestMappingHandlerMapping`:策略,URL 映射实现
- `ProxyFactoryBean`:工厂,AOP 代理工厂
- `@Transactional`:AOP 事务切面典型应用
**对比与权衡**:
| 模式 | Spring 体现 | 解决的问题 | 适用 |
|---|---|---|---|
| 单例 | 默认 scope | 对象复用 | 无状态服务 |
| 代理 | AOP(JDK/CGLIB) | 横切解耦 | 事务/日志/鉴权 |
| 模板方法 | JdbcTemplate | 固定流程+细节定制 | 资源访问 |
| 观察者 | ApplicationEvent | 事件解耦 | 业务事件 |
| 责任链 | Interceptor/Filter | 请求多级处理 | Web 过滤链 |
| 工厂 | BeanFactory/FactoryBean | 对象创建解耦 | IoC 容器 |
**常见误区**:
1. 错误:Spring 用"创建者模式" → 正确:主要是工厂方法 + 建造者(BeanDefinitionBuilder)→ 为什么:Spring 的 Bean 创建是工厂,Builder 是构建 BeanDefinition
2. 错误:AOP 默认 JDK 代理 → 正确:Spring Boot 2+ 默认 CGLIB → 为什么:CGLIB 不依赖接口,类也可代理
3. 错误:单例是硬编码 → 正确:scope=singleton 容器管理 → 为什么:线程安全靠无状态,不是加锁
4. 错误:观察者一定异步 → 正确:默认同步,@Async 才异步 → 为什么:Spring 事件默认同步发布
5. 错误:模板方法只是封装调用 → 正确:是流程骨架 + 回调定制 → 为什么:JdbcTemplate 固定连接/事务/释放流程,只有 SQL 和 RowMapper 是回调
6. 错误:@Transactional 靠自调用生效 → 正确:外部调用才走代理 → 为什么:自调用不经过代理,AOP 失效(同类 this 调用)
**适用边界**:设计模式在 Spring 是恰当时机恰当使用;单例要求 Bean 无状态;事件监听若处理慢会阻塞发布方,需异步;模板方法不适用于流程需要大改的场景。代理模式对自调用(同类 this 调用)失效,需要暴露代理或拆分方法。
### 延伸追问(≥3)
1. **AOP 为什么默认 CGLIB?**
- 要点:Spring Boot 2+ 默认 CGLIB,类无需接口也能代理;JDK 代理只支持接口
- 追问落点:JDK 代理 vs CGLIB 的性能与限制(CGLIB 不能代理 final 类/方法)
2. **Spring 事件是同步还是异步?**
- 要点:默认同步,@Async 或 ApplicationEventMulticaster 可异步
- 追问落点:@TransactionalEventListener 的时机(事务提交后)
3. **单例怎么保证的?**
- 要点:DefaultSingletonBeanRegistry 的 singletonObjects Map + 双重检查
- 追问落点:单例 Bean 的线程安全(无状态 + 不可变)
4. **责任链和 AOP 都能做日志,区别?**
- 要点:责任链是请求处理链,AOP 是方法级横切;Filter/Interceptor 管请求,AOP 管业务方法
- 追问落点:Interceptor 和 Filter 的区别(Servlet 层 vs Spring 层)
### 面试者参考回答(500–800 字)
**【结论】** 面试官您好,Spring 是设计模式的应用典范,我按创建/结构/行为三类挑核心来讲,每个都对应具体类。
**【三点】** 第一,创建型——单例:默认 scope=singleton,DefaultSingletonBeanRegistry 缓存;工厂:BeanFactory/FactoryBean;建造者:BeanDefinitionBuilder。第二,结构型——代理:AOP 用 JDK 动态代理(接口)或 CGLIB(类),Spring Boot 2+ 默认 CGLIB。第三,行为型——模板:JdbcTemplate/RestTemplate/RedisTemplate 固定流程骨架;观察者:ApplicationEvent + ApplicationListener(默认同步,@Async 可异步);责任链:HandlerInterceptor/Filter 链;策略:Resource 接口多实现。
**【边界】** 当然,设计模式不是越多越好,Spring 在恰当时机用恰当模式,别背清单,要能说清"这个模式解决什么问题、Spring 哪里用了";单例要求 Bean 无状态,事件监听别拖慢发布方。
---
> 最后更新:2026-10-27 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:Netty 无锁化核心是单线程串行化,配合无锁队列和内存池,而非简单"不加锁"。
> 关键词:EventLoop 单线程 · MPSC 队列 CAS · PooledByteBufAllocator
> 边界:无锁换来单核,并发靠多 EventLoop。
### L2 · 核心讲解
**核心考点定位**:考察无锁化的多个层面,是否理解"为什么单线程就不用锁"。面试官想排除"无锁 = 不用 synchronized"的浅层回答。
**机制链(≥3 环)**:
1. Channel 操作 → 调度到绑定 EventLoop:为什么——单线程串行化,消除并发访问,无需同步
2. 任务提交 → MPSC 队列 CAS 追加:为什么——多生产者单消费者,写端 CAS 无锁,读端单线程取
3. 内存分配 → PooledByteBufAllocator 预分配:为什么——内存池复用,避免频繁分配 GC
4. ByteBuf 释放 → ReferenceCountUtil.release 原子计数:为什么——引用计数归零才回收,防泄漏
5. 线程本地访问 → FastThreadLocal 数组定位:为什么——替代 JDK ThreadLocal 的哈希查找,更快
6. 计数器更新 → AtomicIntegerFieldUpdater:为什么——用字段更新器而非 AtomicInteger,避免额外对象分配
**证据锚点**(≥12):
- `EventLoop`:单线程 + 任务队列,串行执行
- `MpscQueue`:多生产者单消费者队列,CAS 追加
- `MpscArrayQueue`:JCTools 提供的实现
- `PooledByteBufAllocator`:内存池分配器
- `PoolArena`:内存池的 Arena(类似 jemalloc)
- `ReferenceCountUtil.release()`:引用计数释放
- `RefCnt`:引用计数接口
- `FastThreadLocal`:替代 JDK ThreadLocal,数组+索引定位
- `AtomicIntegerFieldUpdater`:原子字段更新,减少对象
- `ByteBuf`:Netty 缓冲区
- `DefaultEventExecutorGroup`:业务线程池(非 IO 无锁区)
- `Channel`:绑定 EventLoop 的抽象
- `ThreadLocal`:JDK 原生,FastThreadLocal 对标它
- `-Dio.netty.leakDetection.level`:泄漏检测级别配置
**对比与权衡**:
| 方案 | 正确性 | 并发度 | 失败模式 | 适用 |
|---|---|---|---|---|
| 单线程串行化 | 高 | 单核 | 一个阻塞全堵 | Channel 操作 |
| MPSC 队列 | 高 | 多写单读 | 消费者慢堆积 | 任务提交 |
| 内存池 | 高 | 高 | 泄漏 | ByteBuf 分配 |
| 传统加锁 | 中 | 中 | 锁竞争 | 跨线程共享 |
| CAS 原子操作 | 高 | 高 | ABA 问题 | 计数器/队列
**常见误区**:
1. 错误:无锁 = 不加 synchronized → 正确:本质是单线程串行 + 无锁数据结构 → 为什么:避免并发访问才是关键,不是去掉关键字
2. 错误:MPSC 是阻塞队列 → 正确:CAS 无锁,不会阻塞 → 为什么:写端 CAS 自旋,读端单线程
3. 错误:ByteBuf 不释放没事 → 正确:必须 ReferenceCountUtil.release → 为什么:引用计数泄漏导致 OOM
4. 错误:单线程串行化没有并发 → 正确:靠多 EventLoop 并行 → 为什么:每个 EventLoop 单核,整体靠多 EventLoop 并行
5. 错误:业务直接放 EventLoop → 正确:丢 DefaultEventExecutorGroup → 为什么:阻塞会堵住整条串行链
6. 错误:FastThreadLocal 和 ThreadLocal 没区别 → 正确:数组索引定位更快 → 为什么:JDK ThreadLocal 哈希查找有冲突,FastThreadLocal 用索引
**适用边界**:无锁换来"一个 EventLoop 单核",要并发得靠多 EventLoop;业务阻塞要丢线程池;跨 EventLoop 共享数据仍需锁或原子类。CAS 有 ABA 问题,高并发场景需 AtomicStampedReference 版本号。无锁不是万能,读多写少场景锁反而更简单。
### 延伸追问(≥3)
1. **MPSC 队列为什么要 CAS?**
- 要点:多生产者并发写,单消费者读,写端无锁用 CAS 原子追加
- 追问落点:MPSC vs MPMC 的区别(多消费者需额外同步)
2. **引用计数泄漏会怎样?**
- 要点:ByteBuf 不 release 导致内存泄漏,Netty 泄漏检测(paranoid)
- 追问落点:泄漏检测如何开启(-Dio.netty.leakDetection.level)
3. **单线程串行化牺牲了什么?**
- 要点:一个线程只能用一核,多 Channel 靠多 EventLoop 并行
- 追问落点:EventLoop 数怎么配(2×CPU)
4. **无锁和 CAS 什么关系?**
- 要点:无锁数据结构的底层是 CAS 原子操作,乐观并发
- 追问落点:CAS 的 ABA 问题及版本号解法
### 面试者参考回答(500–800 字)
**【结论】** 面试官您好,Netty 无锁化核心是单线程串行化,配合无锁队列和内存池,而不是简单"不加锁"。
**【三点】** 第一,单线程串行化——一个 EventLoop 一个线程,同一 Channel 所有操作都在这线程串行执行,所以无需同步,这是基础。第二,无锁队列——MPSC 队列(MpscArrayQueue)多生产者用 CAS 原子追加,单消费者取走,任务提交无锁。第三,无锁内存——PooledByteBufAllocator 预分配内存池 + ReferenceCountUtil.release 引用计数控制回收,还有 FastThreadLocal 替代 JDK ThreadLocal 减少查找开销。
**【边界】** 当然,无锁换来的是"一个 EventLoop 单核",要并发得靠多 EventLoop;业务阻塞要丢线程池,否则阻塞整条串行链;跨 EventLoop 共享数据仍需锁或原子类。
---