Spring

Posted by 佳运 Blog on May 1, 2025

Spring

Bean的生命周期

  • 通过BeanDefinition获取bean的定义信息
  • 调用构造函数实例化bean
  • bean的依赖注入
  • 处理Aware接口(BeanNameAware、BeanFactoryAware、ApplicationContextAware)
  • Bean的后置处理器BeanPostProcessor-before
  • 初始化方法(InitializingBean、init-method)
  • Bean的后置处理器BeanPostProcessor–after
  • 销毁bean

Bean 是线程安全的吗?

Spring 框架中的 Bean 是否具备线程安全性,主要取决于它的作用域以及是否包含可变状态。

Bean 线程安全性的影响因素

Spring 默认的 Bean 作用域是 singleton(单列模式),即在 IoC 容器中只会创建一个实例,并被多个线程共享。如果这个 Bean 维护了可变的成员变量,就可能在并发访问时引发数据不一致的问题,从而导致线程安全风险。

prototype 作用域 下,每次获取 Bean 都会创建新的实例,因此不会发生资源竞争,自然也就没有线程安全问题。

单例 Bean 是否一定不安全?

不一定!

无状态 Bean 是线程安全的:例如常见的 Service 或 Dao 层 Bean,它们通常不存储可变数据,仅执行业务逻辑,因此不会受到并发影响。

有状态 Bean 可能会引发线程安全问题:如果 Bean 存储了可变成员变量,比如用户会话信息、计数器等,可能会因多个线程同时访问导致数据不一致。

解决有状态 Bean 的线程安全问题

如果一个单例 Bean 需要维护状态,可通过以下方式确保线程安全:

  • 设计为无状态 Bean:尽量避免定义可变成员变量,或在方法内部使用局部变量。
  • 使用 ThreadLocal:让每个线程拥有独立的变量副本,防止数据共享导致冲突。
  • 同步控制:在访问共享资源时,使用 synchronized 或 ReentrantLock 进行加锁,确保线程互斥访问。

循环依赖问题

循环依赖:循环依赖其实就是循环引用,也就是两个或两个以上的bea互相特有对方,最终形成闭环。比如A依赖于B,B依赖于A

循环依赖在spring中是允许存在,spring框架依据三级缓存已经解决了大部分的循环依赖

  • 一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
  • 二级缓存:缓存早期的bean对象生命周期还没走完
  • 三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

为什么使用三级缓存,二级缓存不行吗

先说结论,两级缓存原则上可以解决循环依赖的问题,包括代理,但在某些情况下实现方式可能不够恰当。

缓存字段名 缓存级别 数据类型 描述
singletonObjects 1 Map<String, Object> 存储 Bean 的完成品,完全初始化
earlySingletonObjects 2 Map<String, Object> 存储 Bean 的半成品,尚未完成属性填充和初始化
singletonFactories 3 Map<String, ObjectFactory> 存储创建 Bean 的 ObjectFactory 对象,生成半成品 Bean 放入二级缓存

两级缓存分为两种情况来说,分别是 一级缓存 + 二级缓存 和 一级缓存 + 三级缓存 两种组合。

组合一:一级缓存 + 二级缓存

singletonObjects + earlySingletonObjects 理论可以解决依赖注入,也可以解决代理,但需要每次加入二级缓存都要是代理对象,如果没有代理就完全没有必要,同时也不符合 Spring 对 Bean 生命周期的定义。(对象都应该在创建建之后再进行动态代理而不是单纯的实例化以后就急着进行代理,如果循环依赖就是没办法的事)

组合二:一级缓存 + 三级缓存

singletonObjects + singletonFactories 可以解决依赖注入的问题,但是没法解决代理的问题,若要进行代理从 ObjectFactory 中获取对象实例进行代理,但是这样每次获取对象都不是同一个。(在单例模式下不适合)

以上是在Bean生命周期中的初始化赋值阶段产生的循环依赖,如果是在构造函数阶段产生循环依赖怎么办(如上图)

