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 的生命周期中进行介绍。


目录
相关文章
|
8天前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
21 2
|
1月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
54 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
1月前
|
NoSQL Java Redis
redis的基本命令,并用netty操作redis(不使用springboot或者spring框架)就单纯的用netty搞。
这篇文章介绍了Redis的基本命令,并展示了如何使用Netty框架直接与Redis服务器进行通信,包括设置Netty客户端、编写处理程序以及初始化Channel的完整示例代码。
46 1
redis的基本命令,并用netty操作redis(不使用springboot或者spring框架)就单纯的用netty搞。
|
1月前
|
Java 测试技术 开发者
springboot学习四:Spring Boot profile多环境配置、devtools热部署
这篇文章主要介绍了如何在Spring Boot中进行多环境配置以及如何整合DevTools实现热部署,以提高开发效率。
61 2
|
1月前
|
前端开发 Java 程序员
springboot 学习十五:Spring Boot 优雅的集成Swagger2、Knife4j
这篇文章是关于如何在Spring Boot项目中集成Swagger2和Knife4j来生成和美化API接口文档的详细教程。
100 1
|
1月前
|
Java API Spring
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
这篇文章是关于Spring Boot 2.x中拦截器的入门教程和实战项目场景实现的详细指南。
26 0
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
|
1月前
|
Java API Spring
springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
这篇文章是关于Spring Boot 2.x中过滤器的基础知识和实战项目应用的教程。
24 0
springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
|
1月前
|
Java 测试技术 Spring
springboot学习三:Spring Boot 配置文件语法、静态工具类读取配置文件、静态工具类读取配置文件
这篇文章介绍了Spring Boot中配置文件的语法、如何读取配置文件以及如何通过静态工具类读取配置文件。
54 0
springboot学习三:Spring Boot 配置文件语法、静态工具类读取配置文件、静态工具类读取配置文件
|
1月前
|
SQL Java 数据库
Springboot+spring-boot-starter-data-jdbc实现数据库的操作
本文介绍了如何使用Spring Boot的spring-boot-starter-data-jdbc依赖来操作数据库,包括添加依赖、配置数据库信息和编写基于JdbcTemplate的数据访问代码。
60 2
|
1月前
|
XML Java 应用服务中间件
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
177 2