Starter:Spring Boot 的集成利器

1,145 阅读7分钟

用了 Spring Boot 之后,再也用不回 Spring mvc 了。官网随便搞个脚手架,开箱即用。需要新增第三方组件,只需要引入相应的 starter 组件,可能要在 application.properties 写几个配置项,甚至不用任何配置。约定大于配置的思路让整个框架非常容易上手。

Spring Boot 的这种特性对外体现在大量的 starter。比如我们想要集成 mybatis,在依赖项中加入 mybatis-spring-boot-starter。不需要配置任何 bean,系统自动为我们集成了 mybatis。

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

这些 starter 是如何将第三方组件加入容器的?本文从 starter 的角度出发,探讨一些关键的技术点,以及如何自定义一个 starter。

starter 和普通 jar 包区别在哪里?

starter 也是一个 jar 包,但是它和普通 jar 包是有区别的,具体到以下三点。

1. 设计思路不同

普通的 jar 包为了好的扩展性,都会设计的功能单一,配置丰富,易于移植。但是 starter 最重要的是易用性,所以 starter 一般不会是单个功能的 jar,而是一组 jar 的集合。还是以 mybatis 举例, 它 的 starter 内部依赖了 mybatis,mybatis-spring,spring-boot-starter-jdbc,spring-boot-autoconfigure 等等 jar 包。简单讲,starter 更注重用户体验,这也是 Spring Boot 框架更容易上手的原因。

2. 适用性不同

大部分的 starter,都是用于集成 Spring Boot 的,强依赖于 Spring Boot。Pom 中会引入 spring-boot-autoconfigure。如果没有这个配置,那就说明这个 starter 只是 jar 包的集合,不具备自动配置的功能。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

当然,并不是不用 Spring Boot 就不能引用 starter,还是可以当做普通 jar 包一样使用,只是会丧失最重要的自动配置的能力,总之,starter 可以和普通 jar 包一样使用,但最适用于 Spring Boot 环境

3. 通过 spring.factories 实现自动配置

Spring Boot 使用 @EnableAutoConfiguration 注解实现自动装配。具体的原理是使用 SPI 的方式获得所有包下的 META-INF/spring.factories 文件,并读取其中的 EnableAutoConfiguration 实现类。具体的流程可以看Spring Boot 是如何自动集成 Web 环境的

starter 为了实现自动配置的功能,会有 spring.factories。如下是 mybatis starter 中的 spring.factories。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

MybatisAutoConfiguration 会被加载进来,它本身也是个配置类,会在 Spring 容器总注入 SqlSessionFactory,SqlSessionTemplate 等类,我们代码中可以直接注入他们的实例。同时 MybatisAutoConfiguration 还配置了默认的 mybatis 映射等。

了解了 starter 之后,我们基本可以得出这样一个结论:starter 是有自动装配能力的 jar 包。是 Spring Boot 集成其它组件的利器。

@Conditional 注解

@Conditional 注解是 starter 中使用很频繁的注解。顾名思义,该注解是用于判断是否满足某些条件的。为什么会有这个注解的?

我们在使用某个组件的时候,是有前置条件的。比如我们想要使用 Tomcat,那么当前项目必须是个 Web 项目,也就是必须要有 Servlet。使用 mybatis,项目中必须有 jdbc 接口。而且有些组件是对 JDK 版本有要求。当组件的加载和外部条件相关的时候,需要使用 @Conditional 注解申明需要满足的条件。这样可以避免一些无用的,或是错误的 Bean 加载到容器中,还能提升 Spring Boot 的启动速度。

@Conditional 注解的入参是一个实现了 Condition 接口的子类。 @Conditional 是Spring4 提供的注解,它的作用是按照一定的条件进行判断,满足条件的方能注册。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	Class<? extends Condition>[] value();
}

Condition 接口是一个单方法接口,用户需要实现 matches 方法,返回 true 表示类会被加载,返回 false 表示忽略这个类。

@FunctionalInterface
public interface Condition {
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

Spring Boot 为了 starter 方便使用,在 spring-boot-autoconfigure 中定义了很多的实现。比如:

