使用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使用 DatabaseInitializerDetectorDependsOnDatabaseInitializationDetector 两个接口找到这两类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 默认包含如下实现,其中 FlywayDatabaseInitializerDetectorFlywayMigrationInitializerDatabaseInitializerDetectorJpaDatabaseInitializerDetectorLiquibaseDatabaseInitializerDetector 依赖特定框架或组件,通用性不足,而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 返回 EntityManagerFactoryAbstractEntityManagerFactoryBean 类型的Bean,用于与JPA整合

  • SchedulerDependsOnDatabaseInitializationDetector 返回 SchedulerSchedulerFactoryBean 类型的Bean,用于与Quartz整合

  • SpringJdbcDependsOnDatabaseInitializationDetector 返回 JdbcOperationsNamedParameterJdbcOperations 类型的Bean

2. DataSourceScriptDatabaseInitializer

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

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

DatabaseInitializationMode 是一个枚举,包含 ALWAYS、EMBEDDED、NEVER 三个枚举值,ALWAYS 表示总是执行 schemaLocationsdataLocations 声明的脚本, 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 中配置初始化脚本位置或者初始化模式等属性。