1 前言
最近项目中用到了FTP,于是自己写了一个FTP工具类,现将其分享出来,供大家借鉴使用。
FTP工具类的实现可以分为两部分:
基于apache commons-pool2的ObjectPool接口构建了ftpClientPool(ftpClient对象池),以达到FTPClient的复用,减少频繁创建FTPClient对象而造成的性能开销;
FTP工具类中的方法从ftpClientPool中获取FTPClient,可以实现线程安全的并发访问FTP服务器。
现将其实现细节做如下分享。
2 添加依赖与ftp连接参数
在springboot项目中添加依赖:
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
<!--添加项目需要的其他依赖...-->
在配置文件application.properties中添加ftp连接参数
#ftp config
ftp.enabled=true
ftp.host=localhost
ftp.port=21
ftp.username=ftpuser
ftp.password=ftpuser
3 添加FTP配置类
FTPClient配置类如下:
import javax.annotation.PreDestroy;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import com.wxyh.springbootdemo.common.utils.FtpUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* FTP配置类
*
* @author wxyh
*/
@Slf4j
@Configuration
@ConditionalOnClass({GenericObjectPool.class, FTPClient.class})
@ConditionalOnProperty(value = "ftp.enabled", havingValue = "true")
@EnableConfigurationProperties(FTPConfiguration.FtpConfigProperties.class)
public class FTPConfiguration {
private ObjectPool<FTPClient> pool;
public FTPConfiguration(FtpConfigProperties props) {
// 默认最大连接数与最大空闲连接数都为8,最小空闲连接数为0
// 其他未设置属性使用默认值,可根据需要添加相关配置
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
poolConfig.setTestWhileIdle(true);
poolConfig.setMinEvictableIdleTimeMillis(60000);
poolConfig.setSoftMinEvictableIdleTimeMillis(50000);
poolConfig.setTimeBetweenEvictionRunsMillis(30000);
pool = new GenericObjectPool<>(new FtpClientPooledObjectFactory(props), poolConfig);
preLoadingFtpClient(props.getInitialSize(), poolConfig.getMaxIdle());
// 初始化ftp工具类中的ftpClientPool
FtpUtil.init(pool);
}
/**
* 预先加载FTPClient连接到对象池中
* @param initialSize 初始化连接数
* @param maxIdle 最大空闲连接数
*/
private void preLoadingFtpClient(Integer initialSize, int maxIdle) {
if (initialSize == null || initialSize <= 0) {
return;
}
int size = Math.min(initialSize.intValue(), maxIdle);
for (int i = 0; i < size; i++) {
try {
pool.addObject();
} catch (Exception e) {
log.error("preLoadingFtpClient error...", e);
}
}
}
@PreDestroy
public void destroy() {
if (pool != null) {
pool.close();
log.info("销毁ftpClientPool...");
}
}
/**
* Ftp配置属性类,建立ftpClient时使用
*/
@Data
@ConfigurationProperties(prefix = "ftp")
static class FtpConfigProperties {
private String host = "localhost";
private int port = FTPClient.DEFAULT_PORT;
private String username;
private String password;
private int bufferSize = 8096;
/**
* 初始化连接数
*/
private Integer initialSize = 0;
}
/**
* FtpClient对象工厂类
*/
static class FtpClientPooledObjectFactory implements PooledObjectFactory<FTPClient> {
private FtpConfigProperties props;
public FtpClientPooledObjectFactory(FtpConfigProperties props) {
this.props = props;
}
@Override
public PooledObject<FTPClient> makeObject() throws Exception {
FTPClient ftpClient = new FTPClient();
try {
ftpClient.connect(props.getHost(), props.getPort());
ftpClient.login(props.getUsername(), props.getPassword());
log.info("连接FTP服务器返回码{}", ftpClient.getReplyCode());
ftpClient.setBufferSize(props.getBufferSize());
ftpClient.setControlEncoding(props.getEncoding());
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode();
return new DefaultPooledObject<>(ftpClient);
} catch (Exception e) {
log.error("建立FTP连接失败", e);
if (ftpClient.isAvailable()) {
ftpClient.disconnect();
}
ftpClient = null;
throw new Exception("建立FTP连接失败", e);
}
}
@Override
public void destroyObject(PooledObject<FTPClient> p) throws Exception {
FTPClient ftpClient = getObject(p);
if (ftpClient != null && ftpClient.isConnected()) {
ftpClient.disconnect();
}
}
@Override
public boolean validateObject(PooledObject<FTPClient> p) {
FTPClient ftpClient = getObject(p);
if (ftpClient == null || !ftpClient.isConnected()) {
return false;
}
try {
ftpClient.changeWorkingDirectory("/");
return true;
} catch (Exception e) {
log.error("验证FTP连接失败::{}", ExceptionUtils.getStackTrace(e));
return false;
}
}
@Override
public void activateObject(PooledObject<FTPClient> p) throws Exception {
}
@Override
public void passivateObject(PooledObject<FTPClient> p) throws Exception {
}
private FTPClient getObject(PooledObject<FTPClient> p) {
if (p == null || p.getObject() == null) {
return null;
}
return p.getObject();
}
}
}
4 FtpUtil工具类
FtpUtil工具类代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.pool2.ObjectPool;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.util.Assert;
import lombok.extern.slf4j.Slf4j;
/**
* Ftp工具类
* @author wxyh
*/
@Slf4j
public class FtpUtil {
/**
* ftpClient连接池初始化标志
*/
private static volatile boolean hasInit = false;
/**
* ftpClient连接池
*/
private static ObjectPool<FTPClient> ftpClientPool;
/**
* 初始化ftpClientPool
*
* @param ftpClientPool
*/
public static void init(ObjectPool<FTPClient> ftpClientPool) {
if (!hasInit) {
synchronized (FtpUtil.class) {
if (!hasInit) {
FtpUtil.ftpClientPool = ftpClientPool;
hasInit = true;
}
}
}
}
/**
* 读取csv文件
*
* @param remoteFilePath 文件路径(path+fileName)
* @param header 列头
* @return
* @throws IOException
*/
public static List<CSVRecord> readCsvFile(String remoteFilePath, String... headers) throws IOException {
FTPClient ftpClient = getFtpClient();
try (InputStream in = ftpClient.retrieveFileStream(encodingPath(remoteFilePath))) {
return CSVFormat.EXCEL.withHeader(headers).withSkipHeaderRecord(false)
.withIgnoreSurroundingSpaces().withIgnoreEmptyLines()
.parse(new InputStreamReader(in, "utf-8")).getRecords();
} finally {
ftpClient.completePendingCommand();
releaseFtpClient(ftpClient);
}
}
/**
* 按行读取FTP文件
*
* @param remoteFilePath 文件路径(path+fileName)
* @return
* @throws IOException
*/
public static List<String> readFileByLine(String remoteFilePath) throws IOException {
FTPClient ftpClient = getFtpClient();
try (InputStream in = ftpClient.retrieveFileStream(encodingPath(remoteFilePath));
BufferedReader br = new BufferedReader(new InputStreamReader(in))) {
return br.lines().map(line -> StringUtils.trimToEmpty(line))
.filter(line -> StringUtils.isNotEmpty(line)).collect(Collectors.toList());
} finally {
ftpClient.completePendingCommand();
releaseFtpClient(ftpClient);
}
}
/**
* 获取指定路径下FTP文件
*
* @param remotePath 路径
* @return FTPFile数组
* @throws IOException
*/
public static FTPFile[] retrieveFTPFiles(String remotePath) throws IOException {
FTPClient ftpClient = getFtpClient();
try {
return ftpClient.listFiles(encodingPath(remotePath + "/"),
file -> file != null && file.getSize() > 0);
} finally {
releaseFtpClient(ftpClient);
}
}
/**
* 获取指定路径下FTP文件名称
*
* @param remotePath 路径
* @return ftp文件名称列表
* @throws IOException
*/
public static List<String> retrieveFileNames(String remotePath) throws IOException {
FTPFile[] ftpFiles = retrieveFTPFiles(remotePath);
if (ArrayUtils.isEmpty(ftpFiles)) {
return new ArrayList<>();
}
return Arrays.stream(ftpFiles).filter(Objects::nonNull)
.map(FTPFile::getName).collect(Collectors.toList());
}
/**
* 编码文件路径
*/
private static String encodingPath(String path) throws UnsupportedEncodingException {
// FTP协议里面,规定文件名编码为iso-8859-1,所以目录名或文件名需要转码
return new String(path.replaceAll("//", "/").getBytes("GBK"), "iso-8859-1");
}
/**
* 获取ftpClient
*
* @return
*/
private static FTPClient getFtpClient() {
checkFtpClientPoolAvailable();
FTPClient ftpClient = null;
Exception ex = null;
// 获取连接最多尝试3次
for (int i = 0; i < 3; i++) {
try {
ftpClient = ftpClientPool.borrowObject();
ftpClient.changeWorkingDirectory("/");
break;
} catch (Exception e) {
ex = e;
}
}
if (ftpClient == null) {
throw new RuntimeException("Could not get a ftpClient from the pool", ex);
}
return ftpClient;
}
/**
* 释放ftpClient
*/
private static void releaseFtpClient(FTPClient ftpClient) {
if (ftpClient == null) {
return;
}
try {
ftpClientPool.returnObject(ftpClient);
} catch (Exception e) {
log.error("Could not return the ftpClient to the pool", e);
// destoryFtpClient
if (ftpClient.isAvailable()) {
try {
ftpClient.disconnect();
} catch (IOException io) {
}
}
}
}
/**
* 检查ftpClientPool是否可用
*/
private static void checkFtpClientPoolAvailable() {
Assert.state(hasInit, "FTP未启用或连接失败!");
}
/**
* 上传Excel文件到FTP
* @param workbook
* @param remoteFilePath
* @throws IOException
*/
public static boolean uploadExcel2Ftp(Workbook workbook, String remoteFilePath)
throws IOException {
Assert.notNull(workbook, "workbook cannot be null.");
Assert.hasText(remoteFilePath, "remoteFilePath cannot be null or blank.");
FTPClient ftpClient = getFtpClient();
try (OutputStream out = ftpClient.storeFileStream(encodingPath(remoteFilePath))) {
workbook.write(out);
workbook.close();
return true;
} finally {
ftpClient.completePendingCommand();
releaseFtpClient(ftpClient);
}
}
/**
* 从ftp下载excel文件
* @param remoteFilePath
* @param response
* @throws IOException
*/
public static void downloadExcel(String remoteFilePath, HttpServletResponse response)
throws IOException {
String fileName = remoteFilePath.substring(remoteFilePath.lastIndexOf("/") + 1);
fileName = new String(fileName.getBytes("GBK"), "iso-8859-1");
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
FTPClient ftpClient = getFtpClient();
try (InputStream in = ftpClient.retrieveFileStream(encodingPath(remoteFilePath));
OutputStream out = response.getOutputStream()) {
int size = 0;
byte[] buf = new byte[10240];
while ((size = in.read(buf)) > 0) {
out.write(buf, 0, size);
out.flush();
}
} finally {
ftpClient.completePendingCommand();
releaseFtpClient(ftpClient);
}
}
}