springbatch实现一个完整的flow

其中涉及到了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 

猜你喜欢

转载自www.cnblogs.com/hacker-lsr/p/12509333.html