java实现从FTP服务器上获取图片,并转化为base64编码的定义以及所遇到的坑
1、博客描述
该篇博客主要写的是自己用java从ftp服务器上获取图片,并把图片流转化为base64编码的过程中,所遇到的一些问题。
2、需求描述
需要从18台FTP服务器上获取图片数据,并进行封装,大概是每台FTP的根目录下有9个文件夹,每个文件夹下面有32张图片,数据量是 18932=5184张图片
3、代码
实现:首先考虑的是用一个定时线程,轮询遍历18台ftp服务器,获取图片,但是考虑到该方法只适合数据量较小的情况,所以最后决定使用18个定时线程,去获取图片。每遍历一次就创建一个定时线程,每个线程单独工作。
下面是FtpUtil工具类,获取图片流,并转化为base64编码
import com.alibaba.fastjson.JSONObject;
import com.uniview.entity.FtpInfoEntity;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class FTPUtil {
/**
* 登陆ftp服务器
*/
public FTPClient loginFtp(FtpInfoEntity ftpInfoEntity){
FTPClient ftpClient = new FTPClient();
//设置超时时间
ftpClient.setDefaultTimeout(90 * 1000);
ftpClient.setConnectTimeout(90 * 1000);
ftpClient.setDataTimeout(90 * 1000);
try {
ftpClient.connect(ftpInfoEntity.getFtpIp(), ftpInfoEntity.getFtpPort());
ftpClient.login(ftpInfoEntity.getFtpUserName(), ftpInfoEntity.getFtpPassword());
//是否成功登陆服务器
int reply = ftpClient.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)){
log.error("{}登陆FTP服务器失败...", ConstantUtil.ERROR_LOG);
ftpClient.disconnect();
}
}catch (Exception e){
log.error("{}登陆FTP服务器失败:{}", ConstantUtil.ERROR_LOG, e);
}
return ftpClient;
}
/**
* 下载图片
* @param ftpClient
* @param path
* @return
*/
public Map<String, JSONObject> downloadPic(FTPClient ftpClient, String path){
Map<String, JSONObject> picInfoMap = new HashMap<>();
try{
if(null != ftpClient && !StringUtils.isBlank(path)){
String relPath = path;
String base64 = "";
boolean changeWorkDir = ftpClient.changeWorkingDirectory(path);
if (changeWorkDir){
//切换目录成功
//设置被动模式,防止在Linux上,由于安全限制,可能某些端口没有开启,出现阻塞
ftpClient.enterLocalPassiveMode();
//读取图片需要添加该方法
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
//遍历下载的目录
FTPFile[] ftpFiles = ftpClient.listFiles();
if (null == ftpFiles || 0 == ftpFiles.length){
log.info("{}该目录{}下为空...", ConstantUtil.INFO_LOG, path);
return picInfoMap;
}
for (FTPFile ftpFile : ftpFiles){
//判断该文件是否为文件夹
if (ftpFile.isDirectory()){
log.info("{}{}该文件是文件夹...", ConstantUtil.INFO_LOG, ftpFile.getName());
relPath = path + ftpFile.getName() + "/";
log.info("relPath是:{}", relPath);
Map<String, JSONObject> childMap = downloadPic(ftpClient, relPath);
for (String key : childMap.keySet()){
picInfoMap.put(key, childMap.get(key));
}
}else if (ftpFile.isFile()) {
//解决中文乱码问题,两次解码
byte[] bytes = ftpFile.getName().getBytes("UTF-8");
String fileName = new String(bytes, "iso-8859-1");
//获取时间到 20200709200509
String lastModifyTimeStr = ftpClient.getModificationTime(fileName);
InputStream retrieveFileStream = null;
ByteArrayOutputStream outputStream = null;
try {
//获取文件流
retrieveFileStream = ftpClient.retrieveFileStream(fileName);
if (null != retrieveFileStream) {
byte[] data = null;
outputStream = new ByteArrayOutputStream();
//TimeUnit.MILLISECONDS.sleep(300);
data = new byte[1024];
int len = 0;
while (-1 != (len = retrieveFileStream.read(data))) {
outputStream.write(data, 0, len);
}
if (null != outputStream){
data = outputStream.toByteArray();
Base64.Encoder encoder = Base64.getEncoder();
base64 = encoder.encodeToString(data);
}else {
log.error("{}输出流为空,不能转化为base64位编码...", ConstantUtil.ERROR_LOG);
}
JSONObject dataJson = new JSONObject();
dataJson.put("base64", base64);
dataJson.put("time", lastModifyTimeStr);
picInfoMap.put(fileName, dataJson);
//删除该文件
//文件路径
boolean b = ftpClient.deleteFile(fileName);
if (!b) {
log.error("{}删除图片{}异常...", ConstantUtil.ERROR_LOG, fileName);
} else {
log.info("{}删除文件成功:{}", ConstantUtil.INFO_LOG, fileName);
}
}else {
log.error("{}数据流为空...", ConstantUtil.ERROR_LOG);
}
}catch (Exception e){
log.error("{}读取图片异常:{}", ConstantUtil.ERROR_LOG, e);
}finally {
if (null != outputStream){
outputStream.flush();
outputStream.close();
}
if (null != retrieveFileStream){
retrieveFileStream.close();
ftpClient.completePendingCommand();
}
}
}
}
}
}
}catch (IOException e) {
log.error("{}FTP操作文件异常:{}", ConstantUtil.ERROR_LOG, e);
}
return picInfoMap;
}
/**
* 退出登陆和连接
* @param ftpClient
*/
public void logout(FTPClient ftpClient){
if (ftpClient.isConnected()){
try {
ftpClient.logout();
ftpClient.disconnect();
} catch (IOException e) {
log.error("{}关闭ftp服务器连接异常:{}", ConstantUtil.ERROR_LOG, e);
}
}
}
}
@Data
@ToString
public class FtpInfoEntity implements Serializable {
/**
* 编号
*/
private String id;
/**
* FTP的IP
*/
private String ftpIp;
/**
* FTP的端口
*/
private int ftpPort;
/**
* FTP的用户名
*/
private String ftpUserName;
/**
* FTP的密码
*/
private String ftpPassword;
/**
* 文件所在的位置
*/
private String path;
}
4、遇到的问题及解决方法
在实现中,遇到了好多问题,下面 一 一 列举。
4.1 使用ftpClient.retrieveFileStream()方法的坑
开始编码时,使用了该方法,但是没有调用 ftpClient.completePendingCommand();
该方法,导致程序读取一张图片后就失败了,通过百度发现,使用 ftpClient.retrieveFileStream()
方法批量处理文件时,需要在其流关闭后,调用 ftpClient.completePendingCommand();
该方法。最后添加上去了,程序运行成功。
在每次执行完下载操作之后,completePendingCommand()会一直在等FTP Server返回226 Transfer complete,但是FTP Server只有在接受到InputStream 执行close方法时,才会返回。所以一定先要执行close方法。不然在第一次下载一个文件成功之后,之后再次获取inputStream 就会返回null。
来源:JAVA实现FTP多文件下载,说一说ftpClient.retrieveFileStream方法遇到的坑
4.2 ftpClient.enterLocalPassiveMode();的问题
开始编码测试的时候,我没有使用线程,写了一个测试类,进行测试,能很快的获取到一台ftp服务器上的所有数据,没有任何异常产生。但是当我把测试类的方法运用到项目中的时候,用单线程去执行,发现读了几张图片后,线程就不执行工作了,也没有异常抛出;任何转化为debugbug模式调试,一步一步的调试,发现程序又是正常的,没有任何问题,也没有卡死。这是什么原因呢?百度了好多和请教了同事,最后觉得有问题的地方是读取文件太慢了,程序执行过快,导致了某些异常。最后在获取流后,线程休眠了0.3秒,任何再执行,没有任何问题,执行成功。
重点:我把 ftpClient.enterLocalPassiveMode();
该方法开始的时候写在了ftpClient.login()
的前面
当我连接18台ftp服务器(模拟的18台ftp服务器)进行测试时,18个定时线程都启动成功,并且都在处理数据,但是有几个定时线程在处理的过程中“卡死”了,没有捕获到异常,很难定位。。。。。。。 同时该定时线程之后就不再工作了,一度怀疑是自己的设计方案有问题,但是18个线程,只有几个线程有问题,其它十几个线程都没有问题,这就没有怀疑自己的设计方案了。同时觉得有可能是 服务器性能的问题,或者是我本机性能的问题,不足以支撑这么多线程(35个线程以上),但是没有18台FTP服务器进行测试,减少了线程数量,还是出现了问题同样的问题,思考中。。。。。请教中。。。。。。。
最后怀疑是不是我写的下载方法有问题呢?然后百度、查看同事之前写的代码,发现 ftpClient.enterLocalPassiveMode();
该方法都写在了ftpClient.login()
的后面
最后尝试着把该方法换了个位置,谢天谢地,终于好了。。。。。。。。。。。。。
我不知道这个的原因是啥,反正是成功的捕获到异常了。。。。。。。
当执行18个定时线程时,某个线程出现问题了,能捕获到异常了,线程虽然 在某一步 “死” 了,但是处理异常后,不妨碍下一次的定时操作。。。。。。。。。
4.3 捕获的异常
捕获的异常主要有三个:
-
切换目录异常
-
读取文件异常
-
获取文件时间异常
捕获的异常:
java.net.SocketException: Connection reset by peer: socket write error
java.net.SocketException: Connection reset
org.apache.commons.net.ftp.FTPConnectionClosedException: Connection closed without indication.
以上异常还没有解决,预估是性能问题,因为我是在两台 ftp服务器的根目录下各建了九个文件夹,一共18个文件夹,模拟18台ftp服务器。如有知道该异常的朋友,请告知,谢谢!