【Spring Boot】手把手教你写一个 Starter

介绍

大家在开发的过程中应该经常会看到各种各样的Starter

当我们需要集成某个功能的时候,Spring或是第三方都会提供一个Starter来帮助我们更简单的集成对应的功能到我们的Spring Boot项目中

基于我之前实现的一些功能(如果大家有兴趣可以看看)来分享一下如何写一个Starter

准备

现在我们假定,我们实现了一个A类用于提供我们封装好的功能

public class A { 
    ...
}

一般情况下我们会使用@ComponentSpring容器中注入实例,如下:

@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,当enabledtrue时导入AConfiguration.class,当enabledfalse时不导入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

enabledtrue时,我们返回AConfiguration.class的全限定名;当enabledfalse时,返回空数组即可

最后我们将@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.factoriesAAutoConfiguration(AConfiguration)扫描进去了

配置文件

正常情况下,我们很有可能需要在application.ymlapplication.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有改动需要重新编译才会生效哦

配置代理

@ConfigurationproxyBeanMethods可以指定该配置中的方法是否进行代理,具体有什么作用呢

假设现在我们的A需要依赖B实例,那我们的配置可以这样写

@Configuration
public class AConfiguration { 
    
    @Bean
    public B b() {
        return new B();
    }
    
    @Bean
    public A a() {
        return new A(b());
    }
}

@ConfigurationproxyBeanMethods默认是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 可以指定在ServletReactive环境中生效

配置顺序

在某些情况下,我们可能会发现一些条件注解不生效

这个时候我们可以尝试指定配置顺序(并不保证能够解决所有的失效问题)

  • @AutoConfigureBefore 在某个配置之前进行配置
  • @AutoConfigureAfter 在某个配置之后进行配置
  • @AutoConfigureOrder 指定配置顺序

不过这里需要注意这几个注解只能对自动配置生效,也就是需要定义在spring.factories中的配置

添加注解的类的可以是任意的配置类,但是注解中指定的类需要是spring.factories中的配置的类

打包发布

最后就是打包发布就行啦,之后就可以通过GradleMaven从中央仓库或私库中拉下来使用了

赶快去写一个自己的Starter


其他的文章

【Spring Cloud】协同开发利器之动态路由|Ribbon & LoadBalancer 解析篇

【Spring Cloud】一个配置注解实现 WebSocket 集群方案

【Spring Boot】WebSocket 的 6 种集成方式

【Java】简单优雅的加载外部 jar 中的 Class|插件化

【Spring Boot】一个注解实现下载接口

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值