其中涉及到了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写入(文件/数据库/消息队列)。
JobFlow - 一个job 可以有多个step ,每个step 有自己的逻辑,Flow可以对多个step进行排序,判断。



实例:基于银行信用卡每月自动生成账单,自动扣费
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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
@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: Hacker_lsr@126.com
**/
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: Hacker_lsr@126.com
**/
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
。
来源:https://www.cnblogs.com/hacker-lsr/p/12509333.html