背景
运营中心、安管中心需要定时采集A系统数据,A系统负责推送十几种类型的数据文件到SFTP服务器,为了减少调度系统的开发成本,采用基于Linux服务器的crontab定时工具类实现定时调度,同时,缩小不同任务的开发部署成本,采用SpringBoot命令行应用方式开发,整体架构

Crontab
Crontab 是一个用于在 Unix 和类 Unix 系统上执行定期任务的工具。它使用一个名为 crontab 的文件,该文件包含了一系列的任务和它们的执行时间。每个用户都可以有自己的 crontab 文件,系统也有一个系统级的 crontab 文件(通常位于 /etc/crontab 或 /etc/cron.d/ 目录下)
标准的crontab表达式由五部分组成,分别表示:分钟 小时 日期 月分 星期几,功能场景:数据库备份、日志清理、系统邮件、系统监控、SSL证书检查、API健康检查等
表达式说明
- 分钟
| 取值范围 | 说明 |
|---|
| 0-59 | 表示在当前小时的第几分钟执行任务 |
| * | 每分钟都执行 |
| */N | 每N分钟执行一次(*/5就是每五分钟执行一次) |
| m1,m2,… | 指定多个分钟执行(5,10,15就是在第5,10,15分钟执行) |
| m1-m2 | 指定一个范围内的分钟(5-10表示从第5到第10分钟执行) |
- 小时
| 取值范围 | 说明 |
|---|
| 0-23 | 表示在当天的第几小时执行任务 |
| * | 每小时都执行 |
| */N | 每N小时执行一次(*/5就是每五小时执行一次) |
| h1,h2,… | 指定多个小时执行(5,10,15就是在第5,10,15小时执行) |
| h1-h2 | 指定一个范围内的小时(5-10表示从第5到第10小时执行) |
- 日期
| 取值范围 | 说明 |
|---|
| 1-31 | 表示在当月的第几天执行任务 |
| * | 每天都执行 |
| */N | 每N天执行一次(*/5就是每五天执行一次) |
| d1,d2,… | 指定多个小时执行(5,10,15就是在5号,10号,15号执行) |
| d1-d2 | 指定一个范围内的日期(5-10表示从5号到10号执行) |
- 月份
| 取值范围 | 说明 |
|---|
| 1-12 | 表示在当年的第几月执行任务 |
| * | 每月都执行 |
| */N | 每N月执行一次(*/5就是每五个月执行一次) |
| M1,M2,… | 指定多个月份执行(5,10,15就是在5月,10月,15月执行) |
| M1-M2 | 指定一个范围内的月份(5-10表示从5月到10月执行) |
- 星期
| 取值范围 | 说明 |
|---|
| 0-6 | 表示在星期几执行任务,0代表星期日 |
| * | 每天都执行 |
| */N | 每N天执行一次(*/5就是每五天执行一次) |
| w1,w2,… | 指定多个星期执行(0,2,4就是在星期日,星期二,星期四执行) |
| w1-w2 | 指定一个范围内的星期几(0-5表示从星期日到星期五执行) |
常用命令
crontab -e 修改当前用户的crontab文件
crontab -l 显示当前用户的定时任务
crontab -r 删除当前用户的crontab文件
crontab filename 从指定文件加载crontab配置
常见表达式
* * * * * /path/to/script.sh 每分钟执行,常用于测试
0 * * * * /path/to/script.sh 每小时第0分钟执行
0 8 * * * /path/to/script.sh 每天早上8点执行,常用于日常任务
0 2 * * * /path/to/backup.sh 每天凌晨2点执行,常用于备份任务
0 9 * * 1 /path/to/weekly-report.sh 每周一早上9点执行 常用于周报
0 0 1 * * /path/to/monthly-task.sh 每月1号凌晨执行,常用于月度任务
*/5 * * * * /path/to/monitor.sh 每5分钟执行,常用于监控脚本
*/30 * * * * /path/to/sync.sh 每30分钟执行,常用于同步任务
0 * * * 1-5 /path/to/workday-task.sh 工作日每小时执行,常用于自动化办公
30 9,14,18 * * * /path/to/reminder.sh 每天9.30,14.30,16.30执行,特殊时间点
0 0 1 1,4,7,10 * /path/to/quarterly.sh 每季度第一天执行
0 0 1 1 * /path/to/yearly-cleanup.sh 每年1月1日执行
特殊表达式
@reboot /path/to/startup.sh
@yearly /path/to/yearly.sh
@monthly /path/to/monthly.sh
@weekly /path/to/weekly.sh
@daily /path/to/daily.sh
@hourly /path/to/hourly.sh
SFTP
SFTP(SSH File Transfer Protocol,SSH 文件传输协议)是一种基于 SSH(Secure Shell)的文件传输协议,用于在客户端和服务器之间安全地传输文件,Linux服务器默认安装

