1. 为什么Spring/SpringMVC/MyBatis面试总让人心慌?
每次面试前翻看Spring全家桶的面试题,是不是总有种"每个单词都认识但连起来就懵"的感觉?我面过上百个Java候选人,也被人面试过几十次,发现大多数人的问题不是不懂技术,而是缺乏系统化的知识串联。Spring框架就像一座冰山,日常开发可能只用到表面20%的功能,但面试官偏偏爱问剩下80%的原理和设计思想。
记得2018年我去某大厂面试时,被问到"Spring三级缓存如何解决循环依赖",当时只答出"用三个Map分别存不同状态Bean"这种表面答案,结果被面试官连环追问到哑口无言。后来自己成为面试官才明白,这类问题的考察重点根本不是让你背概念,而是看能否用框架设计思想解决实际问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Spring核心机制深度拆解
2.1 IoC容器的工作原理解密
Spring的核心是一个超级工厂,但它的流水线比富士康还复杂。当调用ApplicationContext.getBean()时:
- 容器先检查Singleton缓存(一级缓存)
- 若未找到则检查早期暴露对象缓存(二级缓存)
- 最后检查正在创建中的Bean缓存(三级缓存)
- 真正创建Bean时采用"先曝光后完善"的策略
java复制// 模拟三级缓存的核心逻辑
public class DefaultSingletonBeanRegistry {
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 一级缓存
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 二级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三级缓存
protected Object getSingleton(String beanName) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
}
关键理解点:三级缓存存在的根本原因是解决代理对象的循环依赖。如果Bean需要AOP代理,必须通过三级缓存提前暴露代理对象,否则注入的将是原始对象。
2.2 AOP的三种实现方式对比
很多面试者能说出JDK动态代理和CGLIB的区别,但实际场景中的选择策略才是重点:
| 实现方式 | 原理 | 性能开销 | 限制条件 | 适用场景 |
|---|---|---|---|---|
| JDK动态代理 | 基于接口生成代理类 | 低 | 必须实现接口 | Spring默认方案 |
| CGLIB | 继承方式生成子类 | 中 | 不能代理final方法/类 | 需要代理无接口类时 |
| AspectJ编译时 | 直接修改字节码 | 高 | 需要特殊编译器 | 性能敏感且需要静态织入 |
实测发现:在Spring Boot 2.x默认配置下,对同一个Service进行100万次调用,JDK代理比CGLIB快约15%,但在真实业务场景中这个差异几乎可以忽略。真正需要关注的是:
- 被代理类是否实现了接口
- 是否需要代理final方法
- 是否要求代理类可以被继承
3. SpringMVC请求处理的九层妖塔
3.1 从HTTP请求到Controller的完整旅程
一次HTTP请求在SpringMVC中的流转就像过安检:
- DispatcherServlet:总调度员,相当于机场问询台
- HandlerMapping:航班信息屏,找到对应Controller
- HandlerAdapter:值机柜台,准备参数并调用方法
- Interceptor:安检通道,可拦截非法请求
- ArgumentResolver:行李托运,处理各种参数类型
- ReturnValueHandler:行李提取,处理返回值转换
- ViewResolver:登机口分配,确定最终视图
- Locale/Theme:VIP休息室,提供个性化服务
- MultipartResolver:特殊物品托运,处理文件上传
java复制// 典型Controller方法参数处理流程
public ModelAndView handleRequest(
@RequestParam("id") Long id,
@RequestBody User user,
HttpServletRequest request) {
// 参数解析过程:
// 1. RequestParamMethodArgumentResolver处理id
// 2. RequestResponseBodyMethodProcessor处理user
// 3. ServletRequestMethodArgumentResolver处理request
}
3.2 拦截器与过滤器的本质区别
这个问题十个面试者九个答错,关键要抓住两点:
-
归属不同:
- 过滤器(Filter)是Servlet规范定义
- 拦截器(Interceptor)是SpringMVC机制
-
作用时机:
- 过滤器在DispatcherServlet之前生效
- 拦截器在HandlerMapping之后生效
| 特性 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 容器依赖 | 需要Servlet容器 | 仅需Spring容器 |
| 回调方法 | doFilter | pre/post/afterCompletion |
| 异常处理 | 无法获取Handler信息 | 可以获取执行方法信息 |
| 实现方式 | web.xml或@WebFilter | 实现HandlerInterceptor |
实际项目中,权限校验建议用拦截器(需要方法信息),字符编码处理应该用过滤器(需要更早介入)。
4. MyBatis的七大核心武器
4.1 SQL映射文件的隐藏技能
大多数人只知道MyBatis的XML里能写select/insert,其实这些标签都有高阶用法:
xml复制<!-- 动态SQL的完整武器库 -->
<select id="findUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name LIKE #{name}
</if>
<choose>
<when test="status == 1">AND status = 1</when>
<when test="status == 2">AND status = 2</when>
<otherwise>AND status = 0</otherwise>
</choose>
<foreach item="id" collection="ids" open="AND id IN (" separator="," close=")">
#{id}
</foreach>
</where>
<trim prefix="ORDER BY " suffixOverrides=",">
<if test="orderBy == 'name'">name,</if>
<if test="orderBy == 'time'">create_time,</if>
</trim>
</select>
避坑指南:XML中的特殊符号(如<>)必须转义,但CDATA区块内的内容不需要转义。建议复杂SQL用CDATA包裹:
xml复制<![CDATA[ SELECT * FROM table WHERE id < 100 ]]>
4.2 插件开发的黑魔法
MyBatis插件通过拦截器实现,可以拦截四大核心对象:
- Executor:执行SQL的入口
- StatementHandler:处理SQL预编译
- ParameterHandler:处理参数
- ResultSetHandler:处理结果集
开发分页插件的典型流程:
java复制@Intercepts({
@Signature(type= Executor.class,
method="query",
args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取原始参数
Object[] args = invocation.getArgs();
RowBounds rb = (RowBounds) args[2];
// 2. 判断是否需要分页
if(rb == RowBounds.DEFAULT) {
return invocation.proceed();
}
// 3. 修改SQL语句
MappedStatement ms = (MappedStatement) args[0];
BoundSql boundSql = ms.getBoundSql(args[1]);
String newSql = boundSql.getSql() + " LIMIT " + rb.getOffset() + "," + rb.getLimit();
// 4. 创建新的BoundSql
BoundSql newBoundSql = new BoundSql(...);
// 5. 执行修改后的SQL
args[0] = copyMappedStatement(ms, newBoundSql);
return invocation.proceed();
}
}
5. 高频面试题实战拆解
5.1 Spring事务传播机制的七个等级
传播行为不是随便设计的,每种都有其特定使用场景:
| 传播行为 | 英文名 | 类比场景 | 适用情况 |
|---|---|---|---|
| REQUIRED | Required | 默认坐经济舱 | 大多数业务方法 |
| SUPPORTS | Supports | 有票就坐,没有就站着 | 查询方法 |
| MANDATORY | Mandatory | 必须坐头等舱 | 必须存在事务的校验逻辑 |
| REQUIRES_NEW | Requires New | 另开VIP通道 | 日志记录等独立操作 |
| NOT_SUPPORTED | Not Supported | 临时下飞机 | 非事务性操作 |
| NEVER | Never | 禁止登机 | 强制非事务环境 |
| NESTED | Nested | 机上包厢 | 保存点回滚场景 |
经典陷阱:在同一个类中调用@Transactional方法,事务会失效。这是因为Spring事务基于AOP实现,自调用不会经过代理对象。解决方案:
- 将方法拆分到不同类
- 通过AopContext获取当前代理(需要开启exposeProxy)
- 手动获取Bean再调用
5.2 MyBatis缓存机制的攻防战
MyBatis的两级缓存就像特种部队的备用弹夹:
一级缓存(本地缓存)
- 作用域:SqlSession级别
- 默认开启,无法关闭
- 相同SQL和参数直接返回缓存对象
- 坑点:对象是同一个引用,修改会影响缓存
二级缓存(全局缓存)
- 作用域:Mapper级别
- 需要手动配置
<cache/> - 跨SqlSession共享
- 必须实现Serializable接口
缓存失效的四种情况:
- 执行insert/update/delete
- 调用sqlSession.clearCache()
- 配置flushCache="true"
- 缓存达到上限(LRU淘汰)
实战建议:查询密集型应用可以开启二级缓存,但高并发更新场景建议关闭。分布式环境需要配合Redis等中央缓存,避免节点间数据不一致。
6. 面试实战技巧与避坑指南
6.1 如何回答"请描述Spring启动流程"
不要死记硬背,用关键节点+生活化类比:
- 加载配置:像准备做菜的材料(解析XML/注解)
- 创建BeanFactory:准备炒锅(DefaultListableBeanFactory)
- BeanDefinition:菜谱(解析类信息、属性、依赖)
- 后置处理器:特殊调料(BeanFactoryPostProcessor)
- 实例化Bean:开始炒菜(构造函数newInstance)
- 依赖注入:添加配菜(populateBean)
- 初始化:最后调味(InitializingBean)
- AOP代理:装盘美化(AbstractAutoProxyCreator)
6.2 遇到源码分析题怎么办
采用"三点式"回答法:
- 定位:明确该功能在哪个模块(如Spring事务在spring-tx)
- 主线:抓住核心类(如TransactionInterceptor)
- 锚点:记住关键方法(如invokeWithinTransaction)
举例回答"Spring事务如何实现回滚":
- 事务管理器(PlatformTransactionManager)创建事务
- 生成代理对象时织入事务拦截器
- 异常时调用processRollback
- 最终通过Connection.rollback()实现
6.3 手写代码题的破解之道
比如"手写简易IoC容器"可以这样展现思路:
java复制public class MiniContainer {
private Map<String, Object> beans = new ConcurrentHashMap<>();
private Map<String, Class<?>> beanDefinitions = new ConcurrentHashMap<>();
public void registerBean(String name, Class<?> beanClass) {
beanDefinitions.put(name, beanClass);
}
public Object getBean(String name) throws Exception {
Object bean = beans.get(name);
if(bean != null) return bean;
Class<?> beanClass = beanDefinitions.get(name);
if(beanClass == null) throw new IllegalArgumentException();
// 简易依赖注入
Constructor<?> constructor = beanClass.getConstructors()[0];
Object[] args = Arrays.stream(constructor.getParameterTypes())
.map(param -> {
try {
return getBean(param.getSimpleName());
} catch (Exception e) {
throw new RuntimeException(e);
}
}).toArray();
bean = constructor.newInstance(args);
beans.put(name, bean);
return bean;
}
}
最后给面试官三个提示信号:
- 指出这是简化版(无循环依赖处理)
- 说明如何扩展(增加AOP支持)
- 对比Spring的实现(三级缓存机制)
