import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern
import org.apache.commons.lang.StringUtils;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.log4j.Logger;
import org.springframework.util.Assert;
/**
* FTP 工具类
* @author Carl He
*/
public class FtpUtils {
private final Logger logger = Logger.getLogger(FtpUtils.class);
private static final String ENCODING_GBK = "GBK";
private static final String ENCODING_ISO88591 = "ISO-8859-1";
private FTPClient ftpClient;
public FtpUtils(){
ftpClient = new FTPClient();
//设置将过程中使用到的命令输出到控制台
//ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out)));
ftpClient.setControlEncoding(ENCODING_GBK);
}
public FtpUtils(String server, int port, String user, String password, String path){
this();
connectServer(server, port, user, password, path);
}
public FTPClient getFtpClient() {
return ftpClient;
}
/**
* 根据FTP配置连接并登录到FTP服务器
* @param server FTP 服务器地址(IP)
* @param port 端口号
* @param user 用户名
* @param password 密码
* @param path 登录后转向的FTP目录
* @return
* @throws SocketException
* @throws IOException
*/
public boolean connectServer(String server, int port, String user, String password, String path){
StringBuffer sb = new StringBuffer();
sb.append("[ip:").append(server);
sb.append(", port:").append(port);
sb.append(", userName:").append(user);
sb.append(", password:").append(password);
sb.append(", workDirectory:").append(password).append("]");
boolean connectResult = false;
try {
ftpClient.connect(server, port);
if(FTPReply.isPositiveCompletion(ftpClient.getReplyCode())){
connectResult = ftpClient.login(user, password);
}else{
ftpClient.disconnect();
logger.debug("连接FTP" + sb + "失败,返回代码:" + ftpClient.getReplyCode());
}
} catch (Exception e) {
logger.debug(e.getMessage(), e);
}
if (connectResult && path != null && path.length() > 0) {
try {
connectResult = ftpClient.changeWorkingDirectory(path);
} catch (IOException e) {
logger.debug(e.getMessage(), e);
}
}
return connectResult;
}
/**
* 断开与远程 FTP 服务器的连接
* @throws IOException
*/
public void disconnectServer() throws IOException {
if (ftpClient.isConnected()) {
ftpClient.disconnect();
}
}
/**
* 从FTP服务器上下载文件,支持断点续传,上传百分比汇报
* @param remote 远程文件路径
* @param local 本地文件路径
* @return 上传的状态
* @throws IOException
*/
public DownloadStatus download(String remote, String local) throws IOException {
// 设置被动模式
ftpClient.enterLocalPassiveMode();
// 设置以二进制方式传输
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
DownloadStatus result;
// 检查远程文件是否存在
FTPFile[] files = ftpClient.listFiles(new String(remote.getBytes(ENCODING_GBK), ENCODING_ISO88591));
if (files.length != 1) {
logger.info("远程文件 不存在 [" + remote + "]");
return DownloadStatus.Remote_File_Noexist;
}
long lRemoteSize = files[0].getSize();
File f = new File(local);
// 本地存在文件,进行断点下载
if (f.exists()) {
long localSize = f.length();
// 判断本地文件大小是否大于远程文件大小
if (localSize >= lRemoteSize) {
logger.info("本地文件大于远程文件,下载中止");
return DownloadStatus.Local_Bigger_Remote;
}
// 进行断点续传,并记录状态
FileOutputStream out = new FileOutputStream(f, true);
ftpClient.setRestartOffset(localSize);
InputStream in = ftpClient.retrieveFileStream(new String(remote.getBytes(ENCODING_GBK), ENCODING_ISO88591));
byte[] bytes = new byte[1024];
long step = lRemoteSize / 100;
long process = localSize / step;
int c;
while ((c = in.read(bytes)) != -1) {
out.write(bytes, 0, c);
localSize += c;
long nowProcess = localSize / step;
if (nowProcess > process) {
process = nowProcess;
if (process % 10 == 0)
logger.debug("文件 [" + remote + "] 的下载进度:" + process);
// TODO 更新文件下载进度,值存放在process变量中
}
}
in.close();
out.close();
boolean isDo = ftpClient.completePendingCommand();
if (isDo) {
result = DownloadStatus.Download_From_Break_Success;
} else {
result = DownloadStatus.Download_From_Break_Failed;
}
} else {
OutputStream out = new FileOutputStream(f);
InputStream in = ftpClient.retrieveFileStream(new String(remote.getBytes(ENCODING_GBK), ENCODING_ISO88591));
byte[] bytes = new byte[1024];
long step = lRemoteSize / 100;
long process = 0;
long localSize = 0L;
int c;
while ((c = in.read(bytes)) != -1) {
out.write(bytes, 0, c);
localSize += c;
long nowProcess = localSize / step;
if (nowProcess > process) {
process = nowProcess;
if (process % 10 == 0)
logger.debug("下载进度:" + process);
// TODO 更新文件下载进度,值存放在process变量中
}
}
in.close();
out.close();
boolean upNewStatus = ftpClient.completePendingCommand();
if (upNewStatus) {
result = DownloadStatus.Download_New_Success;
} else {
result = DownloadStatus.Download_New_Failed;
}
}
return result;
}
/**
* 下载文件
* @param remoteFileName FTP服务器上的文件路径
* @param localFileName 文件下载后的本地存放路径
* @param ftpConfig ftp配置(供下载发生异常时,记录日志用)
* @return 下载成功返回 true, 否则返回 false
*/
public boolean download(String remoteFileName, String localFileName, String ftpConfig){
boolean flag = false;
OutputStream oStream = null;
File outfile = new File(localFileName);
File outDirectory = outfile.getParentFile();
if(!outDirectory.exists()){
outDirectory.mkdirs();
}
try {
oStream = new FileOutputStream(outfile);
flag = ftpClient.retrieveFile(remoteFileName, oStream);
}catch (IOException e) {
flag = false;
logger.debug(e.getMessage(), e);
} finally {
if(oStream != null){
try {
oStream.close();
} catch (IOException e) {
logger.warn(e.getMessage());
}
}
}
return flag;
}
/**
* 上传文件到FTP服务器,支持断点续传
* @param local 本地文件名称,绝对路径
* @param remote 远程文件路径,使用/home/directory1/subdirectory/file.ext或是 http://www.guihua.org/subdirectory/file.ext 按照Linux上的路径指定方式,支持多级目录嵌套,支持递归创建不存在的目录结构
* @return 上传结果
* @throws IOException
*/
public UploadStatus upload(String local, String remote) throws IOException {
// 设置PassiveMode传输
ftpClient.enterLocalPassiveMode();
// 设置以二进制流的方式传输
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
ftpClient.setControlEncoding(ENCODING_GBK);
UploadStatus result;
// 对远程目录的处理
String remoteFileName = remote;
if (remote.contains("/")) {
remoteFileName = remote.substring(remote.lastIndexOf("/") + 1);
// 创建服务器远程目录结构,创建失败直接返回
if (!createDirectory(remote)) {
return UploadStatus.Create_Directory_Fail;
}
}
// 检查远程是否存在文件
FTPFile[] files = ftpClient.listFiles(new String(remoteFileName.getBytes(ENCODING_GBK), ENCODING_ISO88591));
if (files.length == 1) {
long remoteSize = files[0].getSize();
File f = new File(local);
long localSize = f.length();
if (remoteSize == localSize) {
return UploadStatus.File_Exits;
} else if (remoteSize > localSize) {
return UploadStatus.Remote_Bigger_Local;
}
// 尝试移动文件内读取指针,实现断点续传
result = uploadFile(remoteFileName, f, ftpClient, remoteSize);
// 如果断点续传没有成功,则删除服务器上文件,重新上传
if (result == UploadStatus.Upload_From_Break_Failed) {
if (!ftpClient.deleteFile(remoteFileName)) {
return UploadStatus.Delete_Remote_Faild;
}
result = uploadFile(remoteFileName, f, ftpClient, 0);
}
} else {
result = uploadFile(remoteFileName, new File(local), ftpClient, 0);
}
return result;
}
/**
* 上传文件到服务器,新上传和断点续传
* @param remoteFile 远程文件名,在上传之前已经将服务器工作目录做了改变
* @param localFile 本地文件File句柄,绝对路径
* @param ftpClient FTPClient引用
* @param remoteSize 断点续传时上传文件的开始点
* @return
* @throws IOException
*/
public UploadStatus uploadFile(String remoteFile, File localFile, FTPClient ftpClient, long remoteSize) throws IOException {
UploadStatus status;
// 显示进度的上传
long step = localFile.length() / 100;
long process = 0;
long localreadbytes = 0L;
RandomAccessFile raf = new RandomAccessFile(localFile, "r");
OutputStream out = ftpClient.appendFileStream(new String(remoteFile.getBytes(ENCODING_GBK), ENCODING_ISO88591));
// 断点续传
if (remoteSize > 0) {
ftpClient.setRestartOffset(remoteSize);
process = remoteSize / step;
raf.seek(remoteSize);
localreadbytes = remoteSize;
}
byte[] bytes = new byte[1024];
int c;
while ((c = raf.read(bytes)) != -1) {
out.write(bytes, 0, c);
localreadbytes += c;
if (localreadbytes / step != process) {
process = localreadbytes / step;
System.out.println("上传进度:" + process);
// TODO 汇报上传状态
}
}
out.flush();
raf.close();
out.close();
boolean result = ftpClient.completePendingCommand();
if (remoteSize > 0) {
status = result ? UploadStatus.Upload_From_Break_Success : UploadStatus.Upload_From_Break_Failed;
} else {
status = result ? UploadStatus.Upload_New_File_Success : UploadStatus.Upload_New_File_Failed;
}
return status;
}
/**
* 上传文件
* @param fileName
* @param newName
* @return
* @throws IOException
*/
public boolean uploadFile(String fileName, String newName) throws IOException {
boolean flag = false;
InputStream iStream = null;
try {
iStream = new FileInputStream(fileName);
flag = ftpClient.storeFile(newName, iStream);
} catch (IOException e) {
flag = false;
return flag;
} finally {
if (iStream != null) {
iStream.close();
}
}
return flag;
}
/**
* 上传文件
* @param fileName
* @return
* @throws IOException
*/
public boolean uploadFile(String fileName) throws IOException {
return uploadFile(fileName, fileName);
}
/**
* 上传文件
* @param iStream
* @param newName
* @return
* @throws IOException
*/
public boolean uploadFile(InputStream iStream, String newName) throws IOException {
boolean flag = false;
try {
// can execute [OutputStream storeFileStream(String remote)]
// Above method return's value is the local file stream.
flag = ftpClient.storeFile(newName, iStream);
} catch (IOException e) {
flag = false;
return flag;
} finally {
if (iStream != null) {
iStream.close();
}
}
return flag;
}
/**
* 递归创建远程 FTP 服务器目录
* @param directory 远程服务器目录路径, 如"/a/b" 或 "x/y/z"
* @return 目录创建成功返回true, 否则返回false
* @throws IOException
*/
public boolean createDirectory(String directory) throws IOException {
Assert.hasText(directory);
boolean status = true;
directory = directory.replaceAll("\\\\+", "/").replaceAll("/{2,}", "/");
ftpClient.setControlEncoding("GBK");
String workingDirectory = ftpClient.printWorkingDirectory(); //当前的工作目录
if(ftpClient.changeWorkingDirectory(new String(directory.getBytes(ENCODING_GBK), ENCODING_ISO88591))){
logger.debug("目录 ["+ftpClient.printWorkingDirectory()+"] 已在存在");
}else{
if(directory.startsWith("/")){
ftpClient.changeWorkingDirectory("/");
directory = directory.substring(1);
}
if(directory.endsWith("/")){
directory = directory.substring(0, directory.length()-1);
}
String[] subDirectors = directory.split("/");
String subDirector = null;
for (int i = 0; i < subDirectors.length; i++) {
subDirector = new String(subDirectors[i].getBytes(ENCODING_GBK), ENCODING_ISO88591);
if (!ftpClient.changeWorkingDirectory(subDirector)) {
if(ftpClient.makeDirectory(subDirector) && ftpClient.changeWorkingDirectory(subDirector)){
logger.info("创建目录["+ftpClient.printWorkingDirectory() + "]成功");
} else {
logger.info("创建目录["+ftpClient.printWorkingDirectory() + "/" + subDirector +"]失败");
status = false;
break;
}
}
}
}
ftpClient.changeWorkingDirectory(workingDirectory);
return status;
}
/**
* 删除目录
* @param path
* @return
* @throws IOException
*/
public boolean removeDirectory(String path) throws IOException {
return ftpClient.removeDirectory(path);
}
/**
* 删除目录(及目录下所有文件和子目录)
* @param path
* @param isAll
* @return
* @throws IOException
*/
public boolean removeDirectory(String path, boolean isAll) throws IOException {
if (!isAll) {
return removeDirectory(path);
}
FTPFile[] ftpFileArr = ftpClient.listFiles(path);
if (ftpFileArr == null || ftpFileArr.length == 0) {
return removeDirectory(path);
}
for (FTPFile ftpFile : ftpFileArr) {
String name = ftpFile.getName();
if (".".equals(name) || "..".equals(name)) {
continue;
}
if (ftpFile.isDirectory()) {
logger.info("Delete subPath [" + path + "/" + name + "]");
removeDirectory(path + "/" + name, true);
} else if (ftpFile.isFile()) {
logger.info("Delete file [" + path + "/" + name + "]");
deleteFile(path + "/" + name);
} else if (ftpFile.isSymbolicLink()) {
} else if (ftpFile.isUnknown()) {
}
}
return ftpClient.removeDirectory(path);
}
/**
* 检查目录(或文件)是否存在
* @param path
* @return
* @throws IOException
*/
public boolean existDirectory(String path) throws IOException {
//TODO existDirectory方法可以改进
boolean flag = false;
FTPFile[] ftpFileArr = ftpClient.listFiles(path);
for (FTPFile ftpFile : ftpFileArr) {
if (ftpFile.isDirectory() && ftpFile.getName().equalsIgnoreCase(path)) {
flag = true;
break;
}
}
return flag;
}
/**
* 得到FTP指定目录下的所有文件名列表(不包括目录)
* @param path 如果 path 为 null, 则列出当前所在目录的所有文件名
* @return
* @throws IOException
*/
public List<String> getFileList(String path) throws IOException {
return getFileList(path, null);
}
/**
* 得到FTP指定目录下的符合命名规则的文件名列表(不包括目录)
* @param path 如果 path 为 null, 则列出当前所在目录的所有文件名
* @param regex 文件名的命名规则,即正则表达式, 需要注意的是:此处用正则表达式匹配文件名, 不区分大小写。<br/>如 ".*\.(txt|xml)", 则是只取后缀名为 txt 或 xml 的文件
* @return 如果此文件夹下没有文件,则返回一个长度为 0 的List
* @throws IOException
*/
public List<String> getFileList(String path, String regex) throws IOException {
// listFiles return contains directory and file, it's FTPFile instance listNames() contains directory,
//so using following to filer directory.String[] fileNameArr = ftpClient.listNames(path);
FTPFile[] ftpFiles = StringUtils.isBlank(path) ? ftpClient.listFiles() : ftpClient.listFiles(path);
Pattern pattern = StringUtils.isBlank(regex) ? null : Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
List<String> retList = new ArrayList<String>();
if (ftpFiles == null || ftpFiles.length == 0) {
return retList;
}
String fileName = null;
for (FTPFile ftpFile : ftpFiles) {
fileName = ftpFile.getName();
if (ftpFile.isFile() && (pattern == null || pattern.matcher(fileName).matches())) {
retList.add(fileName);
}
}
return retList;
}
/**
* 删除文件
* @param pathName
* @return
* @throws IOException
*/
public boolean deleteFile(String pathName) throws IOException {
//TODO 调用 existDirectory() 判断文件是否存在
return ftpClient.deleteFile(pathName);
}
/**
* 重命名文件(夹),说明:如果目标文件(夹)存在,则直接覆盖目标文件(夹)
* @param from 原文件名
* @param to 新文件名
* @param fileType 文件类型 [{@link FileType#File}:文件;{@link FileType#Directory}:文件夹]
* @return 如果重命名成功,返回true, 否则返回false
* @throws IOException
* @throws UnsupportedEncodingException
*/
public boolean rename(String from, String to, FileType fileType) throws UnsupportedEncodingException, IOException{
Assert.hasText(from);
Assert.hasText(to);
boolean status = true;
from = from.replaceAll("\\\\+", "/").replaceAll("/{2,}", "/");
to = to.replaceAll("\\\\+", "/").replaceAll("/{2,}", "/");
String from_1 = new String(from.getBytes(ENCODING_GBK), ENCODING_ISO88591);
String to_1 = new String(to.getBytes(ENCODING_GBK), ENCODING_ISO88591);
if(fileType == FileType.File){
status = (ftpClient.listFiles(from_1).length == 1);
}else if(fileType == FileType.Directory){
String workingDirectory = ftpClient.printWorkingDirectory();
if(status = ftpClient.changeWorkingDirectory(from_1)){
ftpClient.changeWorkingDirectory(workingDirectory);
}
}else{
logger.warn("不支持的文件类型,文件类型(fileType)的值只能是 1(文件) 或 2(文件夹)");
return false;
}
if(!status){
logger.info("远程文件(夹)不存在 [" + from + "]");
return false;
}
status = ftpClient.rename(from_1, to_1);
int i = -1;
if(!status && (i = to.lastIndexOf('/')) > 0){
createDirectory(to.substring(0, i));
status = ftpClient.rename(from_1, to_1);
}
return status;
}
public static void main(String[] args) {
FtpUtils ftpUtils = new FtpUtils();
ftpUtils.connectServer("61.152.201.12", 21, "cnstock", "cnstock!@#", "/xml");
try {
FTPClient client = ftpUtils.getFtpClient();
System.out.println(ftpUtils.ftpClient.printWorkingDirectory());
InputStream is = client.retrieveFileStream("a/bak/0113101410_24917203.xml");
// File ruleFile = new File("E:\\myEclipseWorkspace\\media360_ftpget_Google\\config\\rule-hailiang.xml");
// News_Document d = (News_Document) DigesterLoader.createDigester(ruleFile.toURI().toURL()).parse(is);
// System.out.println(d.getDocHeadline());
is.close();
ftpUtils.ftpClient.completePendingCommand();
ftpUtils.rename("a/bak/0113101410_24917203.xml", "a/bak//1.xml", FileType.File);
System.out.println("*************************************************");
System.out.println(ftpUtils.ftpClient.printWorkingDirectory());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
ftpUtils.disconnectServer();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("============");
}
public static enum UploadStatus {
Create_Directory_Fail, // 远程服务器相应目录创建失败
Create_Directory_Success, // 远程服务器闯将目录成功
Upload_New_File_Success, // 上传新文件成功
Upload_New_File_Failed, // 上传新文件失败
File_Exits, // 文件已经存在
Remote_Bigger_Local, // 远程文件大于本地文件
Upload_From_Break_Success, // 断点续传成功
Upload_From_Break_Failed, // 断点续传失败
Delete_Remote_Faild; // 删除远程文件失败
}
public enum DownloadStatus {
Remote_File_Noexist, // 远程文件不存在
Local_Bigger_Remote, // 本地文件大于远程文件
Download_From_Break_Success, // 断点下载文件成功
Download_From_Break_Failed, // 断点下载文件失败
Download_New_Success, // 全新下载文件成功
Download_New_Failed; // 全新下载文件失败
}
public enum FileType {
File, // 文件
Directory, // 文件夹
}
}
相关推荐
"util"目录通常包含一些工具类,提供通用的功能,比如数据转换、日志记录、线程管理等,这些工具类在整个FTP程序中会被多个组件复用。 "gui"目录很可能包含了图形用户界面的代码,即使界面设计简单,也包含了一些...
`ftp4j`是一个功能丰富的Java FTP客户端库,它提供了一套完整的API,方便开发者在Java应用程序中集成FTP功能。 `ftp4j`类库的主要特性包括: 1. **连接与断开**:`FtpClient`类是ftp4j的核心,通过`connect()`方法...
`java.util.concurrent`包提供了线程池的相关工具类,如`ExecutorService`。 8. **TCP与UDP的区别** TCP提供的是面向连接的服务,而UDP则是无连接的、不可靠的数据报服务。TCP适合对可靠性要求高的应用,如HTTP、...
1. **基础类库**:这是Java编程的基础,包括`java.lang`、`java.util`、`java.io`等核心包。`java.lang`包提供了所有Java程序的基本功能,如`String`、`Object`和`System`类。`java.util`包提供了集合框架、日期时间...
2. **数据文件压缩**:在Java中,可以使用如`java.util.zip`包中的`ZipOutputStream`和`GZIPOutputStream`类来对数据文件进行压缩。`ZipOutputStream`用于创建ZIP格式的压缩文件,而`GZIPOutputStream`则用于创建...
1. **核心类库**:包括`java.lang`、`java.io`、`java.util`、`java.net`等包,这些都是Java开发的基础。例如,`java.lang`包包含了所有Java程序的基本类,如`String`、`Object`和`System`;`java.io`包提供了输入/...
在Java中,`java.util.Base64`类提供了编码和解码功能。在将中文文件名转换为TAR包之前,可以先将文件名用BASE64编码,然后在服务器端解码后再进行打包操作。 最后,关于标签"remote服务器",这表明我们在讨论的...
在Java编程中,FTP...总结来说,Java中FTP文件上传和下载主要依赖于Apache Commons Net库,而文件的压缩和解压则利用了Java内置的`java.util.zip`包。这些技术在开发需要处理文件传输和数据存储的应用时非常实用。
在JDK 1.6版本中,API涵盖了多个核心包,如`java.lang`、`java.util`、`java.io`和`java.net`等。这些包提供了基本的编程元素和设施,如对象创建、集合框架、输入/输出流以及网络通信等功能。例如: 1. `java.lang`...
ftp工具类,构造方法初始化类,连接ftp,列出所有文件内容,下载文件
在Java中,可以使用`java.util.concurrent`包下的`ExecutorService`、`Future`和`Callable`等接口与类来实现多线程任务的管理和执行。 **断点续传** 断点续传是指在下载过程中如果因某种原因中断(如网络问题或电脑...
6. **日志管理**:为了方便排查问题,最好在脚本中包含日志记录部分,如使用`java.util.logging`库或第三方日志框架(如Log4j、SLF4J)。这样,程序运行的日志会被记录下来,有助于调试和故障排查。 7. **监控与...
同时,为了跟踪上传进度和调试,可以使用日志记录工具,如Log4j或java.util.logging。 总的来说,Java通过Apache Commons Net库为开发者提供了简单易用的接口来实现FTP文件上传。通过理解这些类和方法,开发者可以...
要实现定时从FTP服务器下载文件,可以利用Java的定时框架,如`java.util.Timer`或`java.concurrent.ScheduledExecutorService`。设置定时任务,周期性检查FTP服务器是否有新文件。 下面是一个使用`ftp4j`实现的...
在Java中,可以使用`java.util.Properties`类来读取和处理这些配置信息。通过`Properties.load()`加载配置文件,然后用键值对的形式获取FTP服务器的详细信息。 在实际开发中,为了提高程序的健壮性和用户体验,还...
为了实现定时上传和下载,可以使用Java的定时任务框架,如`java.util.Timer`或`java.util.concurrent.ScheduledExecutorService`。使用ScheduledExecutorService的例子如下: ```java ScheduledExecutorService ...
为了实现同步,可以使用`java.util.concurrent`包中的工具,如`ExecutorService`和`Future`,或者使用Java的并发API来控制线程。在上传文件块的线程中,定期更新进度条的线程安全状态,然后在UI线程中刷新进度条的...
java FTP 开发工具包 examples examples.nntp examples.ntp org.apache.commons.net org.apache.commons.net.bsd org.apache.commons.net.chargen org.apache.commons.net.daytime org.apache.commons.net....
- `java.util.concurrent`包提供了一系列工具类,如`ExecutorService`,便于管理和调度并发任务。 8. **性能优化**: - 为避免频繁的网络通信,搜索引擎可能缓存部分FTP服务器的信息,例如最近访问过的目录或文件...
可以使用`java.util.Properties`类来读取这些配置信息,并创建相应的下载线程。 ```java Properties props = new Properties(); props.load(new FileInputStream("config.properties")); List<String> ...