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


目录
相关文章
|
27天前
|
NoSQL Java 关系型数据库
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 介绍
本文介绍在 Spring Boot 中集成 Redis 的方法。Redis 是一种支持多种数据结构的非关系型数据库(NoSQL),具备高并发、高性能和灵活扩展的特点,适用于缓存、实时数据分析等场景。其数据以键值对形式存储,支持字符串、哈希、列表、集合等类型。通过将 Redis 与 Mysql 集群结合使用,可实现数据同步,提升系统稳定性。例如,在网站架构中优先从 Redis 获取数据,故障时回退至 Mysql,确保服务不中断。
99 0
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 介绍
|
27天前
|
安全 Java Apache
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 身份和权限认证
本文介绍了 Apache Shiro 的身份认证与权限认证机制。在身份认证部分,分析了 Shiro 的认证流程,包括应用程序调用 `Subject.login(token)` 方法、SecurityManager 接管认证以及通过 Realm 进行具体的安全验证。权限认证部分阐述了权限(permission)、角色(role)和用户(user)三者的关系,其中用户可拥有多个角色,角色则对应不同的权限组合,例如普通用户仅能查看或添加信息,而管理员可执行所有操作。
67 0
|
27天前
|
安全 Java 数据安全/隐私保护
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 三大核心组件
本课程介绍如何在Spring Boot中集成Shiro框架,主要讲解Shiro的认证与授权功能。Shiro是一个简单易用的Java安全框架,用于认证、授权、加密和会话管理等。其核心组件包括Subject(认证主体)、SecurityManager(安全管理员)和Realm(域)。Subject负责身份认证,包含Principals(身份)和Credentials(凭证);SecurityManager是架构核心,协调内部组件运作;Realm则是连接Shiro与应用数据的桥梁,用于访问用户账户及权限信息。通过学习,您将掌握Shiro的基本原理及其在项目中的应用。
75 0
|
27天前
|
Java 数据安全/隐私保护 微服务
微服务——SpringBoot使用归纳——Spring Boot中使用监听器——Spring Boot中自定义事件监听
本文介绍了在Spring Boot中实现自定义事件监听的完整流程。首先通过继承`ApplicationEvent`创建自定义事件,例如包含用户数据的`MyEvent`。接着,实现`ApplicationListener`接口构建监听器,用于捕获并处理事件。最后,在服务层通过`ApplicationContext`发布事件,触发监听器执行相应逻辑。文章结合微服务场景,展示了如何在微服务A处理完逻辑后通知微服务B,具有很强的实战意义。
55 0
|
27天前
|
缓存 Java 数据库
微服务——SpringBoot使用归纳——Spring Boot中使用监听器——监听器介绍和使用
本文介绍了在Spring Boot中使用监听器的方法。首先讲解了Web监听器的概念,即通过监听特定事件(如ServletContext、HttpSession和ServletRequest的创建与销毁)实现监控和处理逻辑。接着详细说明了三种实际应用场景:1) 监听Servlet上下文对象以初始化缓存数据;2) 监听HTTP会话Session对象统计在线用户数;3) 监听客户端请求的Servlet Request对象获取访问信息。每种场景均配有代码示例,帮助开发者理解并应用监听器功能。
52 0
|
27天前
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——常见问题总结
本文总结了Spring Boot中使用事务的常见问题,虽然通过`@Transactional`注解可以轻松实现事务管理,但在实际项目中仍有许多潜在坑点。文章详细分析了三个典型问题:1) 异常未被捕获导致事务未回滚,需明确指定`rollbackFor`属性;2) 异常被try-catch“吃掉”,应避免在事务方法中直接处理异常;3) 事务范围与锁范围不一致引发并发问题,建议调整锁策略以覆盖事务范围。这些问题看似简单,但一旦发生,排查难度较大,因此开发时需格外留意。最后,文章提供了课程源代码下载地址,供读者实践参考。
38 0
|
27天前
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——Spring Boot 事务配置
本文介绍了 Spring Boot 中的事务配置与使用方法。首先需要导入 MySQL 依赖,Spring Boot 会自动注入 `DataSourceTransactionManager`,无需额外配置即可通过 `@Transactional` 注解实现事务管理。接着通过创建一个用户插入功能的示例,展示了如何在 Service 层手动抛出异常以测试事务回滚机制。测试结果表明,数据库中未新增记录,证明事务已成功回滚。此过程简单高效,适合日常开发需求。
76 0
|
27天前
|
Java 数据库 微服务
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——事务相关
本文介绍Spring Boot事务配置管理,阐述事务在企业应用开发中的重要性。事务确保数据操作可靠,任一异常均可回滚至初始状态,如转账、购票等场景需全流程执行成功才算完成。同时,事务管理在Spring Boot的service层广泛应用,但根据实际需求也可能存在无需事务的情况,例如独立数据插入操作。
26 0
|
27天前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于 xml 的整合
本教程介绍了基于XML的MyBatis整合方式。首先在`application.yml`中配置XML路径,如`classpath:mapper/*.xml`,然后创建`UserMapper.xml`文件定义SQL映射,包括`resultMap`和查询语句。通过设置`namespace`关联Mapper接口,实现如`getUserByName`的方法。Controller层调用Service完成测试,访问`/getUserByName/{name}`即可返回用户信息。为简化Mapper扫描,推荐在Spring Boot启动类用`@MapperScan`注解指定包路径避免逐个添加`@Mapper`
50 0
|
27天前
|
消息中间件 存储 Java
微服务——SpringBoot使用归纳——Spring Boot中集成ActiveMQ——ActiveMQ安装
本教程介绍ActiveMQ的安装与基本使用。首先从官网下载apache-activemq-5.15.3版本,解压后即可完成安装,非常便捷。启动时进入解压目录下的bin文件夹,根据系统选择win32或win64,运行activemq.bat启动服务。通过浏览器访问`http://127.0.0.1:8161/admin/`可进入管理界面,默认用户名密码为admin/admin。ActiveMQ支持两种消息模式:点对点(Queue)和发布/订阅(Topic)。前者确保每条消息仅被一个消费者消费,后者允许多个消费者同时接收相同消息。
64 0
微服务——SpringBoot使用归纳——Spring Boot中集成ActiveMQ——ActiveMQ安装