Spring Boot 自动装配:创建自己的 spring-boot-starter

简介: 前言上篇介绍了 Spring Boot 的自动装配机制,个人认为理解自动装配主要有两个作用,一个是应付面试,另一个是只有理解它才能更好的使用它,通过 SPI 机制用户可以轻松自定义自己的自动装配。自动装配常与 spring-boot-starter 结合到一起,当为公司开发内部使用的通用框架,或者做开源项目时,经常会自定义 spring-boot-starter。

前言


上篇介绍了 Spring Boot 的自动装配机制,个人认为理解自动装配主要有两个作用,一个是应付面试,另一个是只有理解它才能更好的使用它,通过 SPI 机制用户可以轻松自定义自己的自动装配。自动装配常与 spring-boot-starter 结合到一起,当为公司开发内部使用的通用框架,或者做开源项目时,经常会自定义 spring-boot-starter。


再谈自动装配 SPI 机制


在底层,自动装配是通过标准的 @Configuration 类实现的,那么就需要一种机制发现这个 @Configuration 类,这种机制就是 SPI,即 Service Provider Interface。


Spring 的 SPI 机制其实也不是它的首创,例如 Java JDBC 就通过 SPI 机制查找 /META-INF/services/java.sql.Driver 文件中的驱动实现,Servlet 规范中容器在启动时回调类路径下的 SpringServletContainerInitializer 接口方法。


SPI 机制在 Spring Framework 中使用极少,直到 Spring Boot 才将其发扬光大。当使用注解 @EnableAutoConfiguration 激活自动装配后,/META-INF/spring.factories 文件中的配置类随即被装载。例如想要定义自己的配置类,可以在该文件中写入下述内容。


org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.zzuhkp.autonconfigure.MyCustomAutoConfiguration


自动装配模块划分


其实理解自动装配的 SPI 机制后就能自定义自动装配了,为什么还需要 spring-boot-starter 呢?


这是因为自动装配模块可能会可选的依赖一些第三方的 jar,当这些 jar 包的某个类存在于类路径中时才会条件化的注册某些 bean,因此 spring-boot-starter 的作用更多的是引入这些可选的依赖 jar,然后启用自动装配模块中某些特性。


具体来说,一个完整的 spring-boot-starter 由以下模块组成:


包含自动配置代码的 autoconfigure 模块。

依赖 autoconfigure 模块并提供附加依赖的 starter 模块。

当然了划分两个模块也并非强制,如果业务逻辑比较简单,将 autoconfigure 和 starter 这两个模块合并到一个 starter 模块也可以。


自动装配命名规范


在正式开发一个 spring-boot-starter 之前我们还需要了解一些命名规范,这些命名规范或为约定俗成,或为官方强制要求。


1. 类名规范


这里的类名规范主要指的是自动配置类,Spring 官方也没有对自动配置类指定命名规范,不过通过观察 Spring 官方的自动配置可以可以发现一些规律。


image.png


可以看到,所有的自动配置类都遵循 *AutoConfiguration 模式,这种命名方式无论在 Spring Cloud 还是第三方整合,都得到了体现,建议遵循这种命名方式。


2. 包名规范


同样,Spring 官方也没有对自动配置类的包名做强制要求,不过通过观察上面的配置类,同样可以看出,配置类的包名遵循 {root-package}.{module-package}.autoconfigure 的模式,建议大家遵循。


3. 模块名规范


自动装配的模块通常分为 autoconfigure 和 starter。


对于 autoconfigure 模块的命名方式 Spring 官方并未强制,通常来说为 {module-name}-autoconfigure。但是对于 starter 模块来说,Spring 官方 starter 的命名方式为 spring-boot-starter-{module-name},官方要求用户自定义的命名方式不要和官方保持一致,而是使用 {module-name}-spring-boot-starter 的形式,依此来和官方 starter 做区分。


4. 命名空间


有时候我们定义的 starter 可能会用到一些 Environment 中的属性,这些属性通常会有一个公共的前缀,这个前缀被 Spring 官方称为命名空间。Spring 官方强烈要求用户不应该使用 Spring Boot 内置的命名空间,如 server、management、spring,Spring 升级时很有可能修改这些内置的命名空间。


创建自己的 spring-boot-starter

