Appearance
Spring 与 SpringBoot
Spring 面试的主线:IoC 容器 → AOP → 事务 → SpringBoot 自动装配。能讲清"循环依赖三级缓存"基本就算过关。
Q1: Spring IoC 容器的工作流程? 「🟡 中级」
考察点:对容器核心机制的理解,而非只会注解。
参考答案:
- 启动流程:读取配置(注解扫描/ XML)→ 生成 BeanDefinition(Bean 的元信息)→ 反射实例化 → 属性注入(依赖注入)→ 初始化(Aware 回调、BeanPostProcessor 前后置、
@PostConstruct、InitializingBean)→ 放入容器。 - Bean 默认单例;作用域有 singleton、prototype、request、session。
- BeanPostProcessor 是整个 Spring 扩展的基石:AOP 代理、
@Autowired注入都发生在这一环节。
追问延伸:
- BeanFactory 和 ApplicationContext 的区别?
@Component和@Bean的使用场景区别?
Q2: Spring 是怎么解决循环依赖的? 「🔴 高级」
考察点:三级缓存是 Spring 最高频深度题。
参考答案:
三级缓存:
- 一级
singletonObjects:完整的成品 Bean。 - 二级
earlySingletonObjects:提前暴露的"半成品"(已实例化未初始化)。 - 三级
singletonFactories:ObjectFactory,用于在需要时生成(可能是 AOP 代理后的)早期引用。
流程(A 依赖 B、B 依赖 A):A 实例化后把工厂放进三级缓存 → 填充属性发现需要 B → 创建 B → B 填充属性需要 A,从三级缓存拿到 A 的早期引用 → B 完成进一级 → A 拿到 B 完成。
- 无法解决的场景:构造器注入的循环依赖(实例化都完不成);prototype 作用域。
追问延伸:
- 为什么要三级缓存而不是两级?(为了保证 AOP 代理对象的一致性,只在真正被循环引用时才提前生成代理)
@Async方法出现循环依赖为什么会报错?
Q3: AOP 的实现原理? 「🟡 中级」
考察点:动态代理的两种实现与选择条件。
参考答案:
- JDK 动态代理:基于接口,运行时生成实现目标接口的代理类(
java.lang.reflect.Proxy)。 - CGLIB:基于继承,运行时生成目标类的子类并织入逻辑;对 final 类/方法无效。
- Spring 默认:有接口用 JDK 代理,无接口用 CGLIB;SpringBoot 2.x 起默认全用 CGLIB。
- 经典坑:同类内部方法调用切面失效——因为调用的是 this 而不是代理对象;解法是注入自身代理或用
AopContext。
追问延伸:
- 切面的执行顺序怎么控制?(
@Order) - 事务注解失效的常见场景有哪些?
Q4: Spring 事务的传播行为? 「🟡 中级」
考察点:事务配置的实战能力。
参考答案:
- REQUIRED(默认):有事务就加入,没有就新建。
- REQUIRES_NEW:无论如何新建事务,挂起当前事务(适合操作日志)。
- NESTED:嵌套事务,用 savepoint 实现,外层回滚会连带回滚,内层回滚不影响外层。
- SUPPORTS / NOT_SUPPORTED / MANDATORY / NEVER:辅助型,使用较少。
常见失效场景:方法非 public、同类自调用、异常被 catch、异常类型不匹配(默认只回滚 RuntimeException)、数据库引擎不支持事务。
追问延伸:
- REQUIRES_NEW 和 NESTED 的核心区别?
- 长事务有什么危害?怎么拆分?
Q5: SpringBoot 的自动装配原理? 「🟡 中级」
考察点:SpringBoot 起步原理,几乎必问。
参考答案:
@SpringBootApplication=@Configuration+@ComponentScan+@EnableAutoConfiguration。@EnableAutoConfiguration通过AutoConfigurationImportSelector读取所有依赖包中META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(2.7 之前是spring.factories)里声明的自动配置类。- 配置类上用
@ConditionalOnClass、@ConditionalOnProperty等条件注解决定是否生效,实现"约定大于配置"。
追问延伸:
- 怎么排除某个自动配置?(
exclude属性) - 自定义一个 starter 要做哪些事?
Q6: SpringBoot 中配置文件加载顺序? 「🟢 校招/初级」
考察点:配置管理的细节。
参考答案:
- 优先级(从高到低):命令行参数 →
application-{profile}.yml→application.yml;同级别下 properties 优先于 yml。 - 加载位置:项目根目录
./config/高于./,./高于 classpath。 @Value注入单个属性,@ConfigurationProperties批量绑定且有类型校验,推荐后者。
追问延伸:
- 敏感配置(密码)怎么处理?(环境变量、配置中心、jasypt 加密)
- Nacos 这类配置中心怎么实现动态刷新?
Q7: Spring Bean 的完整生命周期?有哪些扩展点? 「🟡 中级」
考察点:对Spring容器初始化全过程的掌握,扩展点的使用场景。
参考答案:
完整生命周期流程:
- 实例化前:
BeanPostProcessor.postProcessBeforeInstantiation - 实例化:反射调用构造方法
- 属性注入:
@Autowired等依赖注入 - Aware回调:
BeanNameAware、BeanFactoryAware、ApplicationContextAware - 初始化前:
BeanPostProcessor.postProcessBeforeInitialization(@PostConstruct在此之前执行) - 初始化:
InitializingBean.afterPropertiesSet→init-method - 初始化后:
BeanPostProcessor.postProcessAfterInitialization(AOP代理在这里生成) - 使用:Bean 放入容器,正常使用
- 销毁前:
@PreDestroy - 销毁:
DisposableBean.destroy→destroy-method
核心扩展点:
BeanPostProcessor:Bean 初始化前后的钩子(AOP、@Autowired注入都靠它)BeanFactoryPostProcessor:BeanDefinition 加载后、实例化前修改 BeanDefinitionInstantiationAwareBeanPostProcessor:实例化前后的更细粒度控制FactoryBean:复杂 Bean 的创建(如 MyBatis 的 Mapper)
追问延伸:
@PostConstruct、InitializingBean、init-method的执行顺序?BeanPostProcessor和BeanFactoryPostProcessor的区别?
Q8: Spring 事务的实现原理?事务是怎么回滚的? 「🔴 高级」
考察点:Spring事务的AOP本质和底层机制。
参考答案:
- 实现原理:基于 AOP + 事务管理器(
PlatformTransactionManager)+ 连接绑定- Spring 为标注
@Transactional的类生成代理对象 - 方法调用时,事务拦截器(
TransactionInterceptor)拦截 - 开启事务:从数据源拿连接,关闭自动提交(
setAutoCommit(false)) - 把连接绑定到当前线程(ThreadLocal,用
TransactionSynchronizationManager) - 执行目标方法
- 正常执行完:commit
- 抛出异常:判断是否需要回滚(默认
RuntimeException和Error才回滚)
- Spring 为标注
- 事务管理器:
DataSourceTransactionManager(JDBC)、JpaTransactionManager(JPA)等 - 事务属性:传播行为、隔离级别、超时、只读、回滚规则
追问延伸:
- 为什么事务注解标注在非 public 方法上会失效?(CGLIB 可以但 Spring AOP 默认只拦截 public)
- 事务同步管理器(
TransactionSynchronization)有什么用?
Q9: Spring MVC 的工作流程?DispatcherServlet 怎么处理请求? 「🟡 中级」
考察点:Web层核心流程的理解。
参考答案:
核心组件:DispatcherServlet(前端控制器)、HandlerMapping(处理器映射)、HandlerAdapter(处理器适配器)、ViewResolver(视图解析器)
请求处理流程:
- 请求到达
DispatcherServlet DispatcherServlet调用HandlerMapping,根据 URL 找到 Handler(Controller 方法)- 调用
HandlerAdapter,适配器执行 Handler(Controller 方法) - Handler 执行完返回
ModelAndView DispatcherServlet调用ViewResolver解析视图- 渲染视图(把 model 数据填充到 view)
- 返回响应
SpringBoot 中默认的 DispatcherServlet 自动配置,映射到 /
追问延伸:
- Spring MVC 的拦截器怎么实现?和 Filter 有什么区别?
@RestController和@Controller的区别?
Q10: Spring 中用到了哪些设计模式? 「🟡 中级」
考察点:对Spring框架设计思想的理解深度。
参考答案:
- 工厂模式:
BeanFactory、ApplicationContext(创建 Bean) - 单例模式:Spring Bean 默认单例(三级缓存实现)
- 代理模式:AOP 的 JDK 动态代理和 CGLIB
- 模板方法模式:
JdbcTemplate、RedisTemplate、TransactionTemplate - 观察者模式:
ApplicationEvent事件机制(事件、事件发布者、事件监听器) - 策略模式:Bean 的实例化策略、资源加载策略
- 装饰器模式:
BeanWrapper、OutputStream/InputStream 包装 - 适配器模式:
HandlerAdapter(适配不同类型的 Handler)
追问延伸:
- Spring 的事件机制怎么用?(
ApplicationEvent+@EventListener) - 模板方法和策略模式的区别?
Q11: Spring 事件机制(ApplicationEvent)是什么? 「🟡 中级」
考察点:Spring解耦机制的理解和应用。
参考答案:
- 观察者模式的实现,包含三个角色:
- 事件(
ApplicationEvent):自定义事件继承它 - 事件发布者(
ApplicationEventPublisher):发布事件 - 事件监听器(
ApplicationListener):监听并处理事件
- 事件(
- 使用方式:
- 自定义事件:继承
ApplicationEvent - 发布事件:
applicationEventPublisher.publishEvent(event) - 监听事件:实现
ApplicationListener接口 或@EventListener注解
- 自定义事件:继承
- 特点:
- 默认同步执行(发布线程执行监听逻辑)
- 异步需要配合
@Async或ApplicationEventMulticaster配置
- 应用场景:注册后发欢迎邮件、订单完成后通知库存等解耦场景
追问延伸:
- 事件监听异常了会影响发布者吗?(默认会,因为同步)
- Spring 内置了哪些事件?(
ContextRefreshedEvent、ContextStartedEvent等)
Q12: 怎么自定义一个 SpringBoot Starter? 「🔴 高级」
考察点:SpringBoot自动装配的实战能力。
参考答案:
Starter = 依赖集合 + 自动配置类
开发步骤:
- 创建一个 Maven 项目,命名为
xxx-spring-boot-starter - 引入
spring-boot-autoconfigure依赖 - 编写自动配置类(
@Configuration+@ConditionalOnClass等条件注解) - 编写属性配置类(
@ConfigurationProperties) - 在
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中注册自动配置类(SpringBoot 2.7+) - 打包发布
关键条件注解:
@ConditionalOnClass:类路径下有指定类才生效@ConditionalOnMissingBean:容器中没有该 Bean 才创建@ConditionalOnProperty:配置文件有指定属性才生效@ConditionalOnWebApplication:Web 应用才生效
追问延伸:
- Starter 和自动配置的关系?
- 怎么让 starter 的配置属性有 IDE 自动提示?(
spring-configuration-metadata.json)
Q13: Spring Security 的认证授权流程? 「🟡 中级」
考察点:安全框架的核心流程理解。
参考答案:
核心组件:
Authentication:认证信息(用户名、密码、权限)AuthenticationManager:认证管理器(核心接口)AuthenticationProvider:认证提供者(具体认证逻辑)UserDetailsService:加载用户信息PasswordEncoder:密码加密SecurityContext:安全上下文(存当前登录用户信息,存在 ThreadLocal)
认证流程:
- 用户提交用户名密码,封装成
Authentication(通常是UsernamePasswordAuthenticationToken) AuthenticationManager认证(委托给AuthenticationProvider)AuthenticationProvider调用UserDetailsService加载用户信息PasswordEncoder比对密码- 认证成功,把
Authentication放入SecurityContext - 后续请求从
SecurityContext取出当前用户做授权判断
授权:通过 FilterSecurityInterceptor + AccessDecisionManager 判断用户是否有权限访问资源
追问延伸:
- Spring Security 和 Shiro 的区别?
- JWT + SpringSecurity 的认证流程有什么不同?
Q14: Spring Boot 的启动流程?SpringApplication.run() 做了什么? 「🟡 中级」
考察点:SpringBoot 启动全流程的理解。
参考答案:
SpringApplication.run() 的完整流程:
- 初始化阶段:创建 SpringApplication 实例,推断应用类型(SERVLET/REACTIVE/NONE),加载 spring.factories 中的初始化器和监听器。
- 准备阶段:创建环境(Environment),加载 application.yml/properties,打印 Banner。
- 创建上下文:根据应用类型创建 ApplicationContext(SERVLET → AnnotationConfigServletWebServerApplicationContext)。
- 刷新上下文(refresh()):注册 BeanDefinition、执行 BeanFactoryPostProcessor、实例化 Bean(单例)、启动 Web 服务器(Tomcat/Jetty/Undertow)。
- 后处理:发布 ApplicationReadyEvent,执行 CommandLineRunner 和 ApplicationRunner。
关键扩展点(按执行顺序):
- ApplicationContextInitializer:上下文刷新前初始化
- BeanFactoryPostProcessor:BeanDefinition 加载后
- ApplicationListener:监听启动事件
- CommandLineRunner / ApplicationRunner:启动完成后执行
追问延伸:
- SpringBoot 的 SPI 机制和 Java 的 SPI 区别?
- 怎么实现启动后自动执行初始化逻辑?
Q15: Spring 的三种依赖注入方式?为什么推荐构造器注入? 「🟡 中级」
考察点:依赖注入的实战理解和最佳实践。
参考答案:
三种注入方式:
- 构造器注入(推荐):通过构造方法传入依赖
- 优点:依赖不可变(final)、不可遗漏(编译时检查)、便于单元测试、避免循环依赖(编译时就报错)
- 缺点:依赖多时构造方法臃肿
- Setter 注入:通过 setter 方法注入
- 优点:可选依赖、灵活、可以重新设置
- 缺点:依赖可变、可能忘记注入
- 字段注入(
@Autowired在字段上):最简单但不推荐- 缺点:隐藏依赖关系、不能设 final、单元测试困难、容易循环依赖
Spring 4.3+ 推荐:必须依赖用构造器注入,可选依赖用 setter 注入。 @Autowired 在构造器上是隐式的(Spring 4.3+ 单构造器可省略)。
追问延伸:
@Autowired和@Resource的区别?(前者按类型,后者按名称)- 构造器注入怎么解决循环依赖?(编译时就报错,根本不让发生)
Q16: Spring Bean 的作用域有哪些?prototype 注入到 singleton 有什么问题? 「🟡 中级」
考察点:Bean 作用域的理解和代理机制。
参考答案:
六种作用域:
- singleton(默认):容器中只有一个实例
- prototype:每次获取都创建新实例
- request:每个 HTTP 请求一个实例(Web 应用)
- session:每个 HTTP Session 一个实例
- application:ServletContext 级别
- websocket:WebSocket 级别
prototype 注入到 singleton 的问题:
- singleton 在启动时就创建了,注入的 prototype 只创建了一次
- 后续每次访问 singleton 里的 prototype 还是同一个实例
- 解决方案:
- 方案1:
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)生成 CGLIB 代理 - 方案2:
ObjectFactory<T>/Provider<T>延时获取 - 方案3:实现
ApplicationContextAware,每次从容器拿
- 方案1:
追问延伸:
- singleton 是线程安全的吗?(Bean 本身无状态则安全,有共享状态需自己加锁)
- request/session 作用域在非 Web 环境会怎样?(报错)
Q17: Spring 的缓存抽象(@Cacheable)怎么用?原理是什么? 「🟡 中级」
考察点:Spring 缓存抽象的使用和底层原理。
参考答案:
核心注解:
@Cacheable:方法执行前查缓存,命中则直接返回,不执行方法@CachePut:方法执行后把返回值放入缓存(每次都执行方法)@CacheEvict:删除缓存@Caching:组合多个缓存操作@CacheConfig:类级别缓存公共配置
使用方式:
- 开启缓存:
@EnableCaching - 配置 CacheManager:RedisCacheManager、ConcurrentMapCacheManager 等
- 缓存键:默认用参数生成,可用
key属性 + SpEL 表达式自定义
原理:基于 AOP 代理 + CacheInterceptor
@Cacheable方法被代理,调用时先查 Cache- 命中 → 返回缓存值;未命中 → 执行方法,结果存入 Cache
@CachePut每次执行方法,更新缓存@CacheEvict执行后删除缓存
坑:
- 同类调用失效(AOP 代理问题)
- 缓存穿透(null 值不缓存,可用
unless控制) - 缓存雪崩(大量同时失效)
追问延伸:
- @Cacheable 和 @CachePut 同时用要注意什么?
- 怎么实现多级缓存?(Caffeine + Redis,自定义 CacheManager)
Q18: Spring 的异步编程(@Async)怎么用?有什么坑? 「🟡 中级」
考察点:异步编程的实践和常见问题。
参考答案:
使用方式:
- 开启异步:
@EnableAsync - 标注方法:
@Async表示该方法异步执行(用线程池执行) - 返回值:
void或Future<T>/CompletableFuture<T>
原理:基于 AOP 代理
@Async方法被代理,调用时提交到线程池- 默认用
SimpleAsyncTaskExecutor(每次新建线程,不推荐) - 自定义线程池:实现
AsyncConfigurer或定义TaskExecutorBean
常见坑:
- 同类调用失效:
@Async是基于 AOP 代理的,同类内部调用不生效(和@Transactional一样的问题) - 异常丢失:异步方法的异常不会抛给调用者,需要:
- 配置
AsyncUncaughtExceptionHandler处理未捕获异常 - 返回
Future时异常封装在 Future 中
- 配置
- 线程池耗尽:默认线程池没有上限控制,大量异步任务可能导致 OOM
- 循环依赖报错:
@Async导致代理提前创建,可能触发循环依赖报错
最佳实践:
- 自定义线程池(有界队列 + 合理拒绝策略)
- 用
CompletableFuture替代@Async(更灵活、可控)
追问延伸:
- @Async 和 CompletableFuture 怎么选?
- 怎么配置自定义异步线程池?
Q19: Spring MVC 全局异常处理怎么做? 「🟢 校招/初级」
考察点:Web 应用的异常处理方案。
参考答案:
三种方式(从简单到灵活):
- @ControllerAdvice + @ExceptionHandler(推荐):
- 全局异常处理类,统一处理所有 Controller 抛出的异常
- 可以返回 JSON(@ResponseBody)或页面
- 可以限定处理的 Controller 范围
- HandlerExceptionResolver 接口:
- 更底层的异常解析,Spring MVC 内部用
- 默认有三个:ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver
- @ResponseStatus:
- 自定义异常类上标注,抛出时返回指定状态码
- 简单但不灵活
最佳实践:
- 自定义业务异常(BusinessException)+ 全局异常处理器
- 统一返回格式(code、message、data)
- 区分业务异常(4xx)和系统异常(5xx)
- 系统异常记录日志、告警
- 参数校验异常(@Valid)单独处理
追问延伸:
- @ControllerAdvice 怎么只处理部分 Controller?
- Spring Boot 默认的异常处理流程是什么?
Q20: Spring Boot 条件装配的完整注解有哪些? 「🟡 中级」
考察点:自动装配核心机制的全面掌握。
参考答案:
条件注解家族(基于 @Conditional):
- Class 条件:
@ConditionalOnClass:类路径下存在指定类时生效@ConditionalOnMissingClass:类路径下不存在指定类时生效
- Bean 条件:
@ConditionalOnBean:容器中存在指定 Bean 时生效@ConditionalOnMissingBean:容器中不存在指定 Bean 时生效(自定义配置覆盖默认配置的关键)@ConditionalOnSingleCandidate:只有一个或一个主候选 Bean 时生效
- Property 条件:
@ConditionalOnProperty:配置文件中有指定属性且值匹配时生效(支持 havingValue、matchIfMissing)
- Resource 条件:
@ConditionalOnResource:存在指定资源文件时生效
- Web 条件:
@ConditionalOnWebApplication:是 Web 应用时生效@ConditionalOnNotWebApplication:不是 Web 应用时生效
- 组合条件:
@ConditionalOnJava:Java 版本匹配@ConditionalOnExpression:SpEL 表达式成立@Profile:指定环境 profile 时生效
自定义条件:实现 Condition 接口,配合 @Conditional 使用
追问延伸:
- @ConditionalOnMissingBean 怎么实现"自定义配置覆盖默认配置"?
- 条件注解的判断时机是什么时候?(BeanDefinition 注册阶段)
Q21: Spring AOP 多个切面的执行顺序怎么控制? 「🟡 中级」
考察点:AOP 切面执行链的理解和实战控制。
参考答案:
切面执行顺序:
- 多个切面形成拦截器链(Interceptor Chain),类似洋葱模型
- @Around 前半部分 → @Before → 目标方法 → @AfterReturning/@AfterThrowing → @After → @Around 后半部分
- 外层切面先进入、后退出(栈结构)
控制顺序的方式:
@Order(n)注解:n 越小优先级越高(越外层)- 实现
Ordered接口:getOrder()返回值越小越外层 @Priority注解:和 @Order 类似
执行顺序示例(两个切面 A(@Order(1)) 和 B(@Order(2))):
- A @Around 前 → B @Around 前 → B @Before → 目标方法 → B @After → B @AfterReturning → B @Around 后 → A @After → A @AfterReturning → A @Around 后
注意事项:
- @Order 不能控制同一切面内不同通知的顺序
- 同一切面内通知顺序:@Around → @Before → 目标方法 → @AfterReturning → @After(@After 在 @AfterReturning 之后执行,因为 @After 是 finally 语义)
- Spring AOP 默认同类内调用不走代理(和 @Transactional 一样的问题)
追问延伸:
- @After 和 @AfterReturning 的区别?
- 怎么在同一切面内控制 @Before 和 @After 的顺序?
Q22: Spring MVC 拦截器(HandlerInterceptor)和 Filter 有什么区别? 「🟡 中级」
考察点:Web 层两种拦截机制的理解和选型。
参考答案:
对比:
| 维度 | Filter | HandlerInterceptor |
|---|---|---|
| 规范 | Servlet 规范 | Spring MVC |
| 执行时机 | Servlet 容器层(DispatcherServlet 前后) | Spring MVC 层(Handler 前后) |
| 依赖 | 不依赖 Spring | 可以注入 Spring Bean |
| 配置 | web.xml / @WebFilter | WebMvcConfigurer / @Configuration |
| 执行顺序 | FilterChain 按声明顺序 | 按注册顺序 |
| 能力 | 请求/响应包装、编码、CORS | 认证授权、日志、参数预处理 |
Filter 的执行链:Filter1 → Filter2 → DispatcherServlet → Interceptor.preHandle → Controller → Interceptor.postHandle → Interceptor.afterCompletion → DispatcherServlet → Filter2 → Filter1
Interceptor 的三个方法:
preHandle:Handler 执行前,返回 false 中断请求postHandle:Handler 执行后、视图渲染前afterCompletion:视图渲染后(finally 语义,无论成功失败都执行)
选型建议:
- 通用过滤(编码、CORS、安全头)→ Filter
- 业务相关(认证授权、日志、参数校验)→ Interceptor
- 需要注入 Spring Bean → Interceptor
- 需要操作请求/响应体 → Filter(Interceptor 不方便修改请求体)
追问延伸:
- Filter 能拦截到静态资源吗?Interceptor 呢?
- preHandle 返回 false 后,afterCompletion 还会执行吗?
Q23: Spring 的 @Profile 多环境配置怎么用? 「🟢 校招/初级」
考察点:多环境配置管理的基础实践。
参考答案:
@Profile 的使用:
- 标注在 @Configuration 类上:整个配置类在指定 profile 激活时生效
- 标注在 @Component 上:该 Bean 在指定 profile 激活时注册
- 标注在 @Bean 方法上:单个 Bean 在指定 profile 激活时创建
激活 profile 的方式:
- 启动参数:
-Dspring.profiles.active=prod - 环境变量:
SPRING_PROFILES_ACTIVE=prod - 配置文件:
spring.profiles.active=prod - 编程式:
configurableEnvironment.setActiveProfiles("prod")
多环境配置文件(SpringBoot):
application.yml:公共配置application-dev.yml:开发环境application-test.yml:测试环境application-prod.yml:生产环境- 通过
spring.profiles.active激活对应的 profile
Profile 组(SpringBoot 2.4+):
spring.profiles.group.prod[0]=dev→ 激活 prod 时同时激活 dev
常见场景:
- 开发环境用 H2 内存数据库,生产用 MySQL
- 开发环境开启 debug 日志,生产关闭
- 不同环境配置不同的 Bean 实现
追问延伸:
- SpringBoot 的 profile 和 Maven 的 profile 区别?
- profile 激活后不满足条件的 Bean 会怎样?(不创建)
Q24: Spring Boot Actuator 有哪些常用端点?怎么自定义健康检查? 「🟡 中级」
考察点:Spring Boot 运维监控能力的理解。
参考答案:
Actuator 是 Spring Boot 的运维监控模块,提供生产可用的监控端点。
常用端点:
/actuator/health:健康检查(应用+依赖组件状态)/actuator/info:应用信息(版本、描述等)/actuator/metrics:指标数据(JVM、线程、HTTP 请求等)/actuator/loggers:日志级别查看和动态调整/actuator/env:环境变量和配置属性/actuator/beans:所有 Bean 列表/actuator/mappings:所有 URL 映射/actuator/threaddump:线程转储/actuator/heapdump:堆转储(下载文件)/actuator/prometheus:Prometheus 监控格式
配置:
- 默认只暴露 /health 和 /info(安全考虑)
management.endpoints.web.exposure.include=*:暴露所有management.endpoint.health.show-details=always:显示健康详情management.server.port=8081:独立端口(安全隔离)
自定义健康指标:
java
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// 检查外部依赖
boolean ok = checkRedis();
if (!ok) {
return Health.down().withDetail("error", "Redis not responding").build();
}
return Health.up().withDetail("version", "1.0").build();
}
}安全建议:
- 生产环境不要暴露所有端点(敏感信息泄露)
- 用独立端口 + 防火墙隔离
- 配合 Spring Security 做认证
追问延伸:
- health 端点的状态有哪些?(UP、DOWN、OUT_OF_SERVICE、UNKNOWN)
- 怎么和 Prometheus + Grafana 集成?
Q25: SpringBoot 的 SPI 机制和 Java 原生 SPI 有什么区别? 「🟡 中级」
考察点:对 SpringBoot 自动装配底层机制的理解。
参考答案:
Java 原生 SPI:
- 规范:
META-INF/services/接口全限定名文件中列出实现类 - 加载:
ServiceLoader.load(接口.class)一次性加载所有实现 - 缺点:一次性加载所有实现(不管用不用),无法按条件加载
SpringBoot SPI(SpringFactoriesLoader):
- 规范:
META-INF/spring.factories文件中列出需要自动装配的类 - 格式:
key=实现类1,实现类2,实现类3(key 通常是注解或接口全限定名) - 加载:
SpringFactoriesLoader.loadFactoryNames()加载所有配置 - 配合
@Conditional条件注解:按需装配(不是全部加载)
SpringBoot 2.7+ 的变化:
spring.factories中 AutoConfiguration 相关的 key 逐渐废弃- 新方式:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - 文件内容:每行一个自动配置类全限定名
- 好处:加载更高效(不需要解析 key-value),更清晰
区别总结:
| 维度 | Java SPI | SpringBoot SPI |
|---|---|---|
| 配置文件 | META-INF/services/接口名 | META-INF/spring.factories |
| 加载方式 | 一次性全部加载 | 按条件装配(@Conditional) |
| 配置格式 | 每行一个实现类 | key=value 列表 |
| 延迟加载 | 不支持 | 支持(条件判断后) |
| 适用场景 | JDK扩展(JDBC驱动、日志) | SpringBoot自动装配 |
追问延伸:
- SpringBoot 2.7 为什么要改用 .imports 文件?
- spring.factories 和 @Import 的区别?
Q26: SpringBoot 配置类排序(@AutoConfigureBefore/After)怎么用? 「🟡 中级」
考察点:自动装配顺序控制的理解。
参考答案:
配置类排序注解:
@AutoConfigureBefore(XxxConfig.class):当前配置在 XxxConfig 之前加载@AutoConfigureAfter(XxxConfig.class):当前配置在 XxxConfig 之后加载@AutoConfigureOrder(n):数值越小越先加载
使用场景:
- 自动配置类之间有依赖关系,需要保证顺序
- 如:DataSourceAutoConfiguration 必须在 MyBatisAutoConfiguration 之前
排序原理:
- AutoConfigurationSorter 负责排序
- 构建依赖图(@AutoConfigureBefore/After 形成的边)
- 拓扑排序
注意事项:
- 排序只影响自动配置类的加载顺序,不影响 Bean 的实例化顺序
- Bean 的实例化顺序由依赖关系决定(@DependsOn / 构造器依赖)
- @AutoConfigureBefore/After 只在 spring.factories / .imports 中注册的自动配置类之间有效
- 用户自定义的 @Configuration 不参与自动配置排序
实际使用:
- 大多数情况不需要手动排序(SpringBoot 内部已处理好)
- 自定义 Starter 时,如果依赖某个自动配置的 Bean,可以用 @AutoConfigureAfter
- @AutoConfigureOrder 的值建议用正整数
追问延伸:
- 配置类排序和 Bean 排序的区别?
- 如果 A 配置类 @AutoConfigureBefore B,B @AutoConfigureBefore A,会怎样?(循环依赖报错)
Q27: 如果让你设计一个 Spring IoC,你会从哪些方面考虑? 「🔴 高级」
考察点:对 IoC 容器设计原理的系统性理解,能从零推导核心组件。
参考答案:
设计一个 IoC 容器需要从以下几个维度考量:
配置读取与注册
- 定义配置来源:注解扫描、XML 文件、Java Config、API 编程式注册
- 将配置解析为统一的 BeanDefinition(元信息:类名、作用域、依赖、初始化/销毁方法、懒加载等)
- 注册到 BeanDefinitionRegistry(一个 Map 结构)
容器核心数据结构
Map<String, BeanDefinition> beanDefinitionMap:存放 Bean 定义Map<String, Object> singletonObjects:存放成品单例 BeanThreadLocal保证单线程刷新阶段的线程安全
Bean 生命周期管理
- 实例化(反射 newInstance / 构造器)
- 属性注入(依赖查找 / 注解扫描)
- Aware 回调(BeanNameAware、BeanFactoryAware、ApplicationContextAware)
- BeanPostProcessor 前置处理
- 初始化(
@PostConstruct、InitializingBean、init-method) - BeanPostProcessor 后置处理(AOP 代理就在这里生成)
- 销毁(DisposableBean、destroy-method)
依赖注入策略
- 构造器注入、Setter 注入、字段注入(
@Autowired/@Resource) - 按类型 / 按名称匹配
- 解决多个候选:
@Primary、@Qualifier、字段名匹配
- 构造器注入、Setter 注入、字段注入(
循环依赖处理
- 三级缓存机制(详见 Q2、Q30)
- 提前暴露半成品引用
扩展点设计
- BeanFactoryPostProcessor:容器刷新阶段修改 BeanDefinition
- BeanPostProcessor:Bean 实例化后修改 Bean
- FactoryBean:定制单个 Bean 的创建逻辑
- 事件发布机制(ApplicationEvent / ApplicationListener)
作用域支持
- singleton:全局唯一
- prototype:每次创建新实例
- request / session:Web 场景
懒加载与按需实例化
@Lazy标记的 Bean 在首次使用时才创建- 非 lazy 的单例在容器刷新时实例化
核心流程伪代码:
java
// 1. 加载配置,注册 BeanDefinition
refresh() {
// 读取注解/XML/Config,生成 BeanDefinition,注册到 registry
invokeBeanFactoryPostProcessors(beanFactory); // 修改 BeanDefinition
// 实例化所有非懒加载的单例
finishBeanFactoryInitialization(beanFactory);
}
// 2. 获取 Bean
getBean(name) {
Object bean = singletonObjects.get(name);
if (bean != null) return bean;
// 从三级缓存获取早期引用(循环依赖场景)
// 实例化 -> 填充属性 -> 初始化 -> 放入一级缓存
return createBean(beanDefinition);
}设计考量对比:
| 维度 | 设计决策 | 取舍理由 |
|---|---|---|
| 配置方式 | 注解优先,兼容 XML | 注解简洁,XML 利于无侵入 |
| 缓存层级 | 三级缓存 | 兼顾性能与 AOP 代理一致性 |
| 线程安全 | ConcurrentHashMap + 同步块 | 读多写少,并发友好 |
| 扩展机制 | PostProcessor 贯穿全流程 | 开闭原则,框架可插拔 |
| 懒加载 | 默认 eager,支持 lazy | 减少启动时不可预期错误 |
追问延伸:
- BeanDefinition 和 Bean 的关系是什么?(图纸与成品的类比)
- 如果不用三级缓存,你会怎么设计循环依赖解决方案?(二级缓存 + 提前代理)
- FactoryBean 和 BeanFactory 的区别?
Q28: Spring AOP 在 Spring 中的应用有哪些? 「🟡 中级」
考察点:理解 AOP 不仅是理论,而是 Spring 框架各处都在用的核心能力。
参考答案:
AOP(面向切面编程)在 Spring 内部的典型应用:
| 应用场景 | 实现方式 | 说明 |
|---|---|---|
| 声明式事务 | @Transactional | 最经典的应用,通过 AOP 在方法前后开启/提交/回滚事务 |
| 缓存抽象 | @Cacheable / @CacheEvict | 方法执行前查缓存,执行后写缓存 |
| 异步执行 | @Async | 将方法提交到线程池异步执行 |
| 定时任务 | @Scheduled | 按固定频率/延迟执行方法 |
| 安全控制 | Spring Security @PreAuthorize | 方法调用前做权限校验 |
| 日志记录 | 自定义切面 | 记录方法入参、出参、耗时 |
| 性能监控 | 自定义切面 + Actuator | 统计方法执行时间 |
| 重试机制 | Spring Retry @Retryable | 方法异常时自动重试 |
| 事件追踪 | 自定义切面 | 链路追踪埋点 |
| 全局异常 | @ControllerAdvice | 统一捕获 Controller 层异常 |
代码示例——自定义日志切面:
java
@Aspect
@Component
@Slf4j
public class LogAspect {
@Around("@annotation(operationLog)")
public Object around(ProceedingJoinPoint pjp, OperationLog operationLog) throws Throwable {
long start = System.currentTimeMillis();
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
log.info("方法 {} 开始执行, 参数: {}", methodName, Arrays.toString(args));
Object result;
try {
result = pjp.proceed(); // 执行目标方法
} catch (Throwable e) {
log.error("方法 {} 执行异常: {}", methodName, e.getMessage());
throw e;
}
long cost = System.currentTimeMillis() - start;
log.info("方法 {} 执行完成, 耗时: {}ms, 返回: {}", methodName, cost, result);
return result;
}
}自定义缓存切面示例:
java
@Aspect
@Component
public class SimpleCacheAspect {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
@Around("@annotation(cacheable)")
public Object around(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
String key = pjp.getSignature().toShortString();
if (cache.containsKey(key)) {
return cache.get(key); // 命中缓存直接返回
}
Object result = pjp.proceed();
cache.put(key, result);
return result;
}
}追问延伸:
@Transactional和自己写切面记录日志,两个切面怎么控制执行顺序?(@Order)- Spring AOP 和 AspectJ 的区别?(运行时代理 vs 编译时织入)
- 为什么
@Async标注的方法在同类内部调用不生效?
Q29: 动态代理是什么?能使用静态代理的方式实现 AOP 吗? 「🟡 中级」
考察点:代理模式的静态与动态实现,以及 Spring 选择动态代理的原因。
参考答案:
动态代理 是指在程序运行期间(而非编译期)动态生成代理对象,在代理对象中织入增强逻辑,再委托给目标对象执行。
Java 中两种主流动态代理:
| 维度 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 原理 | 基于接口,运行时生成实现接口的代理类 | 基于继承,运行时生成目标类的子类 |
| 依赖 | JDK 自带(java.lang.reflect.Proxy) | 需引入 CGLIB 库(SpringBoot 已内嵌) |
| 要求 | 目标类必须实现接口 | 目标类不能是 final,方法不能是 final |
| 性能 | 创建快,调用稍慢 | 创建慢,调用快 |
| Spring 默认 | 有接口时用 JDK | 无接口时用 CGLIB |
| SpringBoot 2.x | 默认全用 CGLIB | spring.aop.proxy-target-class=true |
JDK 动态代理示例:
java
// 目标接口
public interface UserService {
void save(String name);
}
// 目标实现
public class UserServiceImpl implements UserService {
@Override
public void save(String name) {
System.out.println("保存用户: " + name);
}
}
// JDK 动态代理
public class JdkProxyFactory implements InvocationHandler {
private final Object target;
public JdkProxyFactory(Object target) {
this.target = target;
}
public static Object createProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new JdkProxyFactory(target)
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[前置] 方法: " + method.getName());
Object result = method.invoke(target, args);
System.out.println("[后置] 方法执行完毕");
return result;
}
}
// 使用
UserService proxy = (UserService) JdkProxyFactory.createProxy(new UserServiceImpl());
proxy.save("张三");CGLIB 动态代理示例:
java
public class CglibProxyFactory implements MethodInterceptor {
public Object createProxy(Class<?> targetClass) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass);
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
System.out.println("[前置] " + method.getName());
Object result = methodProxy.invokeSuper(obj, args);
System.out.println("[后置] " + method.getName());
return result;
}
}能否用静态代理实现 AOP?
可以,但有明显缺点:
java
// 静态代理:手动编写代理类
public class UserServiceProxy implements UserService {
private final UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void save(String name) {
System.out.println("[前置]");
target.save(name);
System.out.println("[后置]");
}
}静态代理 vs 动态代理对比:
| 维度 | 静态代理 | 动态代理 |
|---|---|---|
| 生成时机 | 编译期手写 | 运行时自动生成 |
| 代理类数量 | 每个目标类一个代理类 | 一个工厂处理所有类 |
| 可维护性 | 差(接口变动需改代理) | 好(动态适配) |
| 性能 | 直接调用,无反射开销 | JDK 有反射开销,CGLIB 较小 |
| 适用场景 | 代理类少且固定 | 大量类需要统一增强 |
结论:静态代理可以实现 AOP 的功能,但在实际工程中不可行——Spring 需要为成百上千的 Bean 统一织入事务、缓存等逻辑,静态代理会导致类爆炸。动态代理是唯一现实的选择。
追问延伸:
- JDK 动态代理为什么必须基于接口?(Proxy 生成的类继承 Proxy,Java 不支持多继承)
- CGLIB 生成子类后,final 方法能被代理吗?(不能,final 方法无法被子类覆盖)
- 为什么 SpringBoot 2.x 默认改用 CGLIB?(避免接口判断的复杂性,统一行为)
Q30: Spring 为什么用三级缓存解决循环依赖?用二级缓存不行吗? 「🔴 高级」
考察点:深入理解三级缓存设计的根本动机——AOP 代理对象一致性。
参考答案:
先回顾三级缓存结构:
| 缓存层级 | 名称 | 类型 | 存放内容 |
|---|---|---|---|
| 一级 | singletonObjects | Map<String, Object> | 完整的成品 Bean |
| 二级 | earlySingletonObjects | Map<String, Object> | 提前暴露的半成品引用 |
| 三级 | singletonFactories | Map<String, ObjectFactory> | ObjectFactory 工厂(lambda) |
核心问题:为什么不用二级缓存?
如果只用二级缓存(一级成品 + 二级半成品),在没有 AOP 的场景下是完全可行的。但关键矛盾在于 AOP 代理。
考虑场景:A 依赖 B,B 依赖 A,且 A 需要 AOP 代理。
用二级缓存会发生什么?
如果只有二级缓存(一级 + 二级半成品),在 A 实例化后,必须立刻决定是否创建代理对象放入二级缓存。这意味着:
- 不管有没有循环依赖,实例化阶段就要提前执行 AOP 代理生成。
- 这违背了 Spring 的设计原则:AOP 代理应该在初始化后置阶段(
BeanPostProcessor)统一生成,而不是在实例化阶段。
三级缓存如何解决?
三级缓存存放的是 ObjectFactory(工厂 / lambda),它只是一个"承诺":只有当另一个 Bean 真正循环引用到 A 时,才会调用工厂方法生成早期引用(如果需要代理就生成代理,不需要就返回原始对象)。
java
// 三级缓存存的是工厂(lambda),不是成品
singletonFactories.put(beanName, () -> getEarlyBeanReference(beanName, rawBean));
// 只有被循环引用时才调用
protected Object getEarlyBeanReference(String beanName, Object rawBean) {
Object exposedObject = rawBean;
for (SmartInstantiationAwareBeanPostProcessor bp : bpList) {
// 如果需要 AOP,这里返回代理对象;否则返回原对象
exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
}
return exposedObject;
}关键流程(A 有 AOP,A 依赖 B,B 依赖 A):
1. A 实例化(raw A)
2. A 的 ObjectFactory 放入三级缓存
3. A 填充属性,发现需要 B
4. 创建 B → B 填充属性,发现需要 A
5. B 从三级缓存获取 A 的 ObjectFactory,调用它 → 生成 A 的代理对象(early A proxy)
6. early A proxy 放入二级缓存,三级缓存的 A 移除
7. B 拿到 early A proxy,B 完成初始化 → B 进入一级缓存
8. A 拿到 B,A 继续初始化
9. A 在后置处理时发现已经提前生成了代理(early A proxy),直接复用,不再重复生成
10. A 进入一级缓存如果没有循环依赖:三级缓存的 ObjectFactory 永远不会被调用,AOP 代理在后置处理阶段正常生成,符合设计原则。
总结:二级缓存不行的原因:
| 维度 | 二级缓存 | 三级缓存 |
|---|---|---|
| AOP 代理生成时机 | 实例化后立即生成(不管有没有循环依赖) | 只有循环依赖时才提前生成 |
| 设计原则 | 违背"AOP 统一在后置处理"原则 | 保持延迟生成代理的设计原则 |
| 性能 | 所有 Bean 实例化后都要判断 AOP | 仅循环依赖的 Bean 才提前判断 |
| 代理一致性 | 可能实例化阶段和后置阶段生成两个代理 | 通过 earlyProxyReferences 保证只生成一次 |
一句话:三级缓存的目的不是"解决循环依赖"(二级缓存也能解决),而是在解决循环依赖的同时保证 AOP 代理的延迟生成和一致性。
追问延伸:
@Async注解的 Bean 出现循环依赖为什么会报错?(@Async的代理在后置处理生成,不在getEarlyBeanReference中提前暴露)- 构造器注入的循环依赖为什么三级缓存也解决不了?(实例化阶段就需要依赖,还没来得及放入三级缓存)
- Spring 4.x 和 5.x 在循环依赖处理上有什么变化?(lambda化的三级缓存,性能优化)
Q31: Spring 常用注解有哪些? 「🟢 校招/初级」
考察点:Spring 全家桶常用注解的掌握广度。
参考答案:
Spring 常用注解按层级分类:
容器注册类:
| 注解 | 作用 | 典型场景 |
|---|---|---|
@Component | 通用组件注册 | 通用 Bean |
@Service | 业务层组件 | Service 类 |
@Repository | 持久层组件 | DAO 类 |
@Controller | 控制层组件 | MVC Controller |
@RestController | @Controller + @ResponseBody | RESTful API |
@Configuration | 配置类 | 替代 XML 配置 |
@Bean | 方法级注册 | 第三方库 Bean |
@ComponentScan | 包扫描 | 指定扫描路径 |
@Import | 导入配置类 | 组合配置 |
依赖注入类:
| 注解 | 作用 | 说明 |
|---|---|---|
@Autowired | 按类型注入 | Spring 提供,required=false 可选注入 |
@Resource | 按名称注入 | JSR-250 标准,name 属性指定名称 |
@Qualifier | 限定符 | 多个候选时按名称精确匹配 |
@Value | 注入配置值 | 读取 properties/yml 中的值 |
@Primary | 首选 Bean | 多个候选时优先选择 |
Web 层:
| 注解 | 作用 |
|---|---|
@RequestMapping / @GetMapping / @PostMapping | 请求映射 |
@RequestParam | 绑定请求参数 |
@PathVariable | 绑定路径变量 |
@RequestBody | 绑定请求体(JSON) |
@ResponseBody | 返回 JSON |
@RestController | Controller + ResponseBody |
@ExceptionHandler | 异常处理 |
@CrossOrigin | 跨域支持 |
条件装配类(SpringBoot):
| 注解 | 作用 |
|---|---|
@ConditionalOnClass | 类路径存在指定类时生效 |
@ConditionalOnMissingBean | 容器中不存在指定 Bean 时生效 |
@ConditionalOnProperty | 配置属性匹配时生效 |
@EnableConfigurationProperties | 启用配置属性绑定 |
功能增强类:
| 注解 | 作用 |
|---|---|
@Transactional | 声明式事务 |
@Async | 异步执行 |
@Scheduled | 定时任务 |
@Cacheable / @CacheEvict | 缓存 |
@TransactionalEventListener | 事务事件监听 |
代码示例:
java
@Service
@RequiredArgsConstructor // Lombok 构造器注入
public class OrderService {
private final UserService userService; // @Autowired 省略
private final OrderMapper orderMapper;
@Value("${app.name:default}")
private String appName;
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
userService.deductBalance(dto.getUserId(), dto.getAmount());
orderMapper.insert(dto);
}
@Cacheable(value = "orderCache", key = "#id")
public Order getById(Long id) {
return orderMapper.selectById(id);
}
}追问延伸:
@Autowired和@Resource的区别?(类型 vs 名称,Spring vs JSR 标准)@Component/@Service/@Repository功能上有区别吗?(功能一样,语义区分分层)- 为什么推荐构造器注入?(不可变、避免 NPE、方便单元测试)
Q32: Spring 事务在什么情况下会失效?使用 this 调用是否生效? 「🟡 中级」
考察点:Spring 声明式事务的 AOP 代理本质,以及常见的失效陷阱。
参考答案:
Spring 声明式事务基于 AOP 动态代理,因此一切绕过代理的调用都会导致事务失效。
事务失效场景汇总:
| 场景 | 失效原因 | 解决方案 |
|---|---|---|
| 同类内部方法调用(this) | 不走代理,切面不生效 | 注入自身代理 / AopContext / 拆分到另一个类 |
| 方法非 public | Spring 默认只代理 public 方法 | 改为 public |
| 方法被 final / static 修饰 | CGLIB 无法覆盖 final 方法 | 去掉 final |
| 异常被 catch 吞掉 | 切面收不到异常,不会回滚 | catch 后 rethrow 或手动回滚 |
| 异常类型不匹配 | 默认只回滚 RuntimeException 和 Error | @Transactional(rollbackFor = Exception.class) |
| 传播行为不当 | NOT_SUPPORTED 以非事务方式运行 | 检查传播行为配置 |
| Bean 未被 Spring 管理 | 没有经过容器创建,没有代理 | 加 @Service / @Component |
| 数据库引擎不支持事务 | 如 MySQL 的 MyISAM 引擎 | 改用 InnoDB |
| 多线程调用 | 事务绑定在 ThreadLocal,跨线程失效 | 避免在事务方法内开新线程 |
this 调用失效的详细分析:
java
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
orderMapper.insert(dto);
this.updateStock(dto.getProductId()); // this 调用,事务失效!
}
@Transactional(rollbackFor = Exception.class)
public void updateStock(Long productId) {
// 如果 updateStock 抛异常,不会回滚 createOrder 的操作
stockMapper.deduct(productId);
}
}原因:this.updateStock() 调用的是当前对象本身,而不是 Spring 生成的代理对象。代理对象才能触发 @Transactional 切面逻辑。
解决方案:
java
// 方案 1:注入自身代理
@Service
public class OrderService {
@Autowired
@Lazy
private OrderService self; // 注入自身的代理对象
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
orderMapper.insert(dto);
self.updateStock(dto.getProductId()); // 通过代理调用,事务生效
}
}
// 方案 2:使用 AopContext(需开启 exposeProxy)
// 启动配置:@EnableAspectJAutoProxy(exposeProxy = true)
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
orderMapper.insert(dto);
((OrderService) AopContext.currentProxy()).updateStock(dto.getProductId());
}
}
// 方案 3:拆分到不同类(最推荐)
@Service
public class OrderService {
@Autowired
private StockService stockService;
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
orderMapper.insert(dto);
stockService.updateStock(dto.getProductId()); // 跨类调用,走代理
}
}异常被吞掉的示例:
java
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
try {
orderMapper.insert(dto);
riskService.check(dto); // 抛出异常
} catch (Exception e) {
log.error("风险检查失败", e);
// 异常被 catch 吞掉了,切面认为正常执行,不回滚!
}
}
// 修复:catch 后 rethrow
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
try {
orderMapper.insert(dto);
riskService.check(dto);
} catch (Exception e) {
log.error("风险检查失败", e);
throw e; // 重新抛出,事务正常回滚
}
}追问延伸:
@Transactional的传播行为有哪些?(REQUIRED、REQUIRES_NEW、NESTED 等 7 种)@Transactional标注在抽象类方法上,子类调用会生效吗?(取决于是否被代理覆盖)rollbackFor = Exception.class不加会怎样?(默认只回滚 RuntimeException,受检异常不回滚)
Q33: Spring 容器里存的是什么?Bean 是不是单例? 「🟢 校招/初级」
考察点:对容器本质和 Bean 作用域的基础认知。
参考答案:
容器里存的是什么?
Spring 容器(ApplicationContext / BeanFactory)本质上是一个 Map,存放两类数据:
BeanDefinition(Bean 定义 / 元信息):描述 Bean 应该怎么创建
- 类名、作用域、构造参数、属性值、初始化/销毁方法、懒加载等
- 存放在
beanDefinitionMap
单例 Bean 实例:创建好的成品对象
- 存放在
singletonObjects(一级缓存) - key 是 Bean 名称,value 是 Bean 实例
- 存放在
简单来说:容器存的是 "图纸" + "成品"——BeanDefinition 是图纸,单例 Bean 是按图纸造出来的成品。
java
// 容器内部简化结构
public class DefaultListableBeanFactory {
// 图纸:Bean 定义
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
// 成品:单例对象(一级缓存)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
}Bean 是不是单例?
默认是单例(singleton),但可以通过 @Scope 注解指定其他作用域:
| 作用域 | 说明 | 使用方式 |
|---|---|---|
| singleton | 容器中只有一个实例(默认) | @Scope("singleton") 或不标注 |
| prototype | 每次获取都创建新实例 | @Scope("prototype") |
| request | 每个 HTTP 请求一个实例(Web) | @Scope("request") |
| session | 每个 HTTP Session 一个实例(Web) | @Scope("session") |
| application | 每个 ServletContext 一个实例 | @Scope("application") |
java
@Component
@Scope("prototype") // 每次注入/获取都创建新实例
public class OrderContext {
// ...
}
// 验证单例
@Component
public class SingletonDemo {
public void check() {
// 同一个 ApplicationContext 中两次获取是同一个对象
Object b1 = context.getBean("myService");
Object b2 = context.getBean("myService");
System.out.println(b1 == b2); // true(singleton)/ false(prototype)
}
}singleton 和 prototype 的生命周期区别:
| 维度 | singleton | prototype |
|---|---|---|
| 创建时机 | 容器启动时(非懒加载) | 每次 getBean 时 |
| 实例数量 | 容器中唯一 | 每次都是新对象 |
| 生命周期管理 | 容器完整管理(创建到销毁) | 容器只负责创建,不负责销毁 |
| 与循环依赖 | 支持三级缓存解决 | 不支持(每次都是新对象) |
追问延伸:
- 单例 Bean 是线程安全的吗?(容器保证单例,但不保证线程安全,需自己处理并发)
- prototype 作用域的 Bean 发生循环依赖会怎样?(直接报错,无法解决)
@Scope("prototype")注入到 singleton Bean 中会怎样?(只注入一次,不是每次获取新实例)
Q34: SpringBoot 怎么做到导入就可以直接使用的?写过 Starter 吗? 「🟡 中级」
考察点:SpringBoot 自动装配原理与自定义 Starter 的实战能力。
参考答案:
SpringBoot "导入即用"的核心机制是 自动装配(Auto-Configuration)。
自动装配原理:
@SpringBootApplication
→ @EnableAutoConfiguration
→ @Import(AutoConfigurationImportSelector.class)
→ 读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(2.7+)
或 META-INF/spring.factories(2.7 以前)
→ 加载所有自动配置类
→ @Conditional 条件过滤
→ 只有满足条件的配置类才生效
→ 配置类中的 @Bean 方法注册到容器以 DataSourceAutoConfiguration 为例:
java
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // classpath 有 DataSource 才生效
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 用户没自定义才用默认的
public DataSource dataSource(DataSourceProperties props) {
return props.initializeDataSourceBuilder().build();
}
}@ConditionalOnMissingBean 是"约定优于配置"的关键:用户没有自定义就用默认的,用户自定义了就覆盖默认的。
自定义 Starter 实战:
项目结构:
my-spring-boot-starter/
├── pom.xml
└── src/main/
├── java/com/example/starter/
│ ├── MyAutoConfiguration.java
│ ├── MyProperties.java
│ └── MyService.java
└── resources/
└── META-INF/spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports- 配置属性类:
java
@ConfigurationProperties(prefix = "my")
public class MyProperties {
private String name = "default";
private int timeout = 3000;
// getter/setter 省略
}- 核心服务类:
java
public class MyService {
private final MyProperties properties;
public MyService(MyProperties properties) {
this.properties = properties;
}
public String execute(String input) {
return "[" + properties.getName() + "] " + input;
}
}- 自动配置类:
java
@AutoConfiguration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "my", name = "enabled", havingValue = "true", matchIfMissing = true)
public MyService myService(MyProperties properties) {
return new MyService(properties);
}
}- 注册自动配置(SpringBoot 2.7+):
文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
com.example.starter.MyAutoConfiguration旧方式(SpringBoot 2.7 以下):
文件 META-INF/spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.MyAutoConfiguration- 使用方只需引入依赖 + 配置:
xml
<dependency>
<groupId>com.example</groupId>
<artifactId>my-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>yaml
my:
name: "production"
timeout: 5000
enabled: truejava
@Service
public class BizService {
@Autowired
private MyService myService; // 直接注入,开箱即用
public void doSomething() {
String result = myService.execute("hello");
}
}追问延伸:
@ConditionalOnMissingBean怎么实现"用户自定义覆盖默认配置"的?(加载顺序:用户配置优先于自动配置)- Starter 命名规范是什么?(第三方用
xxx-spring-boot-starter,官方用spring-boot-starter-xxx) - 自动配置类的加载顺序怎么控制?(
@AutoConfigureBefore/@AutoConfigureAfter/@AutoConfigureOrder)
Q35: MyBatis 和 JDBC 的区别?MyBatis 用了哪些设计模式?# 和 $ 的区别? 「🟡 中级」
考察点:ORM 框架原理、设计模式应用、SQL 注入防护。
参考答案:
MyBatis 和 JDBC 的区别:
| 维度 | JDBC | MyBatis |
|---|---|---|
| 抽象层级 | 底层 API | 半自动 ORM(SQL 映射) |
| SQL 编写 | 硬编码在 Java 中 | XML / 注解中声明,SQL 与代码分离 |
| 参数设置 | 手动 setXxx | 自动映射(#{} 占位符) |
| 结果映射 | 手动 ResultSet 遍历 | 自动映射到 Java 对象 |
| 连接管理 | 手动获取/关闭 | 集成 Spring 事务管理 |
| 动态 SQL | 字符串拼接(易出错) | <if> / <foreach> / <choose> 标签 |
| 缓存 | 无 | 一级缓存(SqlSession)+ 二级缓存(Mapper) |
| 可维护性 | 差(SQL 散落在代码中) | 好(集中管理,支持复用) |
JDBC 原生操作示例:
java
// JDBC:大量样板代码
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement("SELECT id, name FROM user WHERE id = ?");
ps.setLong(1, userId); // 手动设置参数
rs = ps.executeQuery();
if (rs.next()) {
User user = new User(); // 手动映射结果
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
return user;
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
// 手动关闭资源
if (rs != null) rs.close();
if (ps != null) ps.close();
if (conn != null) conn.close();
}MyBatis 等价写法:
java
// MyBatis:一行搞定
User user = userMapper.selectById(userId);xml
<!-- UserMapper.xml -->
<select id="selectById" parameterType="long" resultType="User">
SELECT id, name FROM user WHERE id = #{id}
</select>MyBatis 用了哪些设计模式:
| 设计模式 | 应用位置 | 说明 |
|---|---|---|
| Builder | SqlSessionFactoryBuilder | 构建 SqlSessionFactory |
| Factory | SqlSessionFactory / ObjectFactory | 创建 SqlSession / 结果对象 |
| 单例模式 | SqlSessionFactory | 全局唯一(Spring 集成后) |
| 代理模式 | MapperProxy(JDK 动态代理) | Mapper 接口无实现类,运行时代理生成 |
| 模板方法 | BaseExecutor / SimpleExecutor | 定义执行骨架,子类扩展 |
| 装饰器 | CachingExecutor | 包装普通 Executor,增加缓存能力 |
| 责任链 | Interceptor(插件机制) | Plugin 拦截器链,依次执行 |
| 适配器 | Logging | 适配不同日志框架(Log4j、SLF4J 等) |
| 迭代器 | DefaultResultSetHandler | 遍历 ResultSet |
# 和 $ 的区别:
| 维度 | #{} | ${} |
|---|---|---|
| 底层实现 | PreparedStatement 参数占位(?) | 字符串直接拼接 |
| SQL 注入 | 防止(参数预编译) | 有 SQL 注入风险 |
| 参数类型 | 自动类型转换 | 原样拼接 |
| 性能 | 预编译可缓存执行计划 | 每次生成不同 SQL,无法复用 |
| 使用场景 | 传值(WHERE、INSERT VALUES) | 传表名、列名、排序字段等结构 |
#{} 示例:
xml
<!-- #{} → PreparedStatement 的 ? 占位 -->
<select id="selectByName">
SELECT * FROM user WHERE name = #{name}
<!-- 实际执行:SELECT * FROM user WHERE name = ? -->
<!-- 参数通过 ps.setString(1, name) 设置,防止注入 -->
</select>${} 示例:
xml
<!-- ${} → 直接字符串拼接 -->
<select id="selectByOrder">
SELECT * FROM user ORDER BY ${column} ${order}
<!-- 实际执行:SELECT * FROM user ORDER BY create_time DESC -->
<!-- 如果 ${order} 传入恶意 SQL,会被直接拼接执行 -->
</select>SQL 注入风险演示:
java
// 危险:${} 直接拼接
userMapper.selectByName("admin' OR '1'='1");
// 实际 SQL:SELECT * FROM user WHERE name = 'admin' OR '1'='1'
// 结果:返回所有用户(注入成功!)
// 安全:#{} 预编译
userMapper.selectByName("admin' OR '1'='1");
// 实际 SQL:SELECT * FROM user WHERE name = ?
// 参数值:'admin' OR '1'='1'(作为一个完整字符串)
// 结果:查不到(安全!)规则:能用 #{} 的地方绝不用 ${},只有表名、列名、排序方向等结构性参数才用 ${},并且必须在代码层做白名单校验。
追问延伸:
- MyBatis 的一级缓存和二级缓存的区别?(SqlSession 级别 vs Mapper 级别,一级默认开启)
- Mapper 接口没有实现类,MyBatis 是怎么调用的?(JDK 动态代理
MapperProxy) - MyBatis 插件(Interceptor)能拦截哪些方法?(Executor、StatementHandler、ParameterHandler、ResultSetHandler)
Q36: Bean 的单例和非单例,生命周期是否一样? 「🟡 中级」
考察点:Bean 作用域和生命周期的深入理解。
参考答案:
单例(singleton)和非单例(prototype)的生命周期不同:
| 生命周期阶段 | singleton | prototype |
|---|---|---|
| 实例化 | 容器启动时(或首次请求时) | 每次请求时 |
| 属性注入 | 容器管理 | 容器管理 |
| 初始化 | 容器管理(@PostConstruct) | 容器管理 |
| 销毁 | 容器关闭时 | 容器不管,由 GC 回收 |
| 缓存 | 存在单例池 | 不缓存,每次 new |
java
@Component
@Scope("singleton") // 默认
public class SingletonBean { }
@Component
@Scope("prototype")
public class PrototypeBean { }关键区别:Spring 不管理 prototype Bean 的完整生命周期——创建后交给调用方,不调用销毁回调。
java
@Scope("prototype")
class MyBean implements DisposableBean {
@Override
public void destroy() {
// prototype 模式下,这个方法不会被 Spring 调用!
// 需要调用方手动释放或依赖 GC
}
}prototype 注入 singleton 的问题:
java
@Scope("singleton")
public class SingletonService {
@Autowired
private PrototypeService prototypeService; // 只注入一次,后续一直是同一个对象
public void doSomething() {
// prototypeService 始终是同一个实例!
// 因为 singleton 只在创建时注入一次
}
}
// 解决方案1:@Lookup(方法注入)
@Scope("singleton")
public class SingletonService {
@Lookup // Spring 用 CGLIB 重写此方法,每次返回新实例
public PrototypeService getPrototypeService() {
return new PrototypeService(); // 此行不会执行,被 Spring 覆盖
}
}
// 解决方案2:ObjectFactory
@Autowired
private ObjectFactory<PrototypeService> prototypeFactory;
public void doSomething() {
PrototypeService service = prototypeFactory.getObject(); // 每次获取新实例
}
// 解决方案3:ApplicationContext
@Autowired
private ApplicationContext context;
public void doSomething() {
PrototypeService service = context.getBean(PrototypeService.class);
}追问延伸:
- 其他作用域有哪些?(request、session、application、websocket)
- @Lookup 底层怎么实现的?(CGLIB 动态代理重写方法,返回
context.getBean())
Q37: HandlerMapping 和 HandlerAdapter 的作用是什么? 「🔴 高级」
考察点:Spring MVC 核心组件的理解。
参考答案:
Spring MVC 请求处理流程中的两大组件:
请求 → DispatcherServlet
↓
1. HandlerMapping → 找到对应的 Handler(Controller 方法)
↓
2. HandlerAdapter → 执行 Handler(适配不同类型的 Controller)
↓
3. ViewResolver / MessageConverter → 渲染结果HandlerMapping:根据 URL 找到对应的处理器(Handler)
| 实现类 | 说明 | 适用场景 |
|---|---|---|
RequestMappingHandlerMapping | @RequestMapping 注解映射 | 现代 Spring MVC |
BeanNameUrlHandlerMapping | Bean 名做 URL | 旧式配置 |
SimpleUrlHandlerMapping | 手动配置 URL 映射 | 静态资源等 |
java
// @RequestMapping 被扫描到
@Controller
@RequestMapping("/api")
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { ... }
// URL: /api/users/{id} → getUser 方法
}HandlerAdapter:执行不同类型的 Handler
| 实现类 | 适配的 Handler 类型 | 说明 |
|---|---|---|
RequestMappingHandlerAdapter | HandlerMethod(@RequestMapping 方法) | 最常用 |
HttpRequestHandlerAdapter | HttpRequestHandler | 静态资源处理 |
SimpleControllerHandlerAdapter | Controller 接口 | 旧式 Controller |
为什么需要 HandlerAdapter:
- 不同类型的 Controller 接口不同(注解式、实现接口式、函数式)
- Adapter 模式统一调用方式
java
// HandlerAdapter 的核心方法
boolean supports(Object handler); // 是否支持该类型
ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler);请求处理完整流程:
1. DispatcherServlet.doDispatch()
2. mappedHandler = getHandler(request) // HandlerMapping 找 Handler
3. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler())
4. mv = ha.handle(request, response, handler) // HandlerAdapter 执行
5. processDispatchResult(request, response, mappedHandler, mv) // 渲染追问延伸:
- @RequestMapping 是怎么注册到 HandlerMapping 的?(
RequestMappingHandlerMapping在初始化时扫描所有 @Controller 类的 @RequestMapping 注解) - 拦截器在哪一步执行?(HandlerMapping 找到 Handler 后,HandlerAdapter 执行前)
Q38: 怎么理解 SpringBoot 的约定大于配置? 「🟡 中级」
考察点:SpringBoot 核心理念的理解。
参考答案:
约定大于配置(Convention over Configuration):遵循框架默认约定,减少显式配置。只有与约定不同时才需要配置。
SpringBoot 的具体约定:
| 约定 | 默认值 | 偏离时配置 |
|---|---|---|
| 包扫描 | @SpringBootApplication 所在包及子包 | @ComponentScan(basePackages="...") |
| 配置文件 | application.yml / application.properties | spring.config.name |
| 端口 | 8080 | server.port=9090 |
| 数据源 | HikariCP 连接池 | 指定其他 DataSource |
| 视图 | 无(REST 优先) | 配置 ViewResolver |
| 静态资源 | /static、/public、/resources | spring.web.resources.static-locations |
| 日志 | Logback + 控制台输出 | logging.config=logback.xml |
| 自动装配 | META-INF/spring.factories(2.7+ 用 AutoConfiguration.imports) | @EnableAutoConfiguration(exclude=...) |
对比传统 Spring:
xml
<!-- 传统 Spring:大量 XML 配置 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/db" />
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
<property name="maximumPoolSize" value="10" />
</bean>
<!-- SpringBoot:约定好了 -->
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/db
username: root
password: 123456
# 连接池、驱动、连接数全部自动配置约定大于配置的代价:
- 黑盒化:不知道背后做了什么
- 调试困难:问题排查需要理解自动装配
- 灵活性受限:覆盖默认配置有时需要额外步骤
追问延伸:
@SpringBootApplication包含哪些注解?(@SpringBootConfiguration+@EnableAutoConfiguration+@ComponentScan)- 自动装配是怎么实现的?(SPI 机制加载
spring.factories/AutoConfiguration.imports)
Q39: SpringBoot 怎么开启事务?@Transactional 的最佳实践? 「🟡 中级」
考察点:Spring 事务的实际使用。
参考答案:
开启事务的方式:
java
// 方式1:在方法上使用 @Transactional(最常用)
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderDao.insert(order);
inventoryDao.deduct(order.getProductId());
}
}
// 方式2:在类上使用 @Transactional(所有 public 方法都开启事务)
@Service
@Transactional
public class OrderService {
public void createOrder() { ... }
public void cancelOrder() { ... }
}
// 方式3:在启动类上开启事务管理(SpringBoot 默认已开启)
@SpringBootApplication
@EnableTransactionManagement // SpringBoot 已自动配置,通常不需要显式加
public class App { }@Transactional 的关键属性:
| 属性 | 说明 | 常用值 |
|---|---|---|
| propagation | 传播行为 | REQUIRED(默认)、REQUIRES_NEW |
| isolation | 隔离级别 | DEFAULT(用数据库默认) |
| rollbackFor | 回滚异常类 | Exception.class |
| noRollbackFor | 不回滚异常 | 自定义异常 |
| timeout | 超时(秒) | 30 |
| readOnly | 只读 | true(查询方法) |
最佳实践:
java
// 1. rollbackFor 必须指定 Exception.class
@Transactional(rollbackFor = Exception.class)
// 默认只回滚 RuntimeException 和 Error,不回滚 Checked Exception
// 2. 查询方法标记只读
@Transactional(readOnly = true)
public List<User> findAll() { ... }
// 3. 新事务用 REQUIRES_NEW(如日志记录)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String action) { ... }
// 4. 超时设置
@Transactional(timeout = 30, rollbackFor = Exception.class)
// 5. 不要加在 private 方法上(AOP 代理无法拦截)
@Transactional // 无效!private 方法
private void process() { ... }
// 6. 不要在同一个类内 self-invocation
@Service
public class OrderService {
@Transactional
public void createOrder() { ... }
public void batchCreate() {
for (Order o : orders) {
createOrder(); // this.createOrder() → 不走代理 → 事务不生效!
}
}
}追问延伸:
@Transactional实现原理是什么?(AOP 动态代理,TransactionInterceptor拦截方法调用)- 为什么 self-invocation 事务不生效?(
this调用不经过代理对象,AOP 无法拦截)
Q40: SpringCloud 和 SpringBoot 的区别?常用微服务组件有哪些? 「🟡 中级」
考察点:微服务生态的整体认知。
参考答案:
| 维度 | SpringBoot | SpringCloud |
|---|---|---|
| 定位 | 单体应用框架 | 微服务治理框架 |
| 关注点 | 应用开发、自动装配 | 服务注册发现、配置、熔断、网关 |
| 关系 | 基础 | 基于 SpringBoot 构建 |
| 独立性 | 可独立使用 | 依赖 SpringBoot |
微服务核心组件:
| 功能 | SpringCloud Netflix(旧) | SpringCloud Alibaba(主流) |
|---|---|---|
| 服务注册发现 | Eureka | Nacos |
| 配置中心 | Config + Bus | Nacos Config |
| 网关 | Zuul / Gateway | Gateway |
| 负载均衡 | Ribbon | LoadBalancer |
| 熔断降级 | Hystrix | Sentinel |
| 远程调用 | Feign | OpenFeign |
| 链路追踪 | Sleuth + Zipkin | SkyWalking |
微服务架构图:
[Gateway 网关]
↓
┌────────┼────────┐
↓ ↓ ↓
[服务A] [服务B] [服务C]
↓ ↓ ↓
[Nacos 注册中心 + 配置中心]
↓
[Sentinel 熔断限流] [OpenFeign 远程调用]SpringCloud 版本演进:
- SpringCloud Netflix(Eureka/Hystrix/Ribbon)→ 停止维护
- SpringCloud Alibaba(Nacos/Sentinel/Seata)→ 主流推荐
- SpringCloud 2023.x(Spring Boot 3.x)
追问延伸:
- Nacos 和 Eureka 的区别?(Nacos 支持 AP/CP 切换,Eureka 只支持 AP;Nacos 支持配置中心)
- Sentinel 和 Hystrix 的区别?(Sentinel 基于滑动窗口限流,Hystrix 基于熔断器模式;Sentinel 有控制台)
Q41: Spring 的 @Conditional 注解族有哪些?条件装配的原理? 「🔴 高级」
考察点:SpringBoot 自动装配的核心机制。
参考答案:
@Conditional 注解族:根据条件决定是否注册 Bean。
java
// 基础注解:满足条件才注册
@Conditional(Condition.class)
public class MyConfig { }
// SpringBoot 预置条件注解:
@ConditionalOnClass(DataSource.class) // classpath 有该类
@ConditionalOnMissingBean(DataSource.class) // 容器中没有该 Bean
@ConditionalOnBean(DataSource.class) // 容器中有该 Bean
@ConditionalOnProperty(name = "app.cache", havingValue = "true") // 配置项满足
@ConditionalOnWebApplication // Web 应用
@ConditionalOnNotWebApplication // 非 Web 应用
@ConditionalOnExpression("#{systemProperties['os.name'].contains('Linux')}") // SpEL
@ConditionalOnJava(JavaVersion.EIGHT) // Java 版本
@ConditionalOnResource(resources = "classpath:db.sql") // 资源存在使用示例:
java
@Configuration
public class CacheConfig {
@Bean
@ConditionalOnProperty(name = "cache.type", havingValue = "redis")
public CacheService redisCache() { return new RedisCache(); }
@Bean
@ConditionalOnProperty(name = "cache.type", havingValue = "local", matchIfMissing = true)
public CacheService localCache() { return new LocalCache(); }
@Bean
@ConditionalOnMissingBean(CacheService.class)
public CacheService defaultCache() { return new NoOpCache(); }
}条件装配原理:
java
// @Conditional 的实现
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
// 在 Bean 注册时检查
// ConfigurationClassParser → ConditionEvaluator.shouldSkip()
// 如果 matches() == false → 跳过该 Bean 的注册自动装配的加载流程:
1. @EnableAutoConfiguration
2. AutoConfigurationImportSelector.selectImports()
3. 加载 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
4. 逐个检查 @Conditional 条件
5. 条件满足 → 注册 Bean;不满足 → 跳过追问延伸:
@ConditionalOnMissingBean的作用是什么?(用户自定义的 Bean 优先于自动装配的 Bean)- 为什么自动装配类不会全部生效?(@Conditional 注解过滤,只加载条件满足的配置)
Q42: Spring Boot 大型应用启动速度慢怎么优化? 「🔴 高级」
考察点:SpringBoot 性能优化的实战经验。
参考答案:
启动慢的原因分析:
- 组件扫描范围过大
- 自动装配加载过多
- 初始化连接(数据库、Redis、MQ)
- @PostConstruct / InitializingBean 初始化逻辑
- 日志框架初始化
优化方案:
java
// 1. 缩小组件扫描范围
@SpringBootApplication(scanBasePackages = "com.example.service")
// 而不是扫描 com(太广)
// 2. 关闭不需要的自动配置
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
RedisAutoConfiguration.class
})
// 或在 application.yml 中配置
// spring.autoconfigure.exclude=...
// 3. 延迟初始化
spring.main.lazy-initialization=true
// 所有非 singleton Bean 延迟到首次使用时创建
// 注意:不建议生产环境用,适合开发环境
// 4. 设置 Headless 模式(跳过 AWT 初始化)
System.setProperty("java.awt.headless", "true");
// 5. 关闭 Banner(微秒级优化)
spring.main.banner-mode=off
// 6. 精简日志
logging.level.root=WARN
// 7. JVM 参数优化
java -Xss256k -XX:TieredStopAtLevel=1 -jar app.jar
// -Xss256k:减少线程栈大小
// -TieredStopAtLevel=1:只用 C1 编译器(启动快)使用 Spring Boot 3.x 的虚拟线程:
java
// application.yml
spring.threads.virtual.enabled=true
// Tomcat 使用虚拟线程处理请求
// 减少平台线程数,提高 IO 密集场景吞吐量使用 GraalVM Native Image(终极优化):
bash
# 编译为原生可执行文件
./gradlew nativeCompile
# 启动时间从秒级降到毫秒级
# 代价:编译时间长、反射需配置、内存占用固定| 优化方案 | 效果 | 代价 |
|---|---|---|
| 缩小扫描范围 | 好 | 无 |
| 排除自动配置 | 好 | 需手动配置 |
| 延迟初始化 | 好 | 首次请求慢 |
| JVM 参数 | 中等 | 无 |
| GraalVM | 极好 | 编译慢、反射受限 |
追问延伸:
- 延迟初始化为什么不适合生产环境?(首次请求慢、问题暴露延迟、内存释放延迟)
- GraalVM Native Image 的局限性?(反射需要预先配置、不支持动态代理、不支持运行时类加载)