1 springboot自定义starter

思考:

面试题: spring 和springboot什么关系?/springboot是什么?

springboot是基于spring框架 ,能够快速开发spring应用的脚手架.

面试题: 为什么springboot能够快速开发spring应用?

  • 简化配置

  • 约定大于配置

springboot扩展了大量自动配置逻辑,所以可以简化配置,所以提倡思想约定大于配置(不配置就可以默认,默认不了的不能用)

学习目标

  • 通过学习springboot自动配置 理解自动配置原理

    • 核心注解: 组合注解

    • 开启自动配注解: @EnableAutoConfiguration流程

  • 实现自定义starter

    • 项目名称**-spring-boot-starter

    • 根据功能需求编写自动配置类**AutoConfiguration

    • 根据META-INF/spring.factories格式 填写这个类名进去

    • 第三方其它项目只要依赖当前自动配置资源

1.1 准备一个测试项目

  • 创建一个项目 luban-spring-boot-starter

image-20230928101150032

  • 依赖 spring-boot-starter;junit

<dependencies>
    <dependency>
        <!--spring的绝大部分依赖传递 beans context core...-->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

1.2 springboot自动配置详解

1.2.1 Spring框架迭代历程

目前spring版本,使用的5.X

  • SPRING1.X 时代

大量编写xml配置文件的节点,spring框架开发应用程序,每个xml中都会使用大量bean标签,来实现

SPRING容器的IOC DI功能.不存在注解 @Autowired @Component @Service @Controller@Repository

  • SPRING2.X时代

java出现了jdk1.5,新特性注解,反射,枚举等功能.SPRING随之推出了基于java5的注解功能的新特性,IOC容器的注解,使得扫描注解能够构造bean对象,@Component @Service @Controller @Repository DI注入@Qualifier @Autowired.让在1.x时代编写大量的xml配置文件的工作减少了很多很多.

什么情况下使用注解:业务层使用注解(Controller Service)

什么情况下使用xml配置:引入的技术 redis,mysql,等使用xml配置

  • SPRING3.X 时代

基于java5的注解功能上,spring扩展了大量的功能注解,比如@Configuration @Bean

@ComponentScan @Import等等,他们可以让在2.x时代残留的那种xml配置,彻底的消失了,从xml配置完全转化成为代码注解的编写;

趋势: 配置越来越简单 springboot的出现打下了坚实的基础

  • SPRING4.X/5.X

都是在基于这个趋势,实现更多注解的扩展,让代码的功能变得更强,开发的效率变得更高,出现了很多组合注解,@RestController 4.X时代,spring提供了一个叫做条件注解的 @Conditional,springboot能够做到0 xml配置文件/约定大于配置 并不是springboot功劳. 本质是 spring的支持.

1.2.2 详解Spring 3.X注解

1.2.2.1 @Configuration

这个注解,是在Spring 3.x出现的一个核心的元数据注解,配置类就是元数据(spring应用程序在加载运行之前,必须要读取所有配置类才能正常运转).

这个注解所在的类,表示的是一个配置类,spring可以加载这种配置类,就像早期加载一个xml一样.

元数据: 描述数据的数据(描述数据的信息)

配置类是spring的元数据: spring管理的bean对象 bean对象的创建使用 参数 基本都是在配置类完成的.spring必须先加载配置类,才能获取这些bean对象.

  • 测试案例: 展示加载xml和加载配置类是相同的.

第一步:准备好一个xml配置文件

image-20230928104159096

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--这里的内容是空的-->
</beans>

第二步: spring的api读取xml运行一个spring容器(容器启动线程停止,容器就停止了)

//加载xml
@Test
public void loadXml(){
    ClassPathXmlApplicationContext context=
            new ClassPathXmlApplicationContext("demo01.xml");
}

第三步: 准备一个配置类,代替xml的加载.

package com.tarena.luban.spring.boot.config;
​
import org.springframework.context.annotation.Configuration;
​
/**
 * 表示这个类,不是一个普通的类
 * 而是spring可以加载的元数据配置类
 */
@Configuration
public class Demo01Conf {
}
//加载配置类
@Test
public void loadConfig(){
    AnnotationConfigApplicationContext context=
            new AnnotationConfigApplicationContext(
                    Demo01Conf.class);
}

只有配置类,需要代替xml的功能,xml能做的事,配置类也一定能做

1.2.2.1 @Bean

作用在一个配置的方法上,可以将方法的返回对象,加载到容器管理成bean.

对象id默认是方法名称.可以使用@Bean注解的属性name自定义id值.

配置类添加.

  • 测试案例: xml加载bean对象配置同样加载这个bean对象

第一步: 准备一个bean对象Bean01

package com.tarena.luban.spring.boot.beans;
​
public class Bean01 {
    public Bean01(){
        System.out.println("bean01被容器加载了");
    }
}

