掘金 后端 ( ) • 2024-05-07 09:45

SpringBoot原理篇

一、SpringBoot IoC补充

SpringBoot和Spring的关系:

Spring:是一个针对于JavaSE和JavaEE提供更简单使用,主要提供了IoC(控制反转,用于解耦)和AOP(面向切面编程,用于增强)两大核心思想,可以简化有关一切Java应用的开发与维护工具,是一个全栈式(服务端全栈涵盖了Web层、Service层、Dao层)的框架

SpringBoot:引导开发人员使用Spring的一个脚手架,它是对Spring框架做了再封装,目的不是为了取代Spring,而是为了让开发人员更方便的使用Spring框架

1. 注册bean的方式

让框架注册生成bean的前提:

  1. 扫描到类,知道这个类存在,通过组件扫描扫到这个类。(SpringBoot默认扫描的是引导类所对应的目录及下级目录)

  2. 解析类上有没有响应的注解

1.1 @Component及其衍生注解与组件扫描

注册bean的注解

注解:就是一个标记、一个记号。它本身没有任何功能,需要其它代码提供功能

在某一个类上,添加以下任何一个注解,就意味着:告诉Spring,这个类你要帮我创建对象,放到IoC容器里去

注解 说明 @Component 注册bean对象的根本注解 @Controller 是@Component的衍生注解,提供了语义化,通常在web层的类上加这个注解 @Service 是@Component的衍生注解,提供了语义化,通常在Service层的类上加这个注解 @Repository 是@Component的衍生注解,提供了语义化,通常在Dao层的类上加这个注解 @Configuration 是@Component的衍生注解,提供了语义化,加这个注解的类要做为一个配置类,Spring底层会有不同的处理

实际使用中:

  • web层的类:加@Controller
  • service层的类:加@Service
  • dao层的类:加@Repository
  • 不在这三层的类:加@Component
  • 一个类要做为配置类:加@Configuration

组件扫描@ComponentScan

如果我们在某个类上加了上边的@Component或衍生注解,还需要由Spring扫描我们项目里所有的类:

  • 当Spring发现某个类上有@Component或它的衍生注解,就会创建对象,并把对象放到IoC容器里

如果没有使用SpringBoot框架,而是使用原始的Spring:必须有一个配置类,类上加@ComponentScan("要扫描的包")

  • 加了这个注解之后,Spring就会扫描我们指定的这个包里所有的类

如果使用了SpringBoot框架:它在引导类上的那个@SpringBootApplication,其实已经包含了@ComponentScan

  • 我们就不需要自己再加@ComponentScan

  • 但是没有指定扫描的包,所以会扫描:添加了@ComponentScan注解的类 所在的包。

    最终是:只会扫描引导类所在的包

    如果一个类加了@Component或衍生注解,但是不在扫描范围内的话,Spring容器里也不会有这个对象

1.2 @Configuration与@Bean

适合于第三方jar包里的类:

  • 我们不能修改类,不可能在类上加@Component
  • 如果想要让Spring创建对象放到容器里,就要使用@Configuration和@Bean

用法:

  1. 创建一个类,类上加@Configuration,就成为一个配置类
  2. 在配置类里增加一个方法,方法上加@Bean:Spring将会调用这个方法,把方法的返回值放到容器里

示例:要把SAXReader对象放到容器里

  1. 在pom.xml里添加dom4j的依赖坐标
<dependency>
    <groupId>org.dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>2.1.4</version>
</dependency>
  1. 创建一个配置类(配置类必须在扫描范围内),类里的方法上使用@Bean注解
@Configuration
public class DemoConfig {

    /**
     * 把SAXReader对象放到Spring容器里进行管理
     */
    @Bean
    public SAXReader saxReader(){
        return new SAXReader();
    }
}
  1. 测试:能够从容器里获取到SAXReader对象,说明@Bean注册bean成功了
@SpringBootTest
public class Demo01IocTest {
    @Autowired
    private SAXReader reader;

    @Test
    public void test(){
        System.out.println("reader = " + reader);
    }
}

1.3 @Import

适合于不在扫描范围内的类,要把对象交给Spring管理。

1.3.1 @Import直接导入类

在引导类或者在配置类上加注解:@Import({类名1.class, 类名2.class, 类名3.class, ....})

例如:

  • 引导类上使用@Import把这些类交给Spring管理
@SpringBootApplication
@Import({Demo01Bean.class, Demo02Bean.class, Demo03Bean.class})//引入需要创建的对象
public class Demo1Application {
    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class, args);
    }
}
  • 测试效果:从容器里可以得到这三个bean对象,说明这三个类对象已经交给Spring管理了
