前言
之前一篇寫了SpringBatch批量加載支付寶賬單的基礎篇http://www.lxweimin.com/p/6f038c1f6037,實現了將支付寶賬單通過springbatch加載、邏輯加工后、輸出到自己定義的賬單格式文件,上篇也說了只是介紹了基本使用,本篇是上一篇的進階,還是會繼續基于Springbatch全程使用javaconfig的方式實現,數據加載入庫、異常數據處理、并行、定時任務等,在寫這篇文章前,發現全網寫的關于Springbatch的文章絕大部分都是基于XML配置的,隨著springboot的逐漸普及,大家也都習慣了拋棄xml,使用javaconfig來配置項目,但是網上的blog包括spring官網對springbatch的javaconfig都介紹的很少,本篇就幫大家通過javaconfig配置整個springbatch,并實現一些高級用法。
1. 加載數據到數據庫
在批量過程中一般都需要將數據持久化,所以我們介紹下如何將批量數據加載到傳統的RDBMS數據庫中,我這里使用的是mysql,對于使用oracle或者其他數據庫的同學,直接替換datasource就可以了。
首先在pom中加上jdbc和mysql-connect依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>
在配置文件中加上數據庫的配置
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=XXXX
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
加上Datasource Config配置類
@Configuration
public class MySQLDataSourceConfig {
@Bean(name = "mysqlDataSource")
@Qualifier("mysqlDataSource")
@ConfigurationProperties(prefix="spring.datasource")
public DataSource mysqlDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "mysqldbcTemplate")
public JdbcTemplate mysqlJdbcTemplate(
@Qualifier("mysqlDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
這里實現一個數據庫寫入的功能,就是將從支付寶賬單中讀取的數據寫入數據庫的ItemWriter類,之前有讀者說賬單格式不對,這里解釋下這里用的賬單是我從網上隨便搜的支付寶賬單和實際支付寶商戶使用的賬單格式是有比較大的差異,這里只是做個例子,讓大家學習下springbatch,實際使用過程中還是要改下數據結構以官方提供的加載數據結構為準。新建AlipayDBItemWriter類,這個類實現了ItemWriter接口,上一篇文章也介紹了實現該接口需要實現write(list)方法,整體的邏輯比較簡單就是將從ItemReader中讀取到的數據加載到數據庫中,代碼一看就懂。
@Service
public class AlipayDBItemWriter implements ItemWriter<AlipayTranDO> {
private static final String INSERT_ALYPAY_TRAN =
"insert into alipay_tran_today(tran_id, channel, tran_type, counter_party, goods, amount, is_debit_credit, state) values(?,?,?,?,?,?,?,?)";
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void write(List<? extends AlipayTranDO> list) throws Exception {
for(AlipayTranDO alipayTran : list){
jdbcTemplate.update(INSERT_ALYPAY_TRAN,
alipayTran.getTranId(),
alipayTran.getChannel(),
alipayTran.getTranType(),
alipayTran.getCounterparty(),
alipayTran.getGoods(),
alipayTran.getAmount(),
alipayTran.getIsDebitCredit(),
alipayTran.getState());
}
}
}
寫好了ItemWriter最后就是寫batchconfig了,直接將上一篇的step1()方法復制一份改為step2,將writer改為剛才新建的AlipayDBItemWriter,將step2()放到job中,直接運行就可以去數據庫看結果了,這里要注意下要提前到數據庫中建好表。
@Bean
public Step step2() {
return stepBuilderFactory.get("step2")
.<AlipayTranDO, AlipayTranDO> chunk(10)
.reader(alipayFileItemReader.getMultiAliReader())
.writer(alipayDBItemWriter)
.build();
}
alipay_tran_today表結構
CREATE TABLE `alipay_tran_today` (
`tran_id` varchar(40) DEFAULT NULL,
`channel` varchar(20) DEFAULT NULL,
`tran_type` varchar(10) DEFAULT NULL,
`counter_party` varchar(20) DEFAULT NULL,
`goods` varchar(40) DEFAULT NULL,
`amount` varchar(20) DEFAULT NULL,
`is_debit_credit` varchar(10) DEFAULT NULL,
`state` varchar(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2. 批量數據校驗清洗
下面我們再介紹下批量數據的校驗清洗,如果通過springbatch來實現,在實現批量的過程中,我們在加載數據前都會去判斷加載的數據格式、內容等是否符合要求,對于不符合要求的數據是拋出異常還是暫時將這些異常數據加載到一張異常數據表中待后續處理等等都需要自己去實現,提前將不符合要求的數據清洗出去后,再對這些格式正確的數據進行加載并進行處理,這樣可以防止加工到最后一步發現數據有問題或者將錯誤數據加工出來出了報表,也可以防止因為臟數據導致后續批量中斷等問題。
在springbatch中,我們可以將數據校驗的過程寫成一個processor,在數據讀取后進行邏輯操作前進行數據校驗,將不合格的數據剔除或者暫存或者進行修復等。
下面我們新建AlipayValidateProcessor類,用于對支付寶賬單數據的校驗,這里實現的邏輯非常簡單,只是將金額字段小于0的作為異常拋出Exception并沒有做其他特殊處理,這里我們最好是自定義EXception,然后在配置Step的時候對自定義的Exception進行skip exception處理。
public class AlipayValidateProcessor implements ItemProcessor<AlipayTranDO, AlipayTranDO> {
private static final Logger log = LoggerFactory.getLogger(AlipayValidateProcessor.class);
@Override
public AlipayTranDO process(AlipayTranDO alipayTranDO) throws Exception {
if(Double.parseDouble(alipayTranDO.getAmount()) < 0){
log.info("validate error: " + alipayTranDO.toString());
throw new Exception();
}else{
return alipayTranDO;
}
}
}
下面就是要將數據校驗processor和數據加工processor多processor串聯執行,如果要串聯執行processor這就要用到CompositeItemProcessor類,將多個processor按順序加入list,并將processor list賦值給CompositeItemProcessor的delegete,將這個CompositeItemProcessor復合處理器作為整個step的processor,這樣在step執行的時候就會按照我們設置的復合processor先進行數據校驗清洗,再進行數據加工,這里有個地方要注意下,順序執行的processor的輸出類型對應的是下一個processor的輸入類型,第一個processor的輸入類型一定要是step的輸入類型,最后一個processor的輸出類型一定是step的輸出類型,例如例子中的step輸入為AlipayTranDO,輸出為HopPayTranDO,所以我們AlipayValidateProcessor的輸入為AlipayTranDO,輸出也為AlipayTranDO,AlipayItemProcessor的輸入為AlipayTranDO,輸出為HopPayTranDO。
public Step step3() {
CompositeItemProcessor<AlipayTranDO,HopPayTranDO> compositeItemProcessor = new CompositeItemProcessor<AlipayTranDO,HopPayTranDO>();
List compositeProcessors = new ArrayList();
compositeProcessors.add(new AlipayValidateProcessor());
compositeProcessors.add(new AlipayItemProcessor());
compositeItemProcessor.setDelegates(compositeProcessors);
return stepBuilderFactory.get("step3")
.<AlipayTranDO, HopPayTranDO> chunk(10)
.reader(alipayFileItemReader.getMultiAliReader())
.processor(compositeItemProcessor)
.writer(alipayFileItemWriter.getAlipayItemWriter())
.build();
}
3. 異常數據處理
對于在處理過程中如果遇到異常數據并且異常數據不是特別多的情況下,為了保證批量的順利執行,一般采取的做法是設置一個允許跳過的次數,這樣在遇到一些可以容忍的錯誤類型并行錯誤次數比較少的情況下就可以繼續執行,而不用中斷批量,做過運維的同學一定深有感觸,夜間批量中斷是多么痛苦的事情,第二天運維的同學一定會頂著黑眼圈找開發負責人去投訴批量中斷的事情,所以為了保證運維人員的身體健康,批量一定要有一定的異常數據容錯機制。springbatch支持設置skip的記錄最高次數,同樣也可以設置哪些錯誤可以跳過,哪些不能跳過,如果不設置那么只要批量執行有Exception,那就會中斷整個step。在springbatch中使用skip非常簡單,我們這個例子中對于異常沒有自定義,只是使用了Exception,這樣可以捕獲到大部分異常,實際使用過程中功能建議自定義Exception,這樣處理錯誤更有針對性,對于跳過的記錄,我們還需要設置一個SkipListener類用于監聽當出現跳過時回調進行處理的動作。SkipListener接口需要實現3個回調方法,開發者可以分別在這3個回調方法中實現相關跳過操作處理,例子中只是簡單記錄日志信息,我們也可以記錄到數據庫中,等待后續運維和運營查看數據是否有問題需要處理。
public class AlipaySkipListener implements SkipListener<AlipayTranDO, AlipayTranDO> {
private static final Logger log = LoggerFactory.getLogger(AlipaySkipListener.class);
@Override
public void onSkipInProcess(AlipayTranDO alipayTranDO, Throwable throwable) {
log.info("AlipayTran was skipped in process: "+alipayTranDO);
}
@Override
public void onSkipInRead(Throwable arg0) {
}
@Override
public void onSkipInWrite(AlipayTranDO alipayTranDO, Throwable throwable) {
log.info("AlipayTran was skipped in process: "+alipayTranDO);
}
}
除了可以設置skip我們還可以設置重試次數,例如我們需求中會去下載商戶的支付寶賬單,那么有可能因為網絡原因導致批量的時候某個商戶賬單無法下載下來,那么為了保證批量能夠執行下去,那么我們可以讓批量程序進行一定次數的重試,如果重試多次后還不行那么將進行跳過或報錯處理。
public Step step2() {
return stepBuilderFactory.get("step2")
.<AlipayTranDO, AlipayTranDO> chunk(10)
.reader(alipayFileItemReader.getMultiAliReader())
.writer(alipayDBItemWriter)
.faultTolerant()
.skipLimit(20)
.skip(Exception.class)
.listener(listener)
.retryLimit(3)
.retry(RuntimeException.class)
.build();
}
4. 并行配置
我們在日常處理批量的過程中,為了減少批量時間我們一般會將一些處理時間比較長的步驟并行執行充分利用系統資源,縮減批量執行時間,批量中一般有兩種并行方式,一種是對單個step多線程并行處理,這種適用于單step數據量特別大的情況,可以利用線程池多線程并行執行數據加工;還有一種是不同step沒有依賴關系并行處理,這種并行處理需要充分分析好這些并行step不存在資源爭奪,同時程序也是線程安全的,否則會出現很多資源競爭或者串數據的情況,我們這里只介紹單step多線程的實現方式。
需要和其他并發編程一樣,需要定義一個ThreadPoolExecutor
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setMaxPoolSize(4);
taskExecutor.afterPropertiesSet();
return taskExecutor;
}
對需要并行的step設置taskExecutor就可以實現任務多線程并行執行了,是不是很簡單?
@Bean
public Step step4() {
return stepBuilderFactory.get("step3")
.<AlipayTranDO, HopPayTranDO> chunk(10)
.reader(alipayFileItemReader.getMultiAliReader())
.processor(alipayItemProcessor)
.writer(alipayFileItemWriter.getAlipayItemWriter())
.taskExecutor(taskExecutor())
.throttleLimit(4)
.build();
}
5. 定時自動任務配置
springbatch本身只是批量框架并沒有定時執行的功能,這里我們需要借助spring的schedule實現定時任務功能,做到批量無人值守自動執行,如果要更強大的功能可以使用Quartz來實現更加花樣百出的定時功能,這里我們需求沒那么復雜使用scheduler就能很好的完成所有功能,只需要一句注解,設置下cron屬性就可以了,cron屬性實例見下:
一個cron表達式有至少6個(也可能7個)有空格分隔的時間元素。
按順序依次為秒(0~59)、分鐘(0~59)、小時(0~23)、天(月)(0~31,但是你需要考慮你月的天數)、月(0~11)、天(星期)(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT)、年份(1970-2099)
0 0 10,14,16 * * ? 每天上午10點,下午2點,4點
0 0/30 9-17 * * ? 朝九晚五工作時間內每半小時
0 0 12 ? * WED 表示每個星期三中午12點
"0 0 12 * * ?" 每天中午12點觸發
"0 15 10 * * ?" 每天上午10:15觸發
"0 * 14 * * ?" 在每天下午2點到下午2:59期間的每1分鐘觸發
"0 0/5 14 * * ?" 在每天下午2點到下午2:55期間的每5分鐘觸發
"0 0/5 14,18 * * ?" 在每天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發
"0 0-5 14 * * ?" 在每天下午2點到下午2:05期間的每1分鐘觸發
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44觸發
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15觸發
"0 15 10 15 * ?" 每月15日上午10:15觸發
"0 15 10 L * ?" 每月最后一日的上午10:15觸發
"0 15 10 ? * 6L" 每月的最后一個星期五上午10:15觸發
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一個星期五上午10:15觸發
"0 15 10 ? * 6#3" 每月的第三個星期五上午10:15觸發
除了使用cron屬性,也可以使用fixedRate來設定應用啟動后多久執行一次,單位為毫秒。
public class BillScheduler {
@Autowired
private BillBatchConfig billBatchConfig;
private static final Logger log = LoggerFactory.getLogger(BillScheduler.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
@Scheduled(initialDelay=10000, fixedRate = 10000)
public void fixedBillBatch() {
log.info("job begin {}", dateFormat.format(new Date()));
billBatchConfig.run();
log.info("job end {}", dateFormat.format(new Date()));
}
@Scheduled(cron="0 15 10 ? * *")
public void fixedTimePerDayBillBatch() {
log.info("job begin {}", dateFormat.format(new Date()));
billBatchConfig.run();
log.info("job end {}", dateFormat.format(new Date()));
}
}
6. 批量監控設計
批量程序寫完了,批量監控也是很大的一塊內容,springbatch已經幫大家實現了大部分的監控功能,能夠對job執行當前情況和歷史情況進行監控并記錄,同時也可以對每個job的所有step執行情況進行監控,對step的順序也可以進行配置,所有的這一切功能都是依賴于springbatch的job repository模塊來實現的,因為工程中使用了mysql作為datasource那么只要springbatch第一次啟動的時候就會在datasource中新建這些用于批量監控的數據表:
這些批量監控表的具體內容就由大家自己去研究吧,我這里就不一一贅述了,如果覺得springbatch監控做的不好,大家也可以自己去實現批量監控功能,自己實現批量監控的話就需要實現一些job和step的listener接口,編寫回調函數,在step和job執行成功后回調這些方法記錄執行情況。如果只是實現簡單批量那么自帶的監控已經夠用了。
小結
這篇springbatch的進階篇主要就是把日常批量程序中經常要用到的功能用springbatch javaconfig的方式實現了一把,對于剛接觸springbatch的同學會有很大幫助,同樣對于原來寫xml配置batch的同學轉javaconfig方式也有很大的幫助,代碼都放到了github上歡迎下載。