用了 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 中定义了很多的实现。比如:
- ConditionalOnBean
- ConditionalOnClass
- ConditionalOnExpression
- ConditionalOnJava
- ConditionalOnProperty
- ...
以 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 的集成流程。
-
Spring Boot 启动的时候,会创建上下文实例,这过程中会预先向容器中注册一些 BeanDefinition。其中有一个是 ConfigurationClassPostProcessor。用于加载和处理配置类。该类是一个 BeanDefinitionRegistryPostProcessor,会在 Bean 实例化之前执行。
-
ConfigurationClassPostProcessor 开始解析配置类Spring Boot 是如何解析配置类的,由于我们在 @SpringBootApplication 注解中包含了 @Import(AutoConfigurationImportSelector.class),开始加载 AutoConfigurationImportSelector。
-
AutoConfigurationImportSelector 使用 SPI 的方式获得所有包下的 META-INF/spring.factories 文件,读取其中的 EnableAutoConfiguration 实现类。这个实现类由 starter 提供。
-
根据 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 的实例了。
总结
-
starter 是 spring Boot 用于集成其余项目的一种特殊的 jar 包。通过 starter,我们可以通过简单的方式集成第三方功能,提高了 Spring Boot 的扩展性和易用性。starter 中除了正常的 jar 包,也就是依赖项之外,会额外的多一个配置类,提供自动装配的能力。
-
starter 配置类是在配置类解析的时候以 SPI 的方式加载的,这些配置类都大量使用 @Conditional 注解,表明当前类的加载条件。
-
如果想要在 Spring Boot 中抽取一些公共的逻辑,建议使用 starter 的方式进行抽取。方便其他项目的集成。
如果您觉得有所收获,就请点个赞吧!