@SpringBootTest
public class Demo01IocTest {
    /**
     * 注入Spring容器对象
     */
    @Autowired
    public ApplicationContext app;

    @Test
    public void testImport(){
        //从Spring容器里分别获取bean对象并打印
        System.out.println(app.getBean(Demo01Bean.class));
        System.out.println(app.getBean(Demo02Bean.class));
        System.out.println(app.getBean(Demo02Bean.class));
    }
}

1.3.2 @Import导入ImportSelector类

在引导类或配置类上加注解@Import(ImportSelector接口的实现类.class)。接口实现类里可以选择一批类名,Spring将会把这些类创建对象放到容器里

步骤:

  1. 准备一个Java类,实现ImportSelector接口
  2. 重写接口的selectImports方法:方法里返回一批类名的数组
  3. Spring将会调用这个方法,把这些类名对应的类,创建对象放到容器里
  1. 准备一个ImportSelector的实现类。
public class DemoImporter implements ImportSelector {
    //自定义导入选择器
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"cn.sdf.bean.Demo01Bean", "cn.sdf.bean.Demo02Bean", "cn.sdf.bean.Demo03Bean"};
    }
}
  1. 在引导类或者配置类上注解
@SpringBootApplication
// 添加@Import注解,导入ImportSelector接口的实现类:DemoImporter
@Import(DemoImporter.class)
public class Demo1Application {
    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class, args);
    }
}

2. 配置bean的注解

2.1 注解介绍

注解 作用 @Scope 用在bean对象上,设置bean对象的作用域。告诉Spring是以单例模式还是多例模式,或者其它模式来维护这个bean对象 @PostConstruct 用在bean对象里的方法上,这个方法将会在Spring创建bean对象之后,执行一次。 这个方法通常被称为bean的初始化方法 @PreDestroy 用在bean对象里的方法上,这个方法将会在Spring销毁bean对象之前,执行一次。这个方法通常被称为bean的销毁方法

@Scope:加在bean对象上,用于设置bean对象的作用域

  • 如果一个bean对象上没有加此注解:Spring默认以单例模式维护bean对象

    从容器里获取这个bean对象,无论获取几次,得到的都是同一个对象

  • 如果想要修改bean的作用域,可以给bean对象加注解

    @Scope("singleton"):单例的。默认就是单例的

    @Scope("prototype"):多例的。从容器里每次获取这个bean对象,Spring都将会创建一个新对象给我们

    其它取值:

    • request:一次请求内是同一个对象,不同请求会有新的
    • session:一次会话内是同一个对象,不同会话会有新的
    • application:服务端只要不关闭重启,就是同一个对象
  • 实际开发中:绝大多数情况下,都使用默认的单例

2.2 效果演示

@Scope效果

bean对象上加@Scope设置作用域

@Scope("singleton")
@RestController
public class DemoController {

    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
}

测试:从容器里多次获取相同的bean,如果是单例,获取的是同一个

@Test
public void testScope(){
    //第1次从容器里获取DemoController对象
    DemoController demo01 = app.getBean(DemoController.class);
    System.out.println("demo01 = " + demo01);

    //第2次从容器里获取DemoController对象
    DemoController demo02 = app.getBean(DemoController.class);
    System.out.println("demo02 = " + demo02);


    //如果bean对象上有@Scope("singleton")或没有此注解的默认情况下,结果是true,Spring是以单例模式维护bean对象的
    //如果bean对象上有@Scope("prototype"),结果是false,Spring将会以多例形式维护bean对象,每次获取时Spring都会创建新的对象
    System.out.println(demo01 == demo02);
}

@PostConstruct和@PreDestroy

作用:

  • @PostConstruct:加在方法上,方法就是在bean对象被创建成功之后执行的方法
  • @PreDestroy:加在方法上,方法就是在bean对象销毁之前执行的方法

注意:

  • 应用于非Lazy的单例bean对象上。

    如果单例bean上@Lazy

    • 表示让Spring不要一启动就创建此单例bean对象,而是晚一点,在第一次使用这bean时再创建
    • 创建之后,仍然以单例模式维护这个bean对象

使用场景:

  • 如果服务器一启动,就要做某些事情:在bean里增加一个方法,加@PostConstruct

    当服务器启动时,Springboot将会自动扫描所有的类,找到所有单例bean,马上创建对象放到容器里==>服务器一启动,就创建单例bean对象

  • 如果在服务器关闭时,有一些收尾的工作:在bean里增加一个方法,加@PreDestroy

    当服务器关闭时,SpringBoot将会先销毁容器里所有的单例bean对象。在销毁之前,会先调用每个bean对象的销毁方法

