大家在开发的过程中应该经常会看到各种各样的Starter
当我们需要集成某个功能的时候,Spring或是第三方都会提供一个Starter来帮助我们更简单的集成对应的功能到我们的Spring Boot项目中
准备 #
现在我们假定,我们实现了一个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吧