Spring Batch端到端测试中数据库未初始化问题排查
在Spring Batch项目中构建端到端测试时,执行单个Step提示BATCH_JOB_INSTANCE表不存在,但contextLoads测试和自定义ItemReader的单独测试正常运行,已配置spring.batch.initialize-schema=always,却无法自动初始化Spring Batch元数据表。
相关代码与配置
应用类
@EnableTransactionManagement @SpringBootApplication public class DadosSegurancaApplication { public static void main(String[] args) { SpringApplication.run(DadosSegurancaApplication.class, args); } }
Job配置类
@Configuration public class JobConfigs extends DefaultBatchConfiguration { ... @Bean public Step loadSubtractedObjectToDbStep(JobRepository jobRepository, PlatformTransactionManager transactionManager, ExcelReader excelReader, RepositoryItemWriter<SubtractedObject> subtractedObjectItemWriter) { return new StepBuilder("loadSubtractedObjectToDbStep", jobRepository) .<SubtractedObject, SubtractedObject>chunk(ROWS_PER_CHUNK, transactionManager) .reader(excelReader) .writer(subtractedObjectItemWriter) .build(); } @Bean public Job subtractedObjectsJob(JobRepository jobRepository, Step loadSubtractedObjectToDbStep) { return new JobBuilder("subtractedObjectsJob", jobRepository) .start(loadSubtractedObjectToDbStep) .build(); } ... }
应用配置(application.properties)
spring.datasource.url=jdbc:h2:file:./local.db spring.datasource.driverClassName=org.h2.Driver spring.datasource.username= spring.datasource.password= spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=update spring.batch.initialize-schema=always
测试类
@SpringJUnitConfig @SpringBatchTest @SpringBootTest(classes = { DadosSegurancaApplication.class }) public class SubtractedObjectExcelStepTest { @Autowired private ExcelReader reader; @Autowired private JobLauncherTestUtils jobLauncherTestUtils; @Autowired private Job subtractedObjectsJob; @Test // FAILS WITH ERROR BELLOW public void executeSingleStep() { jobLauncherTestUtils.setJob(subtractedObjectsJob); jobLauncherTestUtils.launchStep("loadSubtractedObjectToDbStep"); } @Test // WORKS public void testAllReads() throws Exception { assertSingle(reader.read(), "READ"); assertSingle(reader.read(), "READ2"); assertSingle(reader.read(), "READ3"); Assertions.assertNull(reader.read()); } }
错误堆栈信息
org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [SELECT JOB_INSTANCE_ID, JOB_NAME FROM BATCH_JOB_INSTANCE WHERE JOB_NAME = ? and JOB_KEY = ?] at org.springframework.jdbc.support.SQLExceptionSubclassTranslator.doTranslate(SQLExceptionSubclassTranslator.java:103) at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:107) at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1548) at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:677) at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:723) at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:754) at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:767) at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:825) at org.springframework.batch.core.repository.dao.JdbcJobInstanceDao.getJobInstance(JdbcJobInstanceDao.java:183) at org.springframework.batch.core.repository.support.SimpleJobRepository.getLastJobExecution(SimpleJobRepository.java:299) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:569) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:355) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:379) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) at jdk.proxy2/jdk.proxy2.$Proxy102.getLastJobExecution(Unknown Source) at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:111) at org.springframework.batch.core.launch.support.TaskExecutorJobLauncher.run(TaskExecutorJobLauncher.java:59) at org.springframework.batch.test.StepRunner.launchJob(StepRunner.java:175) at org.springframework.batch.test.StepRunner.launchStep(StepRunner.java:165) at org.springframework.batch.test.JobLauncherTestUtils.launchStep(JobLauncherTestUtils.java:241) at org.springframework.batch.test.JobLauncherTestUtils.launchStep(JobLauncherTestUtils.java:188) at unit.SubtractedObjectExcelStepTest.executeSingleStep(SubtractedObjectExcelStepTest.java:56) at java.base/java.lang.reflect.Method.invoke(Method.java:569) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "BATCH_JOB_INSTANCE" not found; SQL statement: SELECT JOB_INSTANCE_ID, JOB_NAME FROM BATCH_JOB_INSTANCE WHERE JOB_NAME = ? and JOB_KEY = ? [42102-224] at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) at org.h2.message.DbException.get(DbException.java:223) at org.h2.message.DbException.get(DbException.java:199) at org.h2.command.Parser.getTableOrViewNotFoundDbException(Parser.java:8064) at org.h2.command.Parser.getTableOrViewNotFoundDbException(Parser.java:8035) at org.h2.command.Parser.readTableOrView(Parser.java:8024) at org.h2.command.Parser.readTablePrimary(Parser.java:1788) at org.h2.command.Parser.readTableReference(Parser.java:2268) at org.h2.command.Parser.parseSelectFromPart(Parser.java:2718) at org.h2.command.Parser.parseSelect(Parser.java:2824) at org.h2.command.Parser.parseQueryPrimary(Parser.java:2708) at org.h2.command.Parser.parseQueryTerm(Parser.java:2564) at org.h2.command.Parser.parseQueryExpressionBody(Parser.java:2543) at org.h2.command.Parser.parseQueryExpressionBodyAndEndOfQuery(Parser.java:2536) at org.h2.command.Parser.parseQueryExpression(Parser.java:2529) at org.h2.command.Parser.parseQuery(Parser.java:2498) at org.h2.command.Parser.parsePrepared(Parser.java:627) at org.h2.command.Parser.parse(Parser.java:592) at org.h2.command.Parser.parse(Parser.java:564) at org.h2.command.Parser.prepareCommand(Parser.java:483) at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:639) at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:559) at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1166) at org.h2.jdbc.JdbcPreparedStatement.<init>(JdbcPreparedStatement.java:93) at org.h2.jdbc.JdbcConnection.prepareStatement(JdbcConnection.java:316) at com.zaxxer.hikari.pool.ProxyConnection.prepareStatement(ProxyConnection.java:328) at com.zaxxer.hikari.pool.HikariProxyConnection.prepareStatement(HikariProxyConnection.java) at org.springframework.jdbc.core.JdbcTemplate$SimplePreparedStatementCreator.createPreparedStatement(JdbcTemplate.java:1711) at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:656) ... 28 more
原因分析
核心原因是自定义继承了DefaultBatchConfiguration的配置类,会禁用Spring Boot对Spring Batch的自动配置,包括元数据表的自动初始化逻辑。
Spring Boot的BatchAutoConfiguration会在没有自定义BatchConfiguration实现类时自动生效,负责处理Spring Batch元数据表的初始化、核心组件(如JobRepository、JobLauncher)的自动配置。但当你定义了继承DefaultBatchConfiguration的配置类后,Spring Boot会认为你要完全自定义Spring Batch配置,自动配置逻辑会退出,此时spring.batch.initialize-schema=always的配置不会触发元数据表初始化。
解决方案
方案1:移除DefaultBatchConfiguration继承,使用Spring Boot自动配置
如果不需要自定义JobRepository、JobLauncher等核心组件,直接去掉配置类的extends DefaultBatchConfiguration,让Spring Boot自动配置接管:
@Configuration public class JobConfigs { // 移除继承关系 // 原有的Step、Job Bean定义保持不变 @Bean public Step loadSubtractedObjectToDbStep(JobRepository jobRepository, PlatformTransactionManager transactionManager, ExcelReader excelReader, RepositoryItemWriter<SubtractedObject> subtractedObjectItemWriter) { return new StepBuilder("loadSubtractedObjectToDbStep", jobRepository) .<SubtractedObject, SubtractedObject>chunk(ROWS_PER_CHUNK, transactionManager) .reader(excelReader) .writer(subtractedObjectItemWriter) .build(); } @Bean public Job subtractedObjectsJob(JobRepository jobRepository, Step loadSubtractedObjectToDbStep) { return new JobBuilder("subtractedObjectsJob", jobRepository) .start(loadSubtractedObjectToDbStep) .build(); } }
修改后,spring.batch.initialize-schema=always会自动触发元数据表初始化,测试执行时即可找到对应的表。
方案2:若必须继承DefaultBatchConfiguration,手动添加元数据表初始化逻辑
如果需要自定义Spring Batch核心组件(比如调整JobRepository的事务隔离级别),可以在配置类中手动引入BatchDataSourceScriptDatabaseInitializer来触发元数据表初始化:
@Configuration public class JobConfigs extends DefaultBatchConfiguration { @Autowired private DataSource dataSource; @Autowired private BatchProperties batchProperties; // 手动配置元数据表初始化器 @Bean public BatchDataSourceScriptDatabaseInitializer batchDataSourceScriptDatabaseInitializer() { return new BatchDataSourceScriptDatabaseInitializer(dataSource, batchProperties.getSchema()); } // 原有的Step、Job Bean定义保持不变 // ... }
此方案需保留application.properties中的spring.batch.initialize-schema=always配置,该属性会控制初始化逻辑是否执行。
额外验证
确保测试类的@SpringBootTest加载的主应用类能扫描到所有配置类,如果测试类与配置类不在同一包下,需确认@SpringBootApplication的扫描范围覆盖了配置类(可通过@ComponentScan指定扫描路径)。
内容的提问来源于stack exchange,提问作者Allan Vital

