
在 Spring Boot 整合 Quartz 时,为了省去手动导入 SQL 的麻烦,很多同学喜欢开启 initialize-schema: always。然而,这个配置在生产环境是个“大坑”,因为它默认策略往往是“先删后建”,导致定时任务数据全丢。本文提供一套企业级智能检测代码,实现“表不存在则创建,存在则跳过”,完美适配 MySQL 及国产数据库。
一、 场景与痛点:为什么官方配置不够用?
1. 进退两难的配置
在 application.yml 中,Spring Boot 提供了 Quartz 的初始化配置:
spring:
quartz:
job-store-type: jdbc
jdbc:
# 选项:ALWAYS, EMBEDDED, NEVER
initialize-schema: ALWAYS
- 选 ALWAYS:方便是方便,但官方默认脚本(如
schema-mysql.sql)第一行通常是DROP TABLE IF EXISTS。每次服务重启,任务全没了! - 选 NEVER:安全是安全,但部署新环境(特别是 DevOps 自动部署或交付给客户私有化部署)时,必须让 DBA 手动导脚本,极易漏导或版本不一致。
- 先使用 ALWAYS 启动,再修改为 NERVER ,测试可以这么搞,投产这样显得太不专业了
2. 国产数据库的挑战
在信创项目中,我们常遇到 达梦 (DM8)、人大金仓 (Kingbase) 等数据库。
手动维护多套 SQL 脚本非常痛苦,我们希望程序能利用 JDBC 标准 API,智能识别当前表结构是否存在。
二、 核心原理:Hook 机制实现“幂等性”
我们要达到的效果是:
启动时先检查 QRTZ_TRIGGERS 表是否存在。
- 👉 不存在:动态将配置改为
ALWAYS,执行建表脚本。 - 👉 已存在:动态将配置改为
NEVER,保护现有数据。
为了实现这一点,我们需要利用 Spring 的 Bean 覆盖机制,自定义 QuartzDataSourceScriptDatabaseInitializer。
三、 最佳实践代码(建议收藏)
只需添加以下两个类到你的工程中即可。
1. 配置类:拦截与决策
利用 Bean 的优先级,接管数据源初始化逻辑。
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSource;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSourceScriptDatabaseInitializer;
import org.springframework.boot.autoconfigure.quartz.QuartzProperties;
import org.springframework.boot.jdbc.DatabaseDriver;
import org.springframework.boot.jdbc.init.DatabaseInitializationMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Slf4j
@Configuration
public class QuartzAutoConfig {
@Bean
public QuartzDataSourceScriptDatabaseInitializer quartzDataSourceScriptDatabaseInitializer(
DataSource dataSource,
@QuartzDataSource ObjectProvider<DataSource> quartzDataSource,
QuartzProperties properties) {
// 1. 获取正确的数据源
DataSource dataSourceToUse = getDataSource(dataSource, quartzDataSource);
QuartzProperties.Jdbc jdbc = properties.getJdbc();
// 2. 核心逻辑:判断是否需要初始化
QuartzDataSourceInitializerUtils initializerUtils =
new QuartzDataSourceInitializerUtils(dataSourceToUse, jdbc.getInitializeSchema());
if (!initializerUtils.isFeatureEnabled()) {
log.info("[Quartz] 检测到内部表已存在,跳过初始化步骤 (Mode: NEVER)");
// 关键点:动态修改配置为 NEVER,阻止 Spring Boot 运行脚本
jdbc.setInitializeSchema(DatabaseInitializationMode.NEVER);
} else {
log.info("[Quartz] 检测到表缺失,准备执行初始化脚本 (Mode: ALWAYS)");
jdbc.setInitializeSchema(DatabaseInitializationMode.ALWAYS);
}
// 3. 返回 Spring Boot 原生初始化器,执行上述决策结果
return new QuartzDataSourceScriptDatabaseInitializer(dataSourceToUse, properties);
}
private DataSource getDataSource(DataSource dataSource, ObjectProvider<DataSource> quartzDataSource) {
DataSource dataSourceIfAvailable = quartzDataSource.getIfAvailable();
return (dataSourceIfAvailable != null) ? dataSourceIfAvailable : dataSource;
}
}
2. 工具类:JDBC 元数据检测(适配国产库)
这个工具类利用 JDBC 标准接口 DatabaseMetaData,兼容性极强。
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; // 建议引入 commons-lang3
import org.springframework.boot.jdbc.DatabaseDriver;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.boot.jdbc.init.DatabaseInitializationMode;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class QuartzDataSourceInitializerUtils {
// 只需要检测其中一张核心表即可
private static final String CHECK_TABLE_NAME = "QRTZ_TRIGGERS";
private static final String[] TYPE_TABLE = new String[]{"TABLE"};
private final DatabaseInitializationMode initializeSchema;
private final DataSource dataSource;
public QuartzDataSourceInitializerUtils(DataSource dataSource, DatabaseInitializationMode properties) {
this.initializeSchema = properties;
this.dataSource = dataSource;
}
public boolean isFeatureEnabled() {
// 如果配置文件显式配置为 NEVER,则尊重配置
if (initializeSchema == DatabaseInitializationMode.NEVER) {
return false;
}
// 默认逻辑:如果是嵌入式数据库(H2等)通常默认开启
boolean enabled = initializeSchema != DatabaseInitializationMode.EMBEDDED
|| EmbeddedDatabaseConnection.isEmbedded(this.dataSource);
// --- 核心改进逻辑 START ---
// 如果原本逻辑认为不需要开启(enabled=false),但我们想强制检查一下表是否存在
// 或者原本逻辑认为要开启(enabled=true),我们也要检查表是否已经存在了,避免重复删表
// 统一策略:只要表存在,就不要开启初始化
List<String> tables = getAllTablesByKeyword(CHECK_TABLE_NAME);
if (!tables.isEmpty()) {
// 表存在,强制关闭初始化
return false;
} else {
// 表不存在,尝试小写再次检查(适配 Postgres/Kingbase 等大小写敏感数据库)
tables = getAllTablesByKeyword(CHECK_TABLE_NAME.toLowerCase());
if (!tables.isEmpty()) {
return false;
}
}
// 表确实不存在,开启初始化
return true;
// --- 核心改进逻辑 END ---
}
/**
* 基于 JDBC 元数据获取表名,完美适配国产数据库
*/
private List<String> getAllTablesByKeyword(String tableNamePattern) {
List<String> tables = new ArrayList<>();
Connection connection = null;
ResultSet resultSet = null;
try {
connection = dataSource.getConnection();
DatabaseMetaData metaData = connection.getMetaData();
// 获取数据库产品名称
String dbProductName = metaData.getDatabaseProductName();
String catalog = connection.getCatalog();
String schema = null;
// --- 国产化适配重点 ---
if (StringUtils.containsIgnoreCase(dbProductName, "Oracle") ||
StringUtils.containsIgnoreCase(dbProductName, "DM")) {
// 达梦(DM) 和 Oracle 一样,Schema 通常是用户名(大写)
schema = metaData.getUserName();
} else if (StringUtils.containsIgnoreCase(dbProductName, "PostgreSQL") ||
StringUtils.containsIgnoreCase(dbProductName, "Kingbase")) {
// 人大金仓(Kingbase) 和 PG 一样,Schema 默认是 public
schema = "public";
// 如果使用了 schema 隔离,这里可以使用 connection.getSchema()
if (StringUtils.isNotBlank(connection.getSchema())) {
schema = connection.getSchema();
}
} else {
// MySQL 等其他数据库
schema = connection.getSchema();
}
// 执行查询
resultSet = metaData.getTables(catalog, schema, tableNamePattern, TYPE_TABLE);
while (resultSet.next()) {
tables.add(resultSet.getString("TABLE_NAME"));
}
} catch (SQLException e) {
log.error("检测 Quartz 表结构失败", e);
// 宁可报错也不要盲目返回 false 导致误删表,根据业务需求决定是否抛出异常
} finally {
// 建议使用 try-with-resources 或工具类关闭
closeQuietly(resultSet);
closeQuietly(connection);
}
return tables;
}
private void closeQuietly(AutoCloseable closeable) {
try {
if (closeable != null) closeable.close();
} catch (Exception ignored) {}
}
}
3. 流程可视化
这套代码的运行逻辑如下,彻底规避了“手滑”导致的事故。