  1. ConditionalOnBean
  2. ConditionalOnClass
  3. ConditionalOnExpression
  4. ConditionalOnJava
  5. ConditionalOnProperty
  6. ...

以 ConditionalOnMissingBean 为例,它给出的 Condition 的实现类是 OnBeanCondition,用于在容器中查找符合条件的类。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnMissingBean {
  ...
}

灵活使用这些注解可以很方便的搭建 starter。以下是 mybatis starter 自动注入类的申明。这里申明了 MybatisAutoConfiguration 存在的必要条件,就是存在 SqlSessionFactory 和 SqlSessionFactoryBean 这两个接口,因为 mybatis starter 会注册这两个接口的实现类,如果接口都不存在,那肯定报错。还需要存在 DataSource 实例,结合后面的 AutoConfigureAfter 注解看,需要先自动配置 DataSource,先有数据源,后接入 mabatis,保证接入的时候容器中存在 DataSource 实例,顺序不能乱。

@Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {
  ...
}

对于日常编程而言,我们可以自己实现 Condition 接口,然后使用 @Conditional 注解来做一些判断,不仅减少 if else,也让代码更加清晰明了。比如我们可以根据用户的某个配置切换实现类;比如根据系统变量判断当前运行环境,加载不同的实现等等。

starter 集成流程

简单梳理下 starter 的集成流程。

  1. Spring Boot 启动的时候,会创建上下文实例,这过程中会预先向容器中注册一些 BeanDefinition。其中有一个是 ConfigurationClassPostProcessor。用于加载和处理配置类。该类是一个 BeanDefinitionRegistryPostProcessor,会在 Bean 实例化之前执行。

  2. ConfigurationClassPostProcessor 开始解析配置类Spring Boot 是如何解析配置类的,由于我们在 @SpringBootApplication 注解中包含了 @Import(AutoConfigurationImportSelector.class),开始加载 AutoConfigurationImportSelector。

  3. AutoConfigurationImportSelector 使用 SPI 的方式获得所有包下的 META-INF/spring.factories 文件,读取其中的 EnableAutoConfiguration 实现类。这个实现类由 starter 提供。

  4. 根据 Conditional 条件判断需要加载的类。完成 starter 集成。

如何自定义 starter

starter 作为 Spring Boot 的集成利器,可以方便的将外部代码加入 Spring 容器中。通过 starter,也可以很方便的抽取一个公用逻辑到 jar 包中,其它项目需要用的时候直接引入 jar 包就能使用,简直不要太爽,那我们如何自定义一个 starter 呢?

SpringBoot 官方提供的 starter 以 spring-boot-starter-xxx 的方式命名的。自定义的建议使用 xxx-spring-boot-starter 命名。

比如我们创建了一个叫做 movie-spring-boot-starter 的项目,依赖中加入 spring-boot-autoconfigure。

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
  </dependency>
</dependencies>

该项目的目的是提供一个 MovieService。我们随便写一个类。

public class MovieService {
    public String getBestMovie(){
        return "GONE WITH THE WIND";
    }
}

创建一个 Config 文件,@ConditionalOnProperty 注解表示如果项目配置了 movie.enable 为 true,那么这个配置类会生效。这个配置类创建一个类型为 MovieService 的 Bean。

@Configuration
@ConditionalOnProperty(name = "movie.enable", havingValue = "true")
public class MovieConfig {
    @Bean
    public MovieService getMovieService(){
        return new MovieService();
    }
}

在该项目的 META-INF 目录下创建 spring.factories 文件,并增加 EnableAutoConfiguration 的实现类。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.demo.starter.config.MovieConfig

打包后主项目引用这个 jar 包,并在 application.properties 中配置 movie.enable 为 true。我们就可以在项目的任何地方注入 MovieService 的实例了。

总结

  1. starter 是 spring Boot 用于集成其余项目的一种特殊的 jar 包。通过 starter,我们可以通过简单的方式集成第三方功能,提高了 Spring Boot 的扩展性和易用性。starter 中除了正常的 jar 包,也就是依赖项之外,会额外的多一个配置类,提供自动装配的能力。

  2. starter 配置类是在配置类解析的时候以 SPI 的方式加载的,这些配置类都大量使用 @Conditional 注解,表明当前类的加载条件。

  3. 如果想要在 Spring Boot 中抽取一些公共的逻辑,建议使用 starter 的方式进行抽取。方便其他项目的集成。

如果您觉得有所收获,就请点个赞吧!