基于 Apache Commons Pool2 和 Hutool 的 FTP 工具类封装 FTP 连接池
一、主要思路
Apache Commons Pool2 提供了两个方便创建通用对象池的类
- 池化对象工厂类:
BasePooledObjectFactory<T>我们只需要继承这个类,然后补充出创建池化对象的方法,以及完善对象销毁、对象验证这些方法即可 - 通用对象池类:
GenericObjectPool<T>这个类可以与BasePooledObjectFactory搭配使用,我们给出 factory 实例对象和对象池的配置信息,即可完成对象池的创建
我们的目标就是把 Ftp 连接对象进行池化,并且保证连接池中对象的连接有效性,就完成了 FTP 连接池的封装。
完整代码:https://github.com/hczs/springboot3-ftp-pool
二、具体实现
2.1 准备 FTP 环境
直接用 docker 启动,注意修改挂载目录为自己的机器目录
docker run -d -v D:\dev\ftp\data:/home/vsftpd -p 20:20 -p 21:21 -p 21100-21110:21100-21110 -e FTP_USER=ftpuser -e FTP_PASS=123456 -e PASV_ADDRESS=127.0.0.1 -e PASV_MIN_PORT=21100 -e PASV_MAX_PORT=21110 --name vsftpd --restart=always fauria/vsftpd
2.2 创建 SpringBoot 项目,引入必要依赖
- lombok 保持代码整洁性
- hutool-extra 和 commons-net 提供 FTP 连接封装相关
- commons-pool2 池化工具包
完整依赖信息如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- ftp工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>5.8.23</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>
<!-- 池化工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.3 FTP 对象工厂类
主要完善对象创建方法 create 对象销毁方法 destroyObject 和对象有效性验证方法 validateObject
package fun.powercheng.ftp;
import cn.hutool.extra.ftp.Ftp;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @author hczs8
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class FtpFactory extends BasePooledObjectFactory<Ftp> {
private final FtpConfig ftpConfig;
@Override
public Ftp create() throws InterruptedException {
log.info("FTP连接中... FTP配置信息: {}", ftpConfig);
// 模拟连接耗时
Thread.sleep(2_000);
Ftp ftp = new Ftp(ftpConfig.getHost(), ftpConfig.getPort(), ftpConfig.getUsername(), ftpConfig.getPassword());
ftp.setMode(ftpConfig.getFtpMode());
// 执行完毕后回到主目录
ftp.setBackToPwd(true);
log.info("FTP连接已创建");
return ftp;
}
@Override
public PooledObject<Ftp> wrap(Ftp ftp) {
return new DefaultPooledObject<>(ftp);
}
@Override
public void destroyObject(PooledObject<Ftp> pooledObject) throws Exception {
log.info("FTP连接销毁");
if (pooledObject == null) {
return;
}
Ftp ftp = pooledObject.getObject();
ftp.close();
}
@Override
public boolean validateObject(PooledObject<Ftp> pooledObject) {
Ftp ftp = pooledObject.getObject();
FTPClient client = ftp.getClient();
try {
return client.sendNoOp();
} catch (IOException e) {
log.error("验证FTP连接失败,FTP连接不可用错误信息:{}", e.getMessage(), e);
}
return false;
}
}
2.4 FTP 连接池初始化创建
这个类主要是做连接池的初始配置和初始化创建操作,并且提供给外部连接池对象使用,连接池的配置可以抽出做外部配置,此处直接配到这里了。
注意,此处的预先初始化连接池是异步的,可以根据实际需求修改为同步。
package fun.powercheng.ftp;
import cn.hutool.extra.ftp.Ftp;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
/**
* @author hczs8
*/
@Slf4j
@Getter
@Component
@RequiredArgsConstructor
public class FtpPoolInitializer {
private GenericObjectPool<Ftp> ftpPool;
private final FtpFactory ftpFactory;
@PostConstruct
public void init() {
GenericObjectPoolConfig<Ftp> poolConfig = new GenericObjectPoolConfig<>();
// 借出和归还的时候都进行有效性验证
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
// 多长时间进行一次后台清理
poolConfig.setTimeBetweenEvictionRuns(Duration.ofMinutes(1L));
// 后台清理时,不能通过有效性检查的对象将回收
poolConfig.setTestWhileIdle(true);
// 最大空闲数
poolConfig.setMaxIdle(10);
// 最小空闲数
poolConfig.setMinIdle(3);
this.ftpPool = new GenericObjectPool<>(ftpFactory, poolConfig);
// 异步初始化连接池,不占用项目启动时间
CompletableFuture.supplyAsync(() -> {
try {
ftpPool.preparePool();
} catch (Exception e) {
log.error("ftp连接池初始化异常,异常信息:{}", e.getMessage(), e);
return false;
}
log.info("FTP连接池 初始化完成");
return true;
});
}
}
2.5 FTP 工具类
这个类是给外部使用的,提供基础的文件上传下载方法,后续需要什么可以进行扩充,并且里面的方法操作都是基于连接池中的 FTP 对象操作的,节省了创建连接的网络开销。
package fun.powercheng.ftp;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.extra.ftp.Ftp;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Optional;
import java.util.function.Function;
/**
* @author hczs8
*/
@Component
@Slf4j
public class FtpTemplate {
private final GenericObjectPool<Ftp> ftpPool;
public FtpTemplate(FtpPoolInitializer ftpPoolInitializer) {
this.ftpPool = ftpPoolInitializer.getFtpPool();
}
private <R> R usePooledFtpConnection(Function<Ftp, R> ftpConsumer) {
Ftp ftp = null;
try {
ftp = ftpPool.borrowObject();
return ftpConsumer.apply(ftp);
} catch (Exception e) {
log.error("从连接池获取 ftp 连接异常,异常信息:{}", e.getMessage(), e);
throw new FtpException(e.getMessage(), e);
} finally {
Optional.ofNullable(ftp).ifPresent(ftpPool::returnObject);
}
}
public boolean upload(String destPath, String fileName, InputStream inputStream) {
log.info("正在上传文件... 目标路径:{} 文件名称:{}", destPath, fileName);
return usePooledFtpConnection(ftp -> ftp.upload(destPath, fileName, inputStream));
}
public byte[] download(String filePath) {
log.info("正在下载文件... 文件路径:{}", filePath);
String fileName = FileUtil.getName(filePath);
String dir = CharSequenceUtil.removeSuffix(filePath, fileName);
ByteArrayOutputStream out = new ByteArrayOutputStream();
return usePooledFtpConnection(ftp -> {
ftp.download(dir, fileName, out);
return out.toByteArray();
});
}
}
2.6 具体使用
-
配置 ftp 连接信息
ftp: host: 127.0.0.1 port: 21 username: ftpuser password: 123456 ftp-mode: passive -
直接注入
FtpTemplate对象即可@Autowired private FtpTemplate ftpTemplate; -
调用文件上传下载方法进行验证
package fun.powercheng.ftp; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.*; import org.junit.platform.commons.util.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; @SpringBootTest @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class Springboot3FtpPoolApplicationTests { @Autowired private FtpTemplate ftpTemplate; @Test @Order(1) void testFtpUpload() { boolean uploadResult = ftpTemplate.upload("/test_dir", "hello.txt", new ByteArrayInputStream("file upload test".getBytes(StandardCharsets.UTF_8))); Assertions.assertTrue(uploadResult, "测试FTP文件上传"); } @Test @Order(2) void testDownload() { byte[] downloadContent = ftpTemplate.download("/test_dir/hello.txt"); String content = new String(downloadContent, StandardCharsets.UTF_8); log.info("download file content: {}", content); Assertions.assertTrue(StringUtils.isNotBlank(content), "测试FTP文件下载"); } }