【 复习SpringBoot 核心内容 | 配置优先级、Bean 管理与底层原理(起步依赖 + 自动配置) 】
摘要:本文详解 SpringBoot 核心内容:配置优先级(含 IDEA 与打包后 Java 系统属性、命令行参数设置)、Bean 管理(3 种获取方式、5 种作用域、第三方 Bean 配置),及核心原理(起步依赖的 Maven 传递、自动配置的多种实现方案)。
SpingBoot原理
1. 配置优先级
在SpringBoot项目当中,我们要想配置一个属性,可以通过这三种方式当中的任意一种来配置都可以,那么如果项目中同时存在这三种配置文件,且都配置了同一个属性。
配置文件优先级排名(从高到低):
properties配置文件
yml配置文件
yaml配置文件
虽然springboot支持多种格式配置文件,但在开发时,推荐统一使用yml格式的配置。
在SpringBoot项目当中除了以上3种配置文件外,还支持另外两种常见的配置方式:
那在idea当中运行程序时,如何来指定Java系统属性和命令行参数呢?
编辑启动程序的配置信息
优先级: 命令行参数 > 系统属性参数> properties参数 > yml参数 > yaml参数
思考:如果项目已经打包上线了,这个时候我们又如何来设置Java系统属性和命令行参数呢?
java -Dserver.port=9000 -jar XXXXX.jar --server.port=10010
下面我们来演示下打包程序运行时指定Java系统属性和命令行参数:
执行maven打包指令package,把项目打成jar文件
使用命令:java -jar 方式运行jar文件程序
1.项目打包:
2.运行jar程序:
2. Bean管理
本节介绍IOC容器中Bean的一些使用细节,主要学习以下三方面:
-
如何从IOC容器中手动的获取到bean对象
-
bean的作用域配置
-
管理第三方的bean对象
2.1 获取Bean
默认情况下,SpringBoot项目在启动的时候会自动的创建IOC容器(也称为Spring容器),并且在启动的过程当中会自动的将bean对象都创建好,存放在IOC容器当中。应用程序在运行时需要依赖什么bean对象,就直接进行依赖注入就可以了。
而在Spring容器中提供了一些方法,可以主动从IOC容器中获取bean对象,下面是3种常用方式:
1.根据name获取bean
Object getBean(String name)
2.根据类型获取bean
<T> T getBean(Class<T> requiredType)
3.根据name获取bean(带类型转换)
<T> T getBean(String name, Class<T> requiredType)
要从IOC容器当中来获取到bean对象,需先拿到IOC容器对象,怎样才能拿到IOC容器呢?
想获取到IOC容器,直接将IOC容器对象注入进来就可以了
@SpringBootTest
class SpringbootWebConfig2ApplicationTests {@Autowiredprivate ApplicationContext applicationContext; //IOC容器对象//获取bean对象@Testpublic void testGetBean(){//根据bean的名称获取DeptController bean1 = (DeptController) applicationContext.getBean("deptController");//Bean对象默认类名首字母小写System.out.println(bean1);//根据bean的类型获取DeptController bean2 = applicationContext.getBean(DeptController.class);System.out.println(bean2);//根据bean的名称 及 类型获取DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);System.out.println(bean3);}
}
Bean对象默认类名首字母小写
程序运行后控制台日志:
问题:输出的bean对象地址值是一样的,说明IOC容器当中的bean对象有几个?
答案:只有一个 (默认情况下,IOC中的bean对象是单例)
那么能不能将bean对象设置为非单例的(每次获取的bean都是一个新对象)?
可以,在下一个知识点(bean作用域)中讲解。
上述所说的 【Spring项目启动时,会把其中的bean都创建好】还会受到作用域及延迟初始化影响,这里主要针对于默认的单例非延迟加载的bean而言。
2.2 Bean作用域
在前面我们提到的IOC容器当中,默认bean对象是单例模式(只有一个实例对象)。那么如何设置bean对象为非单例呢?需要设置bean的作用域。
在Spring中支持五种作用域,后三种在web环境才生效:
作用域 | 说明 |
---|---|
singleton ⭐ | 容器内同名称的bean只有一个实例(单例)(默认) |
prototype⭐ | 每次使用该bean时会创建新的实例(非单例) |
request | 每个请求范围内会创建新的实例(web环境中,了解) |
session | 每个会话范围内会创建新的实例(web环境中,了解) |
application | 每个应用范围内会创建新的实例(web环境中,了解) |
知道了bean的5种作用域了,我们要怎么去设置一个bean的作用域呢?
-
可以借助Spring中的@Scope注解来进行配置作用域
1). 测试一(singleton单例)
结果:
IOC容器中的bean默认使用的作用域:singleton (单例)
默认singleton的bean,在容器启动时被创建,可以使用@Lazy注解来延迟初始化(延迟到第一次使用时)
2). 测试二(prototype非单例)
@Scope("prototype") //bean作用域为非单例
@Lazy //延迟加载
@RestController
@RequestMapping("/depts")
public class DeptController {@Autowiredprivate DeptService deptService;public DeptController(){System.out.println("DeptController constructor ....");}
}
结果:
prototype的bean,每一次使用该bean的时候都会创建一个新的实例
实际开发中,绝大部分的Bean是单例的,也就说绝大部分Bean不需要配置scope属性
2.3 第三方Bean
学习完bean的获取、bean的作用域之后,接下来我们再来学习第三方bean的配置。
之前我们所配置的bean,像controller、service,dao三层体系下编写的类,这些类都是我们在项目当中自己定义的类(自定义类)。当我们要声明这些bean,也非常简单,我们只需要在类上加上@Component以及它的这三个衍生注解(@Controller、@Service、@Repository),就可以来声明这个bean对象了。 但是在我们项目开发当中,还有一种情况就是这个类它不是我们自己编写的,而是我们引入的第三方依赖当中提供的。
在pom.xml文件中,引入依赖:
<dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId><version>2.7.0</version> <!-- 版本号可根据Spring Boot版本适配 --></dependency>
Redis中的RedisChatMemoryStore类就是第三方编写的。
当我们需要使用到RedisChatMemoryStore时,直接进行依赖注入是不是就可以了呢?
结论:第三方提供的类是只读的。无法在第三方类上添加@Component注解或衍生注解。
那么我们应该怎样使用并定义第三方的bean呢?
如果要管理的bean对象来自于第三方(不是自定义的),是无法用@Component 及衍生注解声明bean的,就需要用到@Bean注解。
推荐解决方案:在配置类中定义@Bean标识的方法
如果需要定义第三方Bean时, 通常会单独定义一个配置类
@Configuration
@ConfigurationProperties(prefix = "spring.data.redis")
@Data
public class RedisChatMemoryStoreConfig {private String host;private int port;private String password;private long ttl;/*** 创建RedisChatMemoryStore实例* @return*/@Beanpublic RedisChatMemoryStore redisChatMemoryStore() {return RedisChatMemoryStore.builder().host(host).port(port).password(password).ttl(ttl).build();}
}
注意事项 :
通过@Bean注解的name或value属性可以声明bean的名称,如果不指定,默认bean的名称就是方法名。
如果第三方bean需要依赖其它bean对象,直接在bean定义方法中设置形参即可,容器会根据类型自动装配。
关于Bean大家只需要保持一个原则:
如果是在项目当中我们自己定义的类,想将这些类交给IOC容器管理,我们直接使用@Component以及它的衍生注解来声明就可以。
如果这个类它不是我们自己定义的,而是引入的第三方依赖当中提供的类,而且我们还想将这个类交给IOC容器管理。此时我们就需要在配置类中定义一个方法,在方法上加上一个@Bean注解,通过这种方式来声明第三方的bean对象。
3. SpringBoot原理
我们前面提到,如果我们直接基于Spring框架进行项目的开发,会比较繁琐。
这个繁琐主要体现在两个地方:
-
在pom.xml中依赖配置比较繁琐,在项目开发时,需要自己去找到对应的依赖,还需要找到依赖它所配套的依赖以及对应版本,否则就会出现版本冲突问题。
-
在使用Spring框架进行项目开发时,需要在Spring的配置文件中做大量的配置,这就造成Spring框架入门难度较大,学习成本较高。
SpringBoot框架之所以使用起来更简单更快捷,是因为SpringBoot框架底层提供了两个非常重要的功能:一个是起步依赖,一个是自动配置。
3.1 起步依赖
假如我们没有使用SpringBoot,用的是Spring框架进行web程序的开发,此时我们就需要引入web程序开发所需要的一些依赖。
spring-webmvc依赖:这是Spring框架进行web程序开发所需要的依赖
servlet-api依赖:Servlet基础依赖
jackson-databind依赖:JSON处理工具包
如果要使用AOP,还需要引入aop依赖、aspect依赖
项目中所引入的这些依赖,还需要保证版本匹配,否则就可能会出现版本冲突问题。
如果我们使用了SpringBoot,就不需要像上面这么繁琐的引入依赖了。我们只需要引入一个依赖就可以了,那就是web开发的起步依赖:springboot-starter-web。
为什么我们只需要引入一个web开发的起步依赖,web开发所需要的所有的依赖都有了呢?
-
因为Maven的依赖传递
在SpringBoot给我们提供的这些起步依赖当中,已提供了当前程序开发所需要的所有的常见依赖(官网地址:Spring Boot Reference Documentation)。
比如:springboot-starter-web,这是web开发的起步依赖,在web开发的起步依赖当中,就集成了web开发中常见的依赖:json、web、webmvc、tomcat等。我们只需要引入这一个起步依赖,其他的依赖都会自动的通过Maven的依赖传递进来。
结论:起步依赖的原理就是Maven的依赖传递。
3.2 自动配置
3.2.1 概述
SpringBoot的自动配置就是当Spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。
比如:我们要进行事务管理、要进行AOP程序的开发,此时就不需要我们再去手动的声明这些bean对象了,我们直接使用就可以从而大大的简化程序的开发,省去了繁琐的配置操作。
在第二个CommonConfig中它的bean名字叫commonConfig,为什么还会有这样一个bean对象呢?原因是在配置类上添加了一个注解@Configuration,而@Configuration底层就是@Component
所以配置类最终也是SpringIOC容器当中的一个bean对象
在IOC容器中除了我们自己定义的bean以外,还有很多配置类,这些配置类都是SpringBoot在启动的时候加载进来的配置类。这些配置类加载进来之后,它也会生成很多的bean对象。
配置类GsonAutoConfiguration里面有一个bean,bean的名字叫gson,它的类型是Gson。
com.google.gson.Gson是谷歌包中提供的用来处理JSON格式数据的。
当我们想要使用这些配置类中生成的bean对象时,可以使用@Autowired就自动注入了:
@SpringBootTest
public class AutoConfigurationTests {@Autowiredprivate Gson gson;@Testpublic void testJson(){String json = gson.toJson(Result.success());System.out.println(json);}
}
问题:在当前项目中我们并没有声明谷歌提供的Gson这么一个bean对象,然后我们却可以通过@Autowired从Spring容器中注入bean对象,那么这个bean对象怎么来的?
答案:SpringBoot项目在启动时通过自动配置完成了bean对象的创建。
3.2.2 常见方案
3.2.2.1 概述
我们知道了什么是自动配置之后,接下来我们就要来剖析自动配置的原理。解析自动配置的原理就是分析在 SpringBoot项目当中,我们引入对应的依赖之后,是如何将依赖jar包当中所提供的bean以及配置类直接加载到当前项目的SpringIOC容器当中的。
接下来,我们就直接通过代码来分析自动配置原理。
准备工作
1、在SpringBoot项目 spring-boot-web-config2 工程中,通过坐标引入itheima-utils依赖
@Component
public class TokenParser {public void parse(){System.out.println("TokenParser ... parse ...");}
}
2、在测试类中,添加测试方法
@SpringBootTest
public class AutoConfigurationTests {@Autowiredprivate ApplicationContext applicationContext;@Testpublic void testTokenParse(){System.out.println(applicationContext.getBean(TokenParser.class));}//省略其他代码...
}
3、执行测试方法
异常信息描述: 没有com.example.TokenParse类型的bean
说明:在Spring容器中没有找到com.example.TokenParse类型的bean对象
思考:引入进来的第三方依赖当中的bean以及配置类为什么没有生效?
原因在我们之前讲解IOC的时候有提到过,在类上添加@Component注解来声明bean对象时,还需要保证@Component注解能被Spring的组件扫描到。
SpringBoot项目中的@SpringBootApplication注解,具有包扫描的作用,但是它只会扫描启动类所在的当前包以及子包。
当前包:com.itheima, 第三方依赖中提供的包:com.example(扫描不到)
那么如何解决以上问题的呢?
方案1:@ComponentScan 组件扫描
方案2:@Import 导入(使用@Import导入的类会被Spring加载到IOC容器中)
3.2.2.2 方案一
@ComponentScan组件扫描
@SpringBootApplication
@ComponentScan({"com.itheima","com.example"}) //指定要扫描的包
public class SpringbootWebConfig2Application {public static void main(String[] args) {SpringApplication.run(SpringbootWebConfig2Application.class, args);}
}
大家可以想象一下,如果采用以上这种方式来完成自动配置,那我们进行项目开发时,当需要引入大量的第三方的依赖,就需要在启动类上配置N多要扫描的包,这种方式会很繁琐。而且这种大面积的扫描性能也比较低。
缺点:使用繁琐,性能低
结论:SpringBoot中并没有采用以上这种方案
3.2.2.3 方案二
1)使用@Import导入普通类:
@Import(TokenParser.class) //导入的类会被Spring加载到IOC容器中
@SpringBootApplication
public class SpringbootWebConfig2Application {public static void main(String[] args) {SpringApplication.run(SpringbootWebConfig2Application.class, args);}
}
2)使用@Import导入配置类:
-
配置类
@Configuration
public class HeaderConfig {@Beanpublic HeaderParser headerParser(){return new HeaderParser();}@Beanpublic HeaderGenerator headerGenerator(){return new HeaderGenerator();}
}
-
启动类
@Import(HeaderConfig.class) //导入配置类
@SpringBootApplication
public class SpringbootWebConfig2Application {public static void main(String[] args) {SpringApplication.run(SpringbootWebConfig2Application.class, args);}
}
3)使用@Import导入ImportSelector接口实现类:(创建一个数组存储类名)
-
ImportSelector接口实现类
public class MyImportSelector implements ImportSelector {public String[] selectImports(AnnotationMetadata importingClassMetadata) {//返回值字符串数组(数组中封装了全限定名称的类)return new String[]{"com.example.HeaderConfig"};}
}
-
启动类
@Import(MyImportSelector.class) //导入ImportSelector接口实现类
@SpringBootApplication
public class SpringbootWebConfig2Application {public static void main(String[] args) {SpringApplication.run(SpringbootWebConfig2Application.class, args);}
}
我们使用@Import注解通过这三种方式都可以导入第三方依赖中所提供的bean或者是配置类。
思考:如果基于以上方式完成自动配置,当要引入一个第三方依赖时,是不是还要知道第三方依赖中有哪些配置类和哪些Bean对象?
答案:是的。 (对程序员来讲,很不友好,而且比较繁琐)
思考:当我们要使用第三方依赖,依赖中到底有哪些bean和配置类,谁最清楚?
答案:第三方依赖自身最清楚。
结论:我们不用自己指定要导入哪些bean对象和配置类了,让第三方依赖它自己来指定。
怎么让第三方依赖自己指定bean对象和配置类?
比较常见的方案就是第三方依赖给我们提供一个注解,这个注解一般都以
@EnableXxxx
开头的注解,注解中封装的就是@Import注解
4). 使用第三方依赖提供的 @EnableXxxxx
注解
-
第三方依赖中提供的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)//指定要导入哪些bean对象或配置类
public @interface EnableHeaderConfig {
}
-
在使用时只需在启动类上加上
@EnableXxxxx
注解即可
@EnableHeaderConfig //使用第三方依赖提供的Enable开头的注解
@SpringBootApplication
public class SpringbootWebConfig2Application {public static void main(String[] args) {SpringApplication.run(SpringbootWebConfig2Application.class, args);}
}
以上四种方式都可以完成导入操作,但是第4种方式会更方便更优雅,而这种方式也是
SpringBoot
当中所采用的方式。
3.2.3 自动配置原理
自动配置的核心就在
@SpringBootApplication
注解上,SpringBootApplication
这个注解底层包含了3个注解,分别是:
@SpringBootConfiguration
@ComponentScan
@EnableAutoConfiguration
@EnableAutoConfiguration
这个注解才是自动配置的核心。
它封装了一个@Import注解,Import注解里面指定了一个
ImportSelector
接口的实现类。在这个实现类中,重写了
ImportSelector
接口中的selectImports()
方法。而
selectImports()
方法中会去读取两份配置文件,并将配置文件中定义的配置类做为selectImports()
方法的返回值返回,返回值代表的就是需要将哪些类交给Spring的IOC
容器进行管理。那么所有自动配置类的中声明的bean都会加载到Spring的
IOC
容器中吗? 其实并不会,因为这些配置类中在声明bean时,通常都会添加@Conditional开头的注解,这个注解就是进行条件装配。而Spring会根据Conditional注解有选择性的进行bean的创建。@Enable 开头的注解底层,它就封装了一个注解 import 注解,它里面指定了一个类,是
ImportSelector
接口的实现类。在实现类当中,我们需要去实现ImportSelector
接口当中的一个方法selectImports
这个方法。这个方法的返回值代表的就是我需要将哪些类交给 spring 的IOC
容器进行管理。此时它会去读取两份配置文件,一份儿是
spring.factories
,另外一份儿是autoConfiguration.imports
。而在autoConfiguration.imports
这份儿文件当中,它就会去配置大量的自动配置的类。而前面我们也提到过这些所有的自动配置类当中,所有的 bean都会加载到 spring 的
IOC
容器当中吗?其实并不会,因为这些配置类当中,在声明 bean 的时候,通常会加上这么一类@Conditional 开头的注解。这个注解就是进行条件装配。所以SpringBoot
非常的智能,它会根据 @Conditional 注解来进行条件装配。只有条件成立,它才会声明这个bean,才会将这个 bean 交给IOC
容器管理。
大功告成!