Spring Batch自定义ItemReader获取JobParameter失败致项目启动异常
问题说明
尝试通过SpEL表达式#{jobParameters['filePath']}将JobParameter中的参数注入自定义ItemReader,项目启动失败,报错:EL1008E: Property or field 'jobParameters' cannot be found on object of type 'org.springframework.beans.factory.config.BeanExpressionContext' - maybe not public or not valid?
错误堆栈
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. 2025-04-16 16:12:17.568 SMI-NEW LOS ERROR --- [main] master-service o.s.boot.SpringApplication.reportFailure : Application run failed org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'scopedTarget.debtorReader' defined in file [C:\Users\Indra\IST\SMI\master-service\target\classes\id\co\ist\los\master\service\migration\reader\DebtorReader.class]: Unsatisfied dependency expressed through constructor parameter 0: Expression parsing failed at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:801) at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1352) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1189) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:973) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:950) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:616) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:738) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:440) at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) at id.co.ist.los.MasterServiceApplication.main(MasterServiceApplication.java:17) Caused by: org.springframework.beans.factory.BeanExpressionException: Expression parsing failed at org.springframework.context.expression.StandardBeanExpressionResolver.evaluate(StandardBeanExpressionResolver.java:170) at org.springframework.beans.factory.support.AbstractBeanFactory.evaluateBeanDefinitionString(AbstractBeanFactory.java:1591) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1361) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337) at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:910) at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:788) ... 19 common frames omitted Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'jobParameters' cannot be found on object of type 'org.springframework.beans.factory.config.BeanExpressionContext' - maybe not public or not valid? at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:228) at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:111) at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:99) at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:61) at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:97) at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:114) at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:273) at org.springframework.context.expression.StandardBeanExpressionResolver.evaluate(StandardBeanExpressionResolver.java:167) ... 24 common frames omitted
相关代码
Post API代码
@PostMapping(value = "/upload-and-trigger", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity<String> uploadAndTriggerJob( @RequestParam("file") MultipartFile file) { try { // 保存文件到本地 String filePath = saveFileToLocal(file); // 构建Job参数 JobParametersBuilder jobParameters = new JobParametersBuilder(); jobParameters.addString("filePath", filePath); jobParameters.addLong("time", System.currentTimeMillis()); // 启动Job JobExecution jobExecution = jobLauncher.run(job, jobParameters.toJobParameters()); return ResponseEntity.ok("Job triggered successfully with status: " + jobExecution.getStatus()); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("Failed to trigger job: " + e.getMessage()); } }
Batch配置代码
@Configuration @EnableBatchProcessing public class MasterDebiturBatchConfig { @Value("${edc.etl.transaction-sales.chunksize:1000}") private Integer defaultChunkSize; @Autowired private JobRepository jobRepository; @Autowired private PlatformTransactionManager transactionManager; private String JOB_NAME = "etl-migration-job"; private String EXECUTION_STEP_0 = "execution-debitur-write"; @Bean public Job myJob(Step myStep) { return new JobBuilder(JOB_NAME, jobRepository) .incrementer(new RunIdIncrementer()) .start(myStep) .build(); } @Bean public Step myStep(DebtorReader debtorReader, LoggingDebtorProcessor loggingDebtorProcessor, DatabaseDebtorWriter myItemWriter) { return new StepBuilder(EXECUTION_STEP_0, jobRepository) .<ReadDebtorDto, MDebtor>chunk(defaultChunkSize, transactionManager) .reader(debtorReader) .processor(loggingDebtorProcessor) .writer(myItemWriter) .build(); } }
自定义ItemReader代码
@Component @StepScope @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) @Slf4j public class DebtorReader implements ItemReader<ReadDebtorDto> { private static final String WILL_BE_INJECTED = null; private final String filePath; public DebtorReader(@Value("#{jobParameters['filePath']}") String filePath) { this.filePath = filePath; } @Override public ReadDebtorDto read() throws Exception { PoiItemReader<ReadDebtorDto> reader = new PoiItemReader<>(); reader.setResource(new ClassPathResource(filePath)); reader.setRowMapper(new DebtorRowMapper()); reader.setStrict(false); var readDebtorDto = new ReadDebtorDto(); readDebtorDto.setName(reader.read().getName()); readDebtorDto.setNpwp(reader.read().getNpwp()); readDebtorDto.setDebtorType(reader.read().getDebtorType()); readDebtorDto.setDebtorRating(reader.read().getDebtorRating()); readDebtorDto.setCustomerId(reader.read().getCustomerId()); return readDebtorDto; } }
解决方案
1. 调整参数注入方式
构造器注入会在Spring容器启动时尝试解析SpEL表达式,但此时JobParameter还未传入(Job通过API触发时才生成参数),导致表达式解析失败。改为字段注入+无参构造器,延迟表达式解析到Step执行阶段:
@Component @StepScope @Slf4j public class DebtorReader implements ItemReader<ReadDebtorDto> { @Value("#{jobParameters['filePath']}") private String filePath; // 无参构造器 public DebtorReader() { } // ... 后续read方法 }
2. 修正PoiItemReader初始化逻辑
原代码在read()方法中每次创建新的PoiItemReader,会导致重复加载文件、每次读取都从头开始,严重影响性能和正确性。改为初始化时配置:
@Component @StepScope @Slf4j public class DebtorReader implements ItemReader<ReadDebtorDto> { @Value("#{jobParameters['filePath']}") private String filePath; private PoiItemReader<ReadDebtorDto> poiReader; @PostConstruct public void init() throws Exception { poiReader = new PoiItemReader<>(); // 注意:如果是本地磁盘路径,用FileSystemResource而非ClassPathResource poiReader.setResource(new FileSystemResource(filePath)); poiReader.setRowMapper(new DebtorRowMapper()); poiReader.setStrict(false); poiReader.afterPropertiesSet(); // 初始化reader } @Override public ReadDebtorDto read() throws Exception { // 直接复用初始化好的poiReader读取数据 return poiReader.read(); } }
3. 修正资源类型
若filePath是本地磁盘路径(如saveFileToLocal保存到磁盘的路径),必须使用FileSystemResource,ClassPathResource仅用于读取类路径(如resources目录下)的文件。
关键原因解析
@StepScope标记的Bean会在Step执行时才实例化,构造器注入会导致Spring在容器启动阶段就尝试解析SpEL表达式,但此时jobParameters仅存在于Job执行上下文,容器启动阶段无法访问,因此抛出找不到属性的错误。改用字段注入后,表达式解析会延迟到Bean实例化(即Step执行时),此时jobParameters已存在,可正常获取参数。
内容的提问来源于stack exchange,提问作者iyp.prog