使用@Lazy进行懒加载,什么时候需要对象在进行bean对象的创建

实例的注入方式

首先来看看 Spring 中的实例该如何注入,总结起来,无非三种:

  • 属性注入
  • set 方法注入
  • 构造方法注入

1. 属性注入

属性注入是大家最为常见也是使用最多的一种注入方式了,代码如下:

@Service
public class BService {
    @Autowired
    AService aService;
    //...
}

这里是使用 @Autowired 注解注入。另外也有 @Resource 以及 @Inject 等注解,都可以实现注入。

2. set 方法注入

set 方法注入太过于臃肿,实际上很少使用:

@Service
public class BService {
    AService aService;

    @Autowired
    public void setaService(AService aService) {
        this.aService = aService;
    }
}

3. 构造方法注入

构造方法注入方式如下:

@Service
public class AService {
    BService bService;
    @Autowired
    public AService(BService bService) {
        this.bService = bService;
    }
}

如果类只有一个构造方法,那么 @Autowired 注解可以省略;如果类中有多个构造方法,那么需要添加上 @Autowired 来明确指定到底使用哪个构造方法。

谈谈自己对于 Spring IoC 的了解

Spring IoC(Inversion of Control,控制反转)是 Spring 框架的核心机制之一,负责管理对象的创建、依赖关系和生命周期,从而实现组件解耦,提升代码的可维护性和扩展性。

首先,IoC 的核心思想 是将对象的管理权从应用程序代码中转移到 Spring 容器。传统方式下,类 A 依赖于类 B,A 需要自己创建 B 的实例,而在 IoC 模式下,Spring 负责实例化和注入 B,A 只需要声明依赖即可。

其次,Spring IoC 主要通过依赖注入(DI)来实现。Spring 通过 XML 配置、Java 注解(@Autowired、@Resource)或 Java 代码(@Bean)定义 Bean 及其依赖关系,容器会在运行时自动解析并注入相应的对象。

接着,Spring IoC 的工作流程 可以分为三个阶段:

第一个阶段是IOC 容器初始化,

Spring 解析 XML 配置或注解,获取所有 Bean 的定义信息,生成 BeanDefinition。

BeanDefinition 存储了 Bean 的基本信息(类名、作用域、依赖等),并注册到 IOC 容器的 BeanDefinitionMap 中。

这个阶段完成了 IoC 容器的初始化,但还未实例化 Bean。

第二个阶段是Bean 实例化及依赖注入

Spring 通过反射实例化那些 未设置 lazy-init 且是单例模式 的 Bean。

依赖注入(DI)发生在这个阶段,Spring 根据 BeanDefinition 解析 Bean 之间的依赖关系,并通过构造方法、setter 方法或字段注入(@Autowired)完成对象的注入。

第三个阶段是Bean 的使用

业务代码可以通过 @Autowired 或 BeanFactory.getBean() 获取 Bean。

对于 设置了 lazy-init 的 Bean 或非单例 Bean,它们的实例化不会在 IoC 容器初始化时完成,而是在 第一次调用 getBean() 时 进行创建和初始化,且 Spring 不会长期管理它们。

最后,Spring IoC 主要解决三个问题

第一个是降低耦合,组件之间通过接口和依赖注入解耦,增强了代码的灵活性。

第二个是简化对象管理,开发者无需手动创建对象,Spring 统一管理 Bean 生命周期。

第三个是提升维护性,当需要修改依赖关系时,只需调整配置,而无需修改业务代码。

IoC 和 DI 的区别

IoC(Inversion of Control,控制反转)DI(Dependency Injection,依赖注入)是 Spring 框架中非常重要的两个概念。虽然它们密切相关,但它们的含义和作用有所不同。以下是它们的区别及联系:

(1)定义与核心思想

IoC(控制反转)

定义:控制反转是一种设计原则,指的是将对象的创建、依赖管理和生命周期的控制权从应用程序代码转移到框架或容器中。

