代码以及注释在最下面
这是一个分布式文件监听与同步系统,核心功能为:
FileListener.scanFtp
)@Scheduled(fixedDelay = 1000 * 30)
每30秒执行一次。ftpClientUtilProperties.getDeviceLogin()
),每个设备对应不同的FTP连接参数(如雷达、激光雷达等)。ftpClientUtil.getRemoteDirFtpFiles()
获取FTP目录下的文件列表。ftpClientUtil.clear()
)并重试最多5次,确保网络波动时的稳定性。ReUtil.isMatch()
筛选符合命名规则的文件(如Z_URAD_CR_54399_20220501000000
)。cTime - dataTime.getTime() < 24h
),避免历史数据干扰。getLocalDestPath()
根据设备类型、站点号、日期等生成层级目录(如/data/54399/cloud_radar/2022/05/01
),便于分类存储。.check
标记文件,若存在则跳过下载。.check
文件,作为下载完成的凭证。<10B
)时视为无效文件,等待下次同步。54399
),通过syncService.getDeviceId()
查询数据库或Redis缓存,转换为内部设备ID。KEY_LAST_TIME
记录各站点的最新文件时间,确保按时间顺序处理数据。FileDto
对象,包含设备标签、站点号、文件路径等元数据。mco_filelog_{device}
,不同设备数据隔离,便于下游按需消费。{"deviceTag":"cloud_radar", "stationNum":"54399", "absPath":"/data/.../Z_URAD_CR_54399_20220501000000"}
。SyncService.history
)@Async
注解实现异步执行,避免阻塞实时任务。20220501.zip
)到临时目录。FTPClientUtil
接口)ProducerFactory
模式管理FTP客户端,支持多设备并行连接。get()
)、上传(put()
)、移动(move()
)、删除(deleteFile()
)。getRemoteDirFtpFiles()
)和路径检查(isRemotePathExist()
)。KafkaTemplate
)KafkaTemplate
,支持同步/异步发送、事务管理。executeInTransaction()
确保消息发送与文件下载的原子性。FTP设备配置(FTPClientUtilProperties.LoginInfo
):
device
:设备类型标识(如微波辐射计、云雷达)。homePath
:FTP服务器上的监控目录。regexFilter
:文件名正则表达式(如.*\.DAT
),用于过滤目标文件。本地存储路径:
Const.PATH_FILE
:基础存储目录(如/data
)。城市ID/站点号/设备类型/年/月/日
,便于按时空维度管理。Redis键设计:
KEY_LAST_TIME
:记录各站点的最新文件时间戳,格式为last_time_{站号}_{设备类型}
。DEVICE_FIRST_DEVICEID_PIDCODE_KEY
:缓存站点与设备ID的映射关系,减少数据库查询。Kafka配置:
mco_filelog_{device}
,按设备类型隔离消息。FileDto
对象转为JSON字符串发送。.check
文件标记下载完成,防止重复处理。parallelStream()
)加速文件过滤与下载。@Async
)与实时任务解耦。
/**
* 定时同步文件目录,已经下载的文件存入Redis,4天之后过期
*/
@Scheduled(fixedDelay = 1000*30)
public void scanFtp() {
//查询每种设备
for (FTPClientUtilProperties.LoginInfo loginInfo : ftpClientUtilProperties.getDeviceLogin()) {
String device = loginInfo.getDevice();
String homePath = loginInfo.getHomePath();
if(StrUtil.isBlank(homePath)){
homePath = "";
}
try {
int retryTimes = 5;
//获取根目录下的文件
List ftpFiles = null;
try {
ftpFiles = ftpClientUtil.getRemoteDirFtpFiles(device, homePath);
}catch (Exception e){
log.error( device+":获取文件失败,清理连接池并重试!",e);
ftpClientUtil.clear(device);
ftpFiles = ftpClientUtil.getRemoteDirFtpFiles(device, homePath);
}
while (ftpFiles.size() == 0 && retryTimes > 0){
logger.info("{}获取文件数量为0,开始清理连接池并重试,剩余重试次数:{}。",device,retryTimes);
ftpClientUtil.clear(device);
ftpFiles = ftpClientUtil.getRemoteDirFtpFiles(device, homePath);
retryTimes--;
}
Set rawNames = ftpFiles.stream().map(FTPFile::getName).filter(fileName-> ReUtil.isMatch(loginInfo.getRegexFilter(),fileName.toUpperCase())).collect(Collectors.toSet());
logger.info("{}总计{}个文件,匹配{}个文件。",device,ftpFiles.size(),rawNames.size());
//过滤一天内的文件
Set names = Collections.synchronizedSet(new HashSet<>());
long cTime = System.currentTimeMillis();
int lastMil = 1 * 24 * 60 * 60 * 1000;
rawNames.parallelStream().forEach(e->{
Date dataTime = getDataTime(e,device);
if(dataTime == null){
return;
}
if(cTime-dataTime.getTime()< lastMil){
names.add(e);
}
});
logger.info("{},一天内共{}个文件。",device,names.size());
//下载不同的文件
String finalHomePath = homePath;
names.parallelStream().forEach(fileName->{
try {
String localDestPath = getLocalDestPath(device, fileName);
File checkDestFile = new File(localDestPath+File.separator+fileName+".check");
//判断文件是否在本地目录
if(FileUtil.exist(checkDestFile)){
log.debug("文件已存在本地目录:{}",fileName);
return;
}
//构建文件信息对象,用于向dpc发送
String stationNum = getStationNum(fileName);
Integer deviceId = syncService.getDeviceId(device, stationNum);
if(deviceId == null){
log.debug("设备ID不存在,不下载此文件:{}",fileName);
return;
}
//从ftp服务器下载文件,下载后的文件名与ftp服务器上的文件名一致
ftpClientUtil.get(device,localDestPath, finalHomePath +fileName);
File destFile = new File(localDestPath+File.separator+fileName);
//文件太小就不同步,等待下一次同步
if(destFile.length() < 10){
return;
}
//在本地生成一个检查文件是否存在的文件
checkDestFile.createNewFile();
//记录最后到达时间,last_time_{站号}_{设备类型:例如 Const.TAG_WBFS} 示例:last_time_54399_wbfs
Date dataTime = getDataTime(fileName, device);
//判断是否是衢州站,衢州站的云雷达,激光雷达,微波辐射计,是世界时要向后加8小时
dataTime = McoUtil.getFileDataTime(stationNum,dataTime);
ArrayList stringStationList = McoUtil.getHzStationList();
//杭州地区改为世界时需要添加8小时 云雷达与微波辐射计
if(stringStationList.contains(stationNum) && (Const.TAG_CLOUDRADA.equals(device) || Const.TAG_WBFS.equals(device))){
dataTime = DateUtil.offsetHour(dataTime,8) ;
}
//获取记录的最新时间,避免读取历史数据导致时间错乱
String lastTimeKey = Const.KEY_LAST_TIME + stationNum.toUpperCase() + "_" + device;
Object lastTimeStr = RedisUtil.get(lastTimeKey);
if(lastTimeStr != null){
DateTime lastTime = DateUtil.parseDateTime(lastTimeStr.toString());
if(lastTime.getTime() allGroup0 = ReUtil.findAllGroup0("(20\\d\\d\\d{0,10})", fileName);
DateTime parse;
if(allGroup0.size() == 0){
String[] nameArr = fileName.split("_");
parse = DateUtil.parse(nameArr[4]);
}else{
parse = DateUtil.parse(allGroup0.get(0));
}
if(Const.TAG_WPR_RADAR.equals(device) || Const.TAG_CDWL.equals(device) ){
parse = DateUtil.offsetHour(parse,8);
}
return parse;
}
}
package com.smart.service;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.ZipUtil;
import cn.hutool.json.JSONUtil;
import com.smart.common.Const;
import com.smart.common.dto.FileDto;
import com.smart.ftp.FTPClientUtil;
import com.smart.ftp.FTPClientUtilProperties;
import com.smart.listener.FileListener;
import com.smart.model.Device;
import com.smart.redis.RedisUtil;
import org.apache.commons.net.ftp.FTPFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipFile;
/**
* 文件同步类
*
*/
@Service
public class SyncService {
private final static Logger logger = LoggerFactory.getLogger(SyncService.class);
private final static String DEVICE_FIRST_DEVICEID_PIDCODE_KEY = "device_first_deviceId_pidCode_key_";
@Autowired
private KafkaTemplate kafkaTemplate;
@Autowired
FTPClientUtilProperties ftpClientUtilProperties;
@Autowired
private FTPClientUtil ftpClientUtil;
/**
* 异步下载历史数据文件,并触发解析
*
*/
@Async
public synchronized void history(List dates){
for (Date date : dates) {
try {
//服务端和本地文件的日期路径和文件前缀
String datePath = DateUtil.format(date, "yyyy/MM/dd/");
String filePrefix = DateUtil.format(date, "yyyyMMdd");
//遍历每一个设备
for (FTPClientUtilProperties.LoginInfo loginInfo : ftpClientUtilProperties.getDeviceLogin()) {
String homePath = loginInfo.getHomePath();
String device = loginInfo.getDevice();
String remotePath = homePath + "/"+ datePath + filePrefix + ".zip";
String zipDestPath = Const.PATH_TEMP_FILE+ "/"+device+ "/"+datePath+ filePrefix + ".zip";
try {
//获取压缩包
ftpClientUtil.get(device,zipDestPath, remotePath);
//解压并删除压缩包
File zipFile = new File(zipDestPath);
ZipUtil.unzip(zipFile);
FileUtil.del(zipFile);
//列出解压后的所有文件
List dataFiles = FileUtil.loopFiles(zipFile.getParentFile());
//过滤所需的文件
Set filterFiles = dataFiles.stream().filter(file-> ReUtil.isMatch(loginInfo.getRegexFilter(),file.getName())).collect(Collectors.toSet());
//每一个文件都要先分拣,再发送kafka通知dpc处理
for (File dataFile : filterFiles) {
//分拣的目标地址
String localDestPath = FileListener.getLocalDestPath(device, dataFile.getName());
File destPath = new File(localDestPath);
FileUtil.move(dataFile, destPath,true);
String fileName = destPath.getName();
//构建文件信息对象,用于向dpc发送
String stationNum = FileListener.getStationNum(fileName);
Integer deviceId = getDeviceId(device, stationNum);
FileDto fileDto = new FileDto();
fileDto.setDeviceTag(device);
fileDto.setDeviceId(deviceId);
fileDto.setCityId(Const.CITY_ID);
fileDto.setStationNum(stationNum);
fileDto.setAbsPath(destPath.getAbsolutePath());
kafkaTemplate.send("mco_filelog_"+device, JSONUtil.toJsonStr(fileDto));
}
//执行完毕之后删除解压后的文件,此时有用的文件已经挪走了
FileUtil.del(zipFile.getParentFile());
//记录当前日期已经下载过了
RedisUtil.set(Const.KEY_HISTORY_DATE+date,true);
} catch (Exception e) {
e.printStackTrace();
}
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 获取具体设备号
* @param pidCode
* @param stationNum
* @return
*/
public Integer getDeviceId(String pidCode,String stationNum){
Object deviceId = RedisUtil.get(DEVICE_FIRST_DEVICEID_PIDCODE_KEY + pidCode + stationNum);
if (null != deviceId) {
return Integer.parseInt(deviceId.toString());
} else {
Device res = new Device().findFirst("SELECT d.id FROM device d INNER JOIN device_category c ON c.id = d.dcId INNER JOIN device_category cc ON cc.id = c.pid "
+ "INNER JOIN station s ON s.id = d.stationId WHERE s.num = ? AND cc.`code` = ?", stationNum, pidCode);
if (res == null) {
return null;
}
RedisUtil.set(DEVICE_FIRST_DEVICEID_PIDCODE_KEY + pidCode + stationNum, res.getId());
return res.getId();
}
}
public Integer test(String device,String homePath) {
try {
logger.info("{}:{}",device,homePath);
List ftpFiles = ftpClientUtil.getRemoteDirFtpFiles(device, homePath);
if(ftpFiles.size() == 0){
ftpClientUtil.clear(device);
}
return ftpFiles.size();
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}
package com.smart.ftp;
import org.apache.commons.net.ftp.FTPFile;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
/**
*
*/
public interface FTPClientUtil {
/**
* 上传文件到ftp服务器
*
* @param localInputStream 本地输入流,即需要传输到服务器的数据
* @param remotePathUri ftp服务器目录
* @param remoteFilename 上传后,在ftp服务器上的文件名
* @param suffix 文件上传时添加的文件后缀,上传完成时,重名为name; suffix 为null 时,则不加后缀
* @throws Exception 连接不到ftp服务器,登录失败,文件上传失败等异常
*/
void put(
String device,
InputStream localInputStream,
String remotePathUri,
String remoteFilename,
String suffix) throws Exception;
/**
* 上传文件到ftp服务器 重载put()函数,将InputStream参数变为File
*
* @param localFile 本地文件,File类型参数
* @param remoteDir ftp服务器目录
* @param remoteFilename 上传后,在ftp服务器上的文件名
* @param suffix 文件上传时添加的文件后缀,上传完成时,重名为name; suffix 为null 时,则不加后缀
* @throws Exception 连接不到ftp服务器,登录失败,文件上传失败等异常
*/
void put(
String device,
File localFile,
String remoteDir,
String remoteFilename,
String suffix) throws Exception;
/**
* 上传文件到ftp服务器 重载put()函数,将InputStream参数变为 本地文件路径
*
* @param localFileAbsolutePathUri 本地文件路径
* @param remoteDir ftp 服务器目录
* @param remoteFilename 上传后,在ftp服务器上的文件名
* @param suffix 文件上传时添加的文件后缀,上传完成时,重名为name; suffix 为null 时,则不加后缀
* @throws Exception 连接不到ftp服务器,登录失败,文件上传失败等异常
*/
void put(
String device,
String localFileAbsolutePathUri,
String remoteDir,
String remoteFilename,
String suffix) throws Exception;
/**
* 从ftp服务器下载文件,并写入到本地outputStream中
*
* @param localOutputStream 本地保存数据的输出流, 即下载的数据将写入该输出流
* @param remoteDir ftp 服务器目录
* @param remoteFilename 要下载的文件名
* @throws Exception ftp连接,登录失败,传输失败等异常。
*/
void get(String device,OutputStream localOutputStream, String remoteDir, String remoteFilename) throws Exception;
/**
* 从ftp服务器下载文件
*
* @param localFile 本地文件, File类型参数
* @param remoteDir ftp 服务器目录
* @param remoteFilename 要下载的文件名
* @throws Exception
*/
void get(String device,File localFile, String remoteDir, String remoteFilename) throws Exception;
/**
* 从ftp服务器下载文件, 下载后的文件名与ftp服务器上的文件名一致
*
* @param localFileDir 本地路径
* @param remoteDir ftp 服务器目录
* @param remoteFilename 要下载的文件名
* @throws Exception
*/
void get(String device,String localFileDir, String remoteDir, String remoteFilename) throws Exception;
/**
* 从ftp服务器下载文件,下载后的文件名与ftp服务器上的文件名一致
*
* @param localDir 本地路径
* @param remoteFilePathUri ftp服务器上文件的完整路径名(包括文件名)
* @throws Exception
*/
void get(String device,String localDir, String remoteFilePathUri) throws Exception;
/**
* ftp 服务器文件move
*
* @param remoteSrcDir FTP远程源目录
* @param remoteDestDir FTP远程目的目录
* @param remoteFilename FTP远程文件名
* @throws Exception 连接不到ftp服务器,登录失败,文件上传失败等异常
*/
void move(String device,String remoteSrcDir, String remoteDestDir, String remoteFilename) throws Exception;
/**
* ftp 服务器文件copy
*
* @param remoteSrcDir FTP远程源目录
* @param remoteDestDir FTP远程目的目录
* @param remoteSrcFilename FTP远程文件名
* @param suffix 后缀
* @param remoteNewFilename 重命名后名字(为空则不重命名)
* @throws Exception 连接不到ftp服务器,登录失败,文件上传失败等异常
*/
void copy(String device,
String remoteSrcDir,
String remoteDestDir,
String remoteSrcFilename,
String suffix,
String remoteNewFilename) throws Exception;
/**
* 刪除ftp服務器上的文件
*
* @param remoteFileAbsolutePathUri 文件在ftp服務器上的全路徑(包括文件名)
* @return true 成功刪除, false 刪除失敗。
* @throws Exception
*/
boolean deleteFile(String device,String remoteFileAbsolutePathUri) throws Exception;
/**
* 刪除ftp服務器上的目录(必须确保删除执行该方法时,需删除的目录下不能存在文件否则删除失败)
*
* @param remoteDir 文件夹路径
* @return true 成功刪除, false 刪除失敗。
* @throws Exception
*/
boolean removeDirectory(String device,String remoteDir) throws Exception;
/**
* 获取ftp服务目录下文件或目录名,
*
* @param remoteDir ftp服务器路径
* @return 注意返回的路径是全路径名
* @throws Exception
*/
List getRemoteDirFilenames(String device,String remoteDir) throws Exception;
/**
* 获取ftp服务器目录下的文件或目录
*
* @param remoteDir ftp服务器路径
* @return 返回 FTPFile对象, FTPFile对象可能是文件或目录,
* 通过FTPFile.isFile或FTPFile.isDirectory判断,获取单独的文件通过FTPFile.getName()函数获取
* @throws Exception
* @Description: 获取ftp服务器目录下的文件或目录
*/
List getRemoteDirFtpFiles(String device,String remoteDir) throws Exception;
/**
* 获取远程ftp目录中的所有的文件
*
* @param remoteDir 用户访问ftp路径
* @param recursive 是否扫描子文件夹
* @return 文件列表
* @throws Exception
*/
List getRemoteFileByDir(String device,String remoteDir, boolean recursive) throws Exception;
/**
* 获取远程ftp目录中的所有的文件
*
* @param remoteDir 用户访问ftp路径
* @return 文件列表
* @throws Exception
*/
List getRemoteFileByDir(String device,String remoteDir) throws Exception;
/**
* 判断远程ftp目录或文件是否存在
* @param remotePath
* @return
*/
boolean isRemotePathExist(String device,String remotePath) throws Exception;
void clear(String device);
}