前言:项目中数据传输使用到了文件传输平台,类似于一个FTP服务器。所以需要使用到FTP协议进行文件下载,原本的计划是使用SFTP下载,但是出于各种原因没有按计划执行,反正都一样。下面来讲一下具体需求和实现。
该项目为银行的总账系统,顾名思义;总账系统主要用于查账,调账和预估收益,而这些账单数据则是由各外围系统(其他系统)传递过来,所以就有了文件传输平台,因而需要文件下载。
原本需求:各外围系统不定时的将文件上传至FTP服务器(文件传输平台)的指定目录,总账系统实时下载这些文件到自己的应用服务器,而总账有两台集群的应用服务器。那么该将如何从FTP服务器下载文件呢?于是就有了下面的两种方案。注意:文件下载程序是为了后续的导数程序提供数据文件,总账系统数据采用的是oracle RAC双机热备方案(即两台oracle服务器共享存储)。
对于方案2来说需要注意文件下载程序和导数程序有可能运行在不同的服务器上。譬如说A服务器运行了文件下载程序,B服务器运行导数程序时发现没有需要导入的文件,那么整个系统就会出现一种“死锁状态”,所有需要确保文件下载程序在两台服务器上都运行。而基于现有的条件无法实现,所以最终选择的是第一种方案。
对于FTP文件下载,市面上有着很多成熟的工具,此处选择Apache提供的一个工具包——commons-net.jar,目前最新版本是3.8.0。使用起来也是很简单,具体下载参考下面代码:
/**
* 1、简单任务示例(Bean模式)
*/
@XxlJob("downFtpFileJobHandler")
public void downFtpFileJobHandler() {
String acct = "20220222";
FTPClient ftpClient = new FTPClient();
// 指定本地下载目录
String localPath = "D:\\Users\\zbw\\Desktop\\snbank\\" + acct + "\\";
File localDir = new File(localPath);
if(!localDir.exists()){
localDir.mkdir();
}
try {
ftpClient.connect("192.168.0.102", 21);
boolean loginSuccess = ftpClient.login("xxx", "123456");
if (!loginSuccess) throw new RuntimeException("登录文件服务器失败。");
String fileDir = "/CBS/" + acct + "/";
FTPFile[] ftpFiles = ftpClient.listFiles(fileDir);
for (FTPFile ftpFile : ftpFiles) {
String fileName = ftpFile.getName();
//获取所有的ok文件批次
if(fileName.endsWith(".ok")){
//截取批次号
String bathid = fileName.substring(0, fileName.lastIndexOf("."));
//下载 该批次的 dat文件
fileName = bathid + ".dat";
download(ftpClient, fileDir + fileName, localPath + fileName);
//下载 该批次的 chk文件
fileName = bathid + ".chk";
download(ftpClient, fileDir + fileName, localPath + fileName);
//下载 该批次的 ok文件
fileName = bathid + ".ok";
download(ftpClient, fileDir + fileName, localPath + fileName);
}
}
System.out.println("下载完成");
ftpClient.logout();
} catch (Exception e) {
XxlJobHelper.log(e);
XxlJobHelper.handleFail(e.getMessage());
e.printStackTrace();
}
}
/**
*
* @param ftpClient
* @param ftpPath FTP文件路径
* @param localPath 本地文件路径
* @return
*/
private void download(FTPClient ftpClient, String ftpPath, String localPath){
File file = new File(localPath);
//文件 已存在 不需要下载
if (file.exists()) {
System.out.println(localPath + " 文件已存在。" );
return;
}
OutputStream os = null;
try {
os = new FileOutputStream(localPath);
System.out.println("开始下载文件:" + localPath);
//ftpClient.setControlEncoding(Control_Encoding);
ftpClient.setFileTransferMode(FTP.STREAM_TRANSFER_MODE);
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode(); //被动模式,提高数据传输速度
ftpClient.setBufferSize(1024 * 1024 * 10); //设置缓冲区为10M,提高速度
//下载前先判断 文件是否存在 不存在就不要下载了 否则会生产一个空文件
InputStream inputStream = ftpClient.retrieveFileStream(ftpPath);
if (inputStream == null || ftpClient.getReplyCode() == 550) {
System.out.println(ftpPath + " 文件不存在");
return;
}
if (inputStream != null) {
inputStream.close();
ftpClient.completePendingCommand(); // 必须执行,否则在循环检查多个文件时会出错
}
//下载
boolean result = ftpClient.retrieveFile(ftpPath, os);
System.out.println(result ? "下载成功" : "下载失败");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
需要注意的是FTPClient默认使用主动模式。
主动模式:客户端连接服务端时,向服务端申请开辟一个端口,专门用于通信,即客户端主动向服务端发起的请求。aip:ftpClient.enterLocalActiveMode();
被动模式:一开始服务一起来,服务端就会开启一个端口告诉客户端端,我们之间的通讯就在这个端口下。也就客户端被动的接受服务端。api:ftpClient.enterLocalPassiveMode()
使用主动模式还是被动模式需要注意FTP服务器端口是否开放,作者在使用主动模式下载文件时经常遇到文件下载不全或者下载的文件为空的情况,使用改为被动模式后就未曾出现,还有网络波动也会对文件下载的结果产生影响。
思路1:下载文件时只需判断目标文件在本地是否已存在即可,如果存在则不下载,不存在才下载。
思路2:文件下载记录应该保存到一个下载记录表中,如果目标文件的下载记录表中已经存在该表中则不进行下载,如果不存在才下载,如果追求严谨则可以再结合本地是否存在和下载记录是否存在进行判断是否需要下载。
我个人比较倾向于思路2的实现,因为思路2更方便运维,可以不用进入服务器就能知道文件下载情况,但是现有的实现方式是基于思路1。
在本次功能开发中,其实核心并不是技术实现。并不是说我学会了使用Apache提供的commons-net.jar工具进行文件下载上传就算有了大收获,而是说我通过这次学习的过程能收获到了什么。譬如,根据需求给出多个方案,是否还有更好的方案,这套方案实现成本如何,工作量大不大。对技术实现中出现的问题进行分析,思考出现问题的原因,并找除解决办法等等。远比在百度随便一搜强,像这种api在网上随便一搜遍地都是,而且写就直接能够运行起来,注释也很详细,如果只是简单的使用完全可以copy and paste,但是这并不能让你学会或者学的更好,而且后期如果出现问题,一时也无法解决。