基于 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 启动,注意修改挂载目录为自己的机器目录
1
| 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 池化工具包
完整依赖信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>
<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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| 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;
@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 连接池初始化创建
这个类主要是做连接池的初始配置和初始化创建操作,并且提供给外部连接池对象使用,连接池的配置可以抽出做外部配置,此处直接配到这里了。
注意,此处的预先初始化连接池是异步的,可以根据实际需求修改为同步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| 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;
@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 对象操作的,节省了创建连接的网络开销。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| 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;
@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 连接信息
1 2 3 4 5 6
| ftp: host: 127.0.0.1 port: 21 username: ftpuser password: 123456 ftp-mode: passive
|
直接注入 FtpTemplate
对象即可
1 2
| @Autowired private FtpTemplate ftpTemplate;
|
调用文件上传下载方法进行验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| 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文件下载"); }
}
|