package org.example.util;import com.jcraft.jsch.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;public class SftpClient {private static Logger log = LoggerFactory.getLogger(SftpClient.class);public static boolean uploadFile(String sftpIp, String sftpPort, String user, String pwd, int connectionTimeout, String localPath, String fileName, String uploadPath) {log.info("开始上传文件");Session session = null;ChannelSftp channelSftp = null;boolean result = false;try{//获取SFTP连接log.info("尝试获取SFTP连接");session = getSession(sftpIp, sftpPort, user, pwd, connectionTimeout);//打开sftp通道channelSftp = (ChannelSftp) session.openChannel("sftp");channelSftp.connect();log.info("SFTP通道连接成功");//判断远程目录是否存在,不存在创建目录try{channelSftp.cd(uploadPath);} catch (SftpException e) {log.info("远程目录不存在,开始创建目录");createRemoteDir(channelSftp,uploadPath);channelSftp.cd(uploadPath);}//上传文件File localFile = new File(localPath +File.separator+ fileName);InputStream inputStream = new FileInputStream(localFile);channelSftp.put(inputStream,fileName);log.info("上传文件成功");result = true;} catch (Exception e){log.error("上传文件失败,{}",e.getMessage());}finally {//关闭连接if(channelSftp != null && channelSftp.isConnected()){channelSftp.disconnect();}if(session != null && session.isConnected()){session.disconnect();}log.info("连接关闭");}return result;}private static void createRemoteDir(ChannelSftp channelSftp, String uploadPath) throws SftpException {String[] dirs = uploadPath.split("/");dirs[0] = ""; // 去除第一个空字符串StringBuilder currentDir = new StringBuilder();for (String dir : dirs) {if(dir.isEmpty()){continue;}currentDir.append("/").append(dir);try{channelSftp.cd(currentDir.toString());} catch (SftpException e) {//目录不存在,创建channelSftp.mkdir(currentDir.toString());log.info("创建目录成功:{}",currentDir);}}}private static Session getSession(String sftpIp, String sftpPort, String user, String pwd, int connectionTimeout) {//创建JSch实例JSch jsch = new JSch();Session session;try{//创建会话session = jsch.getSession(user,sftpIp, Integer.parseInt(sftpPort));log.debug("session created...");session.setPassword(pwd);//配置SSH选项,设置跳过主机密钥检查Properties config = new Properties();config.put("StrictHostKeyChecking", "no");session.setConfig(config);session.connect(connectionTimeout);log.debug("session connected...");} catch (JSchException e) {log.error("获取SFTP连接失败,{}",e.getMessage());throw new RuntimeException("获取SFTP连接失败");}return session;}public static boolean downloadFile(String sftpIp, String sftpPort, String user, String pwd, int connectionTimeout, String remotePath, String fileName, String s) {log.info("开始下载文件");Session session = null;ChannelSftp channelSftp = null;boolean result = false;try{//获取SFTP连接log.info("尝试获取SFTP连接");session = getSession(sftpIp, sftpPort, user, pwd, connectionTimeout);//打开sftp通道channelSftp = (ChannelSftp) session.openChannel("sftp");channelSftp.connect();log.info("SFTP通道连接成功");//切换到远程目录channelSftp.cd(remotePath);//下载文件File localFile = new File(s + File.separator + fileName);channelSftp.get(fileName, localFile.getAbsolutePath());log.info("下载文件成功");result = true;} catch (Exception e){log.error("下载文件失败,{}",e.getMessage());}finally {//关闭连接if (channelSftp != null && channelSftp.isConnected()) {channelSftp.disconnect();}if (session != null && session.isConnected()) {session.disconnect();}log.info("连接关闭");}return result;}
}
SpringBoot命令行应用
命令行式SpringBoot应用,专门用来执行单次任务后退出的模式,适用于批处理、定时任务、数据迁移等场景
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import service.TaskService;import java.util.Arrays;
import java.util.HashSet;@SpringBootApplication
@Slf4j
public class TaskApplication {/*** 定义退出码常量,分别为成功、业务异常、系统异常*/private static final int EXIT_SUCCESS_CODE = 0;private static final int EXIT_BUSINESS_ERROR_CODE = 1;private static final int EXIT_SYSTEM_ERROR_CODE = 2;public static void main(String[] args) {int exitCode = EXIT_SUCCESS_CODE;ConfigurableApplicationContext applicationContext = null;log.info("任务开始执行...");try{//启动spring上下文,这里做最小化启动applicationContext = minApplicationContext();//执行具体任务TaskService taskService = applicationContext.getBean(TaskService.class);boolean success = taskService.executeTask();if(!success){log.error("任务执行失败...");exitCode = EXIT_BUSINESS_ERROR_CODE;}}catch (Exception e){log.error("任务执行异常..."+e.getMessage());exitCode = EXIT_SYSTEM_ERROR_CODE;}finally {//资源请理colse(applicationContext);System.exit(exitCode);}}private static void colse(ConfigurableApplicationContext applicationContext) {if(null != applicationContext && applicationContext.isActive()){try{applicationContext.close();log.info("应用上下文已关闭...");}catch (Exception e){log.error("应用上下文关闭异常..."+e.getMessage());}}}private static ConfigurableApplicationContext minApplicationContext() {SpringApplication application = new SpringApplication(TaskApplication.class);//减少不必要的组件扫描//非web应用application.setWebApplicationType(WebApplicationType.NONE);//关闭bannerapplication.setBannerMode(Banner.Mode.OFF);//自定义配置,只加载必要的beanapplication.setSources(new HashSet<>(Arrays.asList("service.TaskService","service.impl.TaskServiceImpl")));return application.run();}
}