四、 进阶:国产数据库适配指南
随着信创国产化的推进,很多项目需要迁移到 达梦 (Dameng)、人大金仓 (Kingbase) 或 OpenGauss。
Quartz 默认自带了 MySQL、Oracle、PG 的脚本,但对国产库支持稍显不足。
1. 核心问题:BLOB 与 驱动代理
Quartz 需要序列化 JobDataMap 存储到数据库的 BLOB 字段中。不同数据库对 BLOB 的处理方式不同,因此需要配置 driverDelegateClass。
2. 适配方案代码
我们需要创建一个 QuartzConfig 配置类来动态指定属性,而不是死写在 yml 里。
@Configuration
@Slf4j
public class QuartzConfig implements SchedulerFactoryBeanCustomizer {
private final DataSource dataSource;
public QuartzConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void customize(SchedulerFactoryBean schedulerFactoryBean) {
schedulerFactoryBean.setOverwriteExistingJobs(true);
// 延时启动,避免影响应用启动速度
schedulerFactoryBean.setStartupDelay(10);
// 动态计算 Delegate,适配国产库
Properties props = new Properties();
props.put("org.quartz.jobStore.driverDelegateClass", getDriverDelegateClass());
schedulerFactoryBean.setQuartzProperties(props);
}
/**
* 根据数据库类型选择适配的代理类
* 这一步是国产化适配的灵魂!
*/
private String getDriverDelegateClass() {
try {
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
String dbProductName = metaData.getDatabaseProductName();
// 打印一下,看看实际运行的是什么库
System.out.println("Current Database: " + dbProductName);
if (dbProductName.contains("Oracle") || dbProductName.contains("DM")) {
// 达梦数据库通常兼容 Oracle 语法
return "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate";
} else if (dbProductName.contains("PostgreSQL") || dbProductName.contains("Kingbase") || dbProductName.contains("Zenith")) {
// 人大金仓、OpenGauss 通常兼容 PG 语法
return "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate";
} else if (dbProductName.contains("Microsoft SQL Server")) {
return "org.quartz.impl.jdbcjobstore.MSSQLDelegate";
} else if (dbProductName.contains("MySQL") || dbProductName.contains("MariaDB")) {
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
} else {
// 默认标准代理
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
}
} catch (Exception e) {
e.printStackTrace();
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
}
}
}
3. 建表脚本哪里找?
- 达梦 (DM):直接使用 Quartz 官方的
tables_oracle.sql,通常可以直接运行。如果报错,只需将类型修改为达梦对应类型(如blob)。 - 人大金仓 (Kingbase):直接使用
tables_postgres.sql。注意大小写敏感问题,建议脚本中表名统一小写。
五、 springboot 2.x/3.x/4.x适配
文中代码是以 springboot 3 的版本做的适配,QuartzDataSourceInitializerUtils源码是通用的,但是Quartz对于 springboot 2、3、4 的版本有一定的差异,完整示例可以参考
springboot 4 (4.0.0)
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.quartz.autoconfigure.QuartzDataSourceScriptDatabaseInitializer;
import org.springframework.boot.quartz.autoconfigure.QuartzJdbcProperties;
import org.springframework.boot.quartz.autoconfigure.SchedulerFactoryBeanCustomizer;
import org.springframework.boot.sql.init.DatabaseInitializationMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import javax.sql.DataSource;
import java.sql.DatabaseMetaData;
import java.util.Properties;
@Configuration
@Slf4j
public class QuartzConfig implements SchedulerFactoryBeanCustomizer {
private final DataSource dataSource;
public QuartzConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void customize(SchedulerFactoryBean schedulerFactoryBean) {
schedulerFactoryBean.setOverwriteExistingJobs(true);
// 延时启动,避免影响应用启动速度
schedulerFactoryBean.setStartupDelay(10);
// 动态计算 Delegate,适配国产库
Properties props = new Properties();
props.put("org.quartz.jobStore.driverDelegateClass", getDriverDelegateClass());
schedulerFactoryBean.setQuartzProperties(props);
}
/**
* 根据数据库类型选择适配的代理类
* 这一步是国产化适配的灵魂!
*/
private String getDriverDelegateClass() {
try {
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
String dbProductName = metaData.getDatabaseProductName();
// 打印一下,看看实际运行的是什么库
System.out.println("Current Database: " + dbProductName);
if (dbProductName.contains("Oracle") || dbProductName.contains("DM")) {
// 达梦数据库通常兼容 Oracle 语法
return "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate";
} else if (dbProductName.contains("PostgreSQL") || dbProductName.contains("Kingbase") || dbProductName.contains("Zenith")) {
// 人大金仓、OpenGauss 通常兼容 PG 语法
return "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate";
} else if (dbProductName.contains("Microsoft SQL Server")) {
return "org.quartz.impl.jdbcjobstore.MSSQLDelegate";
} else if (dbProductName.contains("MySQL") || dbProductName.contains("MariaDB")) {
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
} else {
// 默认标准代理
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
}
} catch (Exception e) {
e.printStackTrace();
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
}
}
@Bean
public org.springframework.boot.quartz.autoconfigure.QuartzDataSourceScriptDatabaseInitializer quartzDataSourceScriptDatabaseInitializer(QuartzJdbcProperties properties) {
QuartzDataSourceInitializerUtils
quartzDataSourceInitializer = new QuartzDataSourceInitializerUtils(dataSource, properties.getInitializeSchema());
if (!quartzDataSourceInitializer.isFeatureEnabled()) {
log.info("Quartz内部表已经进行了初始化,不再进行Quartz内部表初始化");
properties.setInitializeSchema(DatabaseInitializationMode.NEVER);
} else {
properties.setInitializeSchema(DatabaseInitializationMode.ALWAYS);
}
return new QuartzDataSourceScriptDatabaseInitializer (dataSource, properties);
}
}
spring 3.x (3.5.7)
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSource;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSourceScriptDatabaseInitializer;
import org.springframework.boot.autoconfigure.quartz.QuartzProperties;
import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer;
import org.springframework.boot.sql.init.DatabaseInitializationMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import javax.sql.DataSource;
import java.sql.DatabaseMetaData;
import java.util.Properties;
@Configuration
@Slf4j
public class QuartzConfig implements SchedulerFactoryBeanCustomizer {
private final DataSource dataSource;
public QuartzConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void customize(SchedulerFactoryBean schedulerFactoryBean) {
schedulerFactoryBean.setOverwriteExistingJobs(true);
// 延时启动,避免影响应用启动速度
schedulerFactoryBean.setStartupDelay(10);
// 动态计算 Delegate,适配国产库
Properties props = new Properties();
props.put("org.quartz.jobStore.driverDelegateClass", getDriverDelegateClass());
schedulerFactoryBean.setQuartzProperties(props);
}
/**
* 根据数据库类型选择适配的代理类
* 这一步是国产化适配的灵魂!
*/
private String getDriverDelegateClass() {
try {
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
String dbProductName = metaData.getDatabaseProductName();
// 打印一下,看看实际运行的是什么库
System.out.println("Current Database: " + dbProductName);
if (dbProductName.contains("Oracle") || dbProductName.contains("DM")) {
// 达梦数据库通常兼容 Oracle 语法
return "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate";
} else if (dbProductName.contains("PostgreSQL") || dbProductName.contains("Kingbase") || dbProductName.contains("Zenith")) {
// 人大金仓、OpenGauss 通常兼容 PG 语法
return "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate";
} else if (dbProductName.contains("Microsoft SQL Server")) {
return "org.quartz.impl.jdbcjobstore.MSSQLDelegate";
} else if (dbProductName.contains("MySQL") || dbProductName.contains("MariaDB")) {
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
} else {
// 默认标准代理
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
}
} catch (Exception e) {
e.printStackTrace();
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
}
}
@Bean
public QuartzDataSourceScriptDatabaseInitializer quartzDataSourceScriptDatabaseInitializer(
DataSource dataSource, @QuartzDataSource ObjectProvider<DataSource> quartzDataSource,
QuartzProperties properties) {
DataSource dataSourceToUse = getDataSource(dataSource, quartzDataSource);
QuartzProperties.Jdbc jdbc = properties.getJdbc();
QuartzDataSourceInitializerUtils
quartzDataSourceInitializer = new QuartzDataSourceInitializerUtils(dataSource,
jdbc.getInitializeSchema());
if (!quartzDataSourceInitializer.isFeatureEnabled()) {
log.info("Quartz内部表已经进行了初始化,不再进行Quartz内部表初始化");
jdbc.setInitializeSchema(DatabaseInitializationMode.NEVER);
} else {
jdbc.setInitializeSchema(DatabaseInitializationMode.ALWAYS);
}
return new QuartzDataSourceScriptDatabaseInitializer (dataSourceToUse, properties);
}
private DataSource getDataSource(DataSource dataSource, ObjectProvider<DataSource> quartzDataSource) {
DataSource dataSourceIfAvailable = quartzDataSource.getIfAvailable();
return (dataSourceIfAvailable != null) ? dataSourceIfAvailable : dataSource;
}
}
spring 2.x (2.5.15)
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSource;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSourceInitializer;
import org.springframework.boot.autoconfigure.quartz.QuartzProperties;
import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer;
import org.springframework.boot.jdbc.DataSourceInitializationMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import javax.sql.DataSource;
import java.sql.DatabaseMetaData;
import java.util.Properties;
@Configuration
@Slf4j
public class QuartzConfig implements SchedulerFactoryBeanCustomizer {
private final DataSource dataSource;
public QuartzConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void customize(SchedulerFactoryBean schedulerFactoryBean) {
schedulerFactoryBean.setOverwriteExistingJobs(true);
// 延时启动,避免影响应用启动速度
schedulerFactoryBean.setStartupDelay(10);
// 动态计算 Delegate,适配国产库
Properties props = new Properties();
props.put("org.quartz.jobStore.driverDelegateClass", getDriverDelegateClass());
schedulerFactoryBean.setQuartzProperties(props);
}
/**
* 根据数据库类型选择适配的代理类
* 这一步是国产化适配的灵魂!
*/
private String getDriverDelegateClass() {
try {
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
String dbProductName = metaData.getDatabaseProductName();
// 打印一下,看看实际运行的是什么库
System.out.println("Current Database: " + dbProductName);
if (dbProductName.contains("Oracle") || dbProductName.contains("DM")) {
// 达梦数据库通常兼容 Oracle 语法
return "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate";
} else if (dbProductName.contains("PostgreSQL") || dbProductName.contains("Kingbase") || dbProductName.contains("Zenith")) {
// 人大金仓、OpenGauss 通常兼容 PG 语法
return "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate";
} else if (dbProductName.contains("Microsoft SQL Server")) {
return "org.quartz.impl.jdbcjobstore.MSSQLDelegate";
} else if (dbProductName.contains("MySQL") || dbProductName.contains("MariaDB")) {
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
} else {
// 默认标准代理
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
}
} catch (Exception e) {
e.printStackTrace();
return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
}
}
@Bean
public QuartzDataSourceInitializer quartzDataSourceInitializer(DataSource dataSource,
@QuartzDataSource
ObjectProvider<DataSource> quartzDataSource, ResourceLoader resourceLoader,
QuartzProperties properties) {
DataSource dataSourceToUse = getDataSource(dataSource, quartzDataSource);
QuartzProperties.Jdbc jdbc = properties.getJdbc();
DataSourceInitializationMode initializeSchema = jdbc.getInitializeSchema();
QuartzDataSourceInitializerUtils
quartzDataSourceInitializer = new QuartzDataSourceInitializerUtils(dataSource, initializeSchema);
if (!quartzDataSourceInitializer.isFeatureEnabled()) {
log.info("Quartz内部表已经进行了初始化,不再进行Quartz内部表初始化");
jdbc.setInitializeSchema(DataSourceInitializationMode.NEVER);
} else {
jdbc.setInitializeSchema(DataSourceInitializationMode.ALWAYS);
}
return new QuartzDataSourceInitializer (dataSourceToUse, resourceLoader, properties);
}
private DataSource getDataSource(DataSource dataSource, ObjectProvider<DataSource> quartzDataSource) {
DataSource dataSourceIfAvailable = quartzDataSource.getIfAvailable();
return (dataSourceIfAvailable != null) ? dataSourceIfAvailable : dataSource;
}
}
完整示例代码
结语
在云原生和信创时代,基础设施的可移植性和自适应性至关重要。通过这短短几十行代码,我们不仅解决了 Quartz 删库的风险,更让应用具备了在不同数据库环境中“即插即用”的能力。
赶紧把这套代码加入你的企业级脚手架吧!
Q.E.D.


