Spring 面试题
1. Spring IoC 容器启动与 Bean 生命周期
❓ 题目: 请描述 Spring IoC 容器的启动过程,以及一个 Bean 从创建到销毁的完整生命周期。
💡 答案:
Spring IoC 容器启动的核心流程可以概括为:资源定位 → 加载解析 → 注册 BeanDefinition → 实例化 → 属性填充 → 初始化。具体来说是:
- 容器首先通过 BeanDefinitionReader 读取配置文件或扫描注解,将每个 Bean 的元数据封装成 BeanDefinition 并注册到 BeanDefinitionRegistry。
- 然后通过 BeanFactoryPostProcessor 对 BeanDefinition 进行后置处理(比如
${}占位符替换)。 - 之后才是真正地实例化 Bean,这一步通常通过反射调用构造器完成,然后是属性注入(
@Autowired、@Value等),最后执行初始化回调。
对于单例 Bean,初始化完成后会放入一级缓存(单例池)供后续使用。容器关闭时会销毁单例 Bean,调用 @PreDestroy、DisposableBean.destroy() 或 destroy-method。
追问1: Spring 如何解决循环依赖?为什么三级缓存是必要的,二级缓存够吗?
Spring 解决循环依赖的核心是三级缓存,缺一不可。一级缓存(singletonObjects)存放完全创建好的单例 Bean;二级缓存(earlySingletonObjects)存放早期暴露的 Bean 引用;三级缓存(singletonFactories)存放能产生早期引用的 ObjectFactory。流程是:
- A 创建时把自己注册到三级缓存,然后注入属性,发现需要 B;
- B 创建时同样注册到三级缓存,然后注入属性,发现需要 A;
- B 从三级缓存拿到 A 的 ObjectFactory,调用它拿到 A 的早期引用,把 A 升级到二级缓存,B 完成创建进入一级缓存;
- A 拿到 B 完成创建也进入一级缓存。
二级缓存不够,是因为 Spring 的 AOP 动态代理在这个环节有特殊需求。如果只用二级缓存,当 A 需要被 AOP 增强时,B 注入的 A 到底是原始对象还是代理对象?三级缓存的 ObjectFactory 提供了”延迟决定”的能力——在真正需要注入的时候调用 getEarlyBeanReference 方法,如果这个 Bean 需要代理,此时返回代理对象;不需要就返回原始对象。这个判断时机必须在属性填充之前、循环依赖发生时,所以三级缓存实际上是为了 AOP 代理服务的。
追问2: @PostConstruct、InitializingBean.afterPropertiesSet()、init-method 三者的执行顺序是怎样的?为什么这么设计?
执行顺序是 @PostConstruct → afterPropertiesSet() → init-method。这么设计的原因是 Spring 希望保持对 JSR-250 规范的支持同时又兼容自己的扩展机制。@PostConstruct 是 Java 标准注解,优先级最高,最先执行。InitializingBean.afterPropertiesSet() 是 Spring 自己的接口,紧耦合于 Spring 框架,次之执行。init-method 是 XML 配置或 @Bean(initMethod=...) 指定的方法,最后执行,给用户最大的灵活性。三者虽然都能完成初始化逻辑,但在实际项目中推荐使用 @PostConstruct,因为它是标准注解,与 Spring 框架解耦,日后迁移成本更低。
📌 易错点 / 加分项:
- 构造器注入的循环依赖无法解决,会抛
BeanCurrentlyInCreationException - 多例(prototype)Bean Spring 不解决循环依赖
@PostConstruct需要CommonAnnotationBeanPostProcessor来解析执行
2. Spring 事务传播机制详解
❓ 题目: Spring 声明式事务的传播行为有哪些?请重点说明 REQUIRED 和 REQUIRES_NEW 的区别以及各自适用的业务场景。
💡 答案:
Spring 定义了七种事务传播行为,其中最常用也是面试重点的是 REQUIRED 和 REQUIRES_NEW。REQUIRED(默认)的意思是:如果当前存在事务就加入这个事务,如果不存在就新建一个事务。它适用于大多数业务场景——比如下单时扣库存、扣余额、生成订单这三个操作应该在同一个事务中,要么全成功要么全回滚。REQUIRES_NEW 的行为完全不同:不管当前有没有事务,它都会新建一个事务,并且如果当前有事务,就把当前事务挂起。比如说下单时我们需要记录一条操作日志,日志写入不可以因为主流程回滚而丢失——不管下单成功还是失败,日志都要落库。这时候日志方法用 REQUIRES_NEW,它独立于主事务提交,主事务回滚不会影响它。
追问1: REQUIRES_NEW 真的会挂起当前事务吗?底层是如何实现的?
是的,REQUIRES_NEW 确实会挂起当前事务。底层是通过 PlatformTransactionManager 的 suspend() 和 resume() 方法实现的。以 JDBC 为例,挂起事务的本质是:把当前线程持有的数据库连接(Connection)解绑并暂存起来,同时为新事务获取一个新的数据库连接(因为一个连接同时只能有一个事务)。新事务在新的连接上执行、提交或回滚,然后恢复原来挂起的事务,将原来的连接重新绑定到当前线程上。这个过程中涉及物理连接切换,如果连接池不够大,频繁的 REQUIRES_NEW 可能导致连接耗尽。
追问2: 在同一个 Service 类中,非事务方法直接调用 @Transactional 方法,事务会生效吗?为什么?
不会生效。原因在于 Spring 事务的底层实现是 AOP 代理。@Transactional 是通过生成代理对象来实现的——当你从容器中拿到 Bean 时,拿到的实际是代理对象,调用代理对象的方法时,代理会先执行事务切面逻辑再调用目标方法。但在同一个类内部,this 调用直接调用的是目标对象的方法,绕过了代理,事务切面根本没有机会执行。解决方案有三种:
- 一是将事务方法抽取到另一个 Bean 中,通过注入的方式调用;
- 二是通过
AopContext.currentProxy()获取当前代理对象来调用; - 三是在配置类上加
@EnableAspectJAutoProxy(exposeProxy = true)并结合((XxxService)AopContext.currentProxy()).method()来调用。
📌 易错点 / 加分项:
- 类内部调用事务失效是高频坑,务必结合 AOP 原理解释清楚
REQUIRES_NEW和NESTED的区别:前者完全独立事务,后者是父事务的子事务,父事务回滚子事务也回滚- 能提到
TransactionSynchronizationManager在背后管理事务资源的话会是加分项
3. Spring Bean 的作用域与作用域依赖问题
❓ 题目: Spring 的 Bean 有哪几种作用域?当一个单例 Bean 依赖一个多例 Bean 时会发生什么问题?如何解决?
💡 答案:
Spring 定义了五种作用域:
singleton(单例,整个 IoC 容器中只有一个实例,默认作用域)prototype(多例,每次获取都创建新实例)request(每个 HTTP 请求一个实例)session(每个 HTTP Session 一个实例)application(ServletContext 级别,全局唯一)
当单例 Bean 依赖多例 Bean 时会出现一个经典问题:单例 Bean 在容器启动时只创建一次,它依赖的多例 Bean 也在创建时被注入一次,之后每次从单例 Bean 中获取多例 Bean 拿到的都是同一个实例,多例的效果就丢失了。比如一个单例 Controller 注入了多例的 Service(业务上需要每次请求独立的 Service 实例),但实际上每次拿到的都是同一个。解决方案有三种:
- 一是实现
ApplicationContextAware接口,每次通过context.getBean()手动获取; - 二是使用
@Lookup注解,Spring 通过 CGLIB 动态代理让该方法每次都从容器获取新实例; - 三是在注入点用
ObjectFactory或Provider来延迟获取。
追问1: Request 作用域和 Session 作用域在 Web 应用中非常常见,Spring 是如何在 request 开始时自动创建这些 Bean 的?背后的机制是什么?
Request 和 Session 作用域属于”作用域代理”的经典应用场景。它们的生命周期不由 Spring IoC 容器管理,而是绑定在 Web 容器的 request/session 上。Spring 的实现原理:当定义一个 request 作用域的 Bean 时,Spring 不会直接在启动时创建它(因为此时还没有 request),而是注入一个代理对象(默认 CGLIB 或 JDK 动态代理)。当单例 Bean 调用代理对象的方法时,代理会拦截调用,从当前线程绑定的 RequestAttributes 中取出当前请求对象,然后用 request 作为 key 去 AbstractRequestAttributesScope 维护的 map 中查找实例——如果当前 request 没有这个 Bean 就创建新的,有就直接返回。这依赖的核心机制是 RequestContextHolder,底层用 ThreadLocal 绑定请求上下文。也就是说,虽然注入点看起来是普通字段,但拿到的实际上是代理,代理在运行时根据当前线程绑定的 request 来路由到对应的实际实例。
📌 易错点 / 加分项:
- 单例依赖多例的问题本质是”依赖注入只在容器初始化时发生一次”,这和 Spring 的 IoC 容器设计有关
@Lookup注解是 Spring 专门用来解决这个问题的,底层通过 CGLIB 生成子类实现- Request/Session 作用域在非 Web 环境下不能使用,会直接抛出异常
4. Spring Boot 自动配置原理
❓ 题目: Spring Boot 的自动配置是如何实现的?@SpringBootApplication 注解内部包含了哪些关键注解?
💡 答案:
Spring Boot 自动配置的核心机制是三层组合:@SpringBootApplication 是一个组合注解,内部包含 @SpringBootConfiguration(等同于 @Configuration)、@EnableAutoConfiguration 和 @ComponentScan。@EnableAutoConfiguration 是自动配置的入口,它通过 @Import(AutoConfigurationImportSelector.class) 导入选择器,选择器会读取 classpath 下所有 jar 包中的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3.x)或 META-INF/spring.factories(Spring Boot 2.x)文件中配置的自动配置类全限定名。这些自动配置类通常带有 @ConditionalOnClass、@ConditionalOnMissingBean 等条件注解,Spring Boot 逐个评估条件是否满足,满足的配置类就会生效。比如 classpath 下有 DataSource 类且容器中没有用户自定义的 DataSource Bean,DataSourceAutoConfiguration 就会创建默认的 DataSource。@ComponentScan 负责扫描当前包及其子包下的组件。
追问1: 如果你想自定义一个 Starter,需要做哪些事?自定义的自动配置类是如何被 Spring Boot 发现和加载的?
自定义一个 Starter 的步骤:
- 创建一个 autoconfigure 模块,编写自动配置类,用
@Configuration+@Bean定义组件的默认创建逻辑,并用@ConditionalOnClass(classpath 下有相应类时生效)、@ConditionalOnMissingBean(用户未自定义时生效)、@ConditionalOnProperty(按配置开关)等条件注解控制生效条件。 - 在
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中写入自动配置类的全限定名,这是 Spring Boot 3.x 的新规范(spring.factories在 3.x 中已被标记废弃,只保留向后兼容)。 - 通过
@ConfigurationProperties(prefix = "xxx")定义配置属性类,让用户可以在application.yml中对组件进行参数化配置。 - 创建一个 starter 模块(它本身只是一个空 jar,通过
pom.xml依赖 autoconfigure 模块和所需的第三方库),用户只要引入这个 starter,classpath 就自动具备了需要的类和自动配置。 - 在 starter 模块中加入
spring-boot-configuration-processor依赖,编译时生成spring-configuration-metadata.json,让 IDE 有自动提示。
📌 易错点 / 加分项:
- Spring Boot 2.x 用
spring.factories,3.x 用AutoConfiguration.imports,这个演进反映了自动配置机制的简化 @ConditionalOnBean有坑——它的判断顺序取决于 Bean 的创建顺序,可能因为顺序问题导致误判spring-boot-starter和spring-boot-starter-web的区别:前者只包含核心 starter,后者包含 web 相关
5. Spring AOP 代理方式与应用场景
❓ 题目: Spring AOP 的 JDK 动态代理和 CGLIB 代理有什么区别?Spring Boot 3.x 默认使用哪种代理?
💡 答案:
JDK 动态代理和 CGLIB 代理的底层实现完全不同。JDK 动态代理基于反射和接口实现——它在运行时动态生成一个实现目标接口的代理类,拦截方法调用后通过 InvocationHandler.invoke 转发,核心限制是目标类必须实现至少一个接口。CGLIB 基于字节码增强——它在运行时动态生成目标类的子类,通过 ASM 字节码框架重写父类方法,在方法中插入拦截逻辑。CGLIB 的限制是目标类不能是 final,被代理的方法不能是 final(因为 final 方法无法被子类重写),也不能是 private(private 方法 CGLIB 看不到)。Spring Boot 2.x 时代默认还是 JDK 代理,只在没有接口时用 CGLIB;Spring Boot 3.x 开始默认强制使用 CGLIB,因为它的 proxyBeanMethods 默认 true 用 CGLIB 可以处理类的内部调用问题,而且 CGLIB 的性能在现代版本中已经非常接近 JDK 代理。
追问1: AOP 切面的执行顺序可以控制吗?如果一个方法被多个切面拦截,执行顺序是怎样的?
可以控制,通过 @Order 注解来指定切面优先级。数值越小优先级越高。多个切面拦截同一个方法时形成”同心圆”的结构:优先级最高的切面在最外层,先执行”前置”通知,最后执行”后置”和”返回”通知(就像一个栈——前置按优先级正序执行,后置/返回/异常按优先级逆序执行)。也可以实现 Ordered 接口来设置顺序。不带 @Order 的切面执行顺序是不确定的。
追问2: @Transactional 事务注解是一个 AOP 切面,它的优先级比用户自定义切面高还是低?如果自定义切面里抛了异常,事务会回滚吗?
@Transactional 的事务切面由 TransactionInterceptor 实现,Spring 内部将其注册在 Ordered.LOWEST_PRECEDENCE 附近(事务切面的优先级很低,通常在所有自定义切面之后执行)。这意味着自定义切面通常包裹在事务切面的外层。如果自定义切面抛了异常,这个异常会在进入事务切面之前被抛出,所以事务逻辑根本没执行,也就不存在回滚的问题——事务还没开始。如果自定义切面在返回后抛异常(AfterThrowing),事务已经提交了,异常不会触发回滚。如果自定义切面的前置通知抛异常,事务切面还没运行,同样无所谓回滚。这里面比较微妙的是:如果你的切面在事务切面内部(order 设得比事务更大),且你的 AfterThrowing 抛了异常但事务的 AfterThrowing 还没执行,可能会导致事务回滚,但代码顺序非常绕,不建议这样玩。
📌 易错点 / 加分项:
- Spring AOP 只拦截 public 方法,private 和 protected 默认不经过代理
- CGLIB 生成的子类内部
this调用不走代理,这又回到了事务失效那个坑 - AOP 的
@Around可以实现”控制是否执行原方法”,这是其他通知做不到的
6. Spring 循环依赖与三级缓存
❓ 题目: Spring 如何通过三级缓存解决单例 Bean 的循环依赖?为什么不能解决构造器注入的循环依赖?
💡 答案:
三级缓存的核心设计是:一级缓存 singletonObjects 放完整的单例 Bean;二级缓存 earlySingletonObjects 放已经执行过 getEarlyBeanReference 的”早期引用”;三级缓存 singletonFactories 放 ObjectFactory,它是延迟触发的——只在真正需要时才调用生成早期引用。流程为:
- Bean A 实例化后,将自己包装成 ObjectFactory 注册到三级缓存;
- 然后属性填充,需要注入 B;
- B 实例化,同样注册到三级缓存;
- B 属性填充需要注入 A,从三级缓存找到 A 的 ObjectFactory,调用它获得 A 的早期引用,放入二级缓存并从三级缓存删除;
- B 完成属性填充和初始化,进入一级缓存;
- A 拿到 B 完成属性填充和初始化,进入一级缓存并删除二级缓存中的早期引用。
构造器注入无法解决的根因:构造器注入发生在实例化阶段(调用构造方法创建对象),此时 Bean 还没有被构造出来,自然无法被注册到任何缓存中——三级缓存的入口是”实例化后、属性填充前”,如果连实例化都卡在循环依赖中,那就是死锁。
追问1: 如果不用 AOP 代理,二级缓存是否足够?三级缓存的 ObjectFactory 究竟解决了什么问题?
二级缓存确实不够,但原因不是纯粹为了性能。关键在于 Spring AOP 动态代理的时机。一个需要 AOP 增强的 Bean A,它的代理对象可能在初始化阶段的 BeanPostProcessor 里才创建,而循环依赖需要在属性填充阶段就暴露出来。如果只有二级缓存,B 在属性填充时拿到的 A 只能是原始对象——如果 A 需要 AOP 增强,B 持有的 A 应该是代理对象而不是原始对象。三级缓存的 ObjectFactory 解决了这个”延迟决定”的问题:ObjectFactory 在被调用时才调用 getEarlyBeanReference,如果 A 需要 AOP 增强,这个方法会提前创建代理对象返回给 B;如果不需要,返回原始对象。这就把”是否创建代理、什么时候创建”的决定推迟到了需要的时候,而不是在 A 实例化那一刻就定了。
📌 易错点 / 加分项:
- prototype 作用域不解决循环依赖,会抛异常
7. Spring MVC 请求处理全流程
❓ 题目: 从浏览器发起一个 HTTP 请求,到 Spring MVC 返回响应,中间经过了哪些核心组件?DispatcherServlet 的请求处理流程是怎样的?
💡 答案:
Spring MVC 的核心入口是 DispatcherServlet——一个前端控制器,所有请求都由它统一接收和分发。处理流程分为以下步骤:
- 请求到达 DispatcherServlet:Tomcat 将 HTTP 请求封装为 HttpServletRequest,交给 DispatcherServlet 的
doDispatch方法。 - 获取 Handler:遍历所有 HandlerMapping(如 RequestMappingHandlerMapping),根据请求 URL 匹配到对应的 Controller 方法,返回 HandlerExecutionChain(包含 Handler 和所有 HandlerInterceptor)。
- 获取 HandlerAdapter:根据 Handler 类型找到能处理它的 HandlerAdapter。对于
@RequestMapping方法,使用 RequestMappingHandlerAdapter。 - 执行拦截器 preHandle:按注册顺序依次执行 HandlerInterceptor 的
preHandle,任何一个返回 false 则终止请求。 - 参数解析与调用:HandlerAdapter 通过 HandlerMethodArgumentResolver 解析方法参数——
@RequestParam、@RequestBody、@PathVariable等。@RequestBody的 JSON 反序列化通过 HttpMessageConverter(Jackson)完成。然后反射调用 Controller 方法。 - 返回值处理:通过 HandlerMethodReturnValueHandler 处理返回值。
@ResponseBody用 HttpMessageConverter 序列化为 JSON 写入 response。返回 View 名称用 ViewResolver 解析并渲染。 - 执行拦截器 postHandle/afterCompletion:视图渲染前后执行拦截器后处理逻辑。
📌 易错点 / 加分项:
- HandlerMapping 和 HandlerAdapter 是策略模式——Spring MVC 同时支持注解、XML、函数式等多种编程模型
@ResponseBody和@RestController等价——后者只是组合注解- 拦截器的
postHandle在视图渲染之前、afterCompletion在请求完全处理之后
8. Spring 事件机制与异步事件
❓ 题目: Spring 的事件机制是如何工作的?ApplicationEvent 和 ApplicationListener 的设计模式和底层实现是什么?如何让事件变为异步执行?
💡 答案:
Spring 事件机制基于观察者模式,由三个角色组成。ApplicationEvent(事件)——携带事件数据。ApplicationListener(监听器)——处理事件。ApplicationEventPublisher(发布者)——注入后调用 publishEvent() 发布事件。
底层实现:ApplicationContext 初始化时创建内部事件广播器(SimpleApplicationEventMulticaster)。发布事件时广播器遍历所有注册的监听器,找出监听该事件类型的(通过泛型解析),逐个调用 onApplicationEvent。Spring 4.2+ 也支持 @EventListener 注解方法,可按方法参数类型匹配,还能用 SpEL 做条件过滤。
默认情况下事件是同步执行的——publishEvent() 在所有监听器处理完成后才返回。让事件异步执行的方式:用 @Async 注解在 @EventListener 方法上,配合 @EnableAsync 开启异步支持——Spring 在线程池中异步执行。或直接配置广播器的 setTaskExecutor,所有事件都异步执行。
异步事件引入新问题:事务还没提交事件就发了,监听器可能读到脏数据——@TransactionalEventListener 可以指定在事务提交后(AFTER_COMMIT)才触发。
📌 易错点 / 加分项:
- ApplicationContext 自身启动时也发布事件——
ContextRefreshedEvent、ContextClosedEvent,可监听做应用初始化 @TransactionalEventListener配合 AFTER_COMMIT 适合”数据库写入成功后通知其他服务”- Spring 事件默认只支持单机本地事件——需要分布式事件可接 MQ 或用 Spring Cloud Bus
9. Spring Security 核心原理
❓ 题目: Spring Security 的核心架构是怎样的?它的过滤器链(Security Filter Chain)是如何工作的?认证和授权在源码层面的执行流程是什么?
💡 答案:
Spring Security 的核心是一组 Servlet Filter 组成的过滤器链,在请求到达 DispatcherServlet 之前完成安全处理。关键过滤器:
SecurityContextPersistenceFilter:从 SecurityContextRepository 恢复 SecurityContext。UsernamePasswordAuthenticationFilter:处理登录请求。ExceptionTranslationFilter:将 AuthenticationException 转为登录重定向、AccessDeniedException 转为 403。FilterSecurityInterceptor:最终授权决策。
认证流程:用户提交用户名密码 → UsernamePasswordAuthenticationFilter 构造 UsernamePasswordAuthenticationToken(未认证)→ 交给 AuthenticationManager(ProviderManager)→ 遍历所有 AuthenticationProvider,找到支持的(如 DaoAuthenticationProvider 调 UserDetailsService.loadUserByUsername() 查数据库,用 PasswordEncoder.matches() 校验)→ 认证成功返回已认证的 Authentication(含 GrantedAuthority)→ 存入 SecurityContext → SecurityContext 存入 session。
授权流程:请求到达 FilterSecurityInterceptor(或方法级 @PreAuthorize)→ 从 SecurityContext 取出 Authentication → 调用 AccessDecisionManager → 遍历 AccessDecisionVoter → 每个 Voter 根据权限信息投票(授予/拒绝/弃权)→ 任何一个授予且无人拒绝则通过,否则抛 AccessDeniedException。
📌 易错点 / 加分项:
- Spring Security 是过滤器链在 Servlet Filter 层工作,不是 HandlerInterceptor——先于 DispatcherServlet 执行
@PreAuthorize和@Secured的区别——前者支持 SpEL 表达式更灵活- 密码加密必须用 BCryptPasswordEncoder,绝不能明文存储
10. Spring Cloud 核心组件全景
❓ 题目: Spring Cloud 微服务体系中有哪些核心组件?它们各自解决了什么问题?如果让你从零搭建一个微服务系统,你会如何选型?
💡 答案:
Spring Cloud 核心组件按职责分工:
- 服务注册与发现:Nacos / Eureka / Consul。服务启动时注册自己,消费者从注册中心获取可用服务列表。Nacos 同时支持 AP 和 CP 模式,还能做配置中心,已成为国内主流。
- 配置中心:Nacos Config / Spring Cloud Config / Apollo。配置集中管理、动态刷新(
@RefreshScope),不再需要维护本地配置文件。 - 负载均衡:Spring Cloud LoadBalancer(替代 Ribbon)。客户端从服务列表中选取一个实例调用,支持轮询、随机、加权等策略。
- 远程调用:OpenFeign。声明式 HTTP 客户端——
@FeignClient("order-service")自动生成代理,像调本地方法一样调远程服务。 - 服务容错:Sentinel / Resilience4j(替代 Hystrix)。限流、熔断、降级。Sentinel 支持 QPS 限流、热点参数限流、系统自适应限流。
- 网关:Spring Cloud Gateway。统一入口,路由转发 + 认证鉴权 + 限流 + 日志。基于 WebFlux 异步非阻塞。
- 链路追踪:Micrometer Tracing + Zipkin / SkyWalking。TraceId 在全链路传递,Tracing 系统收集并可视化调用链。
选型建议:中小规模用 Nacos(注册中心+配置中心) + OpenFeign + Sentinel + Gateway 四件套。分布式事务场景加 Seata(AT 模式侵入低)。大规模可引入服务网格(Istio)接管网络层。
📌 易错点 / 加分项:
- Nacos 1.x 和 2.x 通信协议不同——1.x HTTP,2.x gRPC 双向流
- Sentinel 的限流规则可以动态调整——通过 Dashboard 或配置中心推送
- Feign 的超时配置和 LoadBalancer 的超时配置需要协调好——否则超时不生效
- 如果 B 的 ObjectFactory 在三级缓存中没有被 A 消费,B 初始化完成后会把三级缓存清除——二级缓存只是中转站
- 能讲清楚
addSingletonFactory、getSingleton、addSingleton这三个核心方法各自操作哪些缓存会加分