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

您所在的位置:网站首页 springboot打包websocket自动装配 Spring Boot 自动装配:创建自己的 spring

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

2024-07-16 12:41| 来源: 网络整理| 查看: 265

前言

上篇介绍了 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 官方的自动配置可以可以发现一些规律。 在这里插入图片描述可以看到,所有的自动配置类都遵循 *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 文件内容如下。

4.0.0 org.springframework.boot spring-boot-starter-parent 2.2.7.RELEASE com.zzuhkp spring-boot-starter-format 1.0-SNAPSHOT org.springframework.boot spring-boot-starter true

注意,我们自定义的 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 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,因此即便给定类不存在也不会抛出异常。

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

注解属性类型属性方法属性说明@ConditionalOnClassClass[]value必须存在的类@ConditionalOnClassString[]name必须存在的类名@ConditionalOnMissingClassString[]value必须不存在的类名

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

com.alibaba fastjson 1.2.80 true

然后修改我们的配置类。

@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,这两个注解比类条件注解稍复杂一些,多数属性相同,其属性如下。

注解属性类型属性方法属性说明@ConditionalOnBean、@ConditionalOnMissingBeanClass[]valuebean 类型,parameterizedContainer 不为空时同时表示泛型类型@ConditionalOnBean、@ConditionalOnMissingBeanString[]namebean 类型,parameterizedContainer 不为空时同时表示泛型类型@ConditionalOnBean、@ConditionalOnMissingBeanClass[]parameterizedContainer带泛型的 bean 类型@ConditionalOnMissingBeanClass[]ignored忽略的 bean 类型@ConditionalOnMissingBeanString[]ignoredType忽略的 bean 类型

对于我们默认的 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,其属性如下。

属性类型属性方法属性说明Stringprefix属性前缀String[]value属性名称String[]name属性名称StringhavingValue属性值booleanmatchIfMissing属性不存在时是否匹配

属性条件注解常用于控制是否开启 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 中加入如下的依赖。

org.springframework.boot spring-boot-autoconfigure-processor true

将这个依赖加入到我们的 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)); }); }

更多内容可参考官网 Testing your Auto-configuration。

总结

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

目前我的 《重学 Spring》专栏已经更新了近 60 篇,内容涵盖 IOC、AOP、Web、Spring Boot 各核心特性及其实现原理。如果你也对 Spring 感兴趣,不妨点个关注,目前持续更新中…



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3