第二步: 在xml中配置加载

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--这里的内容是空的-->
    <bean id="bean01" class="com.tarena.luban.spring.boot.beans.Bean01"/>
</beans>

第三步: 配置类编写代码加载Bean01

@Configuration
public class Demo01Conf {
    @Bean
    public Bean01 bean01(){
        return new Bean01();
    }
}

第四步: 运行test

加载xml

加载配置类

Bean01对象最终效果一样 都在容器中管理(IOC)

1.2.2.3 @ComponentScan

spring 3.0 出现的,功能是配合一个配置类,扫描当前系统自定义的业务bean对象(@ Component,@Controller,@Repository,@Autowired,@Configuration...).

使用这个注解的时候提供一个basePackages的String[] 数据,定义扫描包范围.

不给指定,默认是当前配置类所在的包就是扫描范围.

  • 测试案例

第一步: 准备一个可以被扫描到的Bean02

package com.tarena.luban.spring.boot.beans;
​
import org.springframework.stereotype.Component;
​
@Component
public class Bean02 {
    public Bean02(){
        System.out.println("bean02被容器加载了");
    }
}

第二步: xml配置文件扫描Bean02所在的包

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd 
       http://www.springframework.org/schema/context 
       https://www.springframework.org/schema/context/spring-context.xsd">
    <!--这里的内容是空的-->
    <bean id="bean01" class="com.tarena.luban.spring.boot.beans.Bean01"/>
    <!--扫描标签 component-scan-->
    <context:component-scan base-package="com.tarena.luban.spring.boot.beans"/>
</beans>

第三步: 配置类扫描包

package com.tarena.luban.spring.boot.config;
​
import com.tarena.luban.spring.boot.beans.Bean01;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
​
/**
 * 表示这个类,不是一个普通的类
 * 而是spring可以加载的元数据配置类
 */
@Configuration
@ComponentScan(basePackages ={"com.tarena.luban.spring.boot.beans"} )
public class Demo01Conf {
    @Bean
    public Bean01 bean01(){
        return new Bean01();
    }
}

xml能做的配置类一样能做

2.2.2.4 @Import

对应的是xml配置的import标签.

spring容器,一般情况下只允许加载一个xml配置文件,或者一个配置类.

如果所有配置逻辑在这一个文件或类中完成.文件或者类内容 冗长,不易读.

spring允许将不同配置逻辑放到不同xml或者配置类中的.

可以使用import进行导入.

  • 测试案例

第一步: 准备一个Bean03

package com.tarena.luban.spring.boot.beans;
​
public class Bean03 {
    public Bean03(){
        System.out.println("bean03被容器加载了");
    }
}

第二步: 准备一个新的配置文件demo02.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd">
    <bean id="bean03" class="com.tarena.luban.spring.boot.beans.Bean03"/>
</beans>

第三步: demo01导入 demo02的xml

<!--import-->
<import resource="demo02.xml"/>

第四步: 准备一个新的配置类Demo02Conf

package com.tarena.luban.spring.boot.config;
​
import com.tarena.luban.spring.boot.beans.Bean03;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
@Configuration
public class Demo02Conf {
    @Bean
    public Bean03 bean03(){
        return new Bean03();
    }
}

第五步: 在Demo01Conf配置类中使用@Import注解导入Demo02Conf

@Configuration
@ComponentScan(basePackages ={"com.tarena.luban.spring.boot.beans"} )
@Import(Demo02Conf.class)
public class Demo01Conf {
    @Bean
    public Bean01 bean01(){
        return new Bean01();
    }
}

当前案例功能,指定导入具体的配置类. Import还可以导入一个Selector选择器.

通过选择器的选择逻辑 会返回一个String[] 数组 List<String>

[
    "org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration",
    "org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration",
    "..."
]

spring框架利用这组数据中类名称 通过反射技术加载 配置类. Selector选择器完成批量导入.

思考一下springboot可能实现的逻辑:

现象: 引入spring-boot-starter-web 引用 spring-boot-starter-data-redis依赖

没有过多的配置相关内容,但是springboot容器中 却产生了该功能的作用--自动配置

原因: springboot的大量配置类不是我们自定义编写的,是springboot提前准备好.

image-20230928114828785

1.2.2.5 @PropertySource

可以配合配置类使用,导入一个外部的properties配置文件.

1.2.2.6 @ImportResource

虽然配置类可以代替xml使用,但是有的时候,场景环境中,需要同时存在配置类和xml的.所以这个注解可以配合配置类,加载一个外部的xml文件.

1.2.3 springboot条件衍生注解

思考问题: springboot2.5.9 提供了直接导入的131个配置类.

每个springboot进程 131个配置类,都需要使用么?

答案: 不是都要用到. 根据需求去加载(加载不加载是有条件限制的)

小提示: 在springboot应用中 application.yml 文件配置

