简介:面向 Java 求职者与初中级开发者的面试复习资料,聚焦后端面试中反复出现的高频考点。内容以要点梳理为主,涵盖 Spring Bean 从实例化、依赖注入到初始化与销毁的完整生命周期,Spring MVC 中前端控制器、处理器映射与适配器、视图解析的执行链路,以及 Spring Boot 自动装配里 @SpringBootApplication 三个核心注解与 @Conditional 条件注册机制;数据库部分整理 SQL 执行计划的各字段含义、常见索引类型、索引失效场景与 B+ 树结构,并附带线程池核心参数与拒绝策略。压缩包内为 1 个 pdf 文件,体积约 396KB,条目多为提纲式罗列,轻量易读,适合利用碎片时间翻阅。目前已有 180 人学习,可作为面试前快速串联知识点、对照查漏补缺的复习材料。
1. Java 高频面试题到底是什么:一份把散点连成线的知识地图
有人准备 Java 面试,像收拾一个没有隔层的行李箱:Spring、线程池、索引、集合,一件件往里塞,塞到最后拉链都拉不上,真要用的时候又翻不出来。所谓“Java 高频面试题”,本质上不是一堆题目,而是把这些散落的知识点按真实的调用链重新排布的一张地图——请求怎么进系统、对象怎么被创建和销毁、一条 SQL 怎么被数据库执行。搞懂这张地图,你记住的就不再是孤立答案,而是答案为什么长这样。地图的价值在于:任何一个点被追问,你都能顺着线往回走,而不是当场卡壳。
2. Spring 的三条主线:Bean 生命周期、MVC 流程与自动装配
Spring 相关的问题之所以让人头疼,是因为它不像一个函数那样有明确的输入输出,而更像一整套“谁在什么时候调用谁”的秩序。这套秩序有三条主线:一个对象从生到死怎么走、一次请求从进门到出门怎么走、一堆配置为什么突然不用写了。把这三条线各自走一遍,Spring 的大半问题就自动归位了。
2.1 Bean 从创建到销毁:一条有去有回的流水线
打个比方,Spring 容器像一家装修公司,Bean 是它交付的房子。房子不是凭空变出来的,得先有毛坯(实例化),再通水电、搬家具(依赖注入),然后验收通水电是否正常(初始化),住上一段时间(使用),最后拆迁时把该拆的拆干净(销毁)。这条流水线的每一段,Spring 都留了“你自己动手”的口子,面试问的正是这些口子在哪、谁先谁后。
实例化有两种常见方式。一种是反射:容器拿到类的全限定名,通过反射机制创建实例,灵活但有一定的运行时开销;另一种是工厂方法:容器调用某个工厂类的指定方法,由方法内部决定返回哪个实现,适合创建过程复杂、需要按条件选择实现的对象。前者像照着图纸盖房,后者像找施工队代建,图纸在别人手里但结果一样。
依赖注入是把“这栋房子需要的资源”送进去,包括两类:一是普通属性值,比如端口号、超时时间;二是对其他 Bean 的引用,比如订单服务需要用户服务。这两件事做完,对象在数据上才算“完整”,但往往还不能直接干活——连接池没预热、配置没校验、缓存没加载,一调用就翻车。
初始化阶段就是补这些动作。两条路子:实现InitializingBean接口,容器会回调afterPropertiesSet();或者在配置里用init-method指定一个自定义方法。前者是“跟框架签合同”,后者是“自己写个约定”,通常接口方式先执行、配置方式后执行。对应的销毁侧也有两条:实现DisposableBean的destroy(),或用destroy-method指定方法,用来关连接、刷缓冲、停线程。
public class ReportService implements InitializingBean, DisposableBean { private String outputPath; // 依赖注入:容器把配置里的值塞进来 public void setOutputPath(String outputPath) { this.outputPath = outputPath; } @Override public void afterPropertiesSet() { // 初始化:此时属性已注入完毕,适合做目录检查、资源预热 if (outputPath == null) { throw new IllegalStateException("outputPath 未配置"); } } public void customInit() { // 也可以由 init-method 指向这个方法,做业务层面的初始化 } @Override public void destroy() { // 销毁:容器关闭时释放文件句柄、线程池等资源 } }这段代码的关键不在语法,而在时序:afterPropertiesSet()被调用时,outputPath一定已经有值了,所以可以放心做校验;如果把它写在构造函数里,注入还没发生,拿到的只能是 null,这就是很多人踩过的坑。销毁方法同理,它保证在容器关闭前被调到,让你有机会做“后悔药”式的清理。
为什么要有这么一套看起来啰嗦的流程?因为对象的“可用”和“被创建”从来不是同一时刻。框架把中间那段空档开放出来,你才能把准备动作塞进去,而不是在每个调用点前手写一遍检查。
2.2 一次请求穿过 Spring MVC 的七个站点
把 Spring MVC 的执行流程想成一家餐厅的传菜链路,会好懂得多。客户端是点菜的客人,DispatcherServlet是前厅经理,所有客人进门第一眼看到的都是他;HandlerMapping是座位表,负责把“这位客人要找哪张桌子”对上号;HandlerAdapter是传菜员,负责把菜真的送到后厨并端回来;Controller是厨师;ViewResolver是摆盘说明;最后装盘上桌,就是视图渲染和响应。
具体走一遍:客人点单(浏览器发出 HTTP 请求),前厅经理接单,这是整个链路的核心,它拦截所有进入系统的请求。经理查座位表,把请求的 URL 和某个处理方法建立映射关系。找到人之后,不是经理亲自下厨,而是交给传菜员(处理器适配器)去调用控制器方法,适配器负责让调用顺利发生并处理请求。厨师按参数做业务、准备模型数据,把结果交回传菜员。经理再拿视图解析器把逻辑视图名翻译成真正的页面,渲染成 HTML 之类的内容,最后返回给客户端。
这里有个容易被忽略的细节:整条链路上,除了控制器处理请求、视图渲染和响应客户端这三段,其余环节都需要把结果回传给前端控制器。也就是说,DispatcherServlet不是“发号施令就撒手”,而是每个站点干完活都要回来汇报,它再决定下一步去哪。这也是它被称为“前端控制器”的原因——统一入口、统一调度。
为什么非要绕这么一圈,而不是 URL 直接映射到 Servlet?因为一旦直连,鉴权、日志、编码、异常处理这些横切逻辑就得在每个 Servlet 里重复一遍。集中到一个入口之后,这些事只做一次;URL 与方法的映射可以配置、可以改,控制器只管业务,谁也不越界。代价是链路变长,出问题时你得知道该在哪个站点看日志——这也是为什么面试官爱问流程,他其实在问你有没有真正排查过请求。
2.3 自动装配:为什么加个注解就能少写一堆配置
刚接触 Spring Boot 的人常有一种玄学感:明明什么配置都没写,数据源、事务管理器、Web 环境全都就位了。这份“凭空出现”的能力,来源就在启动类上那个@SpringBootApplication。
它其实是三个注解的组合。@SpringBootConfiguration说明当前类是一个配置类;@ComponentScan默认扫描启动类所在包及其子包,把带组件注解的类收进容器;@EnableAutoConfiguration才是自动装配的开关,它底层封装了@Import,导入一个选择器实现类,由这个选择器去调用获取自动配置条目的方法,再进一步读取候选配置类清单。
候选配置类长什么样?本质上就是普通的配置类,里面用@Bean标注方法,再叠加一堆@Conditional开头的条件注解。条件满足,Spring 就自动调用这个方法,把返回值注册进 IoC 容器;条件不满足,整个配置类或某个方法就被跳过,安静得像没存在过。
@Configuration public class StorageAutoConfig { @Bean @ConditionalOnMissingBean // 用户自己定义过同类型 Bean 时,这段就不生效 public StorageClient storageClient() { return new LocalStorageClient(); // 默认实现,开箱即用 } }@ConditionalOnMissingBean是整个机制的缩影:它表达的是“如果你没配,我才给你兜底”。这样默认配置和自定义配置不会打架,用户想覆盖时也不需要删代码。参数上要注意,条件判断的依据是容器里已注册的 Bean 类型,而不是名字,所以自定义 Bean 的类型必须对得上,否则会出现两个实现同时存在的尴尬。
自动装配解决的是“样板配置太多”这个问题。没有它,一个中等项目光数据源、MVC、序列化的配置就能写几百行;有了它,默认值先跑起来,需要改的地方再覆盖。但它也带来一个副作用:配置变成了黑匣子,出问题时不知道是哪个自动配置类在起作用。血泪经验是,排查这类问题要先确认容器里到底注册了哪些 Bean,再回头找对应的条件注解,而不是凭感觉改配置文件。
3. 数据库那层黑匣子:执行计划、索引和 B+ 树
一条 SQL 慢下来的时候,最没用的动作是“加个索引试试”。数据库不是靠猜的,它每次都给你留了一份执行说明,只是大部分人没去看。这一层要搞懂三件事:怎么读那份说明、索引有哪几种、以及索引为什么能快。
3.1 EXPLAIN 的每一列在回答什么问题
在 MySQL 里,只要在查询语句前面加上explain关键字,就能拿到这条 SQL 的执行计划。所谓执行计划,就是数据库打算怎么干这活:先读哪张表、走不走索引、大概扫多少行。
-- 查某个用户的订单,只看需要的列,避免 SELECT * explain select id, amount, created_at from orders where user_id = 1001 order by created_at desc limit 20;结果里那几列各有分工,读懂它们比背结论有用得多。
| 列名 | 它在回答什么 | 怎么用 |
|---|---|---|
| id | 查询的序列号 | id 相同从上到下执行;id 不同则大的优先级高 |
| select_type | 这条查询的类型 | 区分简单查询、子查询、联合查询等 |
| table | 这一行讲的是哪张表 | 多表时按顺序看 |
| type | 访问类型 | 判断性能好坏的核心列 |
| possible_keys | 可能用上的索引 | 为空说明压根没有相关索引 |
| key | 实际用上的索引 | 为 null 表示没走索引 |
| key_len | 用到的索引长度 | 不损失精度时越短越好 |
| ref | 哪个列或常数和索引一起比较 | 看连接条件是否走索引 |
| rows | 估算要检查的行数 | 越小越好,是估算不是精确值 |
| Extra | 额外执行信息 | 出现临时表、文件排序就要注意 |
type这一列最值得记住,性能由好到坏大致是:system、const、eq_ref、ref、range、index、all。const是查询条件精确匹配到唯一索引的单一值,能立刻定位到那一行;eq_ref常见于连接查询,被驱动表的索引是唯一索引;ref用的是非唯一索引;range是范围扫描;index虽然走了索引但要扫整棵索引树;all就是全表扫描。看到all和很大的rows,基本可以判断这条 SQL 在硬扛。
3.2 索引家族与失效的常见触发条件
索引不是一种东西,是一族。按结构分,最常用的是 B-Tree 索引,也是默认类型;R-Tree 面向空间数据;Hash 索引擅长等值匹配但不支持范围;Full-text 用于全文检索。按用途分,有主键索引、普通索引、唯一索引、组合索引、覆盖索引、逆序索引,它们的差别在约束和列的排列方式上。
真正让人头疼的不是建索引,而是建了不生效。常见触发条件有这么几类:在索引列上使用函数或表达式,等于让数据库先算一遍再比,索引的有序性用不上;使用not或!=,因为“不等于”往往意味着要扫的范围太大;用or连接的条件里如果有一侧没索引,整体就可能退化成全表扫描;对索引列做运算,同样破坏了可比性;like用前置百分号,前缀不确定就无法在树里定位起点;多个索引同时存在时,优化器通常只会选一个(索引合并是另一回事),其余的就白建了。
还有几条写 SQL 时的习惯值得记:查询时明确列出字段,别用select *;in里的值不要塞太多;分页只需要一页时用limit 1;能用内连接就别用外连接;排序字段没走索引时尽量别排序;where里少做 null 判断;能用union all就别用union,后者要多做一次去重。这些不是教条,它们背后都指向同一件事:让优化器有索引可用、让扫描范围尽量小。
3.3 B+ 树为什么能撑住千万级数据
索引之所以快,是因为它把“从头找到尾”换成了“每一层砍掉一大片”。B+ 树是一种多路平衡查找树,用平衡树的思想支持增删改查。它有两个关键特征:所有数据都存在叶子节点上,叶子节点之间用指针串成链表;非叶子节点只存索引不存实际数据。
第一个特征让范围查询变得便宜——找到起点后顺着链表走就行,不用反复回到上层。第二个特征让每个非叶子节点能装下更多索引项,从而把树高压下去。算一笔账:一个非叶子节点大致 16KB,一个 key 一般 8 字节,一个指针一般 6 字节,一条索引项约 14 字节,那么一个节点大约能放一千条左右。这样第二层就能覆盖百万级数据,第三层能到千万级,所以 B+ 树的高度一般在 2 到 4 层。查一条记录,最多读几次磁盘页,这就是它撑得住大表的底气。
为什么不用二叉树?因为二叉树每个节点只有两个分叉,数据量一大树高就上去了,每下一层就是一次磁盘 IO。多路的意义就是把“层数”换成“每层的分叉数”,用更少的 IO 换同样的定位精度。
4. 并发与容器:线程池、线程状态和 HashMap 的取舍
并发这块的知识点有个共同点:单看每个概念都懂,一放到一起就容易串味。线程池的参数和拒绝策略、线程的几种状态和退出方式、HashMap 的结构演进,其实都在回答同一个问题——资源有限的时候,怎么安排才不会互相拖死。
4.1 线程池的七个参数与四种拒绝策略
线程池最直白的理解是“一队固定工人加一个待办筐”。任务来了先找空闲工人,没有就放进筐里排队,筐满了再临时加人,人和筐都到顶了才谈拒绝。这套调度规则全靠构造参数描述。
| 参数 | 含义 | 调错会怎样 |
|---|---|---|
| corePoolSize | 核心线程数,常驻 | 太小则任务排队,太大则空转耗资源 |
| maximumPoolSize | 线程数上限,等于核心线程加救急线程 | 设得和核心数一样,队列满后就直接拒绝 |
| keepAliveTime | 救急线程的存活时间 | 太长则空闲线程占资源,太短则频繁创建销毁 |
| unit | 上面的时间单位 | 秒、毫秒等,别写错量级 |
| workQueue | 核心线程忙时新任务的排队队列 | 无界队列会让上限参数形同虚设 |
| threadFactory | 线程工厂,可定制名字、是否守护线程 | 不命名会让日志和线程栈难以排查 |
| handler | 拒绝策略,队列和线程都满时触发 | 选错会导致任务丢失或调用方被拖慢 |
JDK 自带的四种拒绝策略各有性格:直接抛异常是默认策略,问题暴露得最快;用调用者所在线程执行任务,相当于把压力反压回提交方,能自然限流;丢弃队列里最靠前的一个任务再执行当前任务;以及直接丢弃新任务。选哪个,取决于你更能接受“丢任务”还是“拖慢上游”。
工具类里还提供了几种预置池:固定大小、线程数可动态调整且用无界队列、单线程保证顺序执行、以及支持定时和周期任务。它们用起来方便,但参数被写死了,生产环境里更稳妥的做法是自己构造,把队列容量和拒绝策略显式写出来。
ThreadPoolExecutor pool = new ThreadPoolExecutor( 4, // 核心线程数,常态下保持的工人数量 8, // 上限,包含临时增援的救急线程 60L, // 救急线程空闲 60 秒后回收 TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), // 有界队列,逼出拒绝策略而不是无限堆积 r -> new Thread(r, "order-pool"), // 线程命名,出问题时能一眼定位 new ThreadPoolExecutor.CallerRunsPolicy() // 满了就由提交线程自己跑,形成反压 ); pool.execute(() -> handleOrder()); // 提交任务,注意异常会被吞掉,需要自行捕获这段配置的用意在于“有界”:队列有界、线程有界、拒绝策略明确。无界队列最大的问题是它永远不会触发拒绝,任务越堆越多,最后不是线程池崩,而是内存先撑不住。
线程池自身还有五个状态:正常运行、不再接新任务但会处理完队列、不再接新任务并尝试中断执行中的任务、所有任务执行完毕正在回收资源、完全终止。想观察它的运行情况,可以看当前线程数、正在执行任务的线程数、已提交未执行的任务数这几个指标;想停下来,一个方法是等已提交任务跑完再关,另一个是立即停止并返回没执行完的任务列表。前者温和,后者干脆,别指望后者还能保住队列里的活。
4.2 线程的六种状态与优雅退出
线程的状态不是玄学,是有明确定义的六种:新建(还没启动)、可运行(可以跑,但不代表正在跑,还要等调度)、运行中(正在虚拟机里执行)、阻塞(等锁)、等待(调用了等待或加入之类的方法,需要别人通知)、超时等待(等了但有期限,到点自动恢复),以及终止(跑完或异常退出)。
状态图之所以重要,是因为它决定了你怎么让线程“好好停”。直接杀线程是最糟的做法,正确姿势有三步:设置一个退出标志,让线程在循环里自己检查;对于正在阻塞的线程,用中断方法把它从阻塞里叫醒,让它抛中断异常后退出;对于还要等结果的场景,用加入方法等目标线程结束。线程收尾时别忘了清理资源——关文件、断连接,否则表面上线程没了,句柄还挂着。
多线程还有三大特性需要分清:原子性指一组操作要么全做要么全不做,单次读取或赋值通常是原子的,但自增这种复合操作不是;可见性指一个线程改了共享变量,别的线程能立刻看到;有序性指代码按书写顺序执行,但编译优化和指令重排可能打乱它。解决手段也对应着来:加锁保证原子性和有序,volatile保证可见性和一定的有序,锁机制同样能同步数据。这三者经常被混着答,后面会专门拆开说。
4.3 HashMap 的结构演进与那个“死循环”
HashMap 提供键到值的映射,特点是查询高效、非线程安全、允许一个 null 键和多个 null 值。它的存储结构随着版本变过一次:早期是数组加链表,后来变成数组加链表加红黑树。链表转红黑树的目的是在哈希冲突严重时把查找从线性拉回到对数级,避免极端情况下退化成一条长链。
早期版本还有一个著名问题:扩容时链表采用头插法,数据迁移过程中可能把链表的顺序倒过来,在多线程并发扩容下形成环,于是查询时陷入死循环。后来改成尾插法,这个问题被解决。这段历史常被当成“为什么 HashMap 不安全”的标准答案,但更准确的说法是:它不安全的原因不止一个,扩容成环只是最出名的那一个,并发写入导致的数据覆盖同样致命。所以需要并发场景时,该换容器就换容器,别指望加个同步关键字就万事大吉。
5. 常见误区与易混区别:五组最容易答反的概念
有些概念单独看都懂,放在一起就说不清了。不是记性差,而是它们本来就共享一部分语义,区别藏在“谁管什么、什么时候生效”这些边界上。
5.1 过滤器与拦截器:一个管大门,一个管业务门口
易混点在于两者都叫“拦截”,都能在请求前后插一脚,很多人干脆认为拦截器是过滤器的升级版。之所以混,是因为它们的触发时机看起来高度重叠,都在请求进入业务代码之前。
区分的关键在层次和依赖。过滤器基于 Servlet 规范,属于容器级别,能拦下进入容器的所有请求,静态资源、动态资源都算,甚至可以改写请求头;拦截器在框架层面,只作用于框架管理的请求,能拿到方法参数、目标对象这些业务信息。实现机制也不同:过滤器靠函数回调,实现固定接口;拦截器靠反射和动态代理,控制粒度更细,能决定拦哪些方法。生命周期上,过滤器随应用启动而创建、随应用关闭而销毁,拦截器则随请求创建、随请求结束而消失,短得多。执行顺序上,过滤器在前,拦截器在后。所以该用谁很清楚:编码、安全校验、日志这类全局且与业务无关的事交给过滤器;权限判断、参数校验这类需要知道“调用的是哪个方法”的事交给拦截器。
5.2 接口与抽象类:选择依据在“是不是”还是“能不能”
这两者都能实现多态,都能定义未实现的方法,于是很多人把它们当成可以互换的两种写法。混淆的根源是它们确实在“约束子类必须实现某些行为”这件事上功能重叠。
真正的分水岭是继承关系。接口表达的是“具备某种能力”,一个类可以实现多个接口,所以支持多继承式的组合;抽象类表达的是“是某类东西”,遵循单一继承,一个类只能继承一个抽象类,但可以同时实现多个接口。接口是纯抽象的类型,只声明不实现;抽象类可以包含已实现的方法和状态字段,适合放多个子类共用的逻辑。判断标准很朴素:如果子类之间是“是不是”的关系,用抽象类;如果只是“能不能做某件事”,用接口。还有一个现实因素:抽象类会占用唯一的继承名额,接口不会。
5.3 两种循环依赖:构造注入和 setter 注入为什么结局不同
循环依赖指的是对象之间的依赖构成闭环:自己依赖自己、两个 Bean 互相依赖、或者多个对象绕成一圈。易混点在于很多人以为“循环依赖是错误,必须消灭”,但实际表现取决于注入方式。
为什么混?因为同样是循环依赖,用字段注入或 setter 注入时框架能自动处理,换成构造注入却直接启动失败,看起来像是同一件事有两种脾气。区别在于对象什么时候“算存在”。setter 注入时,容器可以先创建出实例(此时依赖还是空的),把实例的引用提前暴露出去,再慢慢注入,于是环被打断。构造注入则要求依赖必须在创建时就位,A 要创建得先有 B,B 要创建得先有 A,容器没有中间状态可以借用,只能报错。
解决路径也分两种。彻底的做法是重构,把环拆掉,但改动范围常常不可控。折中的做法有三条:改用字段注入或 setter 注入;在某个注入点上加延迟注解,让对象用到时才创建;或者依赖容器内部那三层缓存的机制自动处理——第一层放完整对象,第二层放实例化完成但还没注入完的半成品,第三层放创建工厂,查找时逐层往下问,找到就升级到上一层并从下一层移除。理解这三层的顺序,比背“三级缓存”四个字有用得多。
5.4 原子性、可见性、有序性:三个词不是同义词
这三个词经常被连在一起背,背完却说不清哪个问题该用哪个手段解决。混乱来自它们都描述“多线程下行为不符合预期”,症状相似,病因完全不同。
原子性管的是“操作能不能被打断”,自增这种读-改-写三步走的操作,中间可能被别的线程插进来,结果就少加了一次。可见性管的是“改了别人能不能看见”,一个线程改了共享变量,另一个线程可能还在读自己工作内存里的旧值。有序性管的是“执行顺序会不会被重排”,编译器优化和处理器调度都可能让指令换个顺序跑。区分方法是对症下药:需要不可分割就用锁;需要立刻可见就用volatile或锁;需要固定顺序同样靠锁和volatile的读写屏障。volatile能解决可见性和部分有序,但解决不了原子性——这一点最容易被答错。
5.5 三类集合:List、Set、Map 各管一件事
集合框架的类很多,容易混成一个“装东西的容器”大杂烩。其实它们的分工从接口那一层就定死了:List 管顺序,元素可重复;Set 管唯一,元素不重复;Map 管映射,按键找值。
再往下看实现类的取舍。List 里,基于动态数组的实现随机访问快,中间插入删除慢;基于双向链表的实现插入删除快,随机访问慢;老式的线程安全实现性能通常不如前两者,多线程下也不见得是最优解。Set 里,哈希实现性能高但无序,链表的哈希实现保留插入顺序,红黑树实现按自然顺序或自定义比较器排序。Map 里同理:哈希实现不保证顺序,链表哈希实现保持添加顺序,红黑树实现按键排序。队列这一支还有双端队列和优先队列,前者两端操作都高效,后者每次出队都是最小或最大的那个。记住一句话就不会混:先问“要不要顺序、要不要去重、要不要按键找值”,答案自然落到某个接口上。
6. 什么时候用它补漏,什么时候别指望它救场
这份资料最合适的用法,是当一张“检查表”,而不是一本“背诵手册”。你已经写过一段时间代码,能跑通业务,但被问到“Bean 什么时候销毁”“这条 SQL 为什么没走索引”“线程池队列满了会怎样”时答得含糊,这时候顺着章节把调用链走一遍,把散落的认识串起来,收益最大。反过来,如果连基本语法和常见 API 都还没写熟,直接啃这些结论,记住的只是词,遇到稍微变形的追问照样卡住——那些答案是从实践里长出来的,没有实践,背下来也是浮的。
还有一种情况要避开:把它当成“标准答案库”。同一个问题,不同项目里的取舍可能完全相反。比如线程池,固定大小加无界队列在低负载服务里很省事,在高并发场景下就是灾难;比如索引,组合索引的列顺序在这张表上是优化,换张表可能就是浪费。资料里给的是判断依据,不是判决书。
| 场景 | 适合翻 | 不适合翻 |
|---|---|---|
| 面试前一周,梳理调用链 | 适合,按主线过一遍 | —— |
| 线上问题排查,需要定位方向 | 适合,当索引用 | 不适合当唯一依据,还得看日志和监控 |
| 完全没写过 Spring 项目 | —— | 不适合,先动手写一个 |
| 想找某道题的“正确答案” | —— | 不适合,答案依赖上下文 |
举个具体的例子。有人被问到“为什么事务没回滚”,第一反应是去背注解的属性,翻遍传播行为也找不到原因。真正该做的是按顺序排除:方法是不是 public、是不是被同类内部调用导致没走代理、抛的是不是运行时异常、异常是不是被自己 try-catch 吞掉了、表本身支不支持事务、有没有跨线程。这套排查顺序比记住七个传播级别的名字有用得多,因为前者能定位,后者只能复述。
从那以后我每次准备面试,都强制自己先合上资料,在白纸上画一遍请求链路和对象生命周期,画不出来的地方才去翻对应章节;画得出来的,就不再重复看。这样一轮下来,省下的时间比通读三遍还多。希望帮到你。
本文还有配套的精品资源,点击获取