如何写一个自己的 Spring-Boot-Starter组件

4,471 阅读5分钟

1.前言

我们都知道可以使用 SpringBoot 快速的开发基于 Spring 框架的项目。由于围绕 SpringBoot 存在很多开箱即用的 Starter 依赖,使得我们在开发业务代码时能够非常方便的、不需要过多关注框架的配置,而只需要关注业务即可。

例如我想要在 SpringBoot 项目中集成 Redis,那么我只需要加入 spring-data-redis-starter 的依赖,并简单配置一下连接信息以及 Jedis 连接池配置就可以。这为我们省去了之前很多的配置操作。甚至有些功能的开启只需要在启动类或配置类上增加一个注解即可完成。

那么如果我们想要自己实现自己的 Starter 需要做些什么呢?下面就开始介绍如何实现自己的 spring-boot-starter。

2.原理浅谈

从总体上来看,无非就是将Jar包作为项目的依赖引入工程。而现在之所以增加了难度,是因为我们引入的是Spring Boot Starter,所以我们需要去了解Spring Boot对Spring Boot Starter的Jar包是如何加载的?下面我简单说一下。

SpringBoot 在启动时会去依赖的 starter 包中寻找 /META-INF/spring.factories 文件,然后根据文件中配置的路径去扫描项目所依赖的 Jar 包,这类似于 Java 的 SPI 机制。

细节上可以使用@Conditional 系列注解实现更加精确的配置加载Bean的条件。

JavaSPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

3.实现自动配置

3.1 自动配置的几种方式

第一种: @EnableXXX

@EnableFeignClients({"com.***"})


@EnableDiscoveryClient


Enable 里面引入了一个 @Import 

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

	/**
	 * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
	 * declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of
	 * {@code @ComponentScan(basePackages="org.my.pkg")}.
	 * @return the array of 'basePackages'.
	 */
	String[] value() default {};

    ...
    ...
}


/**
 * Annotation to enable a DiscoveryClient implementation.
 * @author Spencer Gibb
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {

	/**
	 * If true, the ServiceRegistry will automatically register the local server.
	 */
	boolean autoRegister() default true;
}

第二种: 基于class是否存在的自动化配置

比如: 我们springboot工程默认是用的tomcat,我需要用jetty,只需要排除tomcat,引入jetty包就行。

eg1: 

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>
        
eg2:

@Configuration
@ConditionalOnClass({ io.jaegertracing.internal.JaegerTracer.class,
        io.opentracing.contrib.java.spring.jaeger.starter.TracerBuilderCustomizer.class })
@ConditionalOnProperty(value = "opentracing.jaeger.enabled", havingValue = "true", matchIfMissing = true)
@AutoConfigureBefore(io.opentracing.contrib.java.spring.jaeger.starter.JaegerAutoConfiguration.class)
public class ZaJaegerAutoConfiguration {
    /**
     * 自定义调用链辅助功能
     * <ul>
     * <li>将jaeger调用链的traceID等信息打印到logback里面,用于业务日志错误时,可以排查调用链信息,辅助排错
     * </ul>
     *
     * @return TracerBuilderCustomizer
     */
    @ConditionalOnMissingBean(name = "zaTracerBuilderCustomizer")
    @Bean(name = "zaTracerBuilderCustomizer")
    public io.opentracing.contrib.java.spring.jaeger.starter.TracerBuilderCustomizer zaTracerBuilderCustomizer() {
        return builder -> builder.withScopeManager(new WrappedThreadLocalScopeManager());
    }
}

第三种: 基于配置属性是否存在的配置

eg1:

@Configuration
@ConditionalOnClass(io.jaegertracing.internal.JaegerTracer.class)
@ConditionalOnMissingBean(io.opentracing.Tracer.class)
@ConditionalOnProperty(value = "opentracing.jaeger.enabled", havingValue = "true", matchIfMissing = true)
@AutoConfigureBefore(io.opentracing.contrib.spring.tracer.configuration.TracerAutoConfiguration.class)
@EnableConfigurationProperties(JaegerConfigurationProperties.class)
public class JaegerAutoConfiguration {

  @Autowired(required = false)
  private List<TracerBuilderCustomizer> tracerCustomizers = Collections.emptyList();