核心思想:传统开发中,对象需要自己负责创建依赖的对象(即“正向控制”)。而在 IoC 中,对象不再负责创建依赖,而是由容器来管理这些依赖关系。

DI(依赖注入)

定义:依赖注入是 IoC 的一种实现方式,指的是容器通过构造方法、setter 方法或字段注入的方式,将对象的依赖自动传递给它。

核心思想:对象只需要声明它需要的依赖,而不需要关心如何获取这些依赖。容器会负责将依赖注入到对象中。

(2)区别对比

维度 IoC(控制反转) DI(依赖注入)
定义 一种设计原则,强调控制权的转移 一种具体实现方式,用于实现 IoC
关注点 对象的创建、依赖管理和生命周期的控制权 如何将依赖传递给对象
实现方式 通过容器(如 Spring 容器)管理对象 通过构造方法、setter 方法或字段注入依赖
范围 更广泛,包含 DI 和其他实现方式 是 IoC 的一个子集
示例 Spring 容器接管了对象的创建和管理 Spring 容器通过 @Autowired 注入依赖

(3)联系

DI 是 IoC 的实现方式:依赖注入是控制反转的一种具体实现形式。IoC 是一种更广泛的设计原则,而 DI 是 IoC 的一种技术手段。

共同目标:两者都旨在降低代码的耦合性,提升代码的可维护性和扩展性。

什么是动态代理?

动态代理是一种在运行时动态生成代理对象,并在代理对象中增强目标对象方法的技术。它被广泛用于 AOP(面向切面编程)、权限控制、日志记录等场景,使得程序更加灵活、可维护。动态代理可以通过 JDK 原生的 Proxy 机制或 CGLIB 方式实现。接下来我会讲述动态代理的实现方式和执行流程。

首先,JDK 动态代理基于接口,适用于代理实现了接口的对象,当使用 JDK 动态代理时,主要分为四步,

  • 第一步是定义接口,由于动态代理是基于接口进行代理的,因此目标对象必须实现接口。
  • 第二步是创建并实现 InvocationHandler 接口,并在 invoke 方法中定义增强逻辑。
  • 第三步是生成代理对象,使用 Proxy.newProxyInstance 创建代理对象,代理对象内部会调用 invoke 方法。
  • 第四步是调用代理方法,当调用代理对象的方法时,invoke 方法会被触发,执行增强逻辑,并最终调用目标方法。

其次,CGLIB 通过子类继承目标类,适用于没有实现接口的类,当使用 CGLIB 动态代理时,主要分为四步,

  • 第一步是通过 Enhancer 创建代理对象。
  • 第二步是设置父类,CGLIB 代理基于子类继承,因此代理对象是目标类的子类。
  • 第三步是定义并实现 MethodInterceptor 接口,在 intercept 方法中增强目标方法。
  • 第四步是调用代理方法,当调用代理对象的方法时,intercept 方法会被触发,执行增强逻辑,并最终调用目标方法。

JDK动态代理在创建代理实例时的性能优于Cglib,而在方法调用时,Cglib的性能则优于JDK动态代理。这是因为JDK动态代理在方法调用时依赖于反射,而Cglib直接调用生成的子类方法。

动态代理和静态代理的区别

代理是一种常用的设计模式,目的是:为其他对象提供一个代理以控制对某个对象的访问,将两个类的关系解耦。代理类和委托类都要实现相同的接口,因为代理真正调用的是委托类的方法。

区别:

  • 静态代理由程序员创建或者是由特定工具创建,在代码编译时就确定了被代理的类是一个静态代理。静态代理通常只代理一个类
  • 动态代理在代码运行期间,运用反射机制动态创建生成。动态代理代理的是一个接口下的多个实现类。

spring AOP的执行流程

Spring AOP(Aspect-Oriented Programming,面向切面编程)是一种通过代理机制实现方法增强的技术,它允许在不修改原始代码的情况下,对方法的执行过程进行扩展,如日志记录、事务管理、权限控制等。Spring AOP 主要依赖动态代理来实现,接下来我会讲述 Spring AOP 的执行流程,主要分为六步。

