1 springboot自定义starter {#heading-1}
思考:
面试题: spring 和springboot什么关系?/springboot是什么?
springboot是基于spring框架 ,能够快速开发spring应用的脚手架 .
面试题: 为什么springboot能够快速开发spring应用?
-
简化配置
-
约定大于配置
springboot扩展了大量自动配置 逻辑,所以可以简化配置,所以提倡思想约定大于配置(不配置就可以默认,默认不了的不能用 )
学习目标
-
通过学习springboot自动配置 理解自动配置原理
-
核心注解: 组合注解
-
开启自动配注解: @EnableAutoConfiguration流程
-
-
实现自定义starter
-
项目名称**-spring-boot-starter
-
根据功能需求编写自动配置类**AutoConfiguration
-
根据META-INF/spring.factories格式 填写这个类名进去
-
第三方其它项目只要依赖当前自动配置资源
-
1.1 准备一个测试项目 {#heading-2}
- 创建一个项目 luban-spring-boot-starter
-
依赖 spring-boot-starter;junit
org.springframework.boot spring-boot-starter compile junit junit test
1.2 springboot自动配置详解 {#heading-3}
1.2.1 Spring框架迭代历程 {#heading-4}
目前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注解 {#heading-5}
1.2.2.1 @Configuration {#heading-6}
这个注解,是在Spring 3.x出现的一个核心的元数据 注解,配置类就是元数据(spring应用程序在加载运行之前,必须要读取所有配置类才能正常运转).
这个注解所在的类,表示的是一个配置类,spring可以加载这种配置类,就像早期加载一个xml一样.
元数据: 描述数据的数据(描述数据的信息)
配置类是spring的元数据: spring管理的bean对象 bean对象的创建使用 参数 基本都是在配置类完成的.spring必须先加载配置类,才能获取这些bean对象.
- 测试案例: 展示加载xml和加载配置类是相同的.
第一步 :准备好一个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">
<!--这里的内容是空的-->
</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 {#heading-7}
作用在一个配置的方法上,可以将方法的返回对象,加载到容器管理成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 {#heading-8}
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 {#heading-9}
对应的是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提前准备好.
1.2.2.5 @PropertySource {#heading-10}
可以配合配置类使用 ,导入一个外部的properties配置文件.
1.2.2.6 @ImportResource {#heading-11}
虽然配置类可以代替xml使用,但是有的时候,场景环境中,需要同时存在配置类和xml的.所以这个注解可以配合配置类 ,加载一个外部的xml文件.
1.2.3 springboot条件衍生注解 {#heading-12}
思考问题: springboot2.5.9 提供了直接导入的131个配置类.
每个springboot进程 131个配置类,都需要使用么?
答案: 不是都要用到. 根据需求去加载(加载不加载是有条件限制的)
小提示: 在springboot应用中 application.yml 文件配置
logging:
level:
root: debug
debug级别输出日志,可以看到加载和不加载的配置类日志.
如果自定义了日志输出的logback.xml 需要配置debug级别
思考问题: springboot在导入这些配置类的时候,哪些需要用,哪些不需要用,是如何判断的?
1.2.3.1 背景@Conditional {#heading-13}
spring 4.0版本 推出了条件注解@Conditional.
允许我们自定义 条件逻辑类. 以此为基础,编写条件代码不同,条件判断的逻辑就不同.
springboot基于这样的一个条件注解,生成了很多衍生的注解.比如
ConditionalOnClass
ConditionalOnMissingClass ...
1.2.3.2 @ConditionalOnClass/@ConditionalOnMissingClass {#heading-14}
这两个注解的条件逻辑相反,都是类和方法的注解.如果作用在类上,一般也是配置类.
这两个注解会根据条件属性,判断某个各,某几个指定的类是否存在于当前依赖环境.
存在或不存在是满足条件的前提,如果满足条件,对应的类或者方法才会选择加载或者不加载. 一个配置类中代码是否需要加载由这些条件注解判断 .
- 测试案例
第一步: 准备一个配置类 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 配置类是加载的使用的.
1.2.3.3 @ConditionalOnBean/@ConditionalOnMissingBean {#heading-15}
结论:
@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 {#heading-16}
类和方法的注解,可以根据当前环境变量(各种yaml properties属性)
判断最终条件是否满足.指定各种和属性有关的条件逻辑.
- 测试案例
第一步 : ConditionalDemo03Conf
@ConditionalOnProperty 注解属性详解
-
value==name: 这两个属性完全相同,不能同时在注解中出现. 属性值表示环境变量的属性名称,例如: user.email =aaa@qq.com value="email"; spring.datasource.url =jdbc:mysql:///luban-db value="url";
-
prefix: 环境变量前缀名称 prefix.value 才是整个环境变量的全名称.例如: user.email =aaa@qq.com value="email",prefix="user".
spring.datasource.url =jdbc:mysql:///luban-db value="url",prefix="spring.datasource"
-
havingValue: 提供一个字符串,判断prefix.value的值是否等于当前提供的havingValue的值.例如: name=1234, havingValue=123.判断结果是false.
-
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
1.2.3.5 阅读理解 {#heading-17}
阅读一下 在springboot中提供的 自动配置类(随机抽选几个)
RepositoryRestMvcAutoConfiguration
GroovyTemplateAutoConfiguration
HypermediaAutoConfiguration
HazelcastJpaDependencyAutoConfiguration
ProjectInfoAutoConfiguration
1.3 springboot自动配置流程(原理) {#heading-18}
1.3.1 核心注解@SpringBootApplicaiton {#heading-19}
在每个启动类中,都存在这个注解.
组合了三个注解
@SpringBootConfiguration : 标识一个类是配置类. 启动类就是入口的配置类
@ComponentScan : 配合配置类使用@Configuration 默认扫描当前配置类所在包.
如果我们不在启动类上 重新扫描,默认扫描的就是启动类的包.
@EnableAutoConfiguration : 导入功能Import 导入了一批配置类.
1.3.2 spring.factories {#heading-20}
通过对AutoConfigurationImportSelector.getAutoConfigurationEntry断点,发现131个springboot自动配置类被导入,还存在35个其它包提供的自动配置类,包括knife4j nacos spring-cloud等.这些不是springboot写得自动配置,是如何加载到当前的路程中的.
springboot 加载逻辑中 使用SPI加载的方式. 在**/META-INF/** spring.factories文件,提供key-value键值对 表明key值是EnableAutoConfiguration的注解的名字,value就可以是第三方提供的自动配置类全路径名称.
1.4 第三方starter的编写 {#heading-21}
1.4.1 需求 {#heading-22}
luban-spring-boot-starter
提供一个自动配置类 UserAutoConfiguration
开启的条件 在使用我当前starter的应用程序中,必须提供一个 user.enable的属性 值必须是true.
自动配置类才会生效,生效后,会创建一个User对象.
1.4.2 完成步骤 {#heading-23}
-
第一步: 创建一个自动配置类
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文件
在spring.factories中配置一个key-value键值对
key值就是 自动配置的注解@EnableAutoConfiguration 注解类全路径名称
value值就是当前项目想要添加到自动配置流程中的自定义UserAutoConfiguration 全路径名称
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.tedu.luban.starter.UserAutoConfiguration
第五步 : 通过了解的条件 指定是否使用UserAutoConfiguration 指定是否使用自动配置的User对象.