logging:
    level:
        root: debug

debug级别输出日志,可以看到加载和不加载的配置类日志.

如果自定义了日志输出的logback.xml 需要配置debug级别

image-20230928140840539

image-20230928140913375

思考问题: springboot在导入这些配置类的时候,哪些需要用,哪些不需要用,是如何判断的?

1.2.3.1 背景@Conditional

spring 4.0版本 推出了条件注解@Conditional.

允许我们自定义 条件逻辑类. 以此为基础,编写条件代码不同,条件判断的逻辑就不同.

springboot基于这样的一个条件注解,生成了很多衍生的注解.比如

ConditionalOnClass

ConditionalOnMissingClass...

1.2.3.2 @ConditionalOnClass/@ConditionalOnMissingClass

这两个注解的条件逻辑相反,都是类和方法的注解.如果作用在类上,一般也是配置类.

这两个注解会根据条件属性,判断某个各,某几个指定的类是否存在于当前依赖环境.

存在或不存在是满足条件的前提,如果满足条件,对应的类或者方法才会选择加载或者不加载. 一个配置类中代码是否需要加载由这些条件注解判断.

  • 测试案例

第一步: 准备一个配置类 ConditionalDemo01Conf

package com.tarena.luban.spring.boot.config.conditional;
package com.tarena.luban.spring.boot.config.conditional;
​
import com.tarena.luban.spring.boot.beans.Bean03;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
@Configuration
/**在条件注解中指定的类如果在当前依赖环境存在 条件则满足
如果满足 配置类才会被加载,不满足则不加载*/
//案例1 条件满足 所以加载
//@ConditionalOnClass(name={"com.tarena.luban.spring.boot.beans.Bean01"})
//案例2 条件不满足 所以扫描到 也不会加载这个配置类
//@ConditionalOnClass(name={"com.tarena.luban.spring.boot.beans.Bean07"})
//存在类则不满足 条件 不存在类则满足条件
@ConditionalOnMissingClass(value={"com.tarena.luban.spring.boot"})
public class ConditionalDemo01Conf {
    public ConditionalDemo01Conf() {
        System.out.println("条件配置类ConditionalDemo01Conf条件满足");
    }
}

第二步: 通过扫描的方式 加载这个新的配置类

但是配置类是否生效,取决于条件是否满足.

package com.tarena.luban.spring.boot.config;
​
import com.tarena.luban.spring.boot.beans.Bean01;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
​
/**
 * 表示这个类,不是一个普通的类
 * 而是spring可以加载的元数据配置类
 */
@Configuration
@ComponentScan(basePackages =
        {"com.tarena.luban.spring.boot.beans",
        "com.tarena.luban.spring.boot.config.conditional"} )
@Import(Demo02Conf.class)
public class Demo01Conf {
    @Bean
    public Bean01 bean01(){
        return new Bean01();
    }
}

上述两步测试,最终结果是,条件配置类条件注解逻辑是满足的,匹配结果是true 配置类是加载的使用的.

image-20230928142216221

1.2.3.3 @ConditionalOnBean/@ConditionalOnMissingBean

结论:

@ConditionalOnBean : 指定某个类的bean对象在容器中存在 ,条件则满足

@ConditionalOnMissingBean: 指定某个类的bean对象在容器中不存在,条件则满足

  • 测试案例

第一步: ConditionalDemo02Conf

package com.tarena.luban.spring.boot.config.conditional;
​
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Configuration;
​
@Configuration
/**
 * 根据指定的bean对象存在或不存在判断条件是否满足的
 */
//案例1 Bean02 对象在容器存在 条件满足的 所以配置扫描到之后会加载使用
//@ConditionalOnBean(type={"com.tarena.luban.spring.boot.beans.Bean02"})
//案例2 Bean07 根本不存在 所以bean也不存在 条件不满足
@ConditionalOnBean(type={"com.tarena.luban.spring.boot.beans.Bean07"})
//案例3 Bean07 不存在bean 条件满足
//@ConditionalOnMissingBean(type={"com.tarena.luban.spring.boot.beans.Bean07"})
public class ConditionalDemo02Conf {
    public ConditionalDemo02Conf() {
        System.out.println("条件配置类ConditionalDemo02Conf条件满足");
    }
}

1.2.3.4 @ConditionalOnProperty

类和方法的注解,可以根据当前环境变量(各种yaml properties属性)

判断最终条件是否满足.指定各种和属性有关的条件逻辑.

  • 测试案例

第一步: ConditionalDemo03Conf

