使用Spring Boot框架进行开发时通常会将可以复用的功能封装为一个独立的 starter,其他程序只需引入此starter即可使用相应功能。某些功能的starter必须依赖数据库 中的表才能正常工作,例如 spring-boot-starter-quartz 就需要初始化许多 quartz 相关的表。一种选择是手动执行这些表对应的建表语句,但更理想的方式是由 starter 自动初始化所需的表

1. Depend Upon an Initialized Database

Spring Boot官方文档的 Depend Upon an Initialized Database 章节描述了它对数据库初始化的支持。

数据库初始化相关的Bean可以分为两类,一类是执行初始化的Bean,负责创建表插入数据等操作,另一类是依赖于初始化后数据库的Bean, 这些Bean在创建过程中需要执行查询数据库等操作。 Spring Boot使用 DatabaseInitializerDetector 和 DependsOnDatabaseInitializationDetector 两个接口找到这两类Bean, 找到后只要通过 BeanDefinition#setDependsOn 方法指定后者依赖前者就能保证第二类Bean一定在第一类Bean之后创建

1.1. DatabaseInitializerDetector

DatabaseInitializerDetector 的定义如下,detect() 方法返回用来初始化数据库的Bean名称,也就是上文第一类Bean的名称, detectionComplete() 是 detect() 完成的回调方法,getOrder() 控制 DatabaseInitializerDetector 多个实现的执行顺序

public interface DatabaseInitializerDetector extends Ordered {

	/**
	 * Detect beans defined in the given {@code beanFactory} that initialize a
	 * {@link DataSource}.
	 * @param beanFactory bean factory to examine
	 * @return names of the detected {@code DataSource} initializer beans, or an empty set
	 * if none were detected.
	 */
	Set<String> detect(ConfigurableListableBeanFactory beanFactory);

	default void detectionComplete(ConfigurableListableBeanFactory beanFactory,
			Set<String> dataSourceInitializerNames) {
	}

	@Override
	default int getOrder() {
		return 0;
	}

}

DatabaseInitializerDetector 默认包含如下实现,其中 FlywayDatabaseInitializerDetector、FlywayMigrationInitializerDatabaseInitializerDetector、JpaDatabaseInitializerDetector、 LiquibaseDatabaseInitializerDetector 依赖特定框架或组件,通用性不足,而R2DBC目前应用也不广泛,因此本文仅介绍 DataSourceScriptDatabaseInitializerDetector

  • DataSourceScriptDatabaseInitializerDetector

  • FlywayDatabaseInitializerDetector

  • FlywayMigrationInitializerDatabaseInitializerDetector

  • JpaDatabaseInitializerDetector

  • LiquibaseDatabaseInitializerDetector

  • R2dbcScriptDatabaseInitializerDetector

1.1.1. DataSourceScriptDatabaseInitializerDetector

DataSourceScriptDatabaseInitializerDetector 及其父类定义如下,可以看到它会将所有 DataSourceScriptDatabaseInitializer 类型的Bean作为数据库初始化的Bean, DataSourceScriptDatabaseInitializer 在 下文单独介绍

class DataSourceScriptDatabaseInitializerDetector extends AbstractBeansOfTypeDatabaseInitializerDetector {

	static final int PRECEDENCE = Ordered.LOWEST_PRECEDENCE - 100;

	@Override
	protected Set<Class<?>> getDatabaseInitializerBeanTypes() {
		return Collections.singleton(DataSourceScriptDatabaseInitializer.class);
	}

	@Override
	public int getOrder() {
		return PRECEDENCE;
	}

}

public abstract class AbstractBeansOfTypeDatabaseInitializerDetector implements DatabaseInitializerDetector {

	@Override
	public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
		try {
			Set<Class<?>> types = getDatabaseInitializerBeanTypes();
			return new BeansOfTypeDetector(types).detect(beanFactory);
		}
		catch (Throwable ex) {
			return Collections.emptySet();
		}
	}

	/**
	 * Returns the bean types that should be detected as being database initializers.
	 * @return the database initializer bean types
	 */
	protected abstract Set<Class<?>> getDatabaseInitializerBeanTypes();

}