@Scope("singleton")
@RestController
public class DemoController {

    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }

    @PostConstruct
    public void init(){
        System.out.println("init,@PostConstructor,方法将会在当前bean对象创建成功之后自动执行");
    }

    @PreDestroy
    public void destroy(){
        System.out.println("destroy, @PreDestroy,方法将在bean对象销毁之前先执行一次");
    }
}

3. 依赖注入的注解

注解 说明 @Autowired byType注入。根据依赖项的类型,从容器里查找此类型的bean对象,注入进来 @Qualifier 需要在byType基础上使用。表示根据指定的名称,从容器里查找bean对象,注入进来 @Resource byName注入。根据依赖项的名称,从容器里查找此名称的bean对象,注入进来 @Value 通常用于注入简单值(基本数据类型及其包装类,和String)

二、SpringBoot配置参数

项目里边配置参数

有三种配置文件:

  • application.yaml
  • application.yml
  • application.properties

三种方式的使用:

  • 实际开发的时候,选中其中的一种格式使用。不要混用
  • 三种混用时的优先级:application.yaml < application.yml < application.properties

项目外修改参数

有两种方式

  • Java属性参数VM Options:在命令行里执行java命令时添加的参数。java -D参数名=值 -jar jar包名
  • 程序参数Programs arguments:在命令行里执行命令时,给引导类设置的参数。java -jar jar包名 --参数名=值

两种方式的使用:

  • VM Options:java -Dserver.port=8084 -jar jar包名称
  • Program Arguments:java -jar jar包名称 --server.port=8085
  • 两种都用的优先级是:VM Options < Program Arguments

在idea里启动时,也能够配置这两个参数:

  • 配置VM Options
  • 配置Program Arguments

image-20240505174839627.png

三、SpringBoot原理

SpringBoot有什么好处:

  • 提供了依赖版本锁定:只要项目里导入了父工程坐标,父工程会帮我们锁定一些常用依赖的版本号。依赖冲突的情况会减少

  • 提供了起步依赖:一个起步依赖,实际是一批功能相关的依赖的集合体

  • 提供了大量的默认配置:很多功能不需要加配置,使用默认值就可以正常运行。下面这个jar包里的这个文件,提供了默认值

    约定大于配置

image-20230827152634261.png

  • 提供了自动装配:整合其它框架变得非常轻松。很多时候,只要导入第三方框架的起步依赖,就自动整合好了

  • 内置Tomcat

1. SpringBoot父工程坐标

SpringBoot父工程坐标:里边已经提前帮我们锁定了大量常用依赖的版本号。有效减少了依赖冲突的机率

<!--SpringBoot的父工程坐标-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.3</version>
</parent>

我们在导入其它依赖的时候,如果这个依赖版本已经被SpringBoot提前锁定好了,就不需要我们再给设置版本号了

image-20230827154521096.png

2. SpringBoot起步依赖

我们在开发中,可能需要导入数十甚至数百个依赖,pom.xml文件可以要写数百行甚至数千行。

SpringBoot提供了起步依赖:一个起步依赖,就是一批相关功能的依赖集合体。

即:导入一个起步依赖,实际上导入了一批依赖包

  • SpringBoot官方提供的起步依赖,名称通常是:spring-boot-starter-xxx
  • 第三方技术自己提供的起步依赖,名称通常是:xxx-spring-boot-starter

3. SpringBoot自动装配

3.1 什么是自动装配

SpringBoot在整合其它框架时,自动装配功能可以有效的减少整合的难度和复杂度。让很多框架与SpringBoot的整合变得极其简单

整合后的效果:

  • 只要导入某框架的起步依赖,就可以直接使用这个框架了
  • 只要导入某框架的起步依赖,就可以直接从IoC容器里获取框架相关的bean对象了

3.2 自动装配的效果演示

以druid起步依赖为例:

  • 只要pom.xml里添加了druid的起步依赖,SpringBoot就会自动创建druid的连接池DruidDataSource对象放到IoC容器里

  • 我们可以直接注入使用druid连接池对象,而不用自己创建对象了

直接注入DruidDataSource对象的示例代码:

@SpringBootTest
public class DemoDruidStarterTest {
    /**注入Druid连接池对象*/
    @Autowired
    private DruidDataSource dataSource;

    @Test
    public void test(){
        //打印druid连接池对象。如果打印出来不是null,就说明IoC容器里已经有了Druid连接池对象。
        //但是这个对象并不是我们创建然后放到容器里的,那么 谁帮我们创建了Druid连接池对象,并放到IoC容器里的呢?SpringBoot
        System.out.println("dataSource = " + dataSource);
    }
}

