Spring、SpringBoot
IOC(Inversion of Control)
定义
IoC 即控制反转,是一种设计原则,它将应用程序的控制权从程序代码本身转移到外部容器或框架中。传统的程序设计中,程序内部直接控制程序流程和对象的创建与销毁,而在IoC中,对象的创建和管理由容器来完成,应用程序只需要描述组件之间的依赖关系,而不需要负责对象的创建和销毁。
作用
主要作用在于降低了组件之间的耦合度,使得应用程序更加灵活、可扩展和易于维护。通过将对象的创建和管理交给容器,可以更容易地替换、扩展和重用组件,同时也能更好地实现面向接口编程。
实现原理
IoC的实现原理主要通过依赖注入(Dependency Injection)来实现。
依赖注入是IoC的一种具体实现方式,它通过容器来动态地将组件之间的依赖关系注入到组件中,从而实现控制反转。
依赖注入有三种主要的方式:
构造器注入(需要结合@Configuration来使用)
- 通过构造函数来注入依赖对象。
- 优势:明确表明了类的依赖关系,使得类的依赖关系更加明确和可见。
- 劣势:当类有多个依赖关系时,构造函数的参数列表可能变得很长,增加了代码的复杂性。
private final UserRepository userRepository;
// 构造器注入
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
...
@Configuration
public class AppConfig {
@Bean
public UserRepository userRepository() {
return new UserRepository();
}
@Bean
public UserService userService(UserRepository userRepository) {
return new UserService(userRepository);
}
}
Setter注入(需要结合**@Autowired**来使用)
- 通过Setter方法来注入依赖对象。
- 优劣势:同上
private UserRepository userRepository;
// Setter注入
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
字段注入
- 通过字段直接注入依赖对象。
- 优势:简洁明了,不需要额外的构造函数或Setter方法。
- 劣势:对象的依赖关系被直接暴露在类的字段中,降低了类的封装性。
@Autowired
private UserRepository userRepository;
AOP(Aspect-Oriented Programming)
定义
AOP 即面向横面编程,是一种编程范式,它允许你将那些与核心业务逻辑无关,但又分散在代码各处的功能(比如日志记录、事务管理、安全检查)“横切”出来,集中管理。这样做的目的是提高代码的重用性、可维护性和可扩展性。
在Spring中,@Aspect 注解标记一个类后,Spring 会将其视为切面类,并在运行时自动为该类创建代理类对象,并将切面逻辑织入到代理对象中。就可以实现手动创建代理对象相似的功能,但更加简介和方便。
通过使用注解 @Aspect ,Spring 提供了一种声明式的方式来定义切面,并在AOP中应用它们,而无需手动编写代理逻辑和切面逻辑的代码。
- @Before:在目标方法执行前执行切面逻辑。
- @After:在目标方法执行后(无论是否发生异常)执行切面逻辑。
- @AfterReturing:在目标方法正常返回后执行切面逻辑。
- @AfterThrowing:在目标方法抛出异常后执行切面逻辑。
- @Around:在目标方法执行前后,控制目标方法的执行过程,可以自定义是否执行目标方法、执行前后的额外逻辑等。
实现原理
在Spring中,AOP主要通过动态代理实现,具体而言有两种主要的实现方式:JDK动态代理和CGLIB动态代理。
- JDK 动态代理:
- JDK 动态代理是基于接口(UserService)的代理,它要求目标类(UserServiceImpl)必须实现一个接口。
- 当目标类实现了接口时,Spring 容器会自动使用 JDK 动态代理生成一个实现了该接口(UserService)的代理类,并在代理类中织入切面逻辑。
- CGLIB 动态代理:
- CGLIB 动态代理是基于继承的代理,它不要求目标必须实现接口。
- 当目标类没有实现接口时,Spring 容器会自动使用CGLIB动态代理生成一个目标类的子类,并在子类中织入切面逻辑。
- CGLIB 动态代理通过字节码增强技术来实现。
Spring 在什么情况下会选择 JDK 或 CGLIB?这个选择策略是一成不变的吗?
- 对于纯 Spring Framework:Spring 框架会进行“智能”检测。
- 如果一个 Bean 实现了接口,Spring AOP 优先使用 JDK 动态代理。
- 如果一个 Bean 没有实现任何接口,Spring AOP 别无选择,只能使用 CGLIB。
- 对于 Spring Boot (2.0 及以后版本):这是一个关键区别!
默认使用 CGLIB。无论你的目标类是否实现了接口,Spring Boot 都默认创建基于 CGLIB 的子类代理。
为什么这么做? 主要是为解决一个经典问题——“同类方法调用(Self-Invocation)”时的代理失效问题。
如何修改? 如果你希望 Spring Boot 使用 JDK 动态代理,可以在
application.properties
或application.yml
中进行配置:spring.aop.proxy-target-class=false
设置为
false
后,Spring Boot 就会退回到和纯 Spring 框架一样的策略:有接口用 JDK,没接口用 CGLIB。
这两种代理方式各有什么优缺点或者说限制?
JDK 动态代理
- 优点:
- 它是 Java 官方提供的,无需引入任何第三方库。
- 在某些情况下,它的代理对象创建速度可能比 CGLIB 稍快。
- 缺点/限制:
- 必须实现接口:这是它最大的限制。它只能为实现了接口的类创建代理。
- 代理的是接口,不是类:生成的代理对象只能强制类型转换为接口类型,而不能是实现类类型。
CGLIB 动态代理 (Code Generation Library)
- 优点:
- 无需实现接口:可以为任意一个普通类(非
final
类)创建代理。 - 解决了“同类方法调用”问题:由于它代理的是类本身,因此注入的代理对象就是目标类的子类型,可以避免代理失效。
- 无需实现接口:可以为任意一个普通类(非
- 缺点/限制:
- 无法代理
final
类和final
方法:因为 CGLIB 的原理是生成目标类的子类,而final
关键字阻止了继承和重写。如果你的类或方法被声明为final
,AOP 将无法生效。 - 性能开销:在早期版本中,CGLIB 因为涉及字节码操作,其代理对象的创建和方法执行的开销比 JDK 代理稍高。不过随着版本迭代和 JVM 的优化,这种性能差异已经非常微小了。
- 无法代理
什么是“同类方法调用(Self-Invocation)”代理失效问题?
这是 AOP 领域最经典的一个“坑”,也是 CGLIB 被 Spring Boot 设为默认方式的重要原因。
场景:假设你有一个 UserServiceImpl
类,它被 Spring AOP 代理了。类里面有两个方法 methodA()
和 methodB()
,并且 methodA()
上加了事务注解 @Transactional
。
@Service
public class UserServiceImpl implements UserService {
@Transactional
public void methodA() {
System.out.println("Executing method A...");
// ... 业务逻辑 ...
}
public void methodB() {
System.out.println("Executing method B...");
this.methodA(); // 问题在这里!
}
}
问题:当外部代码调用 userService.methodB()
时,methodB()
本身没有事务。在 methodB()
内部,通过 this.methodA()
调用 methodA()
,此时 methodA
的事务会生效吗?
答案:不会。
原因:
- 外部调用
userService.methodB()
时,调用的是 Spring 创建的代理对象。 - 但是,一旦进入了
methodB()
的方法体内部,this
关键字指向的是原始的目标对象(UserServiceImpl 实例),而不是代理对象。 - 所以
this.methodA()
相当于一个普通的内部方法调用,它直接跳过了代理对象,AOP 的切面逻辑(比如事务)自然也就无法被织入。
如何解决?
使用 CGLIB (Spring Boot 默认):因为 CGLIB 创建的是子类代理,并且 Spring Boot 默认 proxy-target-class=true
,Spring 容器中注入的就是这个子类代理。这样,即使在类内部,对方法的调用也有机会被代理拦截(尽管 this
关键字的问题依然存在,但 Spring 通过更复杂的处理使得在某些情况下可以工作)。
自己注入自己:最标准的解决方式是避免使用
this
。@Service public class UserServiceImpl implements UserService { @Autowired private UserService self; // 注入自身的代理对象 @Transactional public void methodA() { ... } public void methodB() { System.out.println("Executing method B..."); self.methodA(); // 通过代理对象调用 } }
使用
AopContext.currentProxy()
:((UserService) AopContext.currentProxy()).methodA();
Spring Bean 的生命周期
第一阶段:实例化 (Instantiation)
- 容器启动,扫描 Bean 定义:Spring 容器(
ApplicationContext
)启动时,会读取配置(XML、注解等),找到所有被管理的 Bean 的“图纸”(BeanDefinition
)。 - 实例化 Bean:当容器需要一个 Bean 时(比如因为懒加载被首次请求,或者因为非懒加载在容器启动时就需要),它会根据
BeanDefinition
,通过反射机制调用该类的构造函数,创建一个原始的、空的 Bean 对象实例。
第二阶段:属性填充 (Populate)
- 依赖注入 (DI):Spring 容器会检查这个 Bean 依赖的其他 Bean(通过
@Autowired
,@Resource
等注解)。它会去容器中找到这些依赖的 Bean,并通过反射(调用 setter 方法或直接设置字段)将它们注入到当前的 Bean 实例中。
第三阶段:初始化 (Initialization)
Aware 接口的注入:Spring 会检查 Bean 是否实现了特定的
Aware
接口(如BeanNameAware
,BeanFactoryAware
,ApplicationContextAware
)。如果实现了,就会调用相应的方法,将 Bean 的名字、所在的 Bean 工厂、应用上下文等环境信息注入给它。setBeanName()
setBeanFactory()
setApplicationContext()
BeanPostProcessor
前置处理:调用所有已注册的BeanPostProcessor
的postProcessBeforeInitialization()
方法。这是一个非常强大的扩展点,它允许你在 Bean 的“正式初始化”之前,对 Bean 进行任意的修改或包装。AOP 代理就是在这个阶段通过一个特殊的BeanPostProcessor
实现的。如果 Spring 发现这个 Bean 需要被代理,它就会在这里返回一个代理对象,替换掉原始的 Bean 对象。@PostConstruct
注解:如果 Bean 的方法上标注了@PostConstruct
注解,Spring 会调用这个方法。这是 JSR-250 规范定义的,是官方推荐的初始化方式,因为它不依赖于 Spring 的特定接口。InitializingBean
接口:如果 Bean 实现了InitializingBean
接口,Spring 会调用它的afterPropertiesSet()
方法。这是 Spring 早期的初始化方式。自定义
init-method
:如果你在 XML 配置中通过init-method
属性,或者在@Bean
注解中通过initMethod
属性指定了自定义的初始化方法,Spring 会在此时调用它。初始化顺序:@PostConstruct -> InitializingBean.afterPropertiesSet() -> init-method。推荐使用 @PostConstruct。
BeanPostProcessor
后置处理:调用所有BeanPostProcessor
的postProcessAfterInitialization()
方法。这是另一个关键的扩展点,它允许你在 Bean 完全初始化之后,再次进行处理。AOP 代理的创建主要发生在前置处理,但后置处理也可能被用到。
第四阶段:Bean 可用
- Bean 准备就绪:经过了以上所有步骤,Bean 现在是一个完全初始化、功能完备的对象了。它被存放在 Spring 容器的单例池(
singletonObjects
)中,等待其他对象来调用它。
第五阶段:销毁 (Destruction)
当 Spring 容器关闭时(比如应用停止),容器会销毁它管理的所有单例 Bean。
@PreDestroy
注解:如果 Bean 的方法上标注了@PreDestroy
注解,Spring 会在销毁前调用这个方法。同样,这是 JSR-250 规范,是推荐的销毁方式。DisposableBean
接口:如果 Bean 实现了DisposableBean
接口,Spring 会调用它的destroy()
方法。自定义
destroy-method
:如果你指定了自定义的销毁方法,它会在这里被调用。销毁顺序:@PreDestroy -> DisposableBean.destroy() -> destroy-method。推荐使用 @PreDestroy。
BeanFactory 和 ApplicationContext 的区别
两者是 Spring 两个核心接口,都可以用来获取 Bean 实例,但在功能上有所不同。
- BeanFactory
- 提供了基本的依赖注入支持。
- 延迟加载,只有在明确请求时才初始化Bean。
- ApplicationContext
- 完全初始化所有单例Bean。
- 支持国际化(i18n)、事件传播、资源加载等。
- 提供了AOP功能。
- 通常在应用程序中使用 ApplicationContext。
Spring 加载 Bean的 方式
- 基于 XML 的配置
- 基于注解的配置
- 配置类(@Configuration + @Bean)
- 通过 FactoryBean
Spring 的 bean 为什么是单例的呢,并且除了单例以外还有什么形式,如果是多例的话,会有什么影响
Spring 框架中Bean的默认作用域是单例(singleton)
,这是出于以下几个原因:
- 性能优化:创建对象通常是一个昂贵的过程,尤其是涉及到 I/O 操作(如数据库连接)时。使用单例可以减少对象创建的次数,节省资源和提升性能。
- 状态共享:单例模式允许在应用的不同部分共享同一个 Bean 实例,这对于状态共享和管理非常有用。
- 资源管理:许多 Bean ,如数据源、会话工厂等,是自然的单例,因为它们封装了共享资源,如数据库连接池。
除了单例模式,Spring 还提供其他几种 Bean 的作用域:
- 单例(Singleton):在 Spring IoC 容器仅存在一个 Bean 实例,Bean 以单例方式存在。
- 原型(prototype):每次注入或通过 Spring 容器的 getBean() 请求时,都会创建一个新的Bean实例(这种模式就是多例)。
- 请求(request):每个 HTTP 请求都会创建一个新的 Bean ,该作用域仅在请求的处理过程中有效。
- 会话(session):在一个 HTTP 会话中,一个 Bean 定义对应一个 Bean 实例,该作用域同样仅在会话期间中有效。
- 应用(application):在一个 ServletContext 的生命周期内,一个 Bean 定义对应一个 Bean 实例,同样仅在 Web 应用的生命周期中有效。
如果将 Bean 定义为多例(prototype)作用域,将会有以下影响:
- 资源使用增加:每次请求 Bean 时都会创建新实例,会增加内存和资源的使用。
- 状态管理:多例 Bean 不会共享状态,每个 Bean 实例都有自己的状态。
- 生命周期管理:Spring 不会管理 prototype Bean 的完整生命周期,也就是说,Spring 不会调用 prototype Bean 的销毁方法。
- 复杂性增加:在使用多例 Bean 时,需要更加小心地管理其生命周期和依赖关系。
总的来说,选择正确的作用域取决于具体的应用需求。单例作用域适合于需要共享状态的全局资源,而原型作用域适合于那些具有独立状态、生命周期较短或需要隔离的Bean、每次都需要一个新实例的情况。
Spring 循环依赖
1. 定义与前提
首先,循环依赖指的是两个或多个Bean之间相互依赖,形成了一个闭环,例如A依赖B,同时B又依赖A。
Spring只解决了单例(Singleton)作用域下,并且是基于setter或字段注入的循环依赖。对于构造器注入和原型(Prototype)作用域的循环依赖,Spring是无法解决的,会直接抛出异常。
2. 核心思想:分离实例化与初始化
Spring解决问题的核心思想,是把Bean的创建过程拆分成了两个主要阶段:
- 实例化 (Instantiation):通过反射创建一个原始的Bean对象,但此时Bean的属性都是null。
- 初始化 (Initialization):为Bean的属性进行依赖注入(DI)和执行各种初始化回调。
通过将这两个阶段分离,Spring有机会将一个虽然还未完成初始化,但已经被实例化的“半成品”Bean提前暴露出去,从而打破循环。
3. 实现机制:三级缓存
为了实现“半成品”Bean的提前暴露,Spring使用了“三级缓存”机制。这三个缓存都是Map结构:
singletonObjects
(一级缓存):用于存放已经完全初始化的Bean,我们称之为“成品Bean缓存”。earlySingletonObjects
(二级缓存):用于存放提前暴露的Bean,即“半成品Bean缓存”。这些Bean已被实例化,但还未完成属性注入。singletonFactories
(三级缓存):这是解决循环依赖最关键的缓存。它存放的不是Bean对象,而是创建Bean的工厂(ObjectFactory)。这个工厂负责在真正需要时,才生成那个“半成品Bean”,并且可以包含AOP代理逻辑。
4. 解决流程(以A、B循环依赖为例)
- 创建A:
getBean("a")
被调用。Spring首先实例化A,得到一个原始对象。然后,它并不会立即初始化A,而是将一个能够产生A的ObjectFactory
放入三级缓存。 - A注入B:Spring开始初始化A,发现它依赖B,于是调用
getBean("b")
。 - 创建B:Spring去创建B,同样先实例化B,然后将能产生B的
ObjectFactory
放入三级缓存。 - B注入A(关键步骤):Spring初始化B,发现它依赖A,于是调用
getBean("a")
。 - 获取A的半成品:此时,Spring按顺序查找A:
- 在一级缓存中查找,找不到(A未完全初始化)。
- 在二级缓存中查找,也找不到。
- 在三级缓存中,成功找到了A的
ObjectFactory
。
- 提前暴露A:Spring调用这个
ObjectFactory
来生成一个A的“半成品”(如果A需要AOP代理,此时会生成代理对象),并将这个半成品A放入二级缓存,然后从三级缓存中移除A的工厂。 - B创建完成:B获取到了A的半成品引用,顺利完成了自己的初始化。之后,完整的B被放入一级缓存。
- A创建完成:回到A的创建流程,它现在可以从一级缓存中获取到完整的B对象,并完成自己的初始化。最后,完整的A也被放入一级缓存。
至此,循环依赖被解决。
5. 为什么必须是三级缓存?
使用三级缓存而不是二级,主要是为了延迟AOP代理对象的创建。如果只有二级缓存,那就必须在Bean实例化后立刻创建代理对象,但实际上这个Bean可能最终并不需要被代理。三级缓存通过ObjectFactory
,将代理对象的创建推迟到了真正发生循环依赖、且有其他Bean需要注入它时,才去执行,这是一种更合理、更优化的设计。
By Type 和 By Name 的区别(@Autowired和@Resource的区别)
@Autowired 基于类型的依赖注入(By Type):
定义:在基于类型的注入中,Spring 容器使用要注入的属性或构造函数参数的类型来在容器中查找匹配的 Bean。
代码示例:
@Autowired private MyService myService;
在这个例子中,Spring 会在其容器中查找 MyService 类型的 Bean,并进行注入。
多个候选 Bean:如果存在多个同类型的 Bean,而没有其他限定信息,Spring 将无法决定使用哪一个,从而导致异常。这种情况下,可以使用 @Qualifier 注解来指定 Bean 的名称。
虽然@Autowired
本身不提供直接的基于名称的注入,通过与@Qualifier
结合使用,它可以非常灵活地实现基于名称的注入
相似的功能。
代码示例:
@Autowired
@Qualifier("mySpecificService")
private MyService myService;
...
@Service("mySpecificService")
public class MyServiceImpl implements MyService {
// 实现细节
}
在这个例子中,Spring 会在其容器中查找名为 mySpecificService
的Bean来注入。
@Resource 基于名称的依赖注入(By Name)(默认)
定义:@Resource
注解是基于 JSR-250 标准,它可以根据名称或类型来注入依赖。 代码示例:
@Resource(name = "mySpecificService")
private MyService myService;
在这个例子中,@Resource
注解通过name
属性直接指定了要注入的 Bean 名称,从而实现了基于名称的注入。 @Resource 基于类型的依赖注入(By Type)
@Resource
private MyService myService;
Spring 事务失效的经典场景
1. 最经典的失效场景:方法内部调用(Self-Invocation)
场景描述:
@Service public class OrderService { public void createOrder() { // ... 其他操作 ... this.updateStock(); // this 调用,事务会失效! } @Transactional public void updateStock() { // ... 更新库存的数据库操作 ... } }
失效原因: 调用
this.updateStock()
时,使用的是原始的OrderService
实例,而不是 Spring 生成的代理对象。调用直接发生在对象内部,完全绕过了代理对象,AOP 根本没有机会介入,自然无法开启事务。解决方案: 避免
this
调用。通过注入自身的代理对象来解决@Autowired private OrderService self; public void createOrder() { // ... 其他操作 ... self.updateStock(); }
2. 方法的访问权限问题(private
, protected
等)
场景描述: 将
@Transactional
注解加在了一个在非public
方法上。@Service public class OrderService { @Transactional private void processPayment() { // private 方法,事务会失效! // ... } }
失效原因: Spring AOP 在创建代理时,无法拦截(或重写)
private
方法,因为它们在类的外部是不可见的。对于protected
和package-private
(默认)方法,虽然 CGLIB 代理理论上可以,但也存在限制且不符合最佳实践。Spring 官方文档明确建议,只在public
方法上使用@Transactional
注解。
3. 异常类型不匹配(rollbackFor
设置错误)
场景描述: 事务方法中抛出了一个受检异常(Checked Exception),但没有指定
rollbackFor
属性。@Service public class UserService { @Transactional public void register() throws Exception { // ... 数据库操作 ... if (someCondition) { throw new Exception("一个受检异常"); // 事务默认不会回滚! } } }
失效原因: Spring 的默认事务回滚策略是:只在遇到
RuntimeException
(运行时异常)或Error
时才回滚。对于普通的Exception
(受检异常),Spring 认为这可能是业务逻辑的一部分,需要开发者明确指示才回滚。因此,上述代码在抛出Exception
后,事务会提交而不是回滚。解决方案: 在注解上明确指定回滚的异常类型:
@Transactional(rollbackFor = Exception.class)
。
4. 方法内部吞掉(catch
)了异常
场景描述: 在事务方法内部使用了
try...catch
块,并且在catch
块中没有将异常重新抛出。@Service public class ProductService { @Transactional public void updateProduct() { try { // ... 执行数据库更新,期间发生 RuntimeException ... } catch (Exception e) { // 异常被捕获了,但没有重新抛出 System.out.println("发生了一个异常,但我处理掉了"); } // 方法正常结束,没有异常抛出 } }
失效原因: 事务回滚的前提是,代理对象需要感知到有异常发生。在上述代码中,异常被
catch
块完全“消化”了,没有向方法外部传播。对于代理对象来说,updateProduct
方法是“正常返回”的,它根本不知道内部曾发生过异常,因此会正常提交事务。
5. Bean 没有被 Spring 容器管理
场景描述: 你手动
new
了一个对象,然后调用它的事务方法。public void someBusinessLogic() { OrderService service = new OrderService(); // 手动 new service.updateStock(); // 事务会失效 }
失效原因: 这个
OrderService
实例是一个普通的 Java 对象,它完全游离在 Spring 容器之外。Spring 根本不知道它的存在,自然也无法为它创建代理对象,所有与 Spring 相关的功能(包括@Transactional
)都将无效。
Spring Boot 的核心思想是什么?
Spring Boot 的核心思想是**“约定优于配置”** (Convention over Configuration)。
这个思想的本质是,Spring Boot 团队认为,对于绝大多数项目,很多技术的配置方式都是固定和通用的。因此,Spring Boot 不应该让开发者每次都去重复编写这些样板化的配置,而是应该提供一套“约定好的、合理的默认配置”。
具体来说,这个核心思想体现在以下三个方面:
- 开箱即用: Spring Boot 致力于提供一种“just run”的开发体验。开发者引入相关依赖后,无需或只需少量配置,就能快速启动和运行一个功能完备的应用。
- opinionated (有主见的)**:它为项目提供了一套“有主见的”最佳实践集合。比如,当它检测到
spring-webmvc
在 classpath 中时,它会“主观地”认为你正在开发一个 Web 应用,因此会自动为你配置好 DispatcherServlet、内嵌的 Tomcat 服务器等。
- opinionated (有主见的)**:它为项目提供了一套“有主见的”最佳实践集合。比如,当它检测到
- 可覆盖的默认值: 虽然它提供了强大的默认配置,但它绝不“绑架”开发者。如果你对默认的配置不满意,比如不想要 Tomcat 而是想用 Undertow,或者想自定义数据源的参数,你随时可以通过简单的配置(如在
application.yml
中修改)来覆盖掉它的默认行为。
总而言之,Spring Boot 的核心思想是通过自动配置、起步依赖等手段,用一套“约定好的”默认值来代替繁琐的手动配置,从而极大简化项目搭建和开发过程,让开发者能更专注于业务逻辑本身。
什么是 Starter?它和普通依赖有什么区别?
Starter (起步依赖) 是 Spring Boot 的核心特性之一,它本质上是一个特殊的 Maven 依赖描述符(POM),而不是一个包含具体代码的 JAR 包。
它的主要作用有两个:
- 聚合相关依赖: Starter 将构建某一特定功能(如 Web 开发、数据访问)所需的一整套相关依赖聚合在一起。例如,我们引入
spring-boot-starter-web
,它就会自动将 Spring MVC、Jackson、Validation API、内嵌的 Tomcat 服务器等所有必要的库都间接引入进来。 - 管理版本兼容性: Starter 依赖于 Spring Boot 的父项目 (
spring-boot-dependencies
),这个父项目像一个“字典”一样,统一管理了所有常用第三方库的版本号。这确保了 Starter 引入的所有传递性依赖之间版本都是互相兼容的,开发者无需再手动管理版本,从而避免了“依赖地狱”。
它和普通依赖的核心区别在于:
- 目的不同: 普通依赖(如
spring-core.jar
)的目的是提供具体的 API 和功能实现。而 Starter 的目的在于简化依赖管理,它本身不提供任何代码,只负责“带入”其他依赖。 - 关注点不同: 使用普通依赖时,开发者需要自己去管理一系列相关依赖的版本和兼容性。而使用 Starter 时,开发者只需要关注“我需要什么功能”(如
web
、jpa
),而不需要关心“为了这个功能我需要引入哪些具体的库、版本是什么”。
简单来说,Starter 是 Spring Boot 提供的一种“一站式”解决方案,让依赖管理从“手动挡”变成了“自动挡”。
请详细解释一下 Spring Boot 的自动配置原理。
Spring Boot 的自动配置是其最核心的功能,其原理主要依赖于三个关键部分:@EnableAutoConfiguration
注解、META-INF/spring.factories
文件和 @Conditional
系列条件注解。
整个流程如下:
启动自动配置总开关: Spring Boot 项目的启动类上通常有一个
@SpringBootApplication
注解,它是一个复合注解,里面包含了@Configuration
、@ComponentScan
、@EnableAutoConfiguration
,其中@EnableAutoConfiguration
。这个注解是激活自动配置功能的总开关。扫描候选配置类: 启动应用时,
@EnableAutoConfiguration
注解会借助 Spring 的SpringFactoriesLoader
机制,去扫描所有引入的 JAR 包中类路径下的META-INF/spring.factories
文件。这个文件中以键值对的形式列出了大量预定义的自动配置类,例如:# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
按需装配(条件判断): 候选人名单有了,但并不是所有的配置都会生效。每一个自动配置类(如
DataSourceAutoConfiguration
)本身都是一个@Configuration
配置类,并且它上面会使用大量的@Conditional
系列注解来进行条件判断。只有当所有条件都满足时,这个自动配置类才会生效。常见的条件注解有:@ConditionalOnClass
: 判断 classpath 中是否存在指定的类。例如,DataSourceAutoConfiguration
会检查是否存在DataSource.class
。@ConditionalOnMissingBean
: 判断 Spring 容器中是否不存在指定类型的 Bean。这是允许用户覆盖默认配置的关键,例如,如果用户自己定义了一个DataSource
Bean,那么 Spring Boot 的默认DataSource
配置就不会生效。@ConditionalOnProperty
: 判断配置文件中是否存在指定的属性及其值。
注入 Bean: 一旦某个自动配置类的所有条件都满足,它就会被 Spring 容器加载,其内部使用
@Bean
注解定义的一系列 Bean(如DataSource
,JdbcTemplate
等)就会被创建并注入到容器中,从而完成了自动配置。
总结来说,自动配置的原理就是 “扫描 -> 过滤 -> 装配”:通过 spring.factories
扫描到所有可能的配置,然后通过 @Conditional
过滤掉不满足条件的,最后把满足条件的配置装配到 Spring 容器中。
如何禁用某个不想要的自动配置?
通常有两种方式可以禁用 Spring Boot 中我们不想要的自动配置:
通过注解的
exclude
属性(推荐):
这是最常用且最直接的方式。我们可以在启动类的@SpringBootApplication
或@EnableAutoConfiguration
注解中,通过exclude
属性来指定要排除的自动配置类。import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; // 禁用数据源的自动配置 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class MyApplication { // ... }
通过配置文件属性:
我们也可以在application.properties
或application.yml
文件中,通过spring.autoconfigure.exclude
属性来禁用一个或多个自动配置。# 在 application.properties 中禁用 spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
Spring Boot 的配置文件加载顺序是怎样的?
Spring Boot 会从多个位置加载配置文件(如 application.properties
或 application.yml
),并且有一个明确的优先级顺序。高优先级的配置会覆盖低优先级的配置。一个常见的、简化的优先级从低到高如下:
JAR 包内部的配置文件: 项目内部resource目录下的
application.properties
或application.yml
。这是最低的优先级,通常作为默认配置。JAR 包外部的配置文件:
在 JAR 包同级目录下的配置文件。
/opt/app/ ├── myapp.jar └── application.properties
操作系统环境变量。
在 Linux 或 macOS 上:
export SERVER_PORT=8081
,在 Windows 上:set SERVER_PORT=8081
环境变量
SERVER_PORT
会被 Spring Boot 识别为配置属性server.port
;环境变量SPRING_DATASOURCE_URL
会被识别为spring.datasource.url
。在 Docker容器化环境中,通过环境变量来注入配置是标准做法。
FROM openjdk:17-jdk-slim WORKDIR /app COPY target/myapp.jar myapp.jar ENTRYPOINT ["java", "-jar", "myapp.jar"]
docker run -p 9090:9090 -e "SERVER_PORT=9090" my-app
Java 系统属性 (通过
java -D<key>=<value>
指定)。定义: 这是专属于某一个Java 虚拟机 (JVM) 实例的变量。它在
java
命令启动时通过-D
参数传入,只对当前这个启动的 Java 进程有效。Spring Boot 如何读取: Spring Boot 会直接读取 JVM 的所有系统属性,属性名是什么,它就识别成什么。
java -Dserver.port=8082 -jar myapp.jar
示例:
-Dserver.port=8082
就直接对应配置属性server.port
。
命令行参数 (通过
-<key>=<value>
传递)。这是最高的优先级。定义: 这是直接传递给 Spring Boot 应用本身的参数。它在
java -jar myapp.jar
命令的最后面,通过--
形式添加。Spring Boot 如何读取: Spring Boot 会专门解析这些以
--
开头的参数,并直接将它们映射为配置属性。java -jar myapp.jar --server.port=8083
示例:
--server.port=8083
就直接对应配置属性server.port
。