  @Bean
  public io.opentracing.Tracer tracer(Sampler sampler,
                                      Reporter reporter,
                                      Metrics metrics,
                                      JaegerConfigurationProperties properties) {

    final JaegerTracer.Builder builder =
        new JaegerTracer.Builder(properties.getServiceName())
            .withReporter(reporter)
            .withSampler(sampler)
            .withTags(properties.determineTags())
            .withMetrics(metrics);

    tracerCustomizers.forEach(c -> c.customize(builder));

    return builder.build();
  }
  

3.2 工程规范

命名规范

  • 官方命名格式为: spring-boot-starter-{name}

  • 非官方建议命名格式:{name}-spring-boot-starter

Starter模块依赖规范

  • starter 模块 : 只有依赖,并且依赖 configuration模块;
  • configuration模块 : 真正的代码在的地方。

我个人一般只写一个模块,因为starter没有任何代码,暂时不知道官方这样做的目的。

4.元数据的配置

到目前为止,spring-boot-starter-hello的自动配置功能已实现,并且正确使用了,但还有一点不够完美,如果你也按上面步骤实现了自己的spring-boot-starter-hello自动配置,在application.properties中配置hello.msg属性时,你会发现并没有提示你有关该配置的信息,但是如果你想配置tomcat端口时,输入server.port是有提示的:

这种功能如何做呢?在Spring Boot官方文档中就已经给出了方法,新建META-INF/spring-configuration-metadata.json文件,进行配置。 这中不推荐,很麻烦。官方提供了一种自动生成spring-configuration-metadata.json的依赖,它的基本原理是在编译期使用注解处理器自动生成spring-configuration-metadata.json文件。文件中的数据来源于你是如何在类中定义hello.msg这个属性的,它会自动采集hello.msg的默认值和注释信息。不过我在测试时发现了中文乱码问题,而且网上有关spring-boot-configuration-processor的学习文档略少。 我们只需要引入如下jar就行:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>

类入口: ConfigurationMetadataAnnotationProcessor


/**
 * Annotation {@link Processor} that writes meta-data file for
 * {@code @ConfigurationProperties}.
 *
 * @author Stephane Nicoll
 * @author Phillip Webb
 * @author Kris De Volder
 * @author Jonas Keßler
 * @since 1.2.0
 */
@SupportedAnnotationTypes({ "*" })
public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor {
    ...
    ...
    ...
}

5.@Conditional 注解及作用

之前提到了在细节上可以使用@Conditional 系列注解实现更加精确的配置加载Bean的条件。下面列举 SpringBoot 中的所有 @Conditional 注解及作用。

注解 作用
@ConditionalOnBean 当容器中有指定的Bean的条件下
@ConditionalOnClass 当类路径下有指定的类的条件下
@ConditionalOnExpression 基于SpEL表达式作为判断条件
@ConditionalOnJava 基于JVM版本作为判断条件
@ConditionalOnJndi 在JNDI存在的条件下查找指定的位
@ConditionalOnMissingBean 当容器中没有指定Bean的情况下
@ConditionalOnMissingClass 当类路径下没有指定的类的条件下
@ConditionalOnNotWebApplication 当前项目不是Web项目的条件下
@ConditionalOnProperty 指定的属性是否有指定的值
@ConditionalOnResource 类路径下是否有指定的资源
@ConditionalOnSingleCandidate 当指定的Bean在容器中只有一个,或者在有多个Bean的情况下,用来指定首选的Bean
@ConditionalOnWebApplication 当前项目是Web项目的条件下

比如,注解@ConditionalOnProperty(prefix = "example.service",name= "enabled",havingValue = "true",matchIfMissing = false)的意思是当配置文件中example.service.enabled=true时,条件才成立。 当这些注解不再满足我们的需求之后,还可以通过实现 Condition 接口,自定义条件判断:

public class RedisExistsCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        StringRedisTemplate redisTemplate = null;
        try {
            redisTemplate = context.getBeanFactory().getBean(StringRedisTemplate.class);
        } catch (BeansException e) {
//            e.printStackTrace();
        }
        if (redisTemplate == null){
            return false;
        }
        return true;
    }
    
}

 //使用示例
@Conditional(RedisExistsCondition.class) 

6、实践例子

zk-spring-boot-starter