其中涉及到了Spring Batch的几个主要组成部分,JobRepository、JobLauncher、ItemReader、ItemProcessor、ItemWriter、Step、Job等。
JobRepository:存储任务执行的状态信息,有内存模式和数据库模式;
JobLauncher:用于执行Job,并返回JobInstance;
ItemReader:读操作抽象接口;
ItemProcessor:处理逻辑抽象接口;
ItemWriter:写操作抽象接口;
Step:组成一个Job的各个步骤;
Job:可被多次执行的任务,每次执行返回一个JobInstance。
其中 JobRepository、JobLauncher无需配置(第二个例子会简化该配置),Spring Boot 的自配置已经实现,当然也可以自定义。
FlatFileItemReader 和 FlatFileItemWriter 就是框架实现好的文件读和写操作,分别采用了两种创建方式:构造器和建造器,Spring官方推荐使用后者。文件与对象的映射则是通过LineMapper,实现与 Spring JDBC 的 RowMapper 极其相似,完成配置关系后,ItemReader会读取(文件/数据库/消息队列)并填充对象给ItemProcessor使用,ItemProcessor通过处理返回的对象则会丢给ItemWriter写入(文件/数据库/消息队列)。
实例:基于银行信用卡每月自动生成账单,自动扣费
git地址传送门:lsr-batch-processing模块
建表SQL在resource下
pom(基于springboot 2.1.0)
<dependencies> <!--######################### 定义 spring batch 版本 #########################--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> <!--######################### spring boot web 依赖 #########################--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--######################### 定义 mysql 版本 #########################--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--######################### 定义 jpa 版本 #########################--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!--######################### 定义 log4j 版本 #########################--> <!-- 支持log4j2的模块,注意把spring-boot-starter和spring-boot-starter-web包中的logging去掉 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <!--######################### 定义 test 版本 #########################--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies>
实体准备:
UserInfo(用户信息)
package cn.lsr.entity; import javax.persistence.*; import java.io.Serializable; /** * @Description: 用户信息 * @Package: lsr-microservice * @author: [email protected] **/ @Entity @Table(name = "user_info") public class UserInfo implements Serializable { @Id @GeneratedValue @Column(name = "id") private Integer id ; @Column(name = "userId") private Integer userId; @Column(name = "name") private String name ; @Column(name = "age") private Integer age; @Column(name = "description") private String description; //get set }
UserAccount(账户类)
package cn.lsr.entity; import javax.persistence.*; import java.math.BigDecimal; import java.util.Date; /** * @Description: 账户 * @Package: lsr-microservice * @author: [email protected] **/ @Entity @Table(name = "user_account") public class UserAccount { @Id @GeneratedValue @Column(name = "id") private Integer id; @Column(name = "username") private String username; @Column(name = "accountBalance") private BigDecimal accountBalance; @Column(name = "accountStatus") private Boolean accountStatus; @Column(name = "createTime") private Date createTime; //get set }
MonthBill(月账单)
package cn.lsr.entity; import javax.persistence.*; import java.math.BigDecimal; import java.util.Date; /** * @Description: 月账单 * @Package: lsr-microservice * @author: [email protected] **/ @Entity @Table(name = "month_bill") public class MonthBill { @Id @GeneratedValue @Column(name = "id") private Integer id; /** * 用户ID */ @Column(name = "userId") private Integer userId; /** * 总费用 */ @Column(name = "totalFee") private BigDecimal totalFee; /** * 是否已还款 */ @Column(name = "isPaid") private Boolean isPaid; /** * 是否通知 */ @Column(name = "isNotice") private Boolean isNotice; /** * 账单生成时间 */ @Column(name = "createTime") private Date createTime; // get set }
ConsumeRecord(消费记录)
package cn.lsr.entity; import javax.persistence.*; import java.math.BigDecimal; /** * @Description: 消费记录 * @Package: lsr-microservice * @author: [email protected] **/ @Entity @Table(name = "consume_record") public class ConsumeRecord { @Id @GeneratedValue @Column(name = "id") private Integer id; /** * 用户Id */ @Column(name = "userId") private Integer userId; /** * 花费金额 */ @Column(name = "consumption") private BigDecimal consumption; /** * 是否生成账单 */ @Column(name = "isGenerateBill") private Boolean isGenerateBill; // get set }
DAO层
package cn.lsr.repository; import cn.lsr.entity.UserInfo; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; /** * @Description: 用户信息 * @Package: lsr-microservice * @author: [email protected] **/ @Repository public interface UserInfoRepository extends JpaRepository<UserInfo,Integer> { } ######################################### package cn.lsr.repository; import cn.lsr.entity.UserAccount; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; /** * @Description: 账户 * @Package: lsr-microservice * @author: [email protected] **/ @Repository public interface UserAccountRepository extends JpaRepository<UserAccount,Integer> { } ######################################### package cn.lsr.repository; import cn.lsr.entity.MonthBill; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.Date; import java.util.List; /** * @Description: 月账单 * @Package: lsr-microservice * @author: [email protected] **/ @Repository public interface MonthBillRepository extends JpaRepository<MonthBill,Integer> { @Query("select m from MonthBill m where m.isNotice = false and m.isPaid = false and m.createTime between ?1 and ?2") List<MonthBill> seleMothBillNoPlayAll(Date start, Date end); } ######################################### package cn.lsr.repository; import cn.lsr.entity.ConsumeRecord; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; /** * @Description: 消费记录 * @Package: lsr-microservice * @author: [email protected] **/ @Repository public interface ConsumeRecordRepository extends JpaRepository<ConsumeRecord,Integer> { }
新建FlowBatchConfig(batch业务类)
package cn.lsr.flow; import cn.lsr.entity.ConsumeRecord; import cn.lsr.entity.MonthBill; import cn.lsr.entity.UserAccount; import cn.lsr.excepiton.MoneyException; import cn.lsr.repository.ConsumeRecordRepository; import cn.lsr.repository.MonthBillRepository; import cn.lsr.repository.UserAccountRepository; import cn.lsr.util.DateUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.batch.core.*; import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; import org.springframework.batch.core.job.flow.FlowExecutionStatus; import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.database.JpaItemWriter; import org.springframework.batch.item.database.JpaPagingItemReader; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.persistence.EntityManagerFactory; import java.math.BigDecimal; import java.util.Date; import java.util.List; import java.util.Optional; /** * @Description: 信用卡账单批处理 * @Package: lsr-microservice * @author: [email protected] **/ @Configuration public class FlowBatchConfig { private static final Logger log = LoggerFactory.getLogger(FlowBatchConfig.class); private EntityManagerFactory entityManagerFactory; private StepBuilderFactory stepBuilderFactory; private JobBuilderFactory jobBuilderFactory; public FlowBatchConfig(EntityManagerFactory entityManagerFactory,StepBuilderFactory stepBuilderFactory,JobBuilderFactory jobBuilderFactory){ this.entityManagerFactory=entityManagerFactory; this.stepBuilderFactory=stepBuilderFactory; this.jobBuilderFactory=jobBuilderFactory; } /** * 生成信用卡账单 * @return */ @Bean public Step generateVisaBillStep(ConsumeRecordRepository consumeRecordRepository){ return stepBuilderFactory.get("generateBillStep") .<ConsumeRecord, MonthBill>chunk(10) .reader(new JpaPagingItemReader<ConsumeRecord>(){{ setQueryString("from ConsumeRecord"); setEntityManagerFactory(entityManagerFactory); }}) .processor((ItemProcessor<ConsumeRecord,MonthBill>) data->{ if (data.getGenerateBill()){ // 已生成的不会生成月账单 return null; }else { MonthBill monthBill = new MonthBill(); //组装账单 monthBill.setUserId(data.getUserId()); monthBill.setPaid(false); monthBill.setNotice(false); //计算利息 monthBill.setTotalFee(data.getConsumption().multiply(BigDecimal.valueOf(1.5d))); monthBill.setCreateTime(new Date()); //是否生成账单 data.setGenerateBill(true); consumeRecordRepository.save(data); return monthBill; } }) .writer(new JpaItemWriter<MonthBill>(){{ setEntityManagerFactory(entityManagerFactory); }}) .build(); } /** * 自动扣费的 * @param monthBillRepository 月账单 * @param userAccountRepository 账户余额 * @return */ @Bean public Step autoDeductionStep(MonthBillRepository monthBillRepository,UserAccountRepository userAccountRepository){ return stepBuilderFactory.get("autoDeductionStep") .<MonthBill, UserAccount>chunk(10) .reader(new JpaPagingItemReader<MonthBill>(){{ setQueryString("from MonthBill"); setEntityManagerFactory(entityManagerFactory); }}) .processor((ItemProcessor<MonthBill,UserAccount>) data->{ if (data.getPaid()||data.getNotice()){ // 如果通知||已还款 return null; } // 根据账单信息查找账户信息 Optional<UserAccount> optionalUserAccount = userAccountRepository.findById(data.getUserId()); if (optionalUserAccount.isPresent()){ UserAccount userAccount = optionalUserAccount.get(); //账户状态检查 if(userAccount.getAccountStatus()==true){ //余额 if (userAccount.getAccountBalance().compareTo(data.getTotalFee()) > -1){ userAccount.setAccountBalance(userAccount.getAccountBalance().subtract(data.getTotalFee())); //已还款 data.setPaid(true); //已通知 data.setNotice(true); }else{ // 余额不足 throw new MoneyException(); } }else{ //状态异常 //设置通知 data.setNotice(true); System.out.println(String.format("Message sent to UserID %s ——> your water bill this month is %s¥",data.getUserId(),data.getTotalFee())); } monthBillRepository.save(data); return userAccount; }else { //账户不存在 log.error(String.format("用户ID %s,的用户不存在",data.getUserId())); return null; } }) .writer(new JpaItemWriter<UserAccount>(){{ setEntityManagerFactory(entityManagerFactory); }}) .build(); } /** * 余额不足,扣款失败通知 * @return */ @Bean public Step visaPaymentNoticeStep(MonthBillRepository monthBillRepository){ return stepBuilderFactory.get("visaPaymentNoticeStep") .tasklet((s,c)->{ List<MonthBill> monthBills = monthBillRepository.seleMothBillNoPlayAll(DateUtils.getBeginDayOfMonth(), DateUtils.getEndDayOfMonth()); monthBills.forEach(mo->{ System.out.println(String.format("Message sent to UserID %s ——> your water bill this month is ¥%s,please pay for it", mo.getUserId(), mo.getTotalFee())); }); return RepeatStatus.FINISHED; }) .build(); } public static void main(String[] args) { System.out.println(DateUtils.getBeginDayOfMonth()+"@"+DateUtils.getEndDayOfMonth()); } /** * 流程开始 * @param generateVisaBillStep 生成月账单 * @param autoDeductionStep 自动扣费 * @param visaPaymentNoticeStep 账户余额不足 * @return */ @Bean public Job flowJob(Step generateVisaBillStep,Step autoDeductionStep,Step visaPaymentNoticeStep){ return jobBuilderFactory.get("flowJob") .listener(new JobExecutionListener() { private long time; @Override public void beforeJob(JobExecution jobExecution) { time = System.currentTimeMillis(); } @Override public void afterJob(JobExecution jobExecution) { System.out.println(String.format("任务耗时:%sms", System.currentTimeMillis() - time)); } }) .flow(generateVisaBillStep) .next(autoDeductionStep) .next((jobExecution,stepExecution)->{ if (stepExecution.getExitStatus().equals(ExitStatus.COMPLETED)&&stepExecution.getCommitCount()>0){ return new FlowExecutionStatus("NOTICE USER"); }else { return new FlowExecutionStatus(stepExecution.getStatus().toString()); } }) .on("COMPLETED").end() .on("NOTICE USER").to(visaPaymentNoticeStep) .end() .build(); } }
Service层
FlowJobService
package cn.lsr.serivce; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobRestartException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.Date; /** * @Description: 流程job * @Package: lsr-microservice * @author: [email protected] **/ @Service public class FlowJobService { private JobLauncher jobLauncher; private Job flowJob; @Autowired public FlowJobService(JobLauncher jobLauncher,Job flowJob){ this.jobLauncher=jobLauncher; this.flowJob=flowJob; } @Scheduled(fixedRate = 24 * 60 * 60 * 1000) public void run() throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException { JobParameters jobParameters = new JobParametersBuilder().addDate("time", new Date()).toJobParameters(); jobLauncher.run(flowJob, jobParameters); } }
Controller层
package cn.lsr.controller; import cn.lsr.serivce.FlowJobService; import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobRestartException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @Description: 流程控制器 * @Package: lsr-microservice * @author: [email protected] **/ @RestController public class FlowJobController { @Autowired private FlowJobService flowJobService; @GetMapping("/run") public void run() throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException { flowJobService.run(); } }
启动类注意注解使用 @EnableBatchProcessing @EnableScheduling
package cn; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; /** * @Description: Batch启动类 * @Package: lsr-microservice * @author: [email protected] **/ @EnableBatchProcessing @EnableScheduling @SpringBootApplication public class BatchServiceApplication { public static void main(String[] args) { SpringApplication.run(BatchServiceApplication.class, args); Logger logger = LoggerFactory.getLogger(BatchServiceApplication.class); logger.info("********************************"); logger.info("**** 启动 batch-service 成功 ****"); logger.info("********************************"); } }
启动自动运行,基于Service层的 @Scheduled(fixedRate = 24 * 60 * 60 * 1000) / 也可以手动访问xxx:端口号/run
附加:
exception类
package cn.lsr.excepiton; /** * @Description: 资金异常 * @Package: lsr-microservice * @author: [email protected] **/ public class MoneyException extends Exception{ public MoneyException(){} public MoneyException(String message) { super(message); } public MoneyException(String message, Throwable cause) { super(message, cause); } public MoneyException(Throwable cause) { super(cause); } }
Util工具类
package cn.lsr.util; import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; /** * @Description: 时间工具类 * @Package: lsr-microservice * @author: [email protected] **/ public class DateUtils { /** * 获取本月的开始时间 */ public static Date getBeginDayOfMonth() { Calendar calendar = Calendar.getInstance(); calendar.set(getNowYear(), getNowMonth() - 1, 1); return getDayStartTime(calendar.getTime()); } /** * 获取本月的结束时间 */ public static Date getEndDayOfMonth() { Calendar calendar = Calendar.getInstance(); calendar.set(getNowYear(), getNowMonth() - 1, 1); int day = calendar.getActualMaximum(5); calendar.set(getNowYear(), getNowMonth() - 1, day); return getDayEndTime(calendar.getTime()); } /** * 获取某个日期的开始时间 */ private static Timestamp getDayStartTime(Date d) { Calendar calendar = Calendar.getInstance(); if (null != d) calendar.setTime(d); calendar.set(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH), 0, 0, 0); calendar.set(Calendar.MILLISECOND, 0); return new Timestamp(calendar.getTimeInMillis()); } /** * 获取某个日期的结束时间 */ private static Timestamp getDayEndTime(Date d) { Calendar calendar = Calendar.getInstance(); if (null != d) calendar.setTime(d); calendar.set(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH), 23, 59, 59); calendar.set(Calendar.MILLISECOND, 999); return new Timestamp(calendar.getTimeInMillis()); } /** * 获取今年是哪一年 */ private static Integer getNowYear() { Date date = new Date(); GregorianCalendar gc = (GregorianCalendar) Calendar.getInstance(); gc.setTime(date); return Integer.valueOf(gc.get(1)); } /** * 获取本月是哪一月 */ private static int getNowMonth() { Date date = new Date(); GregorianCalendar gc = (GregorianCalendar) Calendar.getInstance(); gc.setTime(date); return gc.get(2) + 1; } }
application.properties
server.port=8010 spring.batch.job.enabled=false spring.datasource.driver-class-name = com.mysql.jdbc.Driver spring.datasource.url = jdbc:mysql://192.168.0.104:3306/springbatch spring.datasource.username = root spring.datasource.password = lishirui # JPA #表示在控制台输出hibernate读写数据库时候的SQL。 spring.jpa.show-sql = true spring.jpa.hibernate.ddl-auto = update spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect spring.jpa.properties.hibernate.format_sql = true #字段映射 _ 问题 spring.jpa.hibernate.naming.physical-strategy = org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl # 项目启动时创建数据表(用于记录批处理执行状态)的 SQL 脚本,该脚本由Spring Batch提供 spring.datasource.schema=classpath:/org/springframework/batch/core/schema-mysql.sql # 项目启动时执行建表 SQL spring.batch.initialize-schema=always # 默认情况下,项目启动时就会自动执行配置好的批处理操作。这里将其设为不自动执行,后面我们通过手动触发执行批处理 #当遇到同样名字的时候,是否允许覆盖注册 spring.main.allow-bean-definition-overriding: true
。