介绍
大家在开发的过程中应该经常会看到各种各样的Starter
当我们需要集成某个功能的时候,Spring
或是第三方都会提供一个Starter
来帮助我们更简单的集成对应的功能到我们的Spring Boot
项目中
基于我之前实现的一些功能(如果大家有兴趣可以看看)来分享一下如何写一个Starter
准备
现在我们假定,我们实现了一个A
类用于提供我们封装好的功能
public class A {
...
}
一般情况下我们会使用@Component
往Spring
容器中注入实例,如下:
@Component
public class A {
...
}
现在当我们要把A
单独抽出来做成一个Starter
时@Component
就不太合适了,那么我们应该怎么实现呢,让我们先给我们的Starter
取个名字吧哈哈哈
取名
首先我们要先确定我们的Starter
的名字
Spring
本身就有很多自带的Starter
,比如:
spring-boot-starter-web
spring-boot-starter-data-redis
spring-boot-starter-websocket
spring-cloud-starter-netflix-eureka-client
spring-cloud-starter-openfeign
spring-cloud-starter-gateway
可以发现这些自带的Starter
的名称格式都是spring-boot-starter-xxx
或是spring-cloud-starter-xxx
另外我们也可以看到很多第三方库的Starter
,比如:
redisson-spring-boot-starter
mybatis-plus-boot-starter
一般来说,第三方的Starter
会把starter
放后面,xxx-spring-boot-starter
或是xxx-boot-starter
或是xxx-starter
不过我个人习惯还是xxx-spring-boot-starter
感觉更标准一点
所以现在就把我们要实现的Starter
取名为a-spring-boot-starter
配置类
之前说@Component
已经不太合适了,那么要怎么把A
注入到Spring
的容器中呢
答案是:@Configuration
+@Bean
,如下
@Configuration
public class AConfiguration {
@Bean
public A a() {
return new A();
}
}
这个用法大家应该也是比较熟悉,一般在一个项目中也会有一些标记了@Configuration
的配置类
只要Spring
能够扫描到这个类,A
实例就能被注入
如果这个配置类是写在我们自己的包下,那么Spring
默认的扫描路径就能扫到
但是现在我们如果做成一个Starter
,对应的包名可能就扫不到了
所以我们需要用另外的方式来导入这个配置类
导入方式
接下来就可以决定我们的Starter
的导入方式了
常用的导入方式有两种:使用@EnableXXX
或是spring.factories
我们经常能看到有些组件的会需要你添加@EnableXXX
的注解来启用某个功能,比如:
@EnableDiscoveryClient
@EnableFeignClients
这种方式光引入包还不够,需要手动添加注解来启用
而使用spring.factories
就只要引入包就可以直接生效了
这两种方式其实用哪种都一样,主要是看有没有必要额外配置一个注解
比如@EnableFeignClients
这个注解是可以配置扫描路径的,所以额外添加一个注解更加合适(这里使用配置文件是不合适的,因为我们的包结构是确定的,如果配置在配置文件里面反而多余又容易写错)
注解导入
我们先使用注解的方式来导入,定义一个@EnableA
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AConfiguration.class)
public @interface EnableA {
}
使用@Import
注解导入AConfiguration.class
就可以了
当我们需要集成这个功能的时候只要添加这个注解就行了
@EnableA
@SpringBootApplication
public class SampleApplication {
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
注解参数
这个时候可能就有同学要问了,如果我的注解上有参数呢,上面的写法好像没办法拿到参数啊
接下来我们来解决这个问题
现在我们给@EnableA
注解添加一个参数enabled
,当enabled
为true
时导入AConfiguration.class
,当enabled
为false
时不导入AConfiguration.class
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AConfiguration.class)
public @interface EnableA {
boolean enabled() default true;
}
接着我们实现一个ImportSelector
public class AImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
Map<String, Object> attributes = metadata
.getAnnotationAttributes(EnableA.class.getName());
boolean enabled = (boolean) attributes.get("enabled");
if (enabled) {
return new String[]{AConfiguration.class.getName()};
} else {
return new String[]{};
}
}
}
我们可以通过ImportSelector
中提供给我们的AnnotationMetadata
来获得EnableA
中的属性enabled
当enabled
为true
时,我们返回AConfiguration.class
的全限定名;当enabled
为false
时,返回空数组即可
最后我们将@Import(AConfiguration.class)
改为@Import(AImportSelector.class)
就行了
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AImportSelector.class)
public @interface EnableA {
boolean enabled() default true;
}
当我们将enabled
设置为false
时,就不会配置AConfiguration.class
了
@EnableA(enabled = false)
@SpringBootApplication
public class SampleApplication {
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
其实还有另一种方式也可以拿到注解的属性,那就是ImportBeanDefinitionRegistrar
public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}
和ImportSelector
不同的是,ImportBeanDefinitionRegistrar
可以直接注册BeanDefinition
如果我们用ImportBeanDefinitionRegistrar
来实现上面的功能大概就是这个样子
public class AImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
Map<String, Object> attributes = metadata
.getAnnotationAttributes(EnableA.class.getName());
boolean enabled = (boolean) attributes.get("enabled");
if (enabled) {
registry.registerBeanDefinition("a", new RootBeanDefinition(A.class));
}
}
}
然后同样的把@Import(AConfiguration.class)
改为@Import(AImportBeanDefinitionRegistrar.class)
就行了
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AImportBeanDefinitionRegistrar.class)
public @interface EnableA {
boolean enabled() default true;
}
spring.factories导入
接下来我们使用spring.factories
来导入配置(注解和spring.factories
选择一种就可以啦)
我们需要在resources
目录下新建一个META-INF
目录,然后在META-INF
目录下创建spring.factories
文件
接着我们需要在spring.factories
中将AConfiguration.class
配置上去
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxx.xxx.AConfiguration
一般情况下,如果是配置在spring.factories
中的配置类都会取名xxxAutoConfiguration
,所以我们在这里修改名称为AAutoConfiguration
最后在spring.factories
中的配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxx.xxx.AAutoConfiguration
这样当你的项目启动后,Spring
就会自动读取spring.factories
将AAutoConfiguration(AConfiguration)
扫描进去了
配置文件
正常情况下,我们很有可能需要在application.yml
或application.properties
中配置一些参数
所以我们现在需要一个属性a.enabled
来控制是否注入A
还需要一个属性a.b.type
来配置A
的某个字段
那么怎么在我们的AAutoConfiguration
中获得这两个属性呢
大家可能会想,简单啊,用@Value
不就好了?
虽然@Value
确实能拿到配置文件中的值,但是有更好的方式
那就是用@ConfigurationProperties
+@EnableConfigurationProperties
我们需要先定义一个AProperties
@Data
@ConfigurationProperties(prefix = "a")
public class AProperties {
//映射 a.enabled;
private boolean enabled = true;
private B b = new B();
@Data
public static class B {
//映射 a.b.type;
private String type;
}
}
同时给AProperties
添加ConfigurationProperties
注解并标记前缀为a
接着我们在AAutoConfiguration
上添加@EnableConfigurationProperties
就行了
@Configuration
@EnableConfigurationProperties(AProperties.class)
public class AConfiguration {
@Bean
@ConditionalOnProperty(name = "a.enabled", havingValue = "true", matchIfMissing = true)
public A a(AProperties properties) {
String type = properties.getB().getType();
return new A();
}
}
我们可以通过@ConditionalOnProperty
来根据a.enabled
控制是否注入A
在方法参数中也可以直接注入AProperties
对象,并且里面的属性已经根据配置文件绑定好了
自动提示
不知道大家有没有发现,Spring
自带的配置是会有提示的,但是我们自定义的配置就没有
有没有什么办法让我们的AProperties
也能自动提示呢
只要引入下面这个包就行啦
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
如果AProperties
有改动需要重新编译才会生效哦
配置代理
@Configuration
的proxyBeanMethods
可以指定该配置中的方法是否进行代理,具体有什么作用呢
假设现在我们的A
需要依赖B
实例,那我们的配置可以这样写
@Configuration
public class AConfiguration {
@Bean
public B b() {
return new B();
}
@Bean
public A a() {
return new A(b());
}
}
@Configuration
的proxyBeanMethods
默认是true
,所以在a()
中调用b()
是会从Spring
的容器中获得B
实例
如果我们不启用方法代理可以这样写
@Configuration(proxyBeanMethods = false)
public class AConfiguration {
@Bean
public B b() {
return new B();
}
@Bean
public A a(B b) {
return new A(b);
}
}
直接在方法参数中注入即可
不启用方法代理的情况下,如果直接调用方法,就是普通的方法调用,每调用一次就会新建一个B
实例
配置依赖
接着之前的假设,A
需要依赖B
实例,但是现在B
允许为null
那么之前的配置方式就不行了
@Configuration
public class AConfiguration {
@Bean
public A a(B b) {
return new A(b);
}
}
如果直接在方法上注入B
实例,就会报错找不到对应的Bean
这种情况下,我们可以使用ObjectProvider
,如下:
@Configuration
public class AConfiguration {
@Bean
public A a(ObjectProvider<B> bProvider) {
return new A(bProvider.getIfUnique());
}
}
条件装配
在我们写Starter
的过程中,条件装配也是经常用到的功能
最常用的其实就是@ConditionalOnMissingBean
了
我们可以这样用
@Configuration
public class AConfiguration {
@Bean
@ConditionalOnMissingBean
public A a() {
return new A();
}
}
当Spring
发现当前已经存在A
对应的实例时,就不会再注入这个配置中的A
实例了
一般当我们重写了某个库中的某个组件后,该库中该组件的默认实现就不会生效了,便于我们扩展一些自定义的功能来替换默认实现
但是这个注解如果用不好也可能出现问题
假设现在我们的A
有一个扩展类A1
我们来看下面的配置1
@Configuration
public class AConfiguration {
@Bean
@ConditionalOnMissingBean
public A1 a() {
return new A1();
}
}
@ConditionalOnMissingBean
的判断逻辑是:当容器中存在A1
类型的对象就不会再注入这个配置中的A1
实例
接着我们再看下面的配置2
@Configuration
public class AConfiguration {
@Bean
@ConditionalOnMissingBean
public A a() {
return new A1();
}
}
@ConditionalOnMissingBean
的判断逻辑是:当容器中存在A
类型的对象就不会再注入这个配置中的A1
实例
如果在这个时候,容器中存在A2(A的另一个扩展类)
实例,配置1中的A1
还是会被注入,配置2中A1
不会被注入
因为@ConditionalOnMissingBean
的缺省值是方法的返回类型,所以大家在使用时需要多加注意,保险起见可以指定@ConditionalOnMissingBean
中的值,例如:
@Configuration
public class AConfiguration {
@Bean
@ConditionalOnMissingBean(A.class)
public A1 a() {
return new A1();
}
}
其他常用的条件注解
@ConditionalOnBean
当对应的Bean
存在时生效@ConditionalOnClass
当对应的Class
存在时生效@ConditionalOnMissingClass
当对应的Class
不存在时生效@ConditionalOnProperty
当对应的配置匹配时生效@ConditionalOnWebApplication
可以指定在Servlet
或Reactive
环境中生效
配置顺序
在某些情况下,我们可能会发现一些条件注解不生效
这个时候我们可以尝试指定配置顺序(并不保证能够解决所有的失效问题)
@AutoConfigureBefore
在某个配置之前进行配置@AutoConfigureAfter
在某个配置之后进行配置@AutoConfigureOrder
指定配置顺序
不过这里需要注意这几个注解只能对自动配置生效,也就是需要定义在spring.factories
中的配置
添加注解的类的可以是任意的配置类,但是注解中指定的类需要是spring.factories
中的配置的类
打包发布
最后就是打包发布就行啦,之后就可以通过Gradle
或Maven
从中央仓库或私库中拉下来使用了
赶快去写一个自己的Starter
吧
其他的文章
【Spring Cloud】协同开发利器之动态路由|Ribbon & LoadBalancer 解析篇
【Spring Cloud】一个配置注解实现 WebSocket 集群方案
【Spring Boot】WebSocket 的 6 种集成方式