引言:
当然,在我们学习的过程中,得知其然,还得知其所以然。So理解了其原理,更能让我们对其开发的理解,遇到问题,也更能快速找到解决办法!!!
1. SprngBoot-配置优先级
1.1 属性配置的方式
在SpringBoot中支持以下三种格式的配置文件:
1.2 配置的优先级
如果当配置三个文件同时配置了一个属性,那么谁的优先级比较高?
我们启动项目,看端口是多少就知道了.
启动发现端口是8081,说明当三分配置文件,其Properties的优先较高。OK我们注释掉它的配置,接下来继续比较另外2个优先级。
启动:
发现是8082端口,对应的Yml配置文件,所以说
优先级结果:Properties > yml > yaml
So:虽然SpringBoot支持多种格式配置文件,但是在项目开发时,我们还是使用主流配置yml
到这里还没完呢,其实在SpringBoot中,为了增强程序的扩展性: 除了支持配置文件属性配置,还支持Java系统属性和命令行参数的方式进行属性配置
java系统属性:格式为:-D+key=value (-D是固定的)
命令行参数: 格式为:--Key=value (--是固定的后面加键=值就可以了)
如下:
Idea中运行SpringBoot项目如果指定java系统属性和命令行参数的方式进行属性配置呢?当然,Idea中已经提供了可视化的界面提供操作了
如下:点击编辑配置,然后找到自己的项目启动类
进来,点击选择参数:
添加配置:
点击右下角应用,确定OK!
然后我们这里注释掉以前的三种配置属性,看看这两种的配置谁的优先级较高!!!
欧克,启动!!!
发现端口是10010,欧克我们可以得知,命名行参数配置优先级大于Java系统属性配置!!!
当然,这只是在Idea中来设置属性配置,如果我们没在Idea中,比如我们打包得jar包上线了,又该如何设置 java系统属性和命令行参数?
欧克,我们通过Maven打包运行一下,来在启动得时候添加一下参数
默认不配置任何属性端口8080,没问题,欧克,我们添加java系统属性和命名行参数:
欧克,Ctrl+C终止程序,我们只配置命令行参数,不出意外,就是端口9000
欧克,我们总结一下,在来比较一下这2种配置(命令行参数>java系统属性)和开始得3种配置(Properties > yml > yaml )优先级,我们这边只需要拿 java系统属性来和另外三种配置来比较就可以了!!
欧克启动!
端口9000,没问题,说明我们得java系统属性大于另外三种配置得,综上: 命令行参数>java系统>Properties > yml > yaml属性
So:配置:
2. Bean 管理
Bean的声明
- 注解声明
- @Component及其派生注解:这是最常用的声明Bean的方式。通过在类上添加@Component、@Service、@Repository、@Controller等注解,Spring会自动扫描这些类并将其实例化为Bean。这些注解之间在功能上并无明显区别,但通常遵循以下约定:@Controller用于控制层,@Service用于业务层,@Repository用于数据访问层,@Component用于其他组件。
- @Bean注解:在配置类(@Configuration标注的类)中使用@Bean注解来声明Bean。通过返回实例化的对象,Spring会将其注册为Bean。这种方式在需要自定义Bean的实例化过程或引用第三方库中的类时特别有用。
- @Component声明FactoryBean:FactoryBean是一个特殊的Bean,它可以生成并返回其他Bean的实例。通过在类上添加@Component注解,并将类实现为FactoryBean接口,可以声明一个FactoryBean类型的Bean。
- 编程式声明
- BeanDefinitionRegistryPostProcessor:通过实现此接口,可以在Bean定义加载到Spring容器之前动态地注册Bean定义。
- @Import + ImportBeanDefinitionRegistrar:在配置类或Bootstrap启动类中使用@Import注解导入实现了ImportBeanDefinitionRegistrar接口的类,然后在该接口的实现类中注册Bean定义。
Bean的注入
- 自动装配
- @Autowired:这是Spring提供的自动装配注解,可以放在构造器、setter方法或字段上,Spring会根据类型自动匹配并注入相应的Bean。如果存在多个同类型的Bean,则需要通过@Qualifier注解指定要注入的Bean的名称。
- @Resource:这是JDK提供的注解,功能与@Autowired类似,但可以通过指定name属性来指定要注入的Bean的名称。
- 构造方法注入
- 通过在类的构造方法中接收Bean参数,Spring会在实例化该类时自动注入这些Bean。这种方式有助于确保Bean的不可变性,因为一旦Bean被实例化,其依赖的Bean就不能再被更改。
- Setter方法注入
- 在Bean的setter方法上使用@Autowired或@Resource注解,Spring会在Bean实例化后调用这些setter方法来注入依赖的Bean。
- 属性注入
- 直接在类的字段上使用@Autowired或@Resource注解,Spring会在Bean实例化时通过反射将这些字段设置为相应的Bean实例。然而,这种方式通常不推荐用于生产环境,因为它可能导致字段的不可控访问和难以追踪的依赖关系。
获取Bean
默认情况下,spring项目启动时,会把声明扫描到的这些Bean都创建好放在IOC容器中,如果想主动获取Bean,可以通过如下方式:
从IOC容器中获取到Bean,当然在springBoot环境中直接注入IOC容器就可以了!!
根据name获取bean:
当然里面有很多重载方法,选择第一个就可以了,顺便找一个我们项目的注入的Bean的名称来测试一下就可以了!!!
这里我直接就拿第一个接口来试试:
这里没有声明Bean的名称,默认就是首字母小写的就是 loginController
欧克:我们启动测试:
实例;
@SpringBootTest
public class getTheBean {
@Resource
private ApplicationContext applicationContext; // 自动注入IOC容器
@Test
void testGetBean() {
// 根据bean的名称获取bean
LoginController loginController =(LoginController) applicationContext.getBean("loginController");
System.out.println("loginController = " + loginController);
}
}
效果:
根据类型获取bean:
根据name获取bean(带类型转化):
同理:如下
实例:
@SpringBootTest
public class getTheBean {
@Resource
private ApplicationContext applicationContext; // 自动注入IOC容器
@Test
void testGetBean() {
// 根据bean的名称获取bean
LoginController loginController =(LoginController) applicationContext.getBean("loginController");
System.out.println("loginController = " + loginController);
// 根据bean的类型获取
LoginController loginController1= applicationContext.getBean(LoginController.class);
System.out.println("loginController1 = " + loginController1);
// 根据bean的名称及类型获取
LoginController loginController2 = applicationContext.getBean("loginController", LoginController.class);
System.out.println("loginController2 = " + loginController2);
}
}
效果:
结果:So,我们可以看到三次地址都是一样的,所以说明Ioc容器中这个Bean对象只有一个,是单列的(就是Bean的作用域了),整个生命周期内只会被创建一次,并且多个线程共享使用。
此时如果问:spring中的单列Bean是否有并发线程安全???
答:
Spring的单列Bean默认是非线程安全的,但是只要我们避免多个Bean之间共享一些数据,就不用害怕并发问题。
原因:
单列Bean的生命周期:Spring容器在初始化时会创建并管理单列Bean,这些Bean整个生命周期内只会被创建一次,并且多个线程共享使用。多线程访问:如果单列Bean中包含共享可变状态(如实例变量),多个线程同时访问并修改这些共享状态时,可能会导致并发安全问题,如数据不一致,脏读,死锁等。
Bean的作用域:
以下六种:
默认的其实每次都会在springBoot启动的时候来创建:我们测试一下
列:
同时,这些Bean会在SpringBoot启动就会被创建好:方便测试,弄个构造方法,在创建完毕的时候我们可以观察:Ok,打上断点,开始调试:
效果:
ok:可以看到,在springBoot启动的时候,就已经创建好了!!!
当然;如果我们需要达成我们某个条件还才开始创建,有许多注解可以使用如下:
Bean的延迟创建
1. 使用@Lazy注解
@Lazy 注解是最直接的方式来实现Bean的懒加载。通过在Bean的声明上添加@Lazy注解,可以指示Spring容器在第一次注入或使用时才创建该Bean。这个注解可以应用于类级别或方法级别(在配置类中使用@Bean注解声明Bean时)。
// 类级别
@Component
@Lazy
public class MyLazyBean {
// ...
}
// 方法级别
@Configuration
public class AppConfig {
@Bean
@Lazy
public MyLazyBean myLazyBean() {
return new MyLazyBean();
}
}
2. 使用@Bean注解的lazyInit属性
在Spring的配置类中,使用@Bean注解声明Bean时,可以设置lazyInit属性为true来实现懒加载。这是另一种在方法级别上实现懒加载的方式。
@Configuration
public class MyConfig {
@Bean(lazyInit = true)
public MyLazyBean myLazyBean() {
return new MyLazyBean();
}
}
3. 使用条件懒加载(@Conditional注解)
虽然@Conditional注解本身不直接用于实现懒加载,但它可以根据条件来决定是否创建Bean。通过结合自定义条件,可以在满足特定条件时才创建Bean,这可以间接实现按需创建Bean的效果。然而,它并不等同于懒加载,因为它在容器启动时就会根据条件决定是否创建Bean。
最基础的就是根据IOC容器是否有某个类来决定是否加载这个类!!!
@Configuration
public class SomeConfiguration {
@Bean
@Conditional(OnWebApplicationCondition.class)
public SomeBean someBeanForWeb() {
return new SomeBean();
}
@Bean
@Conditional(OnNotWebApplicationCondition.class)
public SomeBean someBeanForNotWeb() {
return new SomeBean();
}
}
4. 使用XML配置
如果你使用的是基于XML的配置方式,可以通过在<bean>
标签中设置lazy-init属性为true来实现懒加载。
<bean id="myBean" class="com.example.MyBean" lazy-init="true"/>
欧克:回归正题:在spring中可以通过注解: @Scope("")来声明作用域 默认是singleton单咧的,如果改成prototype,则每次拿取都会创建新的对象的。
试试prototype每次获取都会实例化新的Bean,启动测试:
动效果:
没问题,每次都不一样把!!!
总:
第三方Bean
当然除了我们自定义了一些类,比如我们加的这些@Component注解以及衍生类注解@Controller,Service,@Repository 等。还有我们平常的第三方配置!!就是引入的第三方依赖所提供的!!!
如果要管理的bean对象来自第三方(不是自定义的),无法用@Component及衍生注解声明bean的,就需要用到@Bean注解
比如这里就随便测试一下:
比如我们引入的是Spring Date Redis的依赖
当然:它会自动配置RedisTemplate
和StringRedisTemplate
的Bean ,这二种之间也有区别,StringRedisTemplate默认传的字符串,我们通常需要Json来转化,但是相比某条件更节约内存,OK,我们这里不详细介绍,后续补上,这里我们就使用RedisTemplate,当然这个东西我们一般都需要我们自定义的。根据
需求来调整其行为,包括选择序列化器、设置连接参数等。
欧克,我们就来配置下,为了方便集中管理
我们提供@Configuration注解声明当前类为一个配置类,任何在我们的方法上去加@Bean就可以
如下:
@Configuration
@Slf4j
public class RedisConfiguratiom {
@Bean //将当前方法的返回值对象交给IOC容器管理,成为Ioc容器的Bean对象
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// log.info("开始创建Redis模板...");
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
//redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
插曲: 当然,如果这里 第三方bean需要引入依赖注入的化,只需要通过参数的形式来声明这个类型参数就可以了.SpringBoot会在这个容器中去找到这个Bean对象,然后完成注入操作!!!比如这里这个RedisConnectionFactory redisConnectionFactory参数是Spring自动注入的,用于创建Redis连接。
当然其实同原理,自然也可以在启动类下直接配置:
因为我们进入启动类注解中,可以看到他
自然当前类就是一个配置类,自然也可以自己声明@Bean,当然一般不建议这样!!!
当然也可以通过@bean中的name和value来声明Bean的名称,如果不设置,默认就是方法名
总:
项目中自定义的,就使用@Componet及其衍生注解:
项目中引入的第三方的,使用@Bean注解
三 . springBoot-原理
原理:就是通过扫描指定的依赖包下的文件的配置类,任何封装到String【】数组里面,实现自动 装配
2.7之前的版本:
Spring常用的注解:
@SpringBootConfiguration //声明当前启动类也是一个注解
@EnableAutoConfiguration //声明哪些第三类配置当前类中
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })//默认扫描当前类和其子类
配置条件:
三.SpringBoot自动配置的原理:
Spring框架的配置相对复杂,需要手动配置大量的XML文件或注解。而Spring Boot通过自动配置和约定优于配置的原则,大大简化了配置过程。之所以SpringBoot框架更加简单,快捷,是因为其底层提供了2个非常重要的功能
起步依赖 -->>(就是通过Maven的依赖传递)解决Spring中依赖配置的繁琐
自动配置 -->>大大简化框架的使用过程中Bean的声明和配置通过起步依赖,常用的配置基本也就有了!!!
起步依赖:
起步依赖本质上是一个Maven或Gradle的依赖描述符,它包含了构建特定类型应用程序所需的所有依赖项。例如,spring-boot-starter-web
包含了构建Web应用程序所需的所有Spring MVC和Tomcat的依赖项。通过引入起步依赖,开发人员可以轻松地集成所需的组件,而无需手动添加每个依赖项。
自动配置:
Spring Boot的自动配置是另一个核心特性。它基于Spring框架的条件化配置功能,根据应用程序中声明的依赖项和类路径中的资源,自动配置Spring应用程序。自动配置会尝试猜测开发人员可能需要的配置,并自动应用这些配置。如果开发人员需要自定义配置,可以通过配置文件(如application.properties
或application.yml
)或Java配置类来覆盖自动配置。
我们启动一个程序:
可以看到除了我们自定义的配置还有很多配置类。这些配置加载进来,就会生成很多的Bean对象了!!!我们都可以直接DI注入使用了,这就是SpringBoot启动的,自动就帮我们配置好了的效果!!
OK,哪我们就了解下SpringBoot自动配置的原理:它是如何把我们引入的这些依赖定义的配置类,以及Bean如何加载到我们的SpringIOC容器中。
这里新建一个模块,做一些配置,充当第三方依赖,然后在其他项目引入这个模块的坐标
模块中:
当然,我们测试看看能不能获取到这个Bean对象, 按理我们引入了这个依赖,并且也声明了Component注解,然后应该会加载到SpringBoot的容器中
Ok,不出意外,找不到这个Bean说明是没有加载到的,其实spring启动的时候,默认会扫描当前包及其子包下才可以的,需要扫描到才可以交到Ioc容器中实现,不是声明了注解就一定会成为Ioc容器的Bean对象的,这里可以通过注解在启动类声明 @ComponentScan() 里面是数组看源码,然后指定包名就可以了
欧克:可以看到拿到Bean对象
当然,底层肯定不是这样字的,不然我们引入第三方依赖,哪不得爆炸,这种很繁琐
另一种就是@import注解实现得,其实这个就是关键,后面得实现其实也是套用这个了得。
使用@import导入得类会被Spring加载到Ioc容器中,导入形式有以下几种:
导入 普通类 --> 这个类就会交到Ioc容器中
导入 配置类 --> 这个配置类包括下得所以@bean对象都会就会交到Ioc容器中
导入ImportSelector 接口实现类 (关键得重点,后面其实底层就是通过这个接口来扫描得文件)
@import源码也声明了可以导入这些类
返回的是数组呢
如:
//@Import({MyImportSelector.class})
//@Import({TokenParser.class}) // 导入普通类 交给IOC容器
@Import({HeaderConfig.class}) // 导入配置类 交给IOC容器
@SpringBootApplication
public class SpringbootWebConfig2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class, args);
}
}
当然第三种就是 导入ImportSelector接口实现类看下源码:
返回值就是类的全类名,欧克,我们只需要实现这个接口,重写这个方法,然后添加一些我们想引入的Bean的全类名就ok了
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.example.HeaderConfig"};
}
}
@Configuration
public class HeaderConfig {
@Bean
public HeaderParser headerParser(){
return new HeaderParser();
}
@Bean
public HeaderGenerator headerGenerator(){
return new HeaderGenerator();
}
}
@Import({MyImportSelector.class})
@SpringBootApplication
public class SpringbootWebConfig2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class, args);
}
}
当然,上面这些方法,有些鸡肋的,当我们引入第三方依赖,是需要清晰知道我们要导入第三依赖的哪些配置类,哪些包的。 还是繁琐的,当然需要第三方自己自己来封装需要到那些类,然后通过第三方提供的@Enablexxxx注解然后在通过@Import注解来指定哪些需要声明的@Bean来实现
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)
public @interface EnableHeaderConfig {
}
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.example.HeaderConfig"};
}
}
@Configuration
public class HeaderConfig {
@Bean
public HeaderParser headerParser(){
return new HeaderParser();
}
@Bean
public HeaderGenerator headerGenerator(){
return new HeaderGenerator();
}
}
-------
@EnableHeaderConfig
public class SpringbootWebConfig2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class, args);
}
}
只需要在启动类声明这个注解就可以了,层层套娃,我们不需要关注第三方怎么实现的。 就可以把对于的配置类,bean加载到IOC容器里面了(这也是SpringBoot所采用的)
3.1 SpringBoot自动配置源码启动:
我们从启动类点击进去可以看到封装了许多注解
我们不需要关注其他,进入@EnableAutoConfiguration注解中去一探究竟:
其实关键就是找到这个接口这个方法就完事了:
欧克,进入实现类看看找到这个方法:
跟紧:
注:此版本是2.7版本之后的
可以看到其实就是扫描这2个文件的配置类容的。把这2个配置文件的信息加载出来,就会封装到这个Llst<String>集合当中,然后通过返回给给这个String【】只会,spring就会导入这些配置和@bean了
注: 2.7版本之前其实只有META-INF/spring.factories
文件,如下:
注: 从Spring Boot 2.7开始,虽然
META-INF/spring.factories
文件仍然是自动配置的一个重要组成部分,但Spring Boot引入了一种新的自动配置机制,即META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件。这个文件提供了一种更灵活的方式来指定自动配置类,但它并不是完全替代spring.factories
文件,而是作为一种补充。
说简单点就是通过@SpringBootApplication封装的@EnableAutoConfiguration注解然后封装的@Import来扫描指定文件,通过这些文件获取到配置类,@Bean的全类名啊这些在封装到这个String【】数组对象就ok!!!
其实这些文件在我们引入的起步依赖中都有
进去看看:
就是这些全类名,通过读取这些配置文件全类名后,通过@import把这些配置类,Bean加载到Ioc容器中 。(当然,你也就可以自己定义一个启动类了)
进入一个实例看看,其实就是一个配置类,并且下面的加了@Bean,所以我们ioc容器加载到这些,自然就有了Bean了,我们就可以注入使用了!!!
总结:
就是启动了注解底层封装了三个核心的注解,
1) @SpringBootConfiguration -->> 其注解又封装了@Configuration,就是声明当前也是一个配置类
2) @EnableAutoConfiguration ->> 其又封装了@Import注解 ,其注解又指定了ImportSelector实现类,其类有实现了selectImports的方法,其返回值就是String【】,这个数组封装的内容就是我们要导入到SpringIoc容器中的类的全类名,然后这个方法其实就会去扫描加载2个文件的这些全类名,这些全类名就是一个一个配置类,1个是spring.factories 的文件(2.7版本之前早期使用的,3.0版本之后就没了,在此期间会兼容),另一个是spring下的org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(2.7.x版本之后一种新的自动配置机制,3.0版本之后主导使用)。然后这些配置类下其实就是声明了一个一个的@Bean对象,然后就会把这些配置类加载通过String数组全部封装返回,然后通过Import注解把这些全部交给Spring的Ioc容器当中。
3) @ComponentScan -->> 组件扫描,默认扫描当前引导类所在的包及其子包
当然:这里并不是全部就会交给Ioc容器成为@Bean.其实还是有条件的,比如底层有些@Bean是加了@Conditionxxx的条件注解,满足某些条件之后才会加载到IOc容器中的
@Conditional 注解可以作用于 @Bean 方法、配置类或其他组件类上,当 Spring 容器扫描到 @Conditional 注解
时,会调用其 Condition 实现类的 matches 方法,根据返回的布尔值来决定是否实例化对应的 Bean。
当然这里扩展一下常用的Conditional的字注解使用: