基于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 启动,注意修改挂载目录为自己的机器目录

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 池化工具包

完整依赖信息如下:

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

  1. 配置 ftp 连接信息

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

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

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

基于Apache Commons Pool2封装FTP连接池
https://www.powercheng.fun/articles/a53b9572/
作者
powercheng
发布于
2023年11月16日
许可协议