如何写一个自己的Spring Boot Starter
背景
我们知道可以使用SpringBoot快速开发基于Spring框架的项目,由于围绕SpringBoot存在很多开箱即用的Starter依赖,是的我们在开发业务代码时候能够非常方便的、不需要过多关注框架的配置,而只需要关注业务即可。
例如我我想在SpringBoot项目中集成Redis,那么我只需要加入spring-data-redis-starter的依赖,并简单配置一下连接信息以及Jedis连接池就可以。这为我们省去了之前很多的配置操作。甚至有些功能的开启只需要在启动类或配置类上增加一个注释即可完成。
原理
我们指定使用一个公用Starter的时候,只需要将相应的依赖添加到Maven的pom文件中即可,免去了自己需要引用很多的依赖包,并且SpringBoot会自动进行类的自动配置。
那么SpringBoot是如何知道要实例化哪些类并进行配置的?
- SpringBoot在启动时,会去依赖的Starter包中寻找 resouces/META-INF/spring.factories 文件,然后根据文件中配置的Jar包去扫描项目所依赖的Jar包,这类似于Java的SPI机制。
- 根据 spring.factories 配置加载 AutoConfigure 类。
- 根据@Conditional 注释的条件,进行自动配置并将Bean注入Spring Context 上下文当中。
我们也可以使用 @ImportAutoConfiguration({MyServiceAutoConfigration.class})指定自动配置哪些类。
用过SPI机制的同学可能会清楚一个概念,当一个框架需要动态的扩展能力、给使用者充分的扩展能力,那么可能会使用到SPI机制。
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
官方说明
Spring Boot checks for the presence of a META-INF/spring.factories file within your published jar.
The file should list your configuration classes under the EnableAutoConfiguration key, as shown in the following example:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
从上面的说明可以看出,只要我们在自己的Starter的META-INF/spring.factories中配置好AutoConfiguration,SpringBoot就可以检测并且加载。AutoConfiguration需要用到@Configuration 标识,代表它是一个配置类,并且结合 @Conditional 及相关注解灵活读取配置信息。
@Conditional
我们可以控制一个类满足某一条件才能进行实例化。在Spring中就有@Conditional注释和Condition接口,这个接口有一个matches方法,使用者可以重写这个方法。
public class TestCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return true;
}
}
我们就可以把@Conditional(value = TestCondition.class)标注在类上面,表示该类下面的所有@Bean都会启用配置。在spring-boot-autoconfigure中,SpringBoot官方实现了一系列Condition,用来方便平时开发,如下:
| 注解 | 说明 |
|---|---|
| @ConditionalOnBean | 在BeanFactory已经存在指定Bean |
| @ConditionalOnMissingBean | 在BeanFactory不存在指定Bean |
| @ConditionalOnClass | 在classpath下已存在指定class |
| @ConditionalOnCloudPlatform | 指定云平台已生效 |
| @ConditionalOnExpression | 指定SPEL表达式为true |
| @ConditionalOnJava | 指定Java版本(前或后) |
| @ConditionalOnJndi | 指定JNDI存在 |
| @ConditionalOnNotWebApplication | 非web应用 |
| @ConditionalOnWebApplication | web环境 |
| @ConditionalOnProperty | 指定的property有指定的值 |
| @ConditionalOnResource | 在classpath下存在指定的资源 |
| @ConditionalOnSingleCandidate | BeanFactory中该类型Bean只有一个或@Primary的只有一个 |
实现步骤
第一步:命名与分模块
接下来我们开始编写我们自己的,SpringBoot官方推荐自定义starter以xxx-spring-boot-starter命名,并且分两个模块,Autoconfigure模块和Starter模块,主要配置在Autoconfigure模块,Starter模块其实是一个空项目模块。
首先在父项目的pom文件加入SpringBoot依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Starter模块
Starter模块是个空模块,只有一个pom文件,目的就是提供必要的依赖项。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>me.mingshan</groupId>
<artifactId>demo-spring-boot-autoconfigure</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
第二步:编写AutoConfigure模块
Autoconfigure模块加入依赖:
<!-- Compile dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 根据自己项目情况选择对应的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
其中 spring-boot-configuration-processor 主要是用来生成元数据文件(META-INF/spring-configuration-metadata.json)。如果存在该文件,那么SpringBoot会优先读取元数据文件,提供启动速度。
自定义Service
例如我们想实现一个基于AOP切面实现的程序耗时日志打印Starter,本次我们可以选择使用@ConditionalOnProperty,即配置文件中有 aspect.log.enable = true,才加载我们的配置类。
1.定义AspectLog注释
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AspectLog {
}
2.定义配置文件对应类
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("aspect-log")
public class AspectLogProperties {
private boolean enable;
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
}
3.定义自动配置类
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.PriorityOrdered;
@Aspect
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
@Configuration
@ConditionalOnProperty(prefix = "aspect-log", name = "enable",
havingValue = "true", matchIfMissing = true)
public class AspectLogAutoConfiguration implements PriorityOrdered {
protected Logger logger = LoggerFactory.getLogger(getClass());
@Around("@annotation(com.xxx.aspect.log.AspectLog) ")
public Object isOpen(ProceedingJoinPoint thisJoinPoint)
throws Throwable {
//执行方法名称
String taskName = thisJoinPoint.getSignature()
.toString().substring(
thisJoinPoint.getSignature()
.toString().indexOf(" "),
thisJoinPoint.getSignature().toString().indexOf("("));
taskName = taskName.trim();
long time = System.currentTimeMillis();
Object result = thisJoinPoint.proceed();
logger.info("method:{} run :{} ms", taskName,
(System.currentTimeMillis() - time));
return result;
}
@Override
public int getOrder() {
//保证事务等切面先执行
return Integer.MAX_VALUE;
}
}
第三步:配置spring.factories
META-INF/spring.factories是spring的工程机制,在这个文件中定义的类都会自动加载,多个配置使用逗号分割换行用\
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.shanyuan.autoconfiguration.aspectlog.AspectLogAutoConfiguration
第四步:构建成starter包
mvn install ,就可以进行测试验证了,自己的 Spring Boot Starter 开发完成了。
总结
我们描述Starter的原理,以及如何开发一个Starter的步骤,简要说明了@Conditional的作用。
Stater原理的:
- Spring Boot在启动时扫码项目所依赖Jar包,寻找包含spring.factories文件的Jar包
- 根据spring.factories 配置加载AutoConfigure类
- 根据@Conditional 注解条件,进行自动配置并将Bean 注入Spring Context
DEMO
aspect-log-spring-boot (https://github.com/j5land/aspect-log-spring-boot)
References
- Creating Your Own Starter (https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter)
- 编写一个自己的SpringBoot Starter (https://mingshan.fun/2019/06/26/boot-features-custom-starter/)
- MyBatis Stater源码阅读 (https://github.com/mybatis/spring-boot-starter)