3.3 自动装配的原理

启动过程

  1. 先运行引导类的main方法,main方法里执行的是:SpringApplication.run(引导类.class, args)
  2. SpringApplication.run(引导类.class, args)
    • 把引导类传递给SpringApplication类
    • SpringApplication创建了IoC容器对象
    • 开始解析引导类上注解@SpringBootApplication,注解开始生效
  3. @SpringBootApplication注解的作用:
    • @ComponentScan:引导类会自动扫描组件。默认扫描的是“引导类所在的包”
    • @SpringBootConfiguration:是个组合注解,实际上引用的是@Configuration,说明引导类也是个配置类
    • @EnableAutoConfigurutaion:要开启自动装配功能

自动装配原理 ★

  1. 引导类上@SpringBootApplication,实际上引用了@EnableAutoConfiguration

  2. @EnableAutoConfiguration使用的是@Import(AutoConfigurationImportSelector.class)

  3. AutoConfigurationImportSelector是ImportSelector接口的实现类:

    会重写接口的selectImports方法

    selectImports方法里:

    1. 会扫描所有类路径里的META-INF/spring.factories文件
    2. 读取文件里...EnableAutoConfiguration=类名1.class,类名2.class,类名3.class,...
    3. Spring然后找到这些类名对应的类
    4. 如果这些类上的@Conditionalxxxx注解满足条件,Spring就会创建对象放到IoC容器里
  4. 我们的代码里,就可以直接使用@Autowired注入需要用的bean对象了

四、SpringBoot自定义启动器

把阿里云的OSS工具,抽取到一个公用的模块里,把这个模块制作成一个启动器starter

实现步骤

  1. 创建一个maven工程,导入基本的jar包
  2. 导入工具类
  3. 创建属性类,用于读取springboot配置文件配置项,并封装到自身属性上
  4. 创建配置类,用于创建bean对象的
  5. 在配置类中还需要开启属性配置
  6. 在类路径下按照springboot的规范,创建META-INF/factories文件

image-20240505210135115.png

代码实现

1.创建一个maven工程,导入基本的jar包依赖

  <!--web层起步依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--阿里云OSS的依赖包-->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.15.1</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--可选的包,用于@ConfigurationProperties不爆红-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

2.导入工具类

/**
 * 阿里云 OSS 工具类
 */
@Data
public class AliOSSUtils {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    public AliOSSUtils(OssProperties ossProperties){
        endpoint = ossProperties.getEndpoint();
        accessKeyId = ossProperties.getAccessKeyId();
        accessKeySecret = ossProperties.getAccessKeySecret();
        bucketName = ossProperties.getBucketName();

    }

    /**
     * 实现上传图片到OSS
     */
    public String upload(MultipartFile file) throws IOException {
        // 获取上传的文件的输入流
        InputStream inputStream = file.getInputStream();
        // 获取原始文件名
        String originalFilename = file.getOriginalFilename();
        // 把上传到oss,并返回url路径
        return upload(inputStream, originalFilename);
    }

    /**
     * 上传文件到阿里云OSS
     * @param inputStream 文件的输入流,用于读取要上传的文件数据
     * @param filename 文件原始名称
     * @return 文件的url路径
     */
    public String upload(InputStream inputStream, String filename) throws IOException {
        //重命名文件,避免文件覆盖
        filename = UUID.randomUUID() + filename.substring(filename.lastIndexOf("."));

        //上传文件到 OSS
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        ossClient.putObject(bucketName, filename, inputStream);

        //文件访问路径
        String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + filename;
        //关闭ossClient
        ossClient.shutdown();
        return url;// 把上传到oss的路径返回
    }

}

3.创建属性类,用于读取springboot配置文件配置项,并封装到自身属性上

//读取配置文件并且封装为对象
@ConfigurationProperties(prefix = "oss")
@Data
public class OssProperties {
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}

4.创建配置类,用于创建bean对象的

5.在配置类中还需要开启属性配置

//OSS的自动配置类
@EnableConfigurationProperties(OssProperties.class)//开启属性配置把OssProperties拿过来
@Configuration
public class OssAutoConfiguration {
   /* @Autowired
    private  OssProperties ossProperties;
    */
    @Bean
    public AliOSSUtils aliOSSUtils(OssProperties ossProperties){
        return new AliOSSUtils(ossProperties);
    }
}

6.在类路径下按照springboot的规范,创建META-INF/factories文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.sdf.config.OssAutoConfiguration