@ConditionalOnProperty 注解属性详解

  1. value==name: 这两个属性完全相同,不能同时在注解中出现. 属性值表示环境变量的属性名称,例如: user.email=aaa@qq.com value="email"; spring.datasource.url=jdbc:mysql:///luban-db value="url";

  2. prefix: 环境变量前缀名称 prefix.value 才是整个环境变量的全名称.例如: user.email=aaa@qq.com value="email",prefix="user".

    spring.datasource.url=jdbc:mysql:///luban-db value="url",prefix="spring.datasource"

  3. havingValue: 提供一个字符串,判断prefix.value的值是否等于当前提供的havingValue的值.例如: name=1234, havingValue=123.判断结果是false.

  4. matchIfMissing: 如果环境变量中没有prefix.value的属性存在 最终结果是匹配还是不匹配. true匹配 false就是不匹配.

package com.tarena.luban.spring.boot.config.conditional;
​
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
​
@Configuration
//案例1 判断 当前环境变量是否存在一个user.email=123的数据 不存在,或者不等于123 条件都不满足
@ConditionalOnProperty(prefix="user",value="email",havingValue ="123")
public class ConditionalDemo03Conf {
    public ConditionalDemo03Conf() {
        System.out.println("条件配置类ConditionalDemo03Conf条件满足");
    }
}

第二步: 在入口配置类 读取一个test.properties配置文件 修改Demo01Conf

@Configuration
@ComponentScan(basePackages =
        {"com.tarena.luban.spring.boot.beans",
        "com.tarena.luban.spring.boot.config.conditional"} )
@Import(Demo02Conf.class)
@PropertySource("test.properties")
public class Demo01Conf {
    @Bean
    public Bean01 bean01(){
        return new Bean01();
    }
}

第三步: 准备一个test.properties

image-20230928144951336

1.2.3.5 阅读理解

阅读一下 在springboot中提供的 自动配置类(随机抽选几个)

RepositoryRestMvcAutoConfiguration
GroovyTemplateAutoConfiguration
HypermediaAutoConfiguration
HazelcastJpaDependencyAutoConfiguration
ProjectInfoAutoConfiguration

1.3 springboot自动配置流程(原理)

1.3.1 核心注解@SpringBootApplicaiton

在每个启动类中,都存在这个注解.

组合了三个注解

@SpringBootConfiguration: 标识一个类是配置类. 启动类就是入口的配置类

@ComponentScan : 配合配置类使用@Configuration 默认扫描当前配置类所在包.

如果我们不在启动类上 重新扫描,默认扫描的就是启动类的包.

@EnableAutoConfiguration: 导入功能Import 导入了一批配置类.

1.3.2 spring.factories

通过对AutoConfigurationImportSelector.getAutoConfigurationEntry断点,发现131个springboot自动配置类被导入,还存在35个其它包提供的自动配置类,包括knife4j nacos spring-cloud等.这些不是springboot写得自动配置,是如何加载到当前的路程中的.

springboot 加载逻辑中 使用SPI加载的方式. 在/META-INF/spring.factories文件,提供key-value键值对 表明key值是EnableAutoConfiguration的注解的名字,value就可以是第三方提供的自动配置类全路径名称.

1.4 第三方starter的编写

1.4.1 需求

luban-spring-boot-starter

提供一个自动配置类 UserAutoConfiguration

开启的条件 在使用我当前starter的应用程序中,必须提供一个 user.enable的属性 值必须是true.

自动配置类才会生效,生效后,会创建一个User对象.

1.4.2 完成步骤

  • 第一步: 创建一个自动配置类

package cn.tedu.luban.starter;
​
import cn.tedu.luban.starter.user.User;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
@Configuration
@ConditionalOnProperty(prefix = "user",value="enable",havingValue = "true",matchIfMissing = false)
public class UserAutoConfiguration {
    @Bean(name = "user")
    //如果容器中存在User的bean对象 方法不加载了
    @ConditionalOnMissingBean(value={User.class})
    public User initUser(){
        User user=new User();
        user.setUsername("王翠花");
        return user;
    }
}

第二步: 准备User类

package cn.tedu.luban.starter.user;
​
import lombok.Data;
​
@Data
public class User {
    private String username;
}

思考: 我们写完的自动配置类,会不会在cart-main应用程序中加载?

假设 cart-main中使用这个luban-spring-boot-starter

第三步: cart-main依赖这个luban-spring-boot-starter

尝试在依赖之后,是否当前UserAutoConfiguration就会被加载?

第四步: 将luban-spring-boot-starter中的自动配置 放到springboot自动加载流程

resources 文件夹里准备 /META-INF/spring.factories文件

image-20230928154939908

在spring.factories中配置一个key-value键值对

key值就是 自动配置的注解@EnableAutoConfiguration 注解类全路径名称

value值就是当前项目想要添加到自动配置流程中的自定义UserAutoConfiguration 全路径名称

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.tedu.luban.starter.UserAutoConfiguration

第五步: 通过了解的条件 指定是否使用UserAutoConfiguration 指定是否使用自动配置的User对象.

2 总结springboot自动加载流程

image-20230928163848291