当 Spring AOP 拦截一个方法调用时:

  • 第一步是要定义切面(Aspect),可以使用 @Aspect 标注类,并在其中定义切点(Pointcut)和通知(Advice),如 @Before、@After、@Around 等。
  • 第二步是要解析切点,Spring 会解析 @Pointcut 表达式,确定需要增强的方法。
  • 第三步是要创建代理对象如果目标类实现了接口,Spring 使用 JDK 动态代理,通过 Proxy.newProxyInstance 生成代理对象;如果目标类没有实现接口Spring 使用 CGLIB 动态代理,通过创建目标类的子类来生成代理对象。
  • 第四步是要方法调用拦截,如果是JDK 动态代理,代理对象会拦截方法调用,并调用 InvocationHandler#invoke,执行增强逻辑后,再调用目标方法;如果是CGLIB 动态代理,代理对象则通过 MethodInterceptor#intercept 代理方法调用,执行增强逻辑后,再调用目标方法。
  • 第五步是要执行增强逻辑,根据通知类型,在方法执行前后或异常时,执行对应的 AOP 逻辑,如日志记录、事务提交等。
  • 第六步是要执行目标方法,最终调用目标对象的方法,完成实际业务逻辑。

AOP 关键术语

(1) Aspect(切面)

定义:切面是横切关注点的模块化表示,通常包含一组相关的通知(Advice)和切点(Pointcut)。

作用:将通用的功能(如日志、事务管理)封装到一个独立的模块中。

(2) Join Point(连接点)

定义:程序执行过程中的某个特定点,例如方法调用、方法执行、异常抛出等。

作用:Spring AOP 中的连接点通常是方法执行。

(3) Pointcut(切点)

定义:切点是一组连接点的集合,用于定义哪些连接点需要被增强。

作用:通过表达式匹配特定的方法或类。

(4) Advice(通知)

定义:通知是切面在特定连接点上执行的动作,定义了“何时”以及“如何”增强。

作用:在方法执行前、后或异常时执行额外的逻辑。

(5) Target Object(目标对象)

定义:被代理的对象,也就是需要增强的业务逻辑对象。

作用:目标对象的方法会被切面拦截并增强。

(6) Proxy(代理对象)

定义:由 AOP 框架生成的代理对象,用于拦截目标对象的方法调用并执行增强逻辑。

作用:实现方法拦截和增强。

(7) Weaving(织入)

定义:将切面应用到目标对象并创建代理对象的过程。

作用:织入可以在编译时、类加载时或运行时完成。

AOP 常见的通知类型

通知类型 注解 执行时机 用途
Before Advice @Before 在目标方法执行之前 日志记录、权限检查
After Returning @AfterReturning 在目标方法成功返回后 记录返回值、清理资源
After Throwing @AfterThrowing 在目标方法抛出异常后 异常处理、日志记录
After (Finally) @After 在目标方法结束时(无论成功还是失败) 资源释放、日志记录
Around Advice @Around 在目标方法执行前后,可控制目标方法的执行 性能监控、事务管理、日志记录

AOP 的应用场景有哪些?

  • 日志记录:在方法执行前后自动记录日志。
  • 事务管理:自动开启、提交或回滚事务(如 @Transactional)。
  • 权限控制:在方法执行前进行权限校验,决定是否继续执行。
  • 性能监控:统计方法执行时间,进行性能分析。
  • 异常处理:统一捕获和处理异常,避免在业务代码中大量 try-catch。
  • 缓存机制:拦截方法调用,判断是否需要从缓存中获取数据,而不是重复查询数据库。
  • 分布式追踪:在微服务架构下,拦截请求并添加分布式跟踪信息,如 Sleuth + Zipkin。

Spring的事务什么情况下会失效?

Spring 的事务管理是 Spring 框架中非常重要的功能,它通过声明式事务(如 @Transactional 注解)或编程式事务简化了事务的管理。然而,在某些情况下,Spring 的事务可能会失效,导致事务无法正常回滚或提交。接下来我会详细讲述 Spring 事务失效的常见场景及其原因,失效的常见场景主要有六个。

  • 第一个场景是方法未被代理对象调用,当使用 @Transactional 注解时,Spring 会为目标类生成一个代理对象,并通过代理对象拦截方法调用以管理事务。如果目标方法是通过类内部调用(即 this.method())而不是通过代理对象调用,则事务会失效。这是因为代理对象无法拦截类内部的方法调用,导致事务逻辑未被执行。
  • 第二个场景是异常未被捕获或未触发回滚规则(比如文件未发现异常),当事务方法抛出异常时,Spring 默认只会在遇到 RuntimeException 或 Error 时回滚事务,而不会对受检异常(Checked Exception)进行回滚。如果开发者捕获了异常但未重新抛出,或者未正确配置回滚规则(如通过 @Transactional(rollbackFor = Exception.class)),事务也会失效。
  • 第三个场景是在代码中自己用try-catch捕获了异常
  • 第四个场景是事务传播行为配置不当,当多个事务方法相互调用时,Spring 提供了多种事务传播行为(如 REQUIRED、REQUIRES_NEW 等)。如果传播行为配置不当,可能导致事务未按预期工作。例如,如果外部方法的事务传播行为为 NOT_SUPPORTED 或 NEVER,则内部方法的事务可能被挂起或完全不生效。
  • 第五个场景是数据库引擎不支持事务,当使用不支持事务的数据库引擎时,例如 MySQL 的 MyISAM 引擎不支持事务,即使代码中配置了事务管理,也无法生效。因此,确保使用的数据库引擎(如 InnoDB)支持事务是非常重要的。
  • 第六个场景是代理模式配置错误,当使用 @Transactional 注解时,Spring 默认使用 JDK 动态代理或 CGLIB 动态代理来管理事务。如果目标类没有实现接口且未启用 CGLIB 代理(如未设置 proxyTargetClass=true),事务可能会失效。此外,如果目标类被标记为 final 或方法被标记为 private,CGLIB 代理也无法生成,导致事务失效。
  • 第七个场景是事务管理器配置错误,当 Spring 容器中存在多个事务管理器时,如果未明确指定事务管理器(如通过 @Transactional(“transactionManagerName”)),可能导致事务管理器选择错误。这会导致事务无法正常工作,尤其是在多数据源场景下。

SpringMVC执行流程

视图版本(jsp)

  • 用户发送出请求到前端控制器DispatcherServlet
  • DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
  • HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet
  • DispatcherServlet调用HandlerAdapter(处理器适配器)
  • HandlerAdapter经过适配调用具体的处理器(Handler/Controller)
  • Controller执行完成返回ModelAndViewi对象
  • HandlerAdapter将Controller执行结果ModelAndViewi返回给DispatcherServlet
  • DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)
  • ViewReslover解析后返回具体View(视图)
  • DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)
  • DispatcherServlet响应用户

前后端分离版本

image-20250402152117685

  • 用户发送出请求到前端控制器DispatcherServlet
  • DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
  • HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回
  • 回给DispatcherServlet。
  • DispatcherServlet调用HandlerAdapter(处理器适配器)
  • HandlerAdapter经过适配调用具体的处理器(Handler/Controller)
  • 方法上添加了@ResponseBody
  • 通过HttpMessageConverter来返回结果转换为JSON并响应

Springboot比spring好在哪

  • springboot提供了大量的自动配置,开发者不需要进行的繁琐的xml配置,而专注于写Java代码
  • springboot内置了tomcat等服务器,项目部署变得更为方便
  • springboot提供了许多starters组件,方便集成各种框架和中间件

Springboot自动装配原理

1.在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

2.其中@EnableAutoConfiguration是实现自动化配置的核心注解。该注解通过@Importi注解导入对应的配置选择器。 内部就是读取了该项目和该项目引用的jar包的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。 3,条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用.