Java实时监听远程FTP服务器文件夹变化

文章目录

    • 概要
    • 整体实现流程
    • pom依赖
    • yml配置文件
    • 配置类(便于读取)
    • 注入TaskScheduler用于轮询监听
    • 创建service服务处理
    • 服务实现Impl
    • 触发结果

概要

最近有小伙伴反馈询问 如何通过实时监听远程FTP文件夹的变化并下载到本地指定目录
针对此疑问,出一期解决方案,我在冲浪时也找到了一些比较好的案例,但是追求完美的我,怎能屈服于别人的博客,对此我研究了两天解决方案,
最终得出结论:FTP协议本身不支持实时监听文件变化。可以通过定时轮询的方式来检查目录下的文件列表

整体实现流程

1.连接到FTP服务器
2. 监听指定目录
3.检测文件变化
4.下载变化的文件

OK废话不多说,上代码ftp的搭建我就不说了

pom依赖

	<!-- 用于连接监听FTP文件夹-->
	<dependency>
	    <groupId>commons-net</groupId>
	    <artifactId>commons-net</artifactId>
	    <version>3.9.0</version>
	</dependency>
	
	<!-- lombok插件-日志输出 -->
	<dependency>
	    <groupId>org.projectlombok</groupId>
	    <artifactId>lombok</artifactId>
	</dependency>

yml配置文件

#远程ftp相关配置
ftp:
  # IP
  server: 192.168.1.112
  # 端口号
  port: 21
  # ftp用户名
  user: ftpmonitor
  # ftp密码
  pwd: SolveProblem
  # 用户主目录
  dir: /home/vsftpd/ftpmonitor
  # 下载到本地目录
  localDir: /usr/local/tmp/netty/
  # 轮询监听时间
  sleep: 60000

配置类(便于读取)

import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 读取yml配置文件中FTP相关配置
 *
 * @author wusiwee
 */
@Component
@ConfigurationProperties(prefix = "ftp")
public class FtpConfig {

    /** ip */
    @Getter
    private static String server;

    /** 端口 */
    @Getter
    private static Integer port;

    /** 用户 */
    @Getter
    private static String user;

    /** 密码 */
    @Getter
    private static String pwd;

    /** 服务器路径 */
    @Getter
    private static String dir;

    @Getter
    private static String localDir;

    /** 休眠时间 */
    @Getter
    private static Long sleep;

    public void setServer(String server) {
        FtpConfig.server = server;
    }

    public void setPort(Integer port) {
        FtpConfig.port = port;
    }

    public void setUser(String user) {
        FtpConfig.user = user;
    }

    public void setPwd(String pwd) {
        FtpConfig.pwd = pwd;
    }

    public void setDir(String dir) {
        FtpConfig.dir = dir;
    }

    public void setSleep(Long sleep) {
        FtpConfig.sleep = sleep;
    }
    public void setLocalDir(String localDir) {
        FtpConfig.localDir = localDir;
    }
}

注入TaskScheduler用于轮询监听

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;

/**
 * 轮询任务注入
 * @author wusiwee
 */
@Configuration
public class AppConfig {
    @Bean
    public TaskScheduler taskScheduler() {
        return new ConcurrentTaskScheduler();
    }
}

创建service服务处理

先解释一下为什么要创建Service服务:可以作通用的FTP的login,loginOut处理

import org.apache.commons.net.ftp.FTPClient;
import java.io.IOException;

/**
 * Ftp服务
 * @author wusiwee
 */
public interface FtpService {

    /**
     * ftp登陆
     * @return boolean 是否登陆成功
     * @throws IOException 异常
     * */
    FTPClient login() throws IOException;

    /**
     * ftp登出
     *
     * @param ftpClient 应用
     */
    void loginOut(FTPClient ftpClient);

    /**
     * 实时监听处理远程目录文件
     *
     **/
    void handleFile();
}

服务实现Impl

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.*;
import java.util.concurrent.*;

/**
 * @author wusiwee
 * 监听远程FTP服务器目录变化
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class FtpServiceImplTemp implements FtpService {

    /**
     * 轮询任务处理器
     */
    private final TaskScheduler taskScheduler;
    /**
     * 视频文件service,自行更换为自己的业务
     */
    private final VideoFileService videoFileService;
    /**
     * 全局控制开关
     */
    private volatile boolean isRunning = false;
    /**
     * 自定义线程池,用于处理异步下载
     */
    private final ExecutorService downloadExecutor = new ThreadPoolExecutor(
            1, 1, 30, TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(2),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.DiscardOldestPolicy());

    /**
     * 启动时自定触发此方法,轮询触发任务,1分钟检查一次
     *  taskScheduler 具有异步执行效果,所以启动时不影响主程序运行
     */
    @PostConstruct
    public void init() {
        taskScheduler.scheduleWithFixedDelay(this::handleFile, FtpConfig.getSleep());
    }

    /**
     * ftp登陆
     * @return boolean 是否登陆成功
     * */
    @Override
    public FTPClient login() throws IOException {
        FTPClient ftpClient = new FTPClient();
        ftpClient.connect(FtpConfig.getServer(), FtpConfig.getPort());
        ftpClient.login(FtpConfig.getUser(), FtpConfig.getPwd());
        //设置连接超时时间为60s
        ftpClient.setConnectTimeout(60000);
        //设置数据超时时间为60s
        ftpClient.setDataTimeout(60000);
        // 设置为被动模式
        ftpClient.enterLocalPassiveMode();
        // 设置文件类型为二进制,防止文件损坏
        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
        return ftpClient;
    }

    /**
     * ftp登出
     */
    @Override
    public void loginOut(FTPClient ftpClient) {
        if (ftpClient.isConnected()) {
            try {
                ftpClient.logout();
                ftpClient.disconnect();
            } catch (IOException e) {
                // 处理断开连接时的异常
                log.info("断开连接出现异常:{}",e.getMessage(),e);
            }
        }
    }

    /**
     * 处理文件(启动则触发)
     **/
    @Override
    public void handleFile() {
        log.info("执行ftp下载任务..");
        if (isRunning) {
            log.info("上一个任务未结束..");
            return;
        }
        isRunning = true;
        FTPClient ftpClient = null;
        try {
            ftpClient = login();
            checkDirectory(ftpClient, FtpConfig.getDir());
            loginOut(ftpClient);
            isRunning = false;
        } catch (Exception e){
            isRunning = false;
            // 处理异常
            if (ftpClient != null) {
                loginOut(ftpClient);
            }
            log.info("轮询监听FTP文件出现异常:{}",e.getMessage(),e);
        }
    }

    /**
     * 递归检查文件
     * @param ftpClient ftp服务
     * @param directory 读取的文件
     * @throws IOException IO异常
     */
    private void checkDirectory(FTPClient ftpClient, String directory) throws IOException {
        FTPFile[] files = ftpClient.listFiles(directory);
        for (FTPFile file : files) {
            // 远程目录
            String filePath = directory + "/" + file.getName();
            if (file.isDirectory()) {
                // 递归检查子目录
                checkDirectory(ftpClient, filePath);
            } else {
                // 检查文件是否更新,文件变更时间(Linux)
                long modifiedTime = file.getTimestamp().getTimeInMillis();
                // 此处可以使用Redis和数据库 二选一 进行处理,我选择数据库,根据处理后的唯一标识,文件名,服务器上的操作时间 进行校验判断
                String code = extractString(filePath);
                // 校验文件是否在数据库存在
                boolean b = videoFileService.checkVideoExit(code, file.getName().substring(0,file.getName().lastIndexOf(".")), modifiedTime);
                if (b) {
                    downloadFile(file, filePath, code, modifiedTime);
                }else {
                    log.info("数据已记录..");
                }
            }
        }
    }

    /**
     * 将文件下载到本地
     *
     * @param file     文件
     * @param filePath 文件路径
     * @param code     设备编号
     */
    private void downloadFile(FTPFile file, String filePath, String code, long modifiedTime) {
        downloadExecutor.submit(() -> {
            FTPClient ftpClient = null;
            String localPath = FtpConfig.getLocalDir() + file.getName();
            try (OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(localPath))) {
                // 每个下载任务创建自己的FTP连接
                ftpClient = login();
                boolean success = ftpClient.retrieveFile(filePath, outputStream);
                if (success) {
                    log.info("远程文件下载成功,文件名{}: ", file.getName());
                } else {
                    log.info("文件下载失败: " + file.getName());
                }
            } catch (IOException e) {
                log.error("文件:{} 下载异常:{}", filePath, e.getMessage());
            } finally {
                if (ftpClient != null) {
                    loginOut(ftpClient);
                }
            }
        });
    }
    
    /**
     * 处理远程文件夹附带的设备编号
     *
     * @param path 路径(附带设备编号code)
     * @return 结果
     */
    private static String extractString(String path) {
        String[] parts = path.split("/");
        return parts.length > 5 ? parts[5] : "";
    }

}

触发结果

会自动触发 init()函数 进行轮询监听

人,最可悲的是自大的同时 还不努力变成自大的自己

你可能感兴趣的:(java)