1. 引言
Java Web 是后端开发面试中的核心考察方向,覆盖 Servlet、JSP、Spring、Spring MVC、MyBatis、数据库、分布式、缓存、消息队列等众多知识点。本文整理了 Java Web 面试中最常见的高频题目,并给出简洁清晰的参考答案,帮助你在面试前快速回顾核心要点。
2. Servlet 与 JSP 基础
2.1 什么是 Servlet?Servlet 的生命周期是什么?
Servlet 是运行在 Web 容器(如 Tomcat)中的 Java 类,用于处理客户端请求并生成响应。其生命周期由容器管理,分为四个阶段:
- 加载与实例化:容器加载 Servlet 类并创建实例。
- 初始化(init) :容器调用
init()方法,只执行一次,用于加载配置、初始化资源。 - 服务(service) :容器调用
service()方法处理请求,根据请求类型分发到doGet()/doPost()等方法,可被多次调用。 - 销毁(destroy) :容器卸载 Servlet 前调用
destroy()方法释放资源,只执行一次。
2.2 什么是 JSP?JSP 与 Servlet 的区别是什么?
JSP(Java Server Pages)本质上是 Servlet 的一种简化形式,在 HTML 中嵌入 Java 代码,由容器编译为 Servlet 后执行。
| 对比项 | Servlet | JSP |
|---|---|---|
| 定位 | 适合处理业务逻辑 | 适合展示页面 |
| 编写方式 | 纯 Java 类 | HTML + Java 片段 |
| 编译时机 | 部署时编译 | 首次访问时编译 |
| 职责 | 控制层 | 视图层 |
2.3 转发(forward)与重定向(redirect)的区别?
- 转发(forward) :发生在服务器内部,浏览器地址栏不变,只发起一次请求,可以携带
request域中的数据。 - 重定向(redirect) :服务器返回 302 状态码,浏览器重新发起新请求,地址栏变化,无法直接共享
request域数据。
3. Spring 核心
3.1 Spring 的 IOC 和 AOP 是什么?
- IOC(控制反转):将对象的创建和依赖管理交给 Spring 容器,由容器负责实例化、装配和管理 Bean 的生命周期,降低耦合。
- AOP(面向切面编程):将日志、事务、权限等横切逻辑从业务代码中抽取出来,通过动态代理在运行时织入,实现无侵入式扩展。
3.2 Spring Bean 的作用域有哪些?
- singleton:默认作用域,容器中只存在一个实例。
- prototype:每次获取都创建新实例。
- request:每次 HTTP 请求创建一个实例,仅 Web 环境有效。
- session:每个会话创建一个实例,仅 Web 环境有效。
- application:整个 ServletContext 共享一个实例。
3.3 Spring 如何解决循环依赖?
Spring 通过三级缓存解决单例 Bean 的循环依赖:
- 一级缓存:存放完整创建好的 Bean。
- 二级缓存:存放早期暴露的 Bean(未完成属性填充)。
- 三级缓存:存放 Bean 的工厂对象(ObjectFactory)。
下面通过一个配置类示例展示两个相互依赖的 Bean 如何通过 @Autowired 注入,以及三级缓存如何解决该循环依赖:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Autowired;
@Configuration
public class CircularDependencyConfig {
// 创建 BeanA,并注入 BeanB
@Bean
public BeanA beanA(@Autowired BeanB beanB) {
return new BeanA(beanB);
}
// 创建 BeanB,并注入 BeanA
@Bean
public BeanB beanB(@Autowired BeanA beanA) {
return new BeanB(beanA);
}
}
class BeanA {
private final BeanB beanB;
public BeanA(BeanB beanB) {
this.beanB = beanB;
}
}
class BeanB {
private final BeanA beanA;
public BeanB(BeanA beanA) {
this.beanA = beanA;
}
}
三级缓存解决循环依赖的流程说明:
- 实例化 BeanA :容器开始创建
beanA,此时beanA尚未完成属性填充,Spring 将其早期引用 (一个ObjectFactory工厂对象)放入三级缓存 (singletonFactories)。 - 填充 BeanA 依赖 :
beanA需要注入beanB,容器转而创建beanB。 - 实例化 BeanB :
beanB创建后同样被放入三级缓存,随后填充依赖时发现需要注入beanA。 - 从缓存获取 BeanA 早期引用 :容器在三级缓存中找到
beanA的ObjectFactory,调用其getObject()得到beanA的早期引用(此时beanA尚未完全初始化),将其注入beanB。beanB完成创建后放入一级缓存 (singletonObjects)。 - 完成 BeanA 创建 :
beanB创建完成后,容器继续完成beanA的属性填充和初始化,最终将完整的beanA放入一级缓存。
关键点 :三级缓存中的 ObjectFactory 是解决循环依赖的核心------它允许在 Bean 未完全初始化时提前暴露一个可用的引用,从而打破 A、B 之间的创建死锁。注意:该机制仅对单例(singleton)且非构造器注入 的循环依赖有效;若使用构造器注入,则无法通过三级缓存解决,会抛出 BeanCurrentlyInCreationException。
当 A 依赖 B、B 依赖 A 时,A 创建后提前暴露到三级缓存,B 创建时从缓存中获取 A 的早期引用完成注入,从而打破循环。
4. Spring MVC
4.1 Spring MVC 的执行流程是什么?
- 客户端发送请求,由
DispatcherServlet接收。 DispatcherServlet调用HandlerMapping找到对应的Handler(Controller 方法)。- 通过
HandlerAdapter执行处理器方法。 - 方法返回
ModelAndView或直接返回数据。 ViewResolver解析视图,渲染后响应给客户端。
#mermaid-svg-LLrlZYkJ4BSURifx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LLrlZYkJ4BSURifx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LLrlZYkJ4BSURifx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LLrlZYkJ4BSURifx .error-icon{fill:#552222;}#mermaid-svg-LLrlZYkJ4BSURifx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LLrlZYkJ4BSURifx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LLrlZYkJ4BSURifx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LLrlZYkJ4BSURifx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LLrlZYkJ4BSURifx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LLrlZYkJ4BSURifx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LLrlZYkJ4BSURifx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LLrlZYkJ4BSURifx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LLrlZYkJ4BSURifx .marker.cross{stroke:#333333;}#mermaid-svg-LLrlZYkJ4BSURifx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LLrlZYkJ4BSURifx p{margin:0;}#mermaid-svg-LLrlZYkJ4BSURifx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LLrlZYkJ4BSURifx .cluster-label text{fill:#333;}#mermaid-svg-LLrlZYkJ4BSURifx .cluster-label span{color:#333;}#mermaid-svg-LLrlZYkJ4BSURifx .cluster-label span p{background-color:transparent;}#mermaid-svg-LLrlZYkJ4BSURifx .label text,#mermaid-svg-LLrlZYkJ4BSURifx span{fill:#333;color:#333;}#mermaid-svg-LLrlZYkJ4BSURifx .node rect,#mermaid-svg-LLrlZYkJ4BSURifx .node circle,#mermaid-svg-LLrlZYkJ4BSURifx .node ellipse,#mermaid-svg-LLrlZYkJ4BSURifx .node polygon,#mermaid-svg-LLrlZYkJ4BSURifx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LLrlZYkJ4BSURifx .rough-node .label text,#mermaid-svg-LLrlZYkJ4BSURifx .node .label text,#mermaid-svg-LLrlZYkJ4BSURifx .image-shape .label,#mermaid-svg-LLrlZYkJ4BSURifx .icon-shape .label{text-anchor:middle;}#mermaid-svg-LLrlZYkJ4BSURifx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LLrlZYkJ4BSURifx .rough-node .label,#mermaid-svg-LLrlZYkJ4BSURifx .node .label,#mermaid-svg-LLrlZYkJ4BSURifx .image-shape .label,#mermaid-svg-LLrlZYkJ4BSURifx .icon-shape .label{text-align:center;}#mermaid-svg-LLrlZYkJ4BSURifx .node.clickable{cursor:pointer;}#mermaid-svg-LLrlZYkJ4BSURifx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LLrlZYkJ4BSURifx .arrowheadPath{fill:#333333;}#mermaid-svg-LLrlZYkJ4BSURifx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LLrlZYkJ4BSURifx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LLrlZYkJ4BSURifx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LLrlZYkJ4BSURifx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LLrlZYkJ4BSURifx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LLrlZYkJ4BSURifx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LLrlZYkJ4BSURifx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LLrlZYkJ4BSURifx .cluster text{fill:#333;}#mermaid-svg-LLrlZYkJ4BSURifx .cluster span{color:#333;}#mermaid-svg-LLrlZYkJ4BSURifx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LLrlZYkJ4BSURifx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LLrlZYkJ4BSURifx rect.text{fill:none;stroke-width:0;}#mermaid-svg-LLrlZYkJ4BSURifx .icon-shape,#mermaid-svg-LLrlZYkJ4BSURifx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LLrlZYkJ4BSURifx .icon-shape p,#mermaid-svg-LLrlZYkJ4BSURifx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LLrlZYkJ4BSURifx .icon-shape .label rect,#mermaid-svg-LLrlZYkJ4BSURifx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LLrlZYkJ4BSURifx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LLrlZYkJ4BSURifx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LLrlZYkJ4BSURifx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端请求
DispatcherServlet
HandlerMapping 查找处理器
HandlerAdapter 执行方法
返回 ModelAndView
ViewResolver 解析视图
渲染响应
4.2 @Controller 和 @RestController 的区别?
@Controller:返回视图名称,配合ViewResolver渲染页面;如需返回 JSON 需加@ResponseBody。@RestController:是@Controller+@ResponseBody的组合注解,方法返回值直接以 JSON/XML 形式写入响应体,适合前后端分离项目。
5. MyBatis 与数据库
5.1 #{} 和 ${} 的区别?
#{}:预编译占位符,底层使用PreparedStatement,能有效防止 SQL 注入。${}:字符串拼接,直接替换 SQL 片段,存在 SQL 注入风险,一般仅用于动态表名、排序字段等不可控场景。
5.2 什么是数据库事务?事务的四大特性是什么?
事务是一组要么全部成功、要么全部回滚的数据库操作。四大特性(ACID):
- 原子性(Atomicity):事务内操作不可分割,全部成功或全部失败。
- 一致性(Consistency):事务执行前后数据保持一致状态。
- 隔离性(Isolation):并发事务之间相互隔离。
- 持久性(Durability):事务提交后对数据的修改永久保存。
5.3 什么是索引?索引失效的常见场景有哪些?
索引是帮助数据库高效获取数据的数据结构(如 B+ 树),能显著提升查询速度。常见失效场景:
- 对索引列使用函数或计算,如
WHERE YEAR(create_time) = 2026。 - 使用
LIKE '%关键字'前置模糊查询。 - 隐式类型转换导致索引失效。
- 使用
OR连接非索引列。 - 索引列参与
!=、NOT IN等操作。
6. 分布式与微服务
6.1 什么是 CAP 定理?
CAP 定理指出分布式系统无法同时满足以下三点,最多只能满足其中两个:
- 一致性(Consistency):所有节点在同一时刻看到相同的数据。
- 可用性(Availability):每个请求都能在合理时间内获得响应。
- 分区容错性(Partition tolerance):网络分区时系统仍能继续运行。
实际分布式系统中,分区容错性(P)是必须保证的,因此通常在 C 和 A 之间权衡。
6.2 什么是分布式事务?常见解决方案有哪些?
分布式事务指跨多个服务或数据库的事务操作。常见解决方案:
- 两阶段提交(2PC):协调者统一协调提交或回滚,实现强一致性。
- TCC(Try-Confirm-Cancel):通过预留资源、确认、补偿三个阶段实现最终一致性。
- 本地消息表 + 消息队列:通过 MQ 异步通知下游,配合本地消息表保证最终一致性。
- Seata AT 模式:基于全局事务管理器,对业务侵入小。
7. 缓存与消息队列
7.1 Redis 常见的数据结构有哪些?
- String:字符串,可做缓存、计数器。
- Hash:哈希表,适合存储对象。
- List:列表,适合消息队列、最新列表。
- Set:无序集合,适合去重、共同好友。
7.2 Redis 持久化机制(RDB 与 AOF)的区别
Redis 默认将数据保存在内存中,重启后数据会丢失,因此需要通过持久化机制将数据落盘。Redis 提供了 RDB(快照) 和 AOF(追加日志) 两种持久化方式,二者对比如下:
| 对比项 | RDB(快照) | AOF(追加日志) |
|---|---|---|
| 持久化方式 | 将内存数据以二进制快照形式写入磁盘 | 将每次写操作命令以日志形式追加到文件 |
| 触发机制 | 手动触发(save/bgsave)或按配置自动触发(如 save 900 1) |
按 appendfsync 策略触发(always/everysec/no) |
| 恢复速度 | 快,直接加载快照文件 | 慢,需逐条重放日志命令 |
| 数据安全性 | 较低,两次快照之间可能丢失数据 | 较高,可配置为每秒甚至每次写操作同步 |
| 文件大小 | 较小,快照为压缩后的二进制数据 | 较大,日志会持续累积,需定期 rewrite 压缩 |
| 性能影响 | 主进程 fork 子进程写盘,对主进程影响小 | 写操作需同步写日志,always 模式性能开销大 |
适用场景说明:
-
RDB 适用场景:对数据丢失容忍度较高、追求快速恢复的场景,如缓存数据、非关键业务数据;也适合做数据备份和灾难恢复,快照文件便于迁移。
-
AOF 适用场景 :对数据安全性要求高、不允许丢失过多数据的场景,如订单、账户等关键业务数据;可通过
appendfsync everysec在性能与安全之间取得平衡。 -
生产实践:通常同时开启 RDB 与 AOF,RDB 用于快速恢复和备份,AOF 用于保证数据完整性;Redis 重启时会优先加载 AOF 文件(数据更完整),若 AOF 关闭则加载 RDB。
-
ZSet(有序集合):带分数的有序集合,适合排行榜。
7.2 什么是缓存穿透、缓存击穿、缓存雪崩?
- 缓存穿透:查询不存在的数据,缓存和数据库都没有,导致请求直接打到数据库。解决:布隆过滤器、缓存空值。
- 缓存击穿:某个热点 key 过期瞬间,大量请求同时打到数据库。解决:互斥锁、逻辑过期。
- 缓存雪崩:大量 key 同时过期或 Redis 宕机,导致数据库压力骤增。解决:过期时间加随机值、集群高可用。
7.3 消息队列的作用是什么?
- 异步处理:将耗时操作异步化,提升接口响应速度。
- 削峰填谷:应对瞬时高并发流量,平滑请求压力。
- 解耦:生产者和消费者互不依赖,降低系统耦合度。
8. 常见面试追问与总结
面试中除了考察知识点本身,还会追问底层原理和实际场景。建议重点准备:
详细解答:Spring 源码中 Bean 的创建流程
Spring 中单例 Bean 的创建核心入口是 AbstractBeanFactory.doGetBean(),最终由 AbstractAutowireCapableBeanFactory.createBean() 完成实例化。整体流程如下:
- 实例化(Instantiation) :通过构造器或工厂方法创建 Bean 的原始实例,对应
createBeanInstance(),默认使用无参构造器,也可通过@Autowired构造器注入。 - 属性填充(Populate) :调用
populateBean(),将@Autowired、@Resource、@Value等注解标记的依赖注入到 Bean 中,同时处理循环依赖(提前暴露到三级缓存)。 - 初始化(Initialization) :
- 调用
BeanNameAware、BeanFactoryAware、ApplicationContextAware等 Aware 接口回调。 - 执行
BeanPostProcessor.postProcessBeforeInitialization()。 - 调用
InitializingBean.afterPropertiesSet()或自定义init-method。 - 执行
BeanPostProcessor.postProcessAfterInitialization()(此处是 AOP 动态代理生成的关键节点)。
- 调用
- 注册与使用 :初始化完成后,单例 Bean 被放入一级缓存(
singletonObjects),供后续依赖注入和getBean()获取。
关键点 :AOP 代理正是在 postProcessAfterInitialization() 阶段通过 AbstractAutoProxyCreator 生成,因此最终放入容器的可能是代理对象而非原始 Bean。
- Spring 源码中 Bean 的创建流程。
详细解答:MyBatis 一级缓存与二级缓存的区别
| 对比项 | 一级缓存(Local Cache) | 二级缓存(Global Cache) |
|---|---|---|
| 作用范围 | SqlSession 级别,默认开启 | Mapper(Namespace)级别,需手动开启 |
| 生命周期 | 随 SqlSession 创建而存在,关闭或提交时清空 | 跨 SqlSession 共享,随应用存活 |
| 存储结构 | 本地 HashMap(PerpetualCache) | 可配置多种缓存实现(如 Ehcache、Redis) |
| 失效时机 | 执行增删改(insert/update/delete)或调用 clearCache() 时清空 |
执行增删改时按命名空间清空对应缓存 |
| 并发安全 | 单会话内使用,无需考虑并发 | 多会话共享,需考虑并发与序列化 |
原理说明:
- 一级缓存 :MyBatis 默认开启,基于
SqlSession中的Executor维护。同一个 SqlSession 内执行相同 SQL(相同参数)时,第二次查询直接命中缓存,不再访问数据库。注意:Spring 整合 MyBatis 时,每次请求通常新建 SqlSession,因此一级缓存作用有限。 - 二级缓存 :在
mybatis-config.xml中设置<setting name="cacheEnabled" value="true"/>,并在 Mapper XML 中添加<cache/>标签开启。多个 SqlSession 共享同一命名空间的二级缓存,查询结果以序列化形式存储,因此实体类需实现Serializable。开启后,一级缓存的结果在 SqlSession 提交时同步到二级缓存。
面试加分点 :二级缓存存在脏读风险------当多表关联查询时,若另一张表的数据被修改,相关命名空间的缓存不会自动失效,因此复杂业务场景下需谨慎使用,或通过 <cache-ref> 关联命名空间。
- MyBatis 一级缓存与二级缓存的区别。
- 数据库索引底层 B+ 树的结构与优势。
- Redis 持久化机制(RDB 与 AOF)的区别。
- 微服务中服务注册与发现、负载均衡的实现原理。
掌握以上核心知识点,并能够结合项目实践讲清楚「为什么这样做」,是拿下 Java Web 面试的关键。