有了上面的理论基础之后,我们就可以尝试实现自己的 spring-boot-starter,这里假定我们想要开发一个格式化对象为字符串的功能模块,由于功能比较简单,我们将其合并到一个 starter 模块中。


注意,本文所使用的的 Spring Boot 版本均为 2.2.7.RELEASE。


首先创建 spring-boot-starter-format 模块,pom 文件内容如下。


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.7.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.zzuhkp</groupId>
    <artifactId>spring-boot-starter-format</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>


注意,我们自定义的 starter 使用 optional 指定了依赖的 spring-boot-starter 为可选的,以避免依赖传递。

循序面向接口编程,定义我们的格式化接口如下。


public interface Formater {
    String format(Object obj);   
}


然后再为这个接口创建一个默认的实现类。


public class DefaultFormater implements Formater {
    @Override
    public String format(Object obj) {
        return String.valueOf(obj);
    }
}


我们希望能够将 Formater 注册为 bean,以便用户可以直接注入使用,自定义配置类如下。


@Configuration
public class FormatAutoConfiguration {
    @Bean
    public Formater defaultFormater() {
        return new DefaultFormater();
    }
}


为了能够让 Spring Boot 发现这个配置类,我们将其添加到 /META-INF/spring.factories 文件中。


org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.zzuhkp.format.autoconfigure.FormatAutoConfiguration


至此,一个最简单的 starter 就创建完成了,创建一个测试项目,然后引入

我们自定义的 starter,测试代码如下。


@SpringBootApplication
public class FormatTestApplication implements CommandLineRunner {
    @Autowired
    private Formater formater;
    public static void main(String[] args) {
        SpringApplication.run(FormatTestApplication.class, args);
    }
    @Override
    public void run(String... args) throws Exception {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "zzuhkp");
        System.out.println(formater.format(map));
    }
}

项目启动后可以看到控制台打印出 {name=zzuhkp},表明我们自定义的 starter 已生效。


Spring Boot 中的 @Conditional

不过通过 SPI 机制发现配置类,只能为自动装配提供最基础的功能,Spring Boot 自动装配之所以比较灵活还要依托于 Spring Framework 的条件化装配。


Spring Boot 封装了一些常见的 @Conditional 供自动装配使用,并将其命名为 @ConditionalOn*,下面结合这些 @Conditional 对上述的示例进行改造。


类条件注解


Spring Boot 提供了 @ConditionalOnClass 与 ConditionalOnMissingClass 两个注解允许根据类是否在类路径存在,来决定是否注册 bean。由于 Spring Framework 使用 ASM 直接读取 class 而无需将类加载到 JVM,因此即便给定类不存在也不会抛出异常。


这两个注解的属性如下表所示。


image.png


我个人比较喜欢使用阿里的 fastjson 将对象转换为字符串,假定我们希望类路径下存在 fastjson 的 JSON 类时使用 fastjson 来格式化对象,我们可以先引入 fastjson 的依赖。


<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.80</version>
    <optional>true</optional>
</dependency>


然后修改我们的配置类。


@Configuration
public class FormatAutoConfiguration {
    @Bean
    @ConditionalOnMissingClass("com.alibaba.fastjson.JSON")
    public Formater defaultFormater() {
        return new DefaultFormater();
    }
    @Bean
    @ConditionalOnClass(JSON.class)
    public Formater fastjsonFormater() {
        return JSON::toJSONString;
    }
}


这里利用 @ConditionalOnClass 与 @ConditionalOnMissingClass 的互斥性,当 JSON 存在时使用 fastjson 格式化对象,不存在时使用默认的 Formater 格式化对象。


将测试项目引入 fastjson,然后再次运行,控制台打印如下。


{"name":"zzuhkp"}

引入 fastjson 之后成功将 Formater 切换为使用 fastjson 格式化对象。


bean 条件注解


bean 条件注解允许用户控制当哪些 bean 存在或不存在时才注册用户自定义的 bean,对应的两个注解是 @ConditionalOnBean 和 @ConditionalOnMissingBean,这两个注解比类条件注解稍复杂一些,多数属性相同,其属性如下。


image.png


对于我们默认的 Formater,我们希望不满足用户需求时用户可以自定义,那么我们就可以使用 bean 条件注解,当用户未定义时使用默认的配置,修改配置类如下。


@Configuration
public class FormatAutoConfiguration {
    @Bean
    @ConditionalOnMissingClass("com.alibaba.fastjson.JSON")
    @ConditionalOnMissingBean(Formater.class)
    public Formater defaultFormater() {
        return new DefaultFormater();
    }
    @Bean
    @ConditionalOnClass(JSON.class)
    @ConditionalOnMissingBean(Formater.class)
    public Formater fastjsonFormater() {
        return JSON::toJSONString;
    }
}


这下我们的配置类更复杂了,在测试项目中注册 Formater


    @Bean
    public Formater formater() {
        return obj -> "自定义 Formater:" + obj;
    }


然后再次运行,控制台打印如下。


自定义 Formater:{name=zzuhkp}


说明 @ConditionalOnMissingBean 条件注解已生效。


属性条件注解


属性条件注解允许当 Environment 中的某些属性存在并且为指定值时才注册 bean,属性条件注解在 starter 中使用也比较多,对应的注解是 @ConditionalOnProperty,其属性如下。


image.png


属性条件注解常用于控制是否开启 starter 中的某种特性,假定我们希望环境变量中存在 format.eanble 并且值为 true 中才启用时,我们可以修改配置类如下。


@Configuration
@ConditionalOnProperty(prefix = "format", name = "enable", havingValue = "true")
public class FormatAutoConfiguration {
}


去掉测试项目中自定义的 Formater bean,重新启动测试项目,由于没有配置对应属性,可以看到控制台报出如下错误。


Description:
Field formater in com.zzuhkp.format.FormatTestApplication required a bean of type 'com.zzuhkp.format.formater.Formater' that could not be found.
The injection point has the following annotations:
  - @org.springframework.beans.factory.annotation.Autowired(required=true)
The following candidates were found but could not be injected:
  - Bean method 'defaultFormater' in 'FormatAutoConfiguration' not loaded because @ConditionalOnProperty (format.enable=true) did not find property 'enable'
  - Bean method 'fastjsonFormater' in 'FormatAutoConfiguration' not loaded because @ConditionalOnProperty (format.enable=true) did not find property 'enable'
Action:
Consider revisiting the entries above or defining a bean of type 'com.zzuhkp.format.formater.Formater' in your configuration.


错误提示我们有两个候选 Formater,但是由于没有配置 format.enable=true 属性,导致注入失败,在 application.properties 中配置 format.enable=true 再次运行项目可以看到项目正常运行。


如果我们想默认开启 Formater 怎么办呢?可以设置 matchIfMissing 为 true,代码如下。


@Configuration
@ConditionalOnProperty(prefix = "format", name = "enable", havingValue = "true", matchIfMissing = true)
public class FormatAutoConfiguration {
}


去掉 application.properties 配置再次运行则可以看到项目正常运行,如果想要关闭 Formater 特性,直接设置 format.enable=false 即可。


资源条件注解


除了上面类条件注解、bean 条件注解、属性条件注解,还有一些不太常用的条件注解。


首先是资源条件注解 @ConditionalOnResource,这个注解只有一个 String[] 类型的 resources 属性,表示资源的位置。关于 Spring 的资源管理,想要了解更多细节可以参考我之前写的 《Spring 资源管理 (Resource)》。


假定我们希望 application.properties 属性文件存在时才生效 Formater,可以在配置类上添加如下注解。


@Configuration
@ConditionalOnProperty(prefix = "format", name = "enable", havingValue = "true", matchIfMissing = true)
@ConditionalOnResource(resources = "application.properties")
public class FormatAutoConfiguration {
}


Web 应用条件注解


Web 应用条件注解用于判断运行环境是否为 Web,对应的注解为 @ConditionalOnWebApplication 和 @ConditionalOnNotWebApplication。假定希望 Formater 仅运行在 Servlet 环境,可以修改配置类如下。


@Configuration
@ConditionalOnProperty(prefix = "format", name = "enable", havingValue = "true", matchIfMissing = true)
@ConditionalOnResource(resources = "application.properties")
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class FormatAutoConfiguration {
}


Spring 表达式条件注解


属性条件注解诞生前多用于表达式条件注解判断属性值,对应的注解为 @ConditionalOnExpression,表达式的值为 true 时开启特性。使用表达式注解替换属性条件表达式如下。

@Configuration
@ConditionalOnProperty(prefix = "format", name = "enable", havingValue = "true", matchIfMissing = true)
@ConditionalOnResource(resources = "application.properties")
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnExpression("${format.enable:true}")
public class FormatAutoConfiguration {
}


自动装配元数据


为了加快判断条件装配,Spring Boot 读取到配置类之后会先读取 META-INF/spring-autoconfigure-metadata.properties 文件判断条件是否匹配,这个文件内部包含一些配置类的条件。为了生成这个这个配置类的元数据文件,可以在自定义 autoconfiger 或 starter 中加入如下的依赖。


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure-processor</artifactId>
    <optional>true</optional>
</dependency>


将这个依赖加入到我们的 starter,编译后可以看到生成如下的文件内容。


#Sun Apr 24 22:06:03 CST 2022
com.zzuhkp.format.autoconfigure.FormatAutoConfiguration.ConditionalOnWebApplication=SERVLET
com.zzuhkp.format.autoconfigure.FormatAutoConfiguration=


测试 spring-boot-starter


为了测试 spring-boot-starter,Spring Boot 官方提供了一个类 ApplicationContextRunner,在单元测试类中创建这个类的示例,然后调用里面的方法即可。示例如下。


private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(FormatAutoConfiguration.class));
@Test
void defaultServiceBacksOff() {        
 this.contextRunner.run((context) -> {
        assertThat(context).hasSingleBean(Formater.class);
        assertThat(context).getBean("defaultFormater").isSameAs(context.getBean(Formater.class));
    });
}


总结

Spring Boot 自动装配特性基于注解编程模型、条件装配、Spring SPI,这些功能均基于 Spring 应用上下文,在传统的 Spring Framework 项目中,应用上下文是由 Servlet 容器创建的,而 Spring Boot 时代则在应用上下文的生命周期中创建 Servlet 容器,Spring Boot 底层到底做了什么工作呢?后面将在 SpringApplication 的生命周期中进行介绍。


目录
相关文章
|
17天前
|
XML druid Java
Spring5系列学习文章分享---第二篇(IOC的bean管理factory+Bean作用域与生命周期+自动装配+基于注解管理+外部属性管理之druid)
Spring5系列学习文章分享---第二篇(IOC的bean管理factory+Bean作用域与生命周期+自动装配+基于注解管理+外部属性管理之druid)
25 0
|
21天前
|
Java Maven 开发工具
IDEA使用Spring Initializr流畅的创建springboot项目
IDEA使用Spring Initializr流畅的创建springboot项目
56 0
|
6天前
|
Java API 开发工具
Spring Boot与Spring Cloud Config的集成
Spring Boot与Spring Cloud Config的集成
|
12天前
|
消息中间件 负载均衡 Java
最容易学会的springboot gralde spring cloud 多模块微服务项目
最容易学会的springboot gralde spring cloud 多模块微服务项目
|
12天前
|
JavaScript Java 数据安全/隐私保护
基于SpringBoot+Vue毕业生信息招聘平台系统【源码+论文+演示视频+包运行成功】_基于spring vue的校园招聘系统源码(2)
基于SpringBoot+Vue毕业生信息招聘平台系统【源码+论文+演示视频+包运行成功】_基于spring vue的校园招聘系统源码
20 0
基于SpringBoot+Vue毕业生信息招聘平台系统【源码+论文+演示视频+包运行成功】_基于spring vue的校园招聘系统源码(2)
|
18天前
|
NoSQL 前端开发 Java
技术笔记:springboot分布式锁组件spring
技术笔记:springboot分布式锁组件spring
17 1
|
21天前
|
运维 Java 关系型数据库
Spring运维之boot项目bean属性的绑定读取与校验
Spring运维之boot项目bean属性的绑定读取与校验
23 2
|
21天前
|
存储 运维 Java
Spring运维之boot项目开发关键之日志操作以及用文件记录日志
Spring运维之boot项目开发关键之日志操作以及用文件记录日志
27 2
|
20天前
|
Java Maven
springboot项目打jar包后,如何部署到服务器
springboot项目打jar包后,如何部署到服务器
41 1
|
7天前
|
XML 前端开发 Java
Spring Boot与Spring MVC的区别和联系
Spring Boot与Spring MVC的区别和联系