文章目录
- 前言
- 1. 配置优先级
- 2. Bean管理
- 2.1. 获取Bean
- 2.2. Bean作用域
- 2.3. 第三方Bean
- 3. SpringBoot原理
- 3.1. 起步依赖
- 3.2. 自动配置
- 3.2.1. 概述
- 3.2.2. 常见方案
- 概述
- 方案一
- 方案二
- 3.2.3. 原理分析
- 3.2.3.1. 源码跟踪
- 3.2.3.2. @Conditional
- 3.2.4. 案例
- 3.2.4.1. 自定义starter分析
- 3.2.4.2. 自定义starter实现
- 3.2.4.3 自定义starter测试
- 4. Web后端开发总结
前言
在前面的所有章节当中,我们学习的都是web开发的技术使用,都是面向应用层面的,我们学会了怎么样去用。而我们今天所要学习的是web后端开发
的最后一个篇章springboot原理篇,主要偏向于底层原理。
本章节的安排包括三个部分:
1. 配置优先级:Springboot项目当中属性配置的常见方式以及配置的优先级
2. Bean的管理
3. 剖析Springboot的底层原理
1. 配置优先级
在我们前面的章节当中,我们已经讲解了SpringBoot项目当中支持的三类配置文件:
♦ application.properties
♦ application.yml
♦ application.yaml
在SpringBoot项目当中,我们要想配置一个属性,可以通过这三种方式当中的任意一种来配置都可以,那么如果项目中同时存在这三种配置文件,且都配置了同一个属性,如:Tomcat端口号,到底哪一份配置文件生效呢?
♦ application.properties
server.port=8081
♦ application.yml
server:
port: 8082
♦ application.yaml
server:
port: 8082
我们启动SpringBoot程序,测试下三个配置文件中哪个Tomcat端口号生效:
♦ properties
、yaml
、yml
三种配置文件同时存在
properties
、yaml
、yml
三种配置文件,优先级最高的是properties
♦ yaml
、yml
两种配置文件同时存在
配置文件优先级排名(从高到低):
1.properties
配置文件
2.yml
配置文件
3.yaml
配置文件
注意事项:虽然springboot支持多种格式配置文件,但是在项目开发时,推荐统一使用一种格式的配置。(yml是主流)
在SpringBoot项目当中除了以上3种配置文件外,SpringBoot为了增强程序的扩展性,除了支持配置文件的配置方式以外,还支持另外两种常见的配置方式:
-
Java系统属性
配置 (格式: -Dkey=value)
-Dserver.port=9000
-
命令行参数
(格式:–key=value)
--server.port=10010
那在idea当中运行程序时,如何来指定Java系统属性
和命令行参数
呢?
♦ 编辑启动程序的配置信息
重启服务,同时配置Tomcat端口(三种配置文件
、系统属性
、命令行参数
),测试哪个Tomcat端口号生效:
删除命令行参数配置,重启SpringBoot服务:
优先级:
命令行参数
>系统属性参数
>properties参数
>yml参数
>yaml参数
思考:如果项目已经打包上线了,这个时候我们又如何来设置Java系统属性和命令行参数呢?
java -Dserver.port=9000 -jar XXXXX.jar --server.port=10010
下面我们来演示下打包程序运行时指定Java系统属性和命令行参数:
1. 执行maven打包指令package,把项目打成jar文件
2. 使用命令:java -jar 方式运行jar文件程序
项目打包:
运行jar程序:
♦ 同时设置Java系统属性和命令行参数
java -Dserver.port=9000 -jar springboot-web-config-0.0.1-SNAPSHOT.jar --server.port=10010
♦ 仅设置Java系统属性
注意事项:
♦ Springboot项目进行打包时,需要引入插件 spring-boot-maven-plugin (基于官网骨架创建项目,会自动添加该插件)
在SpringBoot项目当中,常见的属性配置方式有5种, 3种配置文件,加上2种外部属性的配置(Java系统属性、命令行参数)。通过以上的测试,我们也得出了优先级(从低到高):
♦ application.yaml
(忽略)
♦ application.yml
♦ application.properties
♦ java系统属性
(-Dxxx=xxx
)
♦ 命令行参数
(--xxx=xxx
)
2. Bean管理
在前面的章节当中,已经讲过了我们可以通过Spring当中提供的注解@Component
以及它的三个衍生注解(@Controller
、@Service
、@Repository
)来声明IOC容器
中的bean对象
,同时我们也学习了如何为应用程序注入运行时所需要依赖的bean对象,也就是依赖注入DI
。
我们本章主要学习IOC容器
中Bean
的其他使用细节,主要学习以下三方面:
1. 如何从IOC容器中手动的获取到bean对象
2. bean的作用域配置
3. 管理第三方的bean对象
接下来我们先来学习第一方面,从IOC容器
中获取bean对象
。
2.1. 获取Bean
默认情况下,SpringBoot项目在启动的时候会自动的创建IOC容器(也称为Spring容器),并且在启动的过程当中会自动的将bean对象都创建好,存放在IOC容器当中。应用程序在运行时需要依赖什么bean对象,就直接进行依赖注入就可以了。
而在Spring容器中提供了一些方法,可以主动从IOC容器中获取到bean对象,下面介绍3种常用方式:
- 根据name获取bean
Object getBean(String name)
- 根据类型获取bean
<T> T getBean(Class<T> requiredType)
- 根据name获取bean(带类型转换)
<T> T getBean(String name, Class<T> requiredType)
思考:要从IOC容器当中来获取到bean对象,需要先拿到IOC容器对象,怎么样才能拿到IOC容器呢?
解答:想获取到IOC容器,直接将IOC容器对象注入进来就可以了
控制器:DeptController.java
:
@RestController
@RequestMapping("/depts")
public class DeptController {
@Autowired
private DeptService deptService;
public DeptController(){
System.out.println("DeptController constructor ....");
}
@GetMapping
public Result list(){
List<Dept> deptList = deptService.list();
return Result.success(deptList);
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
deptService.delete(id);
return Result.success();
}
@PostMapping
public Result save(@RequestBody Dept dept){
deptService.save(dept);
return Result.success();
}
}
业务实现类:DeptServiceImpl.java
:
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
return deptList;
}
@Override
public void delete(Integer id) {
deptMapper.delete(id);
}
@Override
public void save(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.save(dept);
}
}
Mapper接口DeptMapper.java
:
@Mapper
public interface DeptMapper {
//查询全部部门数据
@Select("select * from dept")
List<Dept> list();
//删除部门
@Delete("delete from dept where id = #{id}")
void delete(Integer id);
//新增部门
@Insert("insert into dept(name, create_time, update_time) values
(#{name},#{createTime},#{updateTime})")
void save(Dept dept);
}
test文件夹中的测试类SpringbootWebConfig2ApplicationTests.java
:
@SpringBootTest
class SpringbootWebConfig2ApplicationTests {
@Autowired
private ApplicationContext applicationContext; //IOC容器对象
//获取bean对象
@Test
public void testGetBean(){
//根据bean的名称获取
DeptController bean1 = (DeptController)
applicationContext.getBean("deptController");
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对象地址值是一样的,说明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环境中,了解) |
session | 每个应用范围内会创建新的实例(web环境中,了解) |
知道了bean的5种作用域了,我们要怎么去设置一个bean的作用域呢?
可以借助Spring中的@Scope
注解来进行配置作用域
1). 测试一
♦ 控制器
//默认bean的作用域为:singleton (单例)
@Lazy //延迟加载(第一次使用bean对象时,才会创建bean对象并交给ioc容器管理)
@RestController
@RequestMapping("/depts")
public class DeptController {
@Autowired
private DeptService deptService;
public DeptController(){
System.out.println("DeptController constructor ....");
}
//省略其他代码...
}
♦ 测试类
@SpringBootTest
class SpringbootWebConfig2ApplicationTests {
@Autowired
private ApplicationContext applicationContext; //IOC容器对象
//bean的作用域
@Test
public void testScope(){
for (int i = 0; i < 10; i++) {
DeptController deptController =
applicationContext.getBean(DeptController.class);
System.out.println(deptController);
}
}
}
重启SpringBoot服务,运行测试方法,查看控制台打印的日志:
注意事项:
♦ IOC容器中的bean默认使用的作用域:singleton
(单例)
♦ 默认singleton的bean,在容器启动时被创建,可以使用@Lazy
注解来延迟初始化(延迟到第一次使用时)
2). 测试二
修改控制器DeptController代码:
@Scope("prototype") //bean作用域为非单例
@Lazy //延迟加载
@RestController
@RequestMapping("/depts")
public class DeptController {
@Autowired
private DeptService deptService;
public DeptController(){
System.out.println("DeptController constructor ....");
}
//省略其他代码...
}
重启SpringBoot服务,再次执行测试方法,查看控制吧打印的日志:
注意事项:
♦prototype
的bean,每一次使用该bean的时候都会创建一个新的实例
♦ 实际开发当中,绝大部分的Bean是单例的,也就是说绝大部分Bean不需要配置scope属性
2.3. 第三方Bean
学习完bean的获取、bean的作用域之后,接下来我们再来学习第三方bean的配置。
之前我们所配置的bean,像controller、service,dao三层体系下编写的类,这些类都是我们在项目当中自己定义的类(自定义类)。当我们要声明这些bean,也非常简单,我们只需要在类上加上@Component以及它的这三个衍生注解(@Controller、@Service、@Repository),就可以来声明这个bean对象了。
但是在我们项目开发当中,还有一种情况就是这个类它不是我们自己编写的,而是我们引入的第三方依赖当中提供的。
在pom.xml
文件中,引入dom4j
:
<!--Dom4j-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
dom4j
就是第三方组织提供的。dom4j
中的SAXReader类
就是第三方编写的。
当我们需要使用到SAXReader对象时,直接进行依赖注入是不是就可以了呢?
按照我们之前的做法,需要在SAXReader类上添加一个注解@Component(将当前类交给IOC容器管理)
结论:第三方提供的类是只读的。无法在第三方类上添加@Component注解或衍生注解
那么我们应该怎样使用并定义第三方的bean呢?
如果要管理的bean对象来自于第三方(不是自定义的),是无法用@Component及衍生注解声明bean的,就需要用到@Bean注解
解决方案1:在启动类上添加@Bean
标识的方法
@SpringBootApplication
public class SpringbootWebConfig2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class,
args);
}
//声明第三方bean
@Bean //将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean
public SAXReader saxReader(){
return new SAXReader();
}
}
xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<emp>
<name>Tom</name>
<age>18</age>
</emp>
测试类:
@SpringBootTest
class SpringbootWebConfig2ApplicationTests {
@Autowired
private SAXReader saxReader;
//第三方bean的管理
@Test
public void testThirdBean() throws Exception {
Document document =
saxReader.read(this.getClass().getClassLoader().getResource("1.xml")
);
Element rootElement = document.getRootElement();
String name = rootElement.element("name").getText();
String age = rootElement.element("age").getText();
System.out.println(name + " : " + age);
}
//省略其他代码...
}
重启SpringBoot服务,执行测试方法后,控制台输出日志:Tom : 18
说明:以上在启动类中声明第三方Bean的作法,不建议使用(项目中要保证启动类的纯粹性)
解决方案2:在配置类
中定义@Bean标识的方法
如果需要定义第三方Bean时, 通常会单独定义一个配置类
在com.itheima包
中创建config包
,然后在config包
中创建CommonConfig.java
:
@Configuration //配置类 (在配置类当中对第三方bean进行集中的配置管理)
public class CommonConfig {
//声明第三方bean
@Bean //将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean
//通过@Bean注解的name/value属性指定bean名称, 如果未指定, 默认是方法名
public SAXReader reader(DeptService deptService){
System.out.println(deptService);
return new SAXReader();
}
}
注释掉SpringBoot启动类中创建第三方bean对象的代码
重启服务,执行测试方法,查看控制台日志:Tom : 18
在方法上加上一个@Bean注解
,Spring 容器在启动的时候,它会自动的调用这个方法,并将方法的返回值声明为Spring容器当中的Bean对象。
注意事项 :
♦ 通过@Bean注解
的name
或value
属性可以声明bean的名称
,如果不指定,默认bean的名称
就是方法名
。
♦ 如果第三方bean需要依赖其它bean对象,直接在bean定义方法中设置形参即可,容器会根据类型自动装配
关于Bean大家只需要保持一个原则:
♦ 如果是在项目当中我们自己定义的类,想将这些类交给IOC容器管理,我们直接使用@Component以及它的衍生注解来声明就可以。
♦ 如果这个类它不是我们自己定义的,而是引入的第三方依赖当中提供的类,而且我们还想将这个类交给IOC容器管理。此时我们就需要在配置类中定义一个方法,在方法上加上一个@Bean注解,通过这种方式来声明第三方的bean对象。
3. SpringBoot原理
经过前面章节的学习,大家也会发现基于SpringBoot进行web程序的开发是非常简单、非常高效的。
SpringBoot使我们能够集中精力地去关注业务功能的开发,而不用过多地关注框架本身的配置使用。而我们前面所讲解的都是面向应用层面的技术,接下来我们开始学习SpringBoot的原理,这部分内容偏向于底层的原理分析。
在剖析SpringBoot的原理之前,我们先来快速回顾一下我们前面所讲解的Spring家族的框架。
Spring是目前世界上最流行的Java框架,它可以帮助我们更加快速、更加容易的来构建Java项目。而在Spring家族当中提供了很多优秀的框架,而所有的框架都是基于一个基础框架的SpringFramework
(也就是Spring框架)。而前面我们也提到,如果我们直接基于Spring框架进行项目的开发,会比较繁琐。
这个繁琐主要体现在两个地方:
1. 在pom.xml中依赖配置比较繁琐,在项目开发时,需要自己去找到对应的依赖,还需要找到依赖它所配套的依赖以及对应版本,否则就会出现版本冲突问题。
2. 在使用Spring框架进行项目开发时,需要在Spring的配置文件中做大量的配置,这就造成Spring框架入门难度较大,学习成本较高。
基于Spring存在的问题,官方在Spring框架4.0版本之后,又推出了一个全新的框架:SpringBoot。
通过 SpringBoot来简化Spring框架的开发(是简化不是替代)。我们直接基于SpringBoot来构建Java项目,会让我们的项目开发更加简单,更加快捷。
SpringBoot框架之所以使用起来更简单更快捷,是因为SpringBoot框架底层提供了两个非常重要的功能:一个是起步依赖,一个是自动配置。
通过SpringBoot所提供的起步依赖,就可以大大的简化pom文件当中依赖的配置,从而解决了Spring框架当中依赖配置繁琐的问题。
通过自动配置的功能就可以大大的简化框架在使用时bean的声明以及bean的配置。我们只需要引入程序开发时所需要的起步依赖,项目开发时所用到常见的配置都已经有了,我们直接使用就可以了
简单回顾之后,接下来我们来学习下SpringBoot的原理。其实学习SpringBoot的原理就是来解析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给我们提供的这些起步依赖当中,已提供了当前程序开发所需要的所有的常见依赖(官网地址:https://docs.spring.io/spring-boot/docs/2.7.7/reference/htmlsingle/#using.build-systems.starters )。
♦ 比如:springboot-starter-web
,这是web开发的起步依赖,在web开发的起步依赖当中,就集成了web开发中常见的依赖:json
、web
、webmvc
、tomcat
等。我们只需要引入这一个起步依赖,其他的依赖都会自动的通过Maven的依赖传递进来。
结论:起步依赖的原理就是Maven的依赖传递
。
3.2. 自动配置
我们讲解了SpringBoot当中起步依赖的原理,就是Maven的依赖传递。接下来我们解析下自动配置的原理,我们要分析自动配置的原理,首先要知道什么是自动配置。
3.2.1. 概述
SpringBoot的自动配置就是当Spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。
比如:我们要进行事务管理、要进行AOP程序的开发,此时就不需要我们再去手动的声明这些bean对象了,我们直接使用就可以从而大大的简化程序的开发,省去了繁琐的配置操作。
下面我们打开idea,一起来看下自动配置的效果:
运行SpringBoot启动类
大家会看到有两个CommonConfig
,在第一个CommonConfig类
中定义了一个bean对象
,bean对象
的名字叫reader
。
在第二个CommonConfig
中它的bean
名字叫commonConfig
,为什么还会有这样一个bean对象呢?原因是在CommonConfig配置类
上添加了一个注解@Configuration
,而@Configuration
底层就是@Component
所以配置类最终也是SpringIOC容器当中的一个bean对象
在IOC容器中除了我们自己定义的bean以外,还有很多配置类,这些配置类都是SpringBoot在启动的时候加载进来的配置类。这些配置类加载进来之后,它也会生成很多的bean对象。
比如:配置类
GsonAutoConfiguration
里面有一个bean
,bean
的名字叫gson
,它的类型是Gson
。
com.google.gson.Gson
是谷歌包中提供的用来处理JSON格式数据
的。
当我们想要使用这些配置类中生成的bean对象时,可以使用@Autowired
就自动注入了:
import com.google.gson.Gson;
import com.itheima.pojo.Result;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class AutoConfigurationTests {
@Autowired
private Gson gson;
@Test
public void testJson(){
String json = gson.toJson(Result