基于Apache Commons Pool2封装FTP连接池

基于 Apache Commons Pool2 和 Hutool 的 FTP 工具类封装 FTP 连接池

一、主要思路

Apache Commons Pool2 提供了两个方便创建通用对象池的类

  1. 池化对象工厂类:BasePooledObjectFactory<T> 我们只需要继承这个类,然后补充出创建池化对象的方法,以及完善对象销毁、对象验证这些方法即可
  2. 通用对象池类: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 项目,引入必要依赖

  1. lombok 保持代码整洁性
  2. hutool-extra 和 commons-net 提供 FTP 连接封装相关
  3. 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>

<!-- 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

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;

/**
* @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 连接池初始化创建

这个类主要是做连接池的初始配置和初始化创建操作,并且提供给外部连接池对象使用,连接池的配置可以抽出做外部配置,此处直接配到这里了。

注意,此处的预先初始化连接池是异步的,可以根据实际需求修改为同步。

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;

/**
* @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 对象操作的,节省了创建连接的网络开销。

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;

/**
* @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 具体使用

  1. 配置 ftp 连接信息

    1
    2
    3
    4
    5
    6
    ftp:
    host: 127.0.0.1
    port: 21
    username: ftpuser
    password: 123456
    ftp-mode: passive
  2. 直接注入 FtpTemplate 对象即可

    1
    2
    @Autowired
    private FtpTemplate ftpTemplate;
  3. 调用文件上传下载方法进行验证

    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文件下载");
    }

    }