⚡ Spring核心容器原理与高频面试题
1.1 Spring容器启动流程深度解析
在SSM框架面试中,Spring容器启动流程是高频考点。其核心流程可分为以下阶段:
- ① 读取配置:通过
ClassPathXmlApplicationContext或AnnotationConfigApplicationContext加载配置文件或注解配置类。 - ② BeanDefinition解析:解析XML中的
<bean>标签或@Component等注解,生成BeanDefinition对象并注册到BeanDefinitionRegistry。 - ③ 实例化前处理:调用
InstantiationAwareBeanPostProcessor的postProcessBeforeInstantiation方法,允许自定义代理逻辑(如AOP代理)。 - ④ Bean实例化:通过反射或CGLIB创建Bean实例,触发
postProcessAfterInstantiation回调。 - ⑤ 依赖注入:解析
@Autowired、@Resource等注解完成属性注入,调用postProcessProperties进行后处理。 - ⑥ 初始化:执行
InitializingBean的afterPropertiesSet方法和自定义init-method,触发BeanPostProcessor的postProcessBeforeInitialization与postProcessAfterInitialization。 - ⑦ 使用与销毁:Bean进入可用状态;容器关闭时调用
DisposableBean的destroy方法及自定义destroy-method。
lazy-init="true",则首次请求时才创建实例,可能引发运行时异常(如注入链缺失)。
1.2 Spring IOC容器与BeanFactory、ApplicationContext区别
| 特性 | BeanFactory |
ApplicationContext |
|---|---|---|
| 功能层级 | 基础IoC容器 | 高级容器(扩展自BeanFactory) |
| Bean加载方式 | 懒加载(Lazy Loading) | 预加载单例Bean(启动时实例化) |
| 国际化支持 | ❌ 不支持 | ✅ 支持MessageSource |
| 事件发布 | ❌ 不支持 | ✅ 支持ApplicationEvent与ApplicationListener |
| AOP支持 | ❌ 需手动配置 | ✅ 自动启用AnnotationAwareAspectJAutoProxyCreator |
| 典型实现 | DefaultListableBeanFactory |
ClassPathXmlApplicationContext、AnnotationConfigApplicationContext |
在SSM框架开发中,ApplicationContext是主流选择,因其提供企业级功能(如AOP、事务、国际化),而BeanFactory适用于资源受限环境(如嵌入式系统)。
1.3 AOP(面向切面编程)实现原理与动态代理
Spring AOP基于动态代理实现,核心流程如下:
- 切面定义:通过
@Aspect标注切面类,使用@Pointcut定义切入点(如execution(* com.example.service.*.*(..)))。 - 通知类型:
@Before:前置通知(方法执行前)@After:后置通知(方法执行后,无论是否异常)@AfterReturning:返回通知(方法成功返回后)@AfterThrowing:异常通知(方法抛出异常后)@Around:环绕通知(包裹目标方法,可控制执行流程)
- 代理生成:
- 目标类实现接口 → 使用JDK动态代理(基于
InvocationHandler) - 目标类无接口 → 使用CGLIB生成子类(通过字节码增强)
- 目标类实现接口 → 使用JDK动态代理(基于
// 示例:基于@Around的事务控制切面
@Aspect
@Component
public class TransactionAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("开启事务");
try {
Object result = pjp.proceed(); // 执行目标方法
System.out.println("提交事务");
return result;
} catch (Exception e) {
System.out.println("回滚事务");
throw e;
}
}
}
在SSM框架面试中,常考陷阱题:“为何Spring AOP无法拦截同类中方法调用?”——答案:因Spring代理是基于接口或子类的,同类内方法调用直接通过this执行,未经过代理对象。
【问题】Spring支持哪几种Bean作用域?默认是什么?
Spring提供5种作用域:
| 作用域 | 说明 | 适用场景 |
|---|---|---|
singleton |
单例(容器启动时创建,全局唯一) | 无状态Service、DAO |
prototype |
多例(每次请求创建新实例) | 有状态Bean(如会话数据) |
request |
HTTP请求范围(每个请求新建实例) | Web应用中请求级数据 |
session |
HTTP会话范围(每个会话新建实例) | 用户登录状态管理 |
application |
ServletContext范围(全局唯一) | 应用配置参数 |
默认作用域为singleton。若配置@Scope("prototype"),需注意:与@Lazy组合可延迟初始化;与@Scope("prototype") + @Scope("request")嵌套时,需通过ScopedProxyMode.TARGET_CLASS生成代理。
【问题】Spring如何解决单例Bean的循环依赖?三级缓存机制是什么?
Spring通过三级缓存解决构造器注入和setter注入的循环依赖:
- 一级缓存(singletonObjects):存放完全初始化好的单例Bean
- 二级缓存(earlySingletonObjects):存放早期暴露的Bean(尚未完成属性注入)
- 三级缓存(singletonFactories):存放ObjectFactory,用于生成早期Bean引用
流程示例(A→B→A):
- A开始创建 → 放入三级缓存
- A依赖B → B开始创建 → 放入三级缓存
- B依赖A → 从三级缓存获取A的ObjectFactory → 提前暴露A的引用(放入二级缓存)
- B完成创建 → A继续完成创建
singleton作用域;prototype作用域Bean无法解决循环依赖;构造器注入无法解决循环依赖(因Bean尚未实例化)。
【问题】@Conditional注解如何实现条件化Bean注册?
@Conditional是Spring 4.0引入的注解,用于根据条件决定是否注册Bean:
- 常用元注解:
@ConditionalOnClass:类路径存在指定类@ConditionalOnMissingBean:容器中不存在指定Bean@ConditionalOnProperty:配置文件中存在指定属性
- 自定义条件类:
public class LinuxCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return context.getEnvironment().getProperty("os.name").contains("Linux"); } }
在SSM框架整合中,Spring Boot自动配置大量使用@Conditional(如@ConditionalOnWebApplication),实现按需加载模块。
⚙️ SpringMVC请求处理流程与核心组件
2.1 请求处理全流程图解
SpringMVC基于前端控制器模式,核心流程如下:
2.2 关键组件源码级解析
| 组件 | 作用 | 默认实现 | 扩展方式 |
|---|---|---|---|
DispatcherServlet |
前端控制器(入口) | org.springframework.web.servlet.DispatcherServlet | 继承重写doDispatch() |
HandlerMapping |
查找处理器(URL→Controller) | RequestMappingHandlerMapping |
实现HandlerMapping接口 |
HandlerAdapter |
适配执行处理器 | RequestMappingHandlerAdapter |
实现HandlerAdapter接口 |
ViewResolver |
解析视图 | InternalResourceViewResolver |
实现ViewResolver接口 |
MultipartResolver |
文件上传解析 | StandardServletMultipartResolver |
配置StandardServletMultipartResolver Bean |
在SSM框架面试中,常考题:“如何实现RESTful风格的URL映射?”——答案:通过@RequestMapping(value="/users/{id}", method=RequestMethod.GET)绑定URL参数,结合@PathVariable获取路径变量。
2.3 参数绑定与数据校验机制
SpringMVC支持多种参数绑定方式:
- 基本类型:自动转换(String→int/long/Boolean等)
- POJO对象:按字段名匹配(需提供setter方法)
- 集合类型:如
List<User>,需通过@InitBinder注册自定义编辑器 - 注解参数:
@RequestParam:处理Query String/FormData参数@PathVariable:处理RESTful路径参数@RequestBody:处理JSON/XML请求体(结合Jackson)@RequestHeader:获取HTTP请求头
// 示例:POJO对象绑定 + 数据校验
@PostMapping("/user")
public String saveUser(@Valid @ModelAttribute User user, BindingResult result) {
if (result.hasErrors()) {
return "user-form"; // 返回错误页面
}
userService.save(user);
return "redirect:/user/list";
}
数据校验需在User类中添加JSR-303注解(如@NotNull、@Size(min=2)),并依赖hibernate-validator库。
【问题】SpringMVC拦截器执行顺序是怎样的?
拦截器(HandlerInterceptor)有三个核心方法:
preHandle():Controller执行前调用,返回true继续,false中断postHandle():Controller执行后、视图渲染前调用afterCompletion():视图渲染后调用(用于资源清理)
多拦截器执行顺序:
preHandle:按配置顺序执行postHandle与afterCompletion:按配置顺序的逆序执行
示例(配置A→B):
【问题】@RequestBody与@RequestParam如何选择?
| 特性 | @RequestBody |
@RequestParam |
|---|---|---|
| 数据来源 | 请求体(Body) | Query String / FormData |
| Content-Type | application/json、application/xml | application/x-www-form-urlencoded、multipart/form-data |
| 参数解析 | 通过HttpMessageConverter(如Jackson) | 直接类型转换 |
| 支持对象绑定 | ✅ 直接绑定POJO | ❌ 仅支持简单类型 |
在SSM框架开发中,RESTful API推荐用@RequestBody,传统表单提交用@RequestParam。
【问题】如何配置JSP视图解析器?
在SpringMVC配置文件中添加:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
</bean>
当Controller返回"user/list"时,最终路径为/WEB-INF/jsp/user/list.jsp。
⚡ MyBatis核心机制与动态SQL
3.1 MyBatis执行流程与四大核心对象
MyBatis执行流程可分为以下步骤:
- 加载配置:通过
SqlSessionFactoryBuilder解析mybatis-config.xml生成Configuration对象。 - 创建SqlSessionFactory:构建
SqlSessionFactory(线程安全,单例)。 - 获取SqlSession:通过
SqlSessionFactory.openSession()创建SqlSession(非线程安全)。 - 执行SQL:调用
SqlSession.selectOne()等方法,触发四大对象协作:
在SSM框架面试中,常考题:“MyBatis如何防止SQL注入?”——答案:使用#{} 占位符(预编译),避免${}字符串拼接。
3.2 动态SQL与常用标签详解
MyBatis动态SQL通过XML标签实现条件拼接,核心标签如下:
| 标签 | 功能 | 示例 |
|---|---|---|
<if> |
条件判断 | <if test="name != null">name = #{name}</if> |
<choose>/<when>/<otherwise> |
类似switch-case | <choose><when test="id != null">...</when><otherwise>...</otherwise></choose> |
<where> |
自动添加WHERE并去除多余AND/OR | <where><if test="id != null">id = #{id}</if></where> |
<set> |
UPDATE语句中自动添加SET并去除逗号 | <set>name = #{name}</set> |
<foreach> |
遍历集合(IN查询) | <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach> |
<select id="selectUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="ids != null and ids.size > 0">
AND id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</where>
</select>
在SSM框架开发中,动态SQL可避免拼接字符串导致的SQL注入风险,同时提升SQL可读性。
3.3 一级缓存与二级缓存机制
MyBatis提供两层缓存:
- 一级缓存(SqlSession级别):
- 默认开启,基于
PerpetualCache(HashMap实现) - 同一SqlSession中,相同SQL+参数仅执行一次
- 调用
sqlSession.clearCache()或执行增删改操作时清除
- 默认开启,基于
- 二级缓存(Mapper级别):
- 需在
mybatis-config.xml中启用<cache>标签 - Mapper接口对应缓存,需实体类实现
Serializable - 多线程环境下需注意数据一致性(推荐使用Ehcache等外部缓存)
- 需在
【问题】#{}与${}有何本质区别?
| 特性 | #{} |
${} |
|---|---|---|
| 处理方式 | 预编译(?占位符) | 字符串拼接 |
| SQL注入风险 | ❌ 安全 | ✅ 高风险 |
| 适用场景 | 参数值(如WHERE条件) | 动态表名、列名(如ORDER BY字段) |
正确用法:ORDER BY ${sortColumn} ${sortDirection}(需校验sortColumn合法性)。
【问题】ResultMap与ResultType有何区别?
ResultType:直接指定返回类型(适用于字段名与属性名一致的简单映射)。
ResultMap:自定义映射规则(适用于字段名与属性名不一致、关联查询)。
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id"/>
<result property="userName" column="user_name"/>
</resultMap>
<select id="selectUser" resultMap="userResultMap">
SELECT user_id, user_name FROM user WHERE user_id = #{id}
</select>
在SSM框架面试中,常考关联映射(如<association>、<collection>)。
【问题】PageHelper分页插件原理是什么?
PageHelper通过MyBatis插件机制实现物理分页:
- 拦截
StatementHandler.query()方法 - 解析原始SQL,添加
LIMIT子句(MySQL)或ROWNUM(Oracle) - 执行COUNT查询获取总记录数
- 返回
PageInfo对象(包含分页数据与元信息)
// 示例:使用PageHelper分页
PageHelper.startPage(1, 10);
List list = userMapper.selectAll();
PageInfo pageInfo = new PageInfo<>(list); // 包含total, pages等信息
⚡ SSM框架整合与企业级实践
4.1 整合Spring与SpringMVC
在web.xml中配置核心监听器与前端控制器:
<!-- 加载Spring容器配置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- SpringMVC前端控制器 -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
关键点:ContextLoaderListener创建根容器(父容器),DispatcherServlet创建Web容器(子容器),子容器可访问父容器Bean。
4.2 整合Spring与MyBatis
通过SqlSessionFactoryBean集成MyBatis:
<!-- 数据源 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!-- SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<!-- 扫描Mapper接口 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.mapper"/>
</bean>
注意事项:
- Mapper接口无需实现类,由MyBatis动态代理生成
- 事务管理通过
@Transactional注解(需配置DataSourceTransactionManager) - 避免在Service层直接使用
SqlSession(破坏事务一致性)
4.3 事务管理深度解析
Spring支持声明式事务与编程式事务,推荐使用声明式:
// 配置事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
// 启用注解驱动事务
<tx:annotation-driven transaction-manager="transactionManager"/>
@Transactional常用属性:
| 属性 | 说明 | 示例 |
|---|---|---|
| propagation | 事务传播行为 | REQUIRED(默认)、REQUIRES_NEW |
| isolation | 事务隔离级别 | READ_COMMITTED、REPEATABLE_READ |
| rollbackFor | 指定异常回滚 | rollbackFor = Exception.class |
| noRollbackFor | 指定异常不回滚 | noRollbackFor = RuntimeException.class |
@Transactional仅对public方法生效;同类内方法调用需通过代理对象(如注入自身)才能触发事务。
4.4 SSM框架演进时间轴
⚡ 网友最关心的10个SSM框架面试真题
Q1:Spring事务传播行为有哪些?举例说明REQUIRED与REQUIRES_NEW。
答案:Spring定义7种传播行为:
| 传播行为 | 说明 |
|---|---|
REQUIRED |
有事务则加入,无则新建(默认) |
REQUIRES_NEW |
总是新建独立事务(挂起当前事务) |
NESTED |
在当前事务内创建保存点(JDBC 3.0+) |
SUPPORTS |
有事务则执行,无则非事务执行 |
NOT_SUPPORTED |
以非事务方式执行,挂起当前事务 |
NEVER |
以非事务方式执行,有事务则抛异常 |
MANDATORY |
必须在事务中执行,无事务则抛异常 |
示例:
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder() {
// 主事务
orderMapper.insert(order);
// 调用支付服务(新建独立事务)
paymentService.pay(order);
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void pay(Order order) {
// 独立事务,不影响主事务
paymentMapper.updateStatus(order);
}
}
若支付失败,主事务仍可提交(订单创建成功)。
Q2:SpringMVC中如何实现文件上传?
答案:步骤如下:
- 添加依赖:
commons-fileupload(Spring 5.3+推荐)commons-io
- 配置MultipartResolver:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="defaultEncoding" value="UTF-8"/> <property name="maxUploadSize" value="10485760"/> </bean> - Controller处理:
@PostMapping("/upload") public String upload(@RequestParam("file") MultipartFile file) { if (!file.isEmpty()) { String fileName = file.getOriginalFilename(); file.transferTo(new File("/uploads/" + fileName)); } return "success"; }
enctype="multipart/form-data";生产环境需校验文件类型与大小。
Q3:MyBatis中如何实现批量插入?
答案:有三种方式:
| 方式 | 优点 | 缺点 |
|---|---|---|
<foreach> + INSERT |
单次执行,效率高 | SQL长度受限(MySQL 64MB) |
ExecutorType.BATCH |
支持大批量数据 | 需手动提交,可能积压内存 |
数据库批量语法(如MySQL INSERT ... VALUES (), ()) |
最高效 | 依赖数据库特性 |
<insert id="batchInsert">
INSERT INTO user (name, age) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
Q4:Spring AOP中@Around与@Before/@After区别?
答案:
- 执行时机:
@Before:目标方法执行前@Around:包裹目标方法,可控制执行流程(甚至跳过)
- 返回值:
@Before:无返回值@Around:必须返回目标方法结果(或自定义结果)
- 异常处理:
@Before:无法捕获目标方法异常@Around:可捕获并处理异常
适用场景:
@Around:日志记录、性能监控、权限校验(需动态决策)@Before:参数校验、简单日志
Q5:如何解决Spring循环依赖?
答案:见前文1.2节,核心是三级缓存机制:
- 一级缓存:单例池(完全初始化的Bean)
- 二级缓存:早期Bean引用(尚未完成依赖注入)
- 三级缓存:ObjectFactory(用于生成早期Bean)
注意:仅支持singleton作用域;prototype无法解决;构造器注入无法解决。
Q6:MyBatis一级缓存何时失效?
答案:以下情况清除一级缓存:
- 执行增删改操作(
insert/update/delete) - 手动调用
sqlSession.clearCache() - SqlSession关闭或提交
- 配置
flushCache="true"(针对单条SQL)
默认情况下,SELECT语句不刷新缓存。
Q7:SpringMVC中如何自定义异常处理器?
答案:使用@ControllerAdvice + @ExceptionHandler:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
ErrorResponse error = new ErrorResponse(500, "服务器错误");
return ResponseEntity.status(500).body(error);
}
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<ErrorResponse> handleNPE(NullPointerException e) {
ErrorResponse error = new ErrorResponse(404, "数据不存在");
return ResponseEntity.status(404).body(error);
}
}
注意:优先级按异常类层次结构匹配(子类优先)。
Q8:MyBatis中如何实现分页?物理分页与逻辑分页区别?
答案:
| 方式 | 实现 | 适用场景 |
|---|---|---|
| 物理分页 | SQL添加LIMIT子句 |
大数据量(推荐) |
| 逻辑分页 | 使用RowBounds在内存中截取 |
小数据量(内存消耗大) |
推荐使用PageHelper插件实现物理分页。
Q9:Spring事务失效的常见原因?
答案:
@Transactional标注在非public方法- 同类内方法调用(未经过代理)
- 异常被catch且未重新抛出
- 事务传播行为为
NOT_SUPPORTED或NEVER - 数据源未配置事务管理器
AopContext.currentProxy()获取代理对象调用方法。
Q10:MyBatis动态SQL中<trim>标签作用?
答案:<trim>是通用字符串截取标签,可替代<where>和<set>:
<select id="selectUsers" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</trim>
</select>
属性说明:prefix(添加前缀)、prefixOverrides(移除前缀冗余)、suffix(添加后缀)、suffixOverrides(移除后缀冗余)。
⚠️ SSM框架开发常见误区与避坑指南
7.1 误区:Spring Bean默认是线程安全的
纠正:Spring容器本身不保证线程安全!singleton作用域Bean若包含可变状态,需自行处理并发问题:
- 避免在Bean中定义实例变量
- 使用
ThreadLocal存储线程隔离数据 - 对共享资源加锁(
synchronized或ReentrantLock)
推荐设计无状态Bean(仅包含Service方法),状态由请求参数传递。
7.2 误区:MyBatis的#{}能完全防SQL注入
纠正:#{}仅防止参数注入,但若动态SQL拼接了用户输入的表名/列名,仍可能注入:
SELECT * FROM ${tableName} WHERE id = #{id}
正确做法:对${}参数做白名单校验(如只允许字母数字下划线)。
7.3 误区:Spring事务会自动回滚所有异常
纠正:默认仅回滚RuntimeException和Error,对checked Exception不回滚:
@Transactional(rollbackFor = Exception.class) // 显式指定回滚所有异常
生产环境务必显式配置rollbackFor。
7.4 误区:SpringMVC拦截器可拦截所有请求
纠正:拦截器不拦截静态资源(需额外配置<mvc:interceptors> + <mvc:exclude-mapping>):
<mvc:interceptors>
<bean class="com.example.interceptor.AuthInterceptor"/>
<mvc:interceptor>
<mvc:mapping path="/admin/**"/>
<mvc:exclude-mapping path="/admin/login"/>
<bean class="com.example.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>