1.2. DependsOnDatabaseInitializationDetector

DependsOnDatabaseInitializationDetector 的定义如下,它的 detect() 方法返回依赖于初始化后数据库的Bean的名称

public interface DependsOnDatabaseInitializationDetector {

	/**
	 * Detect beans defined in the given {@code beanFactory} that depend on database
	 * initialization. If no beans are detected, an empty set is returned.
	 * @param beanFactory bean factory to examine
	 * @return names of any beans that depend upon database initialization
	 */
	Set<String> detect(ConfigurableListableBeanFactory beanFactory);

}

DependsOnDatabaseInitializationDetector 默认包含如下实现,其中大部分都用于与其他框架整合,而 AnnotationDependsOnDatabaseInitializationDetector 则比较通用,在任意Bean的声明 上添加 @DependsOnDatabaseInitialization 注解就可以使其成为一个依赖于初始化后数据库的Bean。

  • AnnotationDependsOnDatabaseInitializationDetector 返回声明了 @DependsOnDatabaseInitialization 注解的Bean

  • JdbcIndexedSessionRepositoryDependsOnDatabaseInitializationDetector 返回 JdbcIndexedSessionRepository 类型的Bean,用于与 spring-session 整合

  • JobRepositoryDependsOnDatabaseInitializationDetector 返回 JobRepository 类型的Bean,用于与 spring-batch 整合

  • JooqDependsOnDatabaseInitializationDetector 返回 DSLContext 类型的Bean,用于与 JOOQ 整合

  • JpaDependsOnDatabaseInitializationDetector 返回 EntityManagerFactory 和 AbstractEntityManagerFactoryBean 类型的Bean,用于与JPA整合

  • SchedulerDependsOnDatabaseInitializationDetector 返回 Scheduler 和 SchedulerFactoryBean 类型的Bean,用于与Quartz整合

  • SpringJdbcDependsOnDatabaseInitializationDetector 返回 JdbcOperations 和 NamedParameterJdbcOperations 类型的Bean

2. DataSourceScriptDatabaseInitializer

上文介绍了 DataSourceScriptDatabaseInitializerDetector 会返回所有 DataSourceScriptDatabaseInitializer 类型的Bean,表示这些Bean用于数据库初始化

DataSourceScriptDatabaseInitializer 用于执行特定脚本初始化数据库,它的构造器信息如下,第一个参数 dataSource 指定需要初始化的数据源,第二个参数 settings 指定脚本信息,包含 schema脚本位置、data脚本位置、是否错误继续、 分隔符、编码、模式

DatabaseInitializationMode 是一个枚举,包含 ALWAYS、EMBEDDED、NEVER 三个枚举值,ALWAYS 表示总是执行 schemaLocations 和 dataLocations 声明的脚本, EMBEDDED 表示只有是内存数据库才执行,NEVER 表示不执行。 该字段的设置要特别注意,如果指定了 ALWAYS 就意味着每一次项目启动都会执行初始化脚本,如果数据源是 MySQL 等非内存数据库重复执行脚本很可能会报错,当然如果数据库支持 CREATE IF NOT EXISTS 语法可能不会报错

public class DataSourceScriptDatabaseInitializer extends AbstractScriptDatabaseInitializer {

	private final DataSource dataSource;

	public DataSourceScriptDatabaseInitializer(DataSource dataSource, DatabaseInitializationSettings settings) {
		super(settings);
		this.dataSource = dataSource;
	}

    //...
}

public class DatabaseInitializationSettings {

	private List<String> schemaLocations;

	private List<String> dataLocations;

	private boolean continueOnError = false;

	private String separator = ";";

	private Charset encoding;

	private DatabaseInitializationMode mode = DatabaseInitializationMode.EMBEDDED;

    //getter,setter

}

综上所述,只要在 starter 中声明 DataSourceScriptDatabaseInitializer 类型的Bean就能实现数据库的初始化,如果 starter 本身包含了配置类的话,还可以在其配置类中声明 DatabaseInitializationSettings 相关的属性, 最终实现在 application.yml 中配置初始化脚本位置或者初始化模式等属性。