Skip to content

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}.ymlapplication.yml;同级别下 properties 优先于 yml。
  • 加载位置:项目根目录 ./config/ 高于 ././ 高于 classpath。
  • @Value 注入单个属性,@ConfigurationProperties 批量绑定且有类型校验,推荐后者。

追问延伸

  • 敏感配置(密码)怎么处理?(环境变量、配置中心、jasypt 加密)
  • Nacos 这类配置中心怎么实现动态刷新?

Q7: Spring Bean 的完整生命周期?有哪些扩展点? 「🟡 中级」

考察点:对Spring容器初始化全过程的掌握,扩展点的使用场景。

参考答案

完整生命周期流程:

  1. 实例化前:BeanPostProcessor.postProcessBeforeInstantiation
  2. 实例化:反射调用构造方法
  3. 属性注入:@Autowired 等依赖注入
  4. Aware回调:BeanNameAwareBeanFactoryAwareApplicationContextAware
  5. 初始化前:BeanPostProcessor.postProcessBeforeInitialization@PostConstruct 在此之前执行)
  6. 初始化:InitializingBean.afterPropertiesSetinit-method
  7. 初始化后:BeanPostProcessor.postProcessAfterInitialization(AOP代理在这里生成)
  8. 使用:Bean 放入容器,正常使用
  9. 销毁前:@PreDestroy
  10. 销毁:DisposableBean.destroydestroy-method

核心扩展点:

  • BeanPostProcessor:Bean 初始化前后的钩子(AOP、@Autowired 注入都靠它)
  • BeanFactoryPostProcessor:BeanDefinition 加载后、实例化前修改 BeanDefinition
  • InstantiationAwareBeanPostProcessor:实例化前后的更细粒度控制
  • FactoryBean:复杂 Bean 的创建(如 MyBatis 的 Mapper)

追问延伸

  • @PostConstructInitializingBeaninit-method 的执行顺序?
  • BeanPostProcessorBeanFactoryPostProcessor 的区别?

Q8: Spring 事务的实现原理?事务是怎么回滚的? 「🔴 高级」

考察点:Spring事务的AOP本质和底层机制。

参考答案

  • 实现原理:基于 AOP + 事务管理器(PlatformTransactionManager)+ 连接绑定
    1. Spring 为标注 @Transactional 的类生成代理对象
    2. 方法调用时,事务拦截器(TransactionInterceptor)拦截
    3. 开启事务:从数据源拿连接,关闭自动提交(setAutoCommit(false)
    4. 把连接绑定到当前线程(ThreadLocal,用 TransactionSynchronizationManager
    5. 执行目标方法
    6. 正常执行完:commit
    7. 抛出异常:判断是否需要回滚(默认 RuntimeExceptionError 才回滚)
  • 事务管理器:DataSourceTransactionManager(JDBC)、JpaTransactionManager(JPA)等
  • 事务属性:传播行为、隔离级别、超时、只读、回滚规则

追问延伸

  • 为什么事务注解标注在非 public 方法上会失效?(CGLIB 可以但 Spring AOP 默认只拦截 public)
  • 事务同步管理器(TransactionSynchronization)有什么用?

Q9: Spring MVC 的工作流程?DispatcherServlet 怎么处理请求? 「🟡 中级」

考察点:Web层核心流程的理解。

参考答案

核心组件:DispatcherServlet(前端控制器)、HandlerMapping(处理器映射)、HandlerAdapter(处理器适配器)、ViewResolver(视图解析器)

请求处理流程:

  1. 请求到达 DispatcherServlet
  2. DispatcherServlet 调用 HandlerMapping,根据 URL 找到 Handler(Controller 方法)
  3. 调用 HandlerAdapter,适配器执行 Handler(Controller 方法)
  4. Handler 执行完返回 ModelAndView
  5. DispatcherServlet 调用 ViewResolver 解析视图
  6. 渲染视图(把 model 数据填充到 view)
  7. 返回响应

SpringBoot 中默认的 DispatcherServlet 自动配置,映射到 /

追问延伸

  • Spring MVC 的拦截器怎么实现?和 Filter 有什么区别?
  • @RestController@Controller 的区别?

Q10: Spring 中用到了哪些设计模式? 「🟡 中级」

考察点:对Spring框架设计思想的理解深度。

参考答案

  • 工厂模式:BeanFactoryApplicationContext(创建 Bean)
  • 单例模式:Spring Bean 默认单例(三级缓存实现)
  • 代理模式:AOP 的 JDK 动态代理和 CGLIB
  • 模板方法模式:JdbcTemplateRedisTemplateTransactionTemplate
  • 观察者模式:ApplicationEvent 事件机制(事件、事件发布者、事件监听器)
  • 策略模式:Bean 的实例化策略、资源加载策略
  • 装饰器模式:BeanWrapper、OutputStream/InputStream 包装
  • 适配器模式:HandlerAdapter(适配不同类型的 Handler)

追问延伸

  • Spring 的事件机制怎么用?(ApplicationEvent + @EventListener
  • 模板方法和策略模式的区别?

Q11: Spring 事件机制(ApplicationEvent)是什么? 「🟡 中级」

考察点:Spring解耦机制的理解和应用。

参考答案

  • 观察者模式的实现,包含三个角色:
    • 事件(ApplicationEvent):自定义事件继承它
    • 事件发布者(ApplicationEventPublisher):发布事件
    • 事件监听器(ApplicationListener):监听并处理事件
  • 使用方式:
    1. 自定义事件:继承 ApplicationEvent
    2. 发布事件:applicationEventPublisher.publishEvent(event)
    3. 监听事件:实现 ApplicationListener 接口 或 @EventListener 注解
  • 特点:
    • 默认同步执行(发布线程执行监听逻辑)
    • 异步需要配合 @AsyncApplicationEventMulticaster 配置
  • 应用场景:注册后发欢迎邮件、订单完成后通知库存等解耦场景

追问延伸

  • 事件监听异常了会影响发布者吗?(默认会,因为同步)
  • Spring 内置了哪些事件?(ContextRefreshedEventContextStartedEvent 等)

Q12: 怎么自定义一个 SpringBoot Starter? 「🔴 高级」

考察点:SpringBoot自动装配的实战能力。

参考答案

Starter = 依赖集合 + 自动配置类

开发步骤:

  1. 创建一个 Maven 项目,命名为 xxx-spring-boot-starter
  2. 引入 spring-boot-autoconfigure 依赖
  3. 编写自动配置类(@Configuration + @ConditionalOnClass 等条件注解)
  4. 编写属性配置类(@ConfigurationProperties
  5. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中注册自动配置类(SpringBoot 2.7+)
  6. 打包发布

关键条件注解:

  • @ConditionalOnClass:类路径下有指定类才生效
  • @ConditionalOnMissingBean:容器中没有该 Bean 才创建
  • @ConditionalOnProperty:配置文件有指定属性才生效
  • @ConditionalOnWebApplication:Web 应用才生效

追问延伸

  • Starter 和自动配置的关系?
  • 怎么让 starter 的配置属性有 IDE 自动提示?(spring-configuration-metadata.json

Q13: Spring Security 的认证授权流程? 「🟡 中级」

考察点:安全框架的核心流程理解。

参考答案

核心组件:

  • Authentication:认证信息(用户名、密码、权限)
  • AuthenticationManager:认证管理器(核心接口)
  • AuthenticationProvider:认证提供者(具体认证逻辑)
  • UserDetailsService:加载用户信息
  • PasswordEncoder:密码加密
  • SecurityContext:安全上下文(存当前登录用户信息,存在 ThreadLocal)

认证流程:

  1. 用户提交用户名密码,封装成 Authentication(通常是 UsernamePasswordAuthenticationToken
  2. AuthenticationManager 认证(委托给 AuthenticationProvider
  3. AuthenticationProvider 调用 UserDetailsService 加载用户信息
  4. PasswordEncoder 比对密码
  5. 认证成功,把 Authentication 放入 SecurityContext
  6. 后续请求从 SecurityContext 取出当前用户做授权判断

授权:通过 FilterSecurityInterceptor + AccessDecisionManager 判断用户是否有权限访问资源

追问延伸

  • Spring Security 和 Shiro 的区别?
  • JWT + SpringSecurity 的认证流程有什么不同?

Q14: Spring Boot 的启动流程?SpringApplication.run() 做了什么? 「🟡 中级」

考察点:SpringBoot 启动全流程的理解。

参考答案

SpringApplication.run() 的完整流程:

  1. 初始化阶段:创建 SpringApplication 实例,推断应用类型(SERVLET/REACTIVE/NONE),加载 spring.factories 中的初始化器和监听器。
  2. 准备阶段:创建环境(Environment),加载 application.yml/properties,打印 Banner。
  3. 创建上下文:根据应用类型创建 ApplicationContext(SERVLET → AnnotationConfigServletWebServerApplicationContext)。
  4. 刷新上下文(refresh()):注册 BeanDefinition、执行 BeanFactoryPostProcessor、实例化 Bean(单例)、启动 Web 服务器(Tomcat/Jetty/Undertow)。
  5. 后处理:发布 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,每次从容器拿

追问延伸

  • 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 表示该方法异步执行(用线程池执行)
  • 返回值:voidFuture<T> / CompletableFuture<T>

原理:基于 AOP 代理

  • @Async 方法被代理,调用时提交到线程池
  • 默认用 SimpleAsyncTaskExecutor(每次新建线程,不推荐)
  • 自定义线程池:实现 AsyncConfigurer 或定义 TaskExecutor Bean

常见坑:

  1. 同类调用失效@Async 是基于 AOP 代理的,同类内部调用不生效(和 @Transactional 一样的问题)
  2. 异常丢失:异步方法的异常不会抛给调用者,需要:
    • 配置 AsyncUncaughtExceptionHandler 处理未捕获异常
    • 返回 Future 时异常封装在 Future 中
  3. 线程池耗尽:默认线程池没有上限控制,大量异步任务可能导致 OOM
  4. 循环依赖报错@Async 导致代理提前创建,可能触发循环依赖报错

最佳实践:

  • 自定义线程池(有界队列 + 合理拒绝策略)
  • CompletableFuture 替代 @Async(更灵活、可控)

追问延伸

  • @Async 和 CompletableFuture 怎么选?
  • 怎么配置自定义异步线程池?

Q19: Spring MVC 全局异常处理怎么做? 「🟢 校招/初级」

考察点:Web 应用的异常处理方案。

参考答案

三种方式(从简单到灵活):

  1. @ControllerAdvice + @ExceptionHandler(推荐):
    • 全局异常处理类,统一处理所有 Controller 抛出的异常
    • 可以返回 JSON(@ResponseBody)或页面
    • 可以限定处理的 Controller 范围
  2. HandlerExceptionResolver 接口:
    • 更底层的异常解析,Spring MVC 内部用
    • 默认有三个:ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver
  3. @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 后半部分
  • 外层切面先进入、后退出(栈结构)

控制顺序的方式:

  1. @Order(n) 注解:n 越小优先级越高(越外层)
  2. 实现 Ordered 接口:getOrder() 返回值越小越外层
  3. @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 层两种拦截机制的理解和选型。

参考答案

对比:

维度FilterHandlerInterceptor
规范Servlet 规范Spring MVC
执行时机Servlet 容器层(DispatcherServlet 前后)Spring MVC 层(Handler 前后)
依赖不依赖 Spring可以注入 Spring Bean
配置web.xml / @WebFilterWebMvcConfigurer / @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 的方式:

  1. 启动参数:-Dspring.profiles.active=prod
  2. 环境变量:SPRING_PROFILES_ACTIVE=prod
  3. 配置文件:spring.profiles.active=prod
  4. 编程式: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 SPISpringBoot 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 容器需要从以下几个维度考量:

  1. 配置读取与注册

    • 定义配置来源:注解扫描、XML 文件、Java Config、API 编程式注册
    • 将配置解析为统一的 BeanDefinition(元信息:类名、作用域、依赖、初始化/销毁方法、懒加载等)
    • 注册到 BeanDefinitionRegistry(一个 Map 结构)
  2. 容器核心数据结构

    • Map<String, BeanDefinition> beanDefinitionMap:存放 Bean 定义
    • Map<String, Object> singletonObjects:存放成品单例 Bean
    • ThreadLocal 保证单线程刷新阶段的线程安全
  3. Bean 生命周期管理

    • 实例化(反射 newInstance / 构造器)
    • 属性注入(依赖查找 / 注解扫描)
    • Aware 回调(BeanNameAware、BeanFactoryAware、ApplicationContextAware)
    • BeanPostProcessor 前置处理
    • 初始化(@PostConstruct、InitializingBean、init-method)
    • BeanPostProcessor 后置处理(AOP 代理就在这里生成)
    • 销毁(DisposableBean、destroy-method)
  4. 依赖注入策略

    • 构造器注入、Setter 注入、字段注入(@Autowired / @Resource
    • 按类型 / 按名称匹配
    • 解决多个候选:@Primary@Qualifier、字段名匹配
  5. 循环依赖处理

    • 三级缓存机制(详见 Q2、Q30)
    • 提前暴露半成品引用
  6. 扩展点设计

    • BeanFactoryPostProcessor:容器刷新阶段修改 BeanDefinition
    • BeanPostProcessor:Bean 实例化后修改 Bean
    • FactoryBean:定制单个 Bean 的创建逻辑
    • 事件发布机制(ApplicationEvent / ApplicationListener)
  7. 作用域支持

    • singleton:全局唯一
    • prototype:每次创建新实例
    • request / session:Web 场景
  8. 懒加载与按需实例化

    • @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默认全用 CGLIBspring.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 代理对象一致性。

参考答案

先回顾三级缓存结构:

缓存层级名称类型存放内容
一级singletonObjectsMap<String, Object>完整的成品 Bean
二级earlySingletonObjectsMap<String, Object>提前暴露的半成品引用
三级singletonFactoriesMap<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 + @ResponseBodyRESTful 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
@RestControllerController + 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 / 拆分到另一个类
方法非 publicSpring 默认只代理 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,存放两类数据:

  1. BeanDefinition(Bean 定义 / 元信息):描述 Bean 应该怎么创建

    • 类名、作用域、构造参数、属性值、初始化/销毁方法、懒加载等
    • 存放在 beanDefinitionMap
  2. 单例 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 的生命周期区别

维度singletonprototype
创建时机容器启动时(非懒加载)每次 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
  1. 配置属性类:
java
@ConfigurationProperties(prefix = "my")
public class MyProperties {
    private String name = "default";
    private int timeout = 3000;
    // getter/setter 省略
}
  1. 核心服务类:
java
public class MyService {
    private final MyProperties properties;

    public MyService(MyProperties properties) {
        this.properties = properties;
    }

    public String execute(String input) {
        return "[" + properties.getName() + "] " + input;
    }
}
  1. 自动配置类:
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);
    }
}
  1. 注册自动配置(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
  1. 使用方只需引入依赖 + 配置:
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: true
java
@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 的区别

维度JDBCMyBatis
抽象层级底层 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 用了哪些设计模式

设计模式应用位置说明
BuilderSqlSessionFactoryBuilder构建 SqlSessionFactory
FactorySqlSessionFactory / 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)的生命周期不同

生命周期阶段singletonprototype
实例化容器启动时(或首次请求时)每次请求时
属性注入容器管理容器管理
初始化容器管理(@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
BeanNameUrlHandlerMappingBean 名做 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 类型说明
RequestMappingHandlerAdapterHandlerMethod(@RequestMapping 方法)最常用
HttpRequestHandlerAdapterHttpRequestHandler静态资源处理
SimpleControllerHandlerAdapterController 接口旧式 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.propertiesspring.config.name
端口8080server.port=9090
数据源HikariCP 连接池指定其他 DataSource
视图无(REST 优先)配置 ViewResolver
静态资源/static/public/resourcesspring.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 的区别?常用微服务组件有哪些? 「🟡 中级」

考察点:微服务生态的整体认知。

参考答案

维度SpringBootSpringCloud
定位单体应用框架微服务治理框架
关注点应用开发、自动装配服务注册发现、配置、熔断、网关
关系基础基于 SpringBoot 构建
独立性可独立使用依赖 SpringBoot

微服务核心组件

功能SpringCloud Netflix(旧)SpringCloud Alibaba(主流)
服务注册发现EurekaNacos
配置中心Config + BusNacos Config
网关Zuul / GatewayGateway
负载均衡RibbonLoadBalancer
熔断降级HystrixSentinel
远程调用FeignOpenFeign
链路追踪Sleuth + ZipkinSkyWalking

微服务架构图

         [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 性能优化的实战经验。

参考答案

启动慢的原因分析

  1. 组件扫描范围过大
  2. 自动装配加载过多
  3. 初始化连接(数据库、Redis、MQ)
  4. @PostConstruct / InitializingBean 初始化逻辑
  5. 日志框架初始化

优化方案

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 的局限性?(反射需要预先配置、不支持动态代理、不支持运行时类加载)