有尝试过开发自定义Spring Boot Starter吗?没有的话不妨来试一下!
问题引入
Spring Boot 框架如今是一名 JavaER 必备的技能,使用它非常简单,只要创建好一个工程之后引入 parent 依赖以及几个必要的 Spring Boot Starter 就可以构建一个 Spring Boot 应用程序了。
很多人工作了很多年了,都只是引入官方或者第三方的 Spring Boot Starter 却从来没有尝试过自己写过一个自定义的 Starter。(🐶笔者也是第一次写)
今天就尝试来写一个简单的 Starter,以常见的 Redis 的客户端 Lettuce 为例,我们来简单地“套个壳子”!
前置知识
什么是 Spring Boot Starter?
先来简单说说 Spring Boot Starter 的概念。
Spring Boot Starter 是 Spring Boot 中用于简化依赖管理和配置的一种机制。它通过提供预定义的依赖集合和自动配置来简化项目的搭建和开发过程。下面是关于 Spring Boot Starter 的简单介绍:
-
依赖管理:
- Spring Boot Starter 提供了一系列预定义的依赖集合,这些依赖集合针对不同的场景和功能进行了组织和打包。比如,
spring-boot-starter-web
针对 Web 应用开发,包含了 Spring MVC、Tomcat、Jackson 等相关依赖;spring-boot-starter-data-redis
针对 Redis 数据库访问,包含了 Lettuce 或 Jedis 等相关依赖。 - 使用 Spring Boot Starter 可以大大简化项目的依赖管理工作,不需要手动引入一堆相关的依赖,只需引入对应的 Starter 即可。
- Spring Boot Starter 提供了一系列预定义的依赖集合,这些依赖集合针对不同的场景和功能进行了组织和打包。比如,
-
自动配置:
- Spring Boot Starter 还提供了自动配置的功能。它会根据你引入的 Starter 和项目的其他配置信息,自动配置 Spring 应用的各种组件,比如数据库连接、缓存、消息队列等。
- 这样可以减少开发者对项目配置的复杂性,让开发者专注于业务逻辑的实现,而不必过多关注底层框架的配置细节。
-
简化项目搭建:
- 基于 Spring Boot Starter,开发者可以快速搭建一个符合最佳实践的 Spring Boot 应用。只需选择对应的 Starter,并进行少量的配置,就可以快速启动项目开发。
- Starter 还可以根据不同的环境(开发、测试、生产)提供不同的默认配置,让项目在不同环境中表现良好。
-
模块化设计:
- Spring Boot Starter 的设计理念是模块化的,每个 Starter 都是一个独立的模块,包含了相关的功能和依赖。这样可以让开发者根据项目需求,选择性地引入需要的 Starter,而不必引入整个框架的所有功能。
- Starter 之间还可以进行组合,形成更复杂的功能集合。比如,可以同时引入
spring-boot-starter-web
和spring-boot-starter-data-jpa
来实现 Web 应用和持久化功能的集成。
总之,Spring Boot Starter 是 Spring Boot 的重要特性之一,它简化了项目的依赖管理和配置工作,提高了开发效率,让开发者更专注于业务逻辑的实现。
理解约定优于配置
Spring Boot 的核心思想就是约定优于配置。那什么又是约定优于配置呢?
约定优于配置(Convention Over Configuration)是一种软件设计模式,目的在于减少配置的数量或者降低理解难度,从而提升开发效率。需要注意的是,它并不是一种新的思想,实际上从我们开始接触 Java 的 jar 包依赖,就会发现很多地方都有这种思想的体现。比如数据库表中字段对应实体类的属性名。
按照一种约定的规范在一定程度上可以减少配置。
在 Spring Boot 中,有下面几个方面体现这种思想:
- Maven 的目录。
- 默认配置文件名称
application.yml
/application.properties
。 - 默认使用 Tomcat 作为 Web Server。
- 对于 Starter 依赖自动装配。
自动装配原理分析
自动装配的核心是扫描约定目录下的文件进行解析,解析完成之后把得到的 Configuration 配置类通过 ImportSelector
进行导入,从而完成 Bean 的自动装配过程。
自动装配在 Spring Boot 种通过 @EnableAutoConfiguration
注解来开启,这个注解定义如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
/**
* Environment property that can be used to override when auto-configuration is
* enabled.
*/
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};
}
可以在声明中看到一个类:AutoConfigurationImportSelector
,它实现了 ImportSelector
。ImportSelector
定义如下:
public interface ImportSelector {
/**
* Select and return the names of which class(es) should be imported based on
* the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
* @return the class names, or an empty array if none
*/
String[] selectImports(AnnotationMetadata importingClassMetadata);
/**
* Return a predicate for excluding classes from the import candidates, to be
* transitively applied to all classes found through this selector's imports.
* <p>If this predicate returns {@code true} for a given fully-qualified
* class name, said class will not be considered as an imported configuration
* class, bypassing class file loading as well as metadata introspection.
* @return the filter predicate for fully-qualified candidate class names
* of transitively imported configuration classes, or {@code null} if none
* @since 5.2.4
*/
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
selectImports
方法返回一个数组,在这个数组中可以指定需要装配到 IoC 容器的类,当在 @Import
中导入一个 ImportSelector
的实现类之后,会把该实现类中返回的 Class 名称都加载到 IoC 容器中。
和 @Configuration
不同的是,ImportSelector
可以实现批量装配,并且还可以通过逻辑处理来实现 Bean 的选择性装配,也就是可以根据上下文决定哪些类能够被 IoC 容器初始化。
这种实现方式比 @Import(*Configuration.class)
的好处在于装配的灵活性,还可以实现批量加载。比如在一个 ImportSelector
的实现类的 selectImports
方法返回含有多个 Configuration 类的数组。一个配置类可以声明很多个 Bean,所以在自动装配过程中只需要扫描指定路径即可。
上面说到了通过 ImportSelect
导入配置类,从而完成自动装配过程。在 @EnbaleAutoConfiguration
注解上可以看到 AutoConfigurationImportSelector
类,这个类完成了导入 Bean 的操作。
可以查看一下 AutoConfigurationImportSelector
类的 selectImports
方法:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
简单来讲完成两件事情:
-
AutoConfigurationMetadataLoader.loadMetaData
从 META-INF/spring-autoconfigure-metadata.properties 文件中加载自动装配的条件元数据,只有满足条件的 Bean 才能够进行装配。 -
收集所有符合条件的配置类
autoConfigurationEntry.getConfigurations()
,完成自动装配。
在 AutoConfigurationImportSelector
中不执行 selectImports
方法,而是通过 ConfigurationClassPostProcessor
中的 processConfigBeanDefinitions
方法来扫描和注册所有配置类的 Bean,最终还是会调用 getAutoConfigurationEntry
方法获得所有需要自动装配的配置类。
再来看一下配置类的收集方法 getAutoConfigurationEntry
:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
主要干了以下几件事情:
getAttributes
获得@EnableAutoConfiguration
注解中的属性 exclude、excludeName 等。getCandidateConfigurations
获得所有自动装配的配置类。removeDuplicates
去重配置项。getExclusions
根据@EnableAutoConfiguration
注解中配置的 exclude 等属性,把不需要自动装配的配置类移除。fireAutoConfigurationImportEvents
广播事件。- 最后返回经过多层判断和过滤之后的配置类集合。
最后来重点关注一下 getCandidateConfigurations
方法,它是获取配置类最核心的方法:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
这里用到了 SpringFactoriesLoader
,它是 Spring 内部提供的一种约定俗成的加载方式,类似于 Java 的 SPI 机制。Spring 会扫描 classpath 下的 META-INF/spring.factories 文件,spring.factories 文件中的数据以 properties 文件格式存储。Key 为 EnbaleAutoConfiguration
,Value 是多个配置类,也就是 getCandidateConfigurations
方法的返回值。
可以看下 Mybatis Spring Boot Starter 的 spring.factories 文件:
org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\
org.mybatis.spring.boot.autoconfigure.MybatisDependsOnDatabaseInitializationDetector
其实有两个:
另一个如下:
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
找到 MybatisAutoConfiguration
这个类:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
// ...
}
自动配置类就类似于这个写法。
除了基本的 @Configuration
注解,还有一个 @ConfigurationOnClass
注解,这个条件控制机制在这里的用途是,判断 classpath 下是否存在 SqlSessionFactory
、SqlSessionFactoryBean
这两个类,如果有,则把当前配置类注册到 IoC 容器。@EnableConfigurationProperties
是属性配置,可以按照约定在 application.yml 文件中配置参数,这些参数会覆盖 MybatisProperties
的参数默认值。
理解@ConditionalXXX注解
@Conditional
是 Spring Boot 提供的一个核心注解,这个注解可以提供自动装配的条件约束,一般与 @Configuration
、@Bean
配合使用。
简单来说,Spring Boot 在解析 @Configuration
配置类时,如果该配置类增加了 @Conditional
注解,那么会根据该注解配置的条件来决定是否要实现 Bean 的装配。
@Conditional
注解定义如下:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
/**
* All {@link Condition} classes that must {@linkplain Condition#matches match}
* in order for the component to be registered.
*/
Class<? extends Condition>[] value();
}
Condition
是一个函数式接口,提供了 matches
方法,用于条件匹配规则,返回 true 则表示可以注入,反之不行。
@FunctionalInterface
public interface Condition {
/**
* Determine if the condition matches.
* @param context the condition context
* @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
* or {@link org.springframework.core.type.MethodMetadata method} being checked
* @return {@code true} if the condition matches and the component can be registered,
* or {@code false} to veto the annotated component's registration
*/
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
简单列举一下 @ConditionalXXX
注解列表:
@ConditionalOnClass
:当类路径中存在指定的类时,才会创建 Bean。@ConditionalOnMissingClass
:当类路径中缺少指定的类时,才会创建 Bean。@ConditionalOnBean
:当容器中存在指定的 Bean 时,才会创建当前 Bean。@ConditionalOnMissingBean
:当容器中不存在指定的 Bean 时,才会创建当前 Bean。@ConditionalOnSingleCandidate
:当容器中只有一个符合条件的 Bean 时,才会创建当前 Bean。@ConditionalOnExpression
:基于 SpEL 表达式的条件判断,满足条件时才会创建 Bean。@ConditionalOnProperty
:当配置文件中指定的属性满足条件时,才会创建 Bean。@ConditionalOnResource
:当类路径中存在指定资源文件时,才会创建 Bean。@ConditionalOnWebApplication
:当应用是 Web 应用时,才会创建 Bean。@ConditionalOnNotWebApplication
:当应用不是 Web 应用时,才会创建 Bean。@ConditionalOnEnabledHealthIndicator
:当健康检查指示器启用时,才会创建 Bean。
这些注解可以单独使用,也可以结合使用,通过合理的条件控制,可以实现更加灵活的 Bean 加载和配置策略。例如,结合 @ConditionalOnClass
和 @ConditionalOnProperty
可以根据类路径中是否存在某个类以及配置文件中的属性值来决定是否加载某个 Bean。
开发自定义Starter
有了上面的铺垫,终于可以开发自定义 Starter 了。我们来开发一个 redis-spring-boot-starter。
创建工程,导入依赖
新建一个 redis-spring-boot-starter 工程,导入依赖:
<dependencies>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.3.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.3.7.RELEASE</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
</dependencies>
spring-boot-autoconfigure 提供了 @ConditionalXXX
注解支持。
spring-boot-configuration-processor 是 Spring Boot 提供的一个用于生成配置元数据(metadata)的处理器。它的作用是分析项目中的配置类(Configuration Class),并生成相关的配置元数据,用于IDE的智能提示、配置文件的验证和文档生成等功能。
编写配置信息类
编写 LettuceProperties
类存储基本参数:
@Getter
@Setter
@ConfigurationProperties("spring.redis")
public class LettuceProperties {
private String host = "localhost";
private int port = 6379;
private int database = 0;
private long timeout = 1000;
private boolean ssl;
}
spring.redis
用于指定 application.yml 文件配置项的前缀。
编写自动配置类
编写 LettuceAutoConfiguration
自动配置类:
@Configuration
@ConditionalOnClass(RedisClient.class)
@EnableConfigurationProperties(LettuceProperties.class)
public class LettuceAutoConfiguration {
@Autowired
private LettuceProperties properties;
@Bean
public RedisClient redisClient() {
RedisURI uri = RedisURI.builder()
.withHost(properties.getHost())
.withPort(properties.getPort())
.withDatabase(properties.getDatabase())
.withTimeout(Duration.ofMillis(properties.getTimeout()))
.withSsl(properties.isSsl())
.build();
return RedisClient.create(uri);
}
}
这里注入了 LettuceProperties
Bean,用于获取参数。自动配置类装配条件是 RedisClient
类存在。
编写spring.factories文件
在 resources 文件夹下编写 META-INF/spring.factories 文件:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.codeart.redis.config.LettuceAutoConfiguration
最后执行 mvn install
命令,保存到本地仓库。
测试
新建一个 Spring Boot 工程,导入刚才的库以及其他几个库:
<dependencies>
<dependency>
<groupId>org.codeart</groupId>
<artifactId>redis-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
编写 application.yml 文件:
spring:
redis:
host: 127.0.0.1
port: 6379
ssl: false
timeout: 500
编写引导类 App
:
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
编写测试类:
package org.codeart.test;
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class RedisTest {
@Autowired
private RedisClient redisClient;
@After
public void after() {
redisClient.shutdown();
}
@Test
public void testGet() {
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommands<String, String> commands = connection.sync();
commands.set("key1", "HelloWorld");
String value = commands.get("key1");
System.out.println(value);
connection.close();
}
}
执行效果如下:
大功告成,怎么样你学会了吗?😆😆😆
转载自:https://juejin.cn/post/7367778766984396863