【JavaWeb后端开发-第七章】SpingBoot原理

时间:2024-01-22 18:48:15

文章目录

  • 前言
  • 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端口号生效:

    ♦ propertiesyamlyml三种配置文件同时存在

在这里插入图片描述

propertiesyamlyml三种配置文件,优先级最高的是properties

    ♦ yamlyml两种配置文件同时存在

在这里插入图片描述

配置文件优先级排名(从高到低):
    1. properties配置文件
    2. yml配置文件
    3. yaml配置文件

注意事项:虽然springboot支持多种格式配置文件,但是在项目开发时,推荐统一使用一种格式的配置。(yml是主流

    

    在SpringBoot项目当中除了以上3种配置文件外,SpringBoot为了增强程序的扩展性,除了支持配置文件的配置方式以外,还支持另外两种常见的配置方式

  1. Java系统属性配置 (格式: -Dkey=value)
-Dserver.port=9000
  1. 命令行参数(格式:–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种常用方式:

  1. 根据name获取bean
Object getBean(String name)
  1. 根据类型获取bean
<T> T getBean(Class<T> requiredType)
  1. 根据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注解namevalue属性可以声明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开发中常见的依赖:jsonwebwebmvctomcat等。我们只需要引入这一个起步依赖,其他的依赖都会自动的通过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里面有一个beanbean的名字叫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