springboot实现FTP服务器的上传下载同步删除

声明,此文章参考了各个文章,作为一个新手,有的地方可能存在纰漏,在异常捕获方面没有实施,各位可以进行参考,自行完善

首先pom文件引入依赖

 <!--commons-net 此依赖用于上传文件至 ftp 之类的服务器  单纯使用 commons-net 的 FTP 功能,原则上只导入 commons-net 依赖即可-->
        <dependency>
            <groupId>commons-net</groupId>
            <artifactId>commons-net</artifactId>
            <version>3.6</version>
        </dependency>

        <!-- 引入 Apache 的 commons-lang3 包,方便操作字符串-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8</version>
        </dependency>

        <!-- 引入 Apache commons-io 包,方便操作文件-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>

完整的pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>house</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>house</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
        <mybatis.version>2.1.3</mybatis.version>
        <mybatis-plus.version>3.3.2</mybatis-plus.version>
        <druid.version>1.2.3</druid.version>
        <lombok.version>1.18.16</lombok.version>
    </properties>

    <dependencies>

        <!--用于达梦数据库代码生成的需要的依赖-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>

        <!--达梦数据库驱动-->
        <dependency>
            <groupId>com.dm</groupId>
            <artifactId>Dm7JdbcDriver</artifactId>
            <version>1.7</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/Dm7JdbcDriver18.jar</systemPath>
        </dependency>


        <!--commons-net 此依赖用于上传文件至 ftp 之类的服务器  单纯使用 commons-net 的 FTP 功能,原则上只导入 commons-net 依赖即可-->
        <dependency>
            <groupId>commons-net</groupId>
            <artifactId>commons-net</artifactId>
            <version>3.6</version>
        </dependency>

        <!-- 引入 Apache 的 commons-lang3 包,方便操作字符串-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8</version>
        </dependency>

        <!-- 引入 Apache commons-io 包,方便操作文件-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>


        <!--阿里巴巴的easyExcel实现导入导出Excel-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.1.6</version>
        </dependency>


        <!--导出PDF文件需要的依赖 pdf start-->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.10</version>
        </dependency>

        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>
        <!--pdf end-->

        <!--spring-web 使用会开启tomcat-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

        <!--SpringBoot整合的thymeleaf 有这个才能使用templates文件夹下的html  实现html代码的复用-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

        <!--Spring Validation ,验证请求参数的格式 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

        <!-- Lombok 在实体类进行@Data的注解 自动使用get set toString等方法-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- Druid数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!-- Mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.version}</version>
        </dependency>
        <!-- Mybatis Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <!-- SpringBoot单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>${spring-boot.version}</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 代码生成器 达梦数据库也需要-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <!-- 代码生成器辅助 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>

        <!--jstl依赖 使用JSP需要的依赖 没有版本-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>

        <!--使jsp页面生效 使用JSP需要的依赖 没有版本-->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>

        <!--添加httpClient jar包 未知,目前未使用 -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>


        <!-- 引入aop支持 做切面编程,面向接口 目前未使用-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--spring整合redis 缓存 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <!--添加配置跳过测试-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
            <!--添加配置跳过测试-->
        </plugins>
    </build>

</project>

这里,我们一共用到四个工具包

**

一、FileWmxUtil

**

package com.example.house.SYSDBA.util;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.TrueFileFilter;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
/**
 * 这个工具类的方法在FTPUtil工具类的同步文件夹中被使用
 * 自定义文件工具类
 */
public class FileWmxUtil {

    /**
     * 遍历目录下的所有文件--方式1
     *
     * @param targetDir
     */
    public static Collection<File> localListFiles(File targetDir) {
        Collection<File> fileCollection = new ArrayList<>();
        if (targetDir != null && targetDir.exists() && targetDir.isDirectory()) {
            /**
             * targetDir:不要为 null、不要是文件、不要不存在
             * 第二个 文件过滤 参数如果为 FalseFileFilter.FALSE ,则不会查询任何文件
             * 第三个 目录过滤 参数如果为 FalseFileFilter.FALSE , 则只获取目标文件夹下的一级文件,而不会迭代获取子文件夹下的文件
             */
            fileCollection = FileUtils.listFiles(targetDir, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE);
        }
        return fileCollection;
    }
}

**

二、FTPUtil

**

package com.example.house.SYSDBA.util;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class FTPUtil {

    //这个变量代表上传的方法被调用了几次 0为第一次
    public static int count = 0;



    /**
     * 连接 FTP 服务器
     *
     * @param ip     FTP 服务器 IP 地址
     * @param// port     FTP 服务器端口号 此处我们暂时没有端口号
     * @param username 登录用户名
     * @param password 登录密码
     * @return
     * @throws Exception
     */
    public static FTPClient connectFtpServer(String ip,  String username, String password, String controlEncoding) {
        FTPClient ftpClient = new FTPClient();
        try {
            /**设置文件传输的编码*/
            ftpClient.setControlEncoding(controlEncoding);

            /**连接 FTP 服务器
             * 如果连接失败,则此时抛出异常,如ftp服务器服务关闭时,抛出异常:
             * java.net.ConnectException: Connection refused: connect*/
            //ftpClient.connect(ip, port);
            ftpClient.connect(ip);
            /**登录 FTP 服务器
             * 1)如果传入的账号为空,则使用匿名登录,此时账号使用 "Anonymous",密码为空即可*/
            if (StringUtils.isBlank(username)) {
                ftpClient.login("Anonymous", "");
            } else {
                ftpClient.login(username, password);
            }

            /** 设置传输的文件类型
             * BINARY_FILE_TYPE:二进制文件类型
             * ASCII_FILE_TYPE:ASCII传输方式,这是默认的方式
             * ....
             */
            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);

            /**
             * 确认应答状态码是否正确完成响应
             * 凡是 2开头的 isPositiveCompletion 都会返回 true,因为它底层判断是:
             * return (reply >= 200 && reply < 300);
             */
            int reply = ftpClient.getReplyCode();
            if (!FTPReply.isPositiveCompletion(reply)) {
                /**
                 * 如果 FTP 服务器响应错误 中断传输、断开连接
                 * abort:中断文件正在进行的文件传输,成功时返回 true,否则返回 false
                 * disconnect:断开与服务器的连接,并恢复默认参数值
                 */
                ftpClient.abort();
                ftpClient.disconnect();
            } else {
            }
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println(">>>>>FTP服务器连接登录失败,请检查连接参数是否正确,或者网络是否通畅*********");
        }
        return ftpClient;
    }

    /**
     * 使用完毕,应该及时关闭连接
     * 终止 ftp 传输
     * 断开 ftp 连接
     *
     * @param ftpClient
     * @return
     */
    public static FTPClient closeFTPConnect(FTPClient ftpClient) {
        try {
            if (ftpClient != null && ftpClient.isConnected()) {
                ftpClient.abort();
                ftpClient.disconnect();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return ftpClient;
    }





    /**
     * 下载 FTP 服务器上指定的单个文件,文件只下载到我们的相对路径最后一级,不会将相对路径的全部文件夹给下载
     * @param ftpClient              :连接成功有效的 FTP客户端连接
     * @param absoluteLocalDirectory :本地存储文件的绝对路径,如 E:\gxg\ftpDownload
     * @param relativePath     :ftpFile 文件在服务器所在的相对路径,此方法强制路径使用右斜杠"\",如 "\video\2018.mp4"
     * @return
     */
    public static File downloadSingleFile(FTPClient ftpClient, String absoluteLocalDirectory, String relativePath,String relativeDirectoryPath) {

        System.out.println("准备下载的服务器文件:" + relativePath);

        String subPath = relativeDirectoryPath.substring(0,relativeDirectoryPath.lastIndexOf("/"));
        int index = subPath.length();

        //这个最终得到的是相对路径的最后的那一截路径 用于拼接本地的绝对路径
        String downPath = relativePath.substring(index);

        /**如果 FTP 连接已经关闭,或者连接无效,则直接返回*/
        if (!ftpClient.isConnected() || !ftpClient.isAvailable()) {
            System.out.println(">>>>>FTP服务器连接已经关闭或者连接无效*********");

             return null;
        }
        if (StringUtils.isBlank(absoluteLocalDirectory) || StringUtils.isBlank(relativePath)) {
            System.out.println(">>>>>下载时遇到本地存储路径或者ftp服务器文件路径为空,放弃...*********");

             return null;
        }
        try {
            /**没有对应路径时,FTPFile[] 大小为0,不会为null*/
            //你这不是查询ftp文件夹下的所有文件么    他根据相对路径找到那个文件的 没有完整的路径就找不到文件
            FTPFile[] ftpFiles = ftpClient.listFiles(relativePath);
            FTPFile ftpFile = null;
            if (ftpFiles.length >= 1) {
                ftpFile = ftpFiles[0];
            }
            if (ftpFile != null && ftpFile.isFile()) {
                /** ftpFile.getName():获取的是文件名称,如 123.mp4
                 * 必须保证文件存放的父目录必须存在,否则 retrieveFile 保存文件时报错
                 */
                System.out.println("ftpFile.getName()+ftpFile.getSize() "+ftpFile.getName()+ftpFile.getSize());

                //创建本地文件
                File localFile = new File(absoluteLocalDirectory, downPath);
                //没有父级目录
                if (!localFile.getParentFile().exists()) {
                    //创建父级目录
                    localFile.getParentFile().mkdirs();
                }
                OutputStream outputStream = new FileOutputStream(localFile);
                    //截取前面的一段文件夹的相对路径
                    String workDir = relativePath.substring(0, relativePath.lastIndexOf("/"));
                    if (StringUtils.isBlank(workDir)) {
                        workDir = "/";
                    }
                    /**文件下载前,FTPClient工作目录必须切换到文件所在的目录,否则下载失败
                     * "/" 表示用户根目录*/
                    ftpClient.changeWorkingDirectory(workDir);
                    /**下载指定的 FTP 文件 到本地
                     * 1)注意只能是文件,不能直接下载整个目录
                     * 2)如果文件本地已经存在,默认会重新下载
                     * 3)下载文件之前,ftpClient 工作目录必须是下载文件所在的目录
                     * 4)下载成功返回 true,失败返回 false
                     */

                ftpClient.retrieveFile(ftpFile.getName(), outputStream);

                outputStream.flush();
                outputStream.close();
                System.out.println(">>>>>FTP服务器文件下载完毕  ftpFile.getName()*********" + ftpFile.getName());

                return localFile;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;

    }


    /**
     * 下载 FTP 服务器上指定的单个文件,而且本地存放的文件相对部分路径 会与 FTP 服务器结构保持一致
     * 也就是说下载的文件如果外面有父级文件夹 则下载下来的文件也会有父级文件夹 一直到根文件夹
     * @param ftpClient              :连接成功有效的 FTP客户端连接
     * @param absoluteLocalDirectory :本地存储文件的绝对路径,如 E:\gxg\ftpDownload
     * @param relativePath     :ftpFile 文件在服务器所在的绝对路径,此方法强制路径使用右斜杠"\",如 "\video\2018.mp4"
     * @return
     */
    public static File downloadSingleFile2(FTPClient ftpClient, String absoluteLocalDirectory, String relativePath) {

        System.out.println("准备下载的服务器文件:" + relativePath);

        /**如果 FTP 连接已经关闭,或者连接无效,则直接返回*/
        if (!ftpClient.isConnected() || !ftpClient.isAvailable()) {
            System.out.println(">>>>>FTP服务器连接已经关闭或者连接无效*********");

            return null;
        }
        if (StringUtils.isBlank(absoluteLocalDirectory) || StringUtils.isBlank(relativePath)) {
            System.out.println(">>>>>下载时遇到本地存储路径或者ftp服务器文件路径为空,放弃...*********");

            return null;
        }
        try {
            /**没有对应路径时,FTPFile[] 大小为0,不会为null*/
            //你这不是查询ftp文件夹下的所有文件么    他根据相对路径找到那个文件的 没有完整的路径就找不到文件
            FTPFile[] ftpFiles = ftpClient.listFiles(relativePath);
            FTPFile ftpFile = null;
            if (ftpFiles.length >= 1) {
                ftpFile = ftpFiles[0];
            }
            if (ftpFile != null && ftpFile.isFile()) {
                /** ftpFile.getName():获取的是文件名称,如 123.mp4
                 * 必须保证文件存放的父目录必须存在,否则 retrieveFile 保存文件时报错
                 */
                System.out.println(ftpFile.getName()+"-->"+ftpFile.getSize());
                //创建本地文件
                File localFile = new File(absoluteLocalDirectory, relativePath);
                //没有父级目录
                if (!localFile.getParentFile().exists()) {
                    //创建父级目录
                    localFile.getParentFile().mkdirs();
                }
                OutputStream outputStream = new FileOutputStream(localFile);
                    //截取前面的一段文件夹的相对路径
                    String workDir = relativePath.substring(0, relativePath.lastIndexOf("/"));
                    if (StringUtils.isBlank(workDir)) {
                        workDir = "/";
                    }
                    /**文件下载前,FTPClient工作目录必须切换到文件所在的目录,否则下载失败
                     * "/" 表示用户根目录*/
                    ftpClient.changeWorkingDirectory(workDir);
                    /**下载指定的 FTP 文件 到本地
                     * 1)注意只能是文件,不能直接下载整个目录
                     * 2)如果文件本地已经存在,默认会重新下载
                     * 3)下载文件之前,ftpClient 工作目录必须是下载文件所在的目录
                     * 4)下载成功返回 true,失败返回 false
                     */
                ftpClient.retrieveFile(ftpFile.getName(), outputStream);

                outputStream.flush();
                outputStream.close();

                System.out.println(">>>>>FTP服务器文件下载完毕*********" + ftpFile.getName());


                return localFile;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;

    }




    /**
     * 下载 FTP 服务器上指定的单个文件,直接返回到浏览器,不在本地下载
     * @param ftpClient              :连接成功有效的 FTP客户端连接
     * @param relativePath     :ftpFile 文件在服务器所在的相对,如 "/解小川-私人/备份文件/备份.docx"
     * @return
     */
    public static void downloadFile(FTPClient ftpClient, String relativePath, HttpServletResponse response) throws Exception {
        /**如果 FTP 连接已经关闭,或者连接无效,则直接返回*/
        if (!ftpClient.isConnected() || !ftpClient.isAvailable()) {
            System.out.println(">>>>>FTP服务器连接已经关闭或者连接无效*********");
            return ;
        }
        if (StringUtils.isBlank(relativePath)) {
            System.out.println(">>>>>下载时遇到本地存储路径或者ftp服务器文件路径为空,放弃...*********");
            return ;
        }

        FTPFile[] files = ftpClient.listFiles(relativePath);
        String fileName = files[0].getName();
        System.out.println("files[0].getName()   >>>"+files[0].getName());

        // 设置文件ContentType类型,这样设置,会自动判断下载文件类型
        response.setContentType("application/x-msdownload");
        // 设置文件头:最后一个参数是设置下载的文件名并编码为UTF-8
        response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(fileName, "UTF-8"));


        InputStream is = ftpClient.retrieveFileStream(relativePath);
        BufferedInputStream bis = new BufferedInputStream(is);

        OutputStream out = response.getOutputStream();

        byte[] buf = new byte[1024];
        int len = 0;
        while ((len = bis.read(buf)) > 0) {
            out.write(buf, 0, len);
        }
        out.flush();
        out.close();

        if (bis != null) {
            try {
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (is != null) {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println(">>>>>FTP服务器文件下载完毕*********" + fileName);


    }



    /**
     * 遍历 FTP 服务器指定目录下的所有文件(包含子孙文件)
     * 下载整个目录,包括所有子孙目录下的文件,实质也是遍历目录下的文件逐个下载,然后调用上面下载单个文件的方法进行下载
     *
     * @param ftpClient        :连接成功有效的 FTP客户端连接
     * @param relativeDirectoryPath       :查询的 FTP 服务器目录,如果文件,则视为无效,使用绝对路径,如"/"、"/video"、"\\"、"\\video"
     * @param relativePathList :返回查询结果,其中为服务器目录下的文件相对路径,如:\1.png、\docs\overview-tree.html 等
     * @return
     */
    public static List<String> loopServerPath(FTPClient ftpClient, String relativeDirectoryPath, List<String> relativePathList) {
        /**如果 FTP 连接已经关闭,或者连接无效,则直接返回*/
        if (!ftpClient.isConnected() || !ftpClient.isAvailable()) {
            System.out.println("ftp 连接已经关闭或者连接无效......");
            return relativePathList;
        }
        try {
            /**转移到FTP服务器根目录下的指定子目录
             * 1)"/":表示用户的根目录,为空时表示不变更
             * 2)参数必须是目录,当是文件时改变路径无效
             * */
            ftpClient.changeWorkingDirectory(relativeDirectoryPath);
            /** listFiles:获取FtpClient连接的当前下的一级文件列表(包括子目录)
             * 1)FTPFile[] ftpFiles = ftpClient.listFiles("/docs/info");
             *      获取服务器指定目录下的子文件列表(包括子目录),以 FTP 登录用户的根目录为基准,与 FTPClient 当前连接目录无关
             * 2)FTPFile[] ftpFiles = ftpClient.listFiles("/docs/info/springmvc.txt");
             *      获取服务器指定文件,此时如果文件存在时,则 FTPFile[] 大小为 1,就是此文件
             * */
            FTPFile[] ftpFiles = ftpClient.listFiles();
            if (ftpFiles != null && ftpFiles.length > 0) {
                for (FTPFile ftpFile : ftpFiles) {
                    String relativePath = relativeDirectoryPath + "/" + ftpFile.getName();

                    if (ftpFile.isFile()) {
                        System.out.println("发现一个文件  >>>>"+relativePath);
                        relativePathList.add(relativePath);
                    } else {
                        System.out.println("发现一个文件夹  >>>>"+relativePath);
                        loopServerPath(ftpClient, relativePath, relativePathList);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return relativePathList;
    }



    /**
     * 上传本地文件 或 目录 至 FTP 服务器的根目录----保持 FTP 服务器与本地 文件目录结构一致
     * 上传时不会将他的父级文件夹给上传 只有下载时会层层父级文件夹都下载
     *
     * @param ftpClient  连接成功有效的 FTPClinet
     * @param uploadFile 待上传的文件 或 文件夹(此时会遍历逐个上传)
     * @param basePath 服务器上的路径,如果路径上有的文件夹不存在会进行创建 但是前面的一段路径必须存在
     * @throws Exception
     */
    public static void uploadFiles(FTPClient ftpClient, File uploadFile,String basePath) {


        /**如果 FTP 连接已经关闭,或者连接无效,则直接返回*/
        if (!ftpClient.isConnected() || !ftpClient.isAvailable()) {
            System.out.println(">>>>>FTP服务器连接已经关闭或者连接无效*****放弃文件上传****");
            return;
        }
        if (uploadFile == null || !uploadFile.exists()) {
            System.out.println(">>>>>待上传文件为空或者文件不存在*****放弃文件上传****");
            return;
        }
        try {

            //由于上传文件夹是可能进行递归调用这个方法的 但是我们需要只进入这个目录一次 将上传的文件夹都放进来 否则文件夹位置会出错
            if(count == 0) {
                //如果不能进入这个文件夹就进来创建文件夹
                if(!ftpClient.changeWorkingDirectory(basePath)){

                    if (!ftpClient.makeDirectory(basePath)) {
                    //没有成功创建文件夹
                        System.out.println("文件夹创建失败");
                        return;
                    } else {
                        //如果创建成功就切换文件夹的路径
                        ftpClient.changeWorkingDirectory(basePath);
                        System.out.println("创建成功,切换目录  "+basePath);
                    }
                }
            }


            if (uploadFile.isDirectory()) {
                /**如果被上传的是目录时
                 * makeDirectory:在 FTP 上创建目录(方法执行完,服务器就会创建好目录,如果目录本身已经存在,则不会再创建)
                 * 1)可以是相对路径,即不以"/"开头,相对的是 FTPClient 当前的工作路径,如 "video"、"视频" 等,会在当前工作目录进行新建目录
                 * 2)可以是绝对路径,即以"/"开头,与 FTPCLient 当前工作目录无关,如 "/images"、"/images/2018"
                 * 3)注意多级目录时,必须确保父目录存在,否则创建失败,
                 *      如 "video/201808"、"/images/2018" ,如果 父目录 video与images不存在,则创建失败
                 * */
                ftpClient.makeDirectory(uploadFile.getName());
                System.out.println("uploadFile.getName()   >>>"+uploadFile.getName());

                /**变更 FTPClient 工作目录到新目录
                 * 1)不以"/"开头表示相对路径,新目录以当前工作目录为基准,即当前工作目录下不存在此新目录时,变更失败
                 * 2)参数必须是目录,当是文件时改变路径无效*/
                ftpClient.changeWorkingDirectory(uploadFile.getName());

                File[] listFiles = uploadFile.listFiles();
                for (int i = 0; i < listFiles.length; i++) {
                    File loopFile = listFiles[i];
                    if (loopFile.isDirectory()) {

                        count++;
                        /**如果有子目录,则迭代调用方法进行上传*/
                        uploadFiles(ftpClient, loopFile,basePath);
                        /**changeToParentDirectory:将 FTPClient 工作目录移到上一层
                         * 这一步细节很关键,子目录上传完成后,必须将工作目录返回上一层,否则容易导致文件上传后,目录不一致
                         * */
                        ftpClient.changeToParentDirectory();
                    } else {
                        /**如果目录中全是文件,则直接上传*/
                        FileInputStream input = new FileInputStream(loopFile);
                        ftpClient.storeFile(loopFile.getName(), input);
                        input.close();
                        System.out.println(">>>>>文件上传成功****" + loopFile.getPath());
                    }
                }
            } else {
                /**如果被上传的是文件时*/
                FileInputStream input = new FileInputStream(uploadFile);
                /** storeFile:将本地文件上传到服务器
                 * 1)如果服务器已经存在此文件,则不会重新覆盖,即不会再重新上传
                 * 2)如果当前连接FTP服务器的用户没有写入的权限,则不会上传成功,但是也不会报错抛异常
                 * */

                ftpClient.storeFile(uploadFile.getName(), input);
                input.close();
                System.out.println(">>>>>文件上传成功****" + uploadFile.getPath());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 迭代删除文件夹
     * @param dirPath 文件夹路径
     */
    public static void deleteDir(String dirPath)
    {
        File file = new File(dirPath);
        if(file.isFile())
        {
            file.delete();
        }else
        {
            File[] files = file.listFiles();
            if(files == null)
            {
                file.delete();
            }else
            {
                for (int i = 0; i < files.length; i++)
                {
                    deleteDir(files[i].getAbsolutePath());
                }
                file.delete();
            }
        }
    }



    /**
     * 同步本地目录与 FTP 服务器目录
     * 1)约定:FTP 服务器有,而本地没有的,则下载下来;本地有,而ftp服务器没有的,则将本地多余的删除
     * 2)始终确保本地与 ftp 服务器内容一致
     * 2)让 FTP 服务器与 本地目录保持结构一致,如 服务器上是 /docs/overview-tree.html,则本地也是 localDir/docs/overview-tree.html
     *
     * @param ftpClient        连接成功有效的 FTPClinet
     * @param localSyncFileDir :与 FTP 目录进行同步的本地目录
     */
    public static void syncLocalDir(FTPClient ftpClient, String localSyncFileDir) throws IOException {
        /**如果 FTP 连接已经关闭,或者连接无效,则直接返回*/
        if (!ftpClient.isConnected() || !ftpClient.isAvailable() || StringUtils.isBlank(localSyncFileDir)) {
            System.out.println(">>>>>FTP服务器连接已经关闭或者连接无效*********");
            return;
        }

        /** 获取本地存储目录下的文件*/
        Collection<File> fileCollection = FileWmxUtil.localListFiles(new File(localSyncFileDir));


        System.out.println(">>>>>本地存储目录共有文件数量*********" + fileCollection.size());

        /**获取 FTP 服务器下的相对路径*/
        List<String> relativePathList = new ArrayList<>();
        relativePathList = loopServerPath(ftpClient, "", relativePathList);
        System.out.println(">>>>>FTP 服务器端共有文件数量*********" + relativePathList.size());

        /**
         * 遍历 本地存储目录下的文件
         * 1)如果本地文件在 FTP 服务器上不存在,则删除它
         * 2)如果本地文件在 FTP 服务器上存在,则比较两种大小
         *    如果大小不一致,则重新下载
         */
        for (File localFile : fileCollection) {
            String localFilePath = localFile.getPath();
            String localFileSuffi = localFilePath.replace(localSyncFileDir, "");
            if (relativePathList.contains(localFileSuffi)) {
                /**本地此文件在 FTP 服务器存在
                 * 1)比较大小,如果本地文件与服务器文件大小一致,则跳过
                 * 2)如果大小不一致,则删除本地文件,重新下载
                 * 3)最后都要删除relativePathList中的此元素,减轻后一次循环的压力*/
                FTPFile[] ftpFiles = ftpClient.listFiles(localFileSuffi);
                System.out.println(">>>>>本地文件 在 FTP 服务器已存在*********" + localFile.getPath());
                if (ftpFiles.length >= 1 && localFile.length() != ftpFiles[0].getSize()) {
                    downloadSingleFile2(ftpClient, localSyncFileDir, localFileSuffi);
                    System.out.println(">>>>>本地文件与 FTP 服务器文件大小不一致,重新下载*********" + localFile.getPath());
                }
                relativePathList.remove(localFileSuffi);
            } else {
                System.out.println(">>>>>本地文件在 FTP 服务器不存在,删除本地文件*********" + localFile.getPath());
                /**本地此文件在 FTP 服务器不存在
                 * 1)删除本地文件
                 * 2)如果当前文件所在目录下文件已经为空,则将此父目录也一并删除*/
                localFile.delete();
                File parentFile = localFile.getParentFile();
                while (parentFile.list().length == 0) {
                    parentFile.delete();
                    parentFile = parentFile.getParentFile();
                }
            }
        }
        for (int i = 0; i < relativePathList.size(); i++) {
            System.out.println(">>>>> FTP 服务器存在新文件,准备下载*********" + relativePathList.get(i));
            downloadSingleFile2(ftpClient, localSyncFileDir, relativePathList.get(i));
        }
    }


    /**
     * 删除服务器的文件
     *
     * @param ftpClient   连接成功且有效的 FTP客户端
     * @param deleteFiles 待删除的文件或者目录,为目录时,会逐个删除,
     *                    路径必须是绝对路径,如 "/1.png"、"/video/3.mp4"、"/images/2018"
     *                    "/" 表示用户根目录,则删除所有内容
     */
    public static void deleteServerFiles(FTPClient ftpClient, String deleteFiles) {
        /**如果 FTP 连接已经关闭,或者连接无效,则直接返回*/
        if (!ftpClient.isConnected() || !ftpClient.isAvailable()) {
            System.out.println(">>>>>FTP服务器连接已经关闭或者连接无效*****放弃文件上传****");
            return;
        }
        try {
            /** 尝试改变当前工作目录到 deleteFiles
             * 1)changeWorkingDirectory:变更FTPClient当前工作目录,变更成功返回true,否则失败返回false
             * 2)如果变更工作目录成功,则表示 deleteFiles 为服务器已经存在的目录
             * 3)否则变更失败,则认为 deleteFiles 是文件,是文件时则直接删除
             */
            boolean changeFlag = ftpClient.changeWorkingDirectory(deleteFiles);
            if (changeFlag) {
                /**当被删除的是目录时*/
                FTPFile[] ftpFiles = ftpClient.listFiles();
                for (FTPFile ftpFile : ftpFiles) {
                    System.out.println("----------------::::" + ftpClient.printWorkingDirectory());
                    if (ftpFile.isFile()) {
                        boolean deleteFlag = ftpClient.deleteFile(ftpFile.getName());
                        if (deleteFlag) {
                            System.out.println(">>>>>删除服务器文件成功****" + ftpFile.getName());
                        } else {
                            System.out.println(">>>>>删除服务器文件失败****" + ftpFile.getName());
                        }
                    } else {
                        /**printWorkingDirectory:获取 FTPClient 客户端当前工作目录
                         * 然后开始迭代删除子目录
                         */
                        String workingDirectory = ftpClient.printWorkingDirectory();
                        deleteServerFiles(ftpClient, workingDirectory + "/" + ftpFile.getName());
                    }
                }
                /**printWorkingDirectory:获取 FTPClient 客户端当前工作目录
                 * removeDirectory:删除FTP服务端的空目录,注意如果目录下存在子文件或者子目录,则删除失败
                 * 运行到这里表示目录下的内容已经删除完毕,此时再删除当前的为空的目录,同时将工作目录移动到上移层级
                 * */
                String workingDirectory = ftpClient.printWorkingDirectory();
                ftpClient.removeDirectory(workingDirectory);
                ftpClient.changeToParentDirectory();
            } else {
                /**deleteFile:删除FTP服务器上的文件
                 * 1)只用于删除文件而不是目录,删除成功时,返回 true
                 * 2)删除目录时无效,方法返回 false
                 * 3)待删除文件不存在时,删除失败,返回 false
                 * */
                boolean deleteFlag = ftpClient.deleteFile(deleteFiles);
                if (deleteFlag) {
                    System.out.println(">>>>>删除服务器文件成功****" + deleteFiles);
                } else {
                    System.out.println(">>>>>删除服务器文件失败****" + deleteFiles);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }







}


三、ZipUtils

package com.example.house.SYSDBA.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * ZipUtils
 * @author
 * @date
 * @version v.0
 */
public class ZipUtils {

    private static final int  BUFFER_SIZE = 2 * 1024;


    /**
         * 压缩成ZIP 方法
         * @param srcDir 压缩文件夹路径
         * @param out    压缩文件输出流
         * @param KeepDirStructure  是否保留原来的目录结构,true:保留目录结构;
         *                          false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
         * @throws RuntimeException 压缩失败会抛出运行时异常
         */
    public static void toZip(String srcDir, OutputStream out, boolean KeepDirStructure) throws RuntimeException{

        long start = System.currentTimeMillis();

        ZipOutputStream zos = null ;

        try {
            zos = new ZipOutputStream(out);
            File sourceFile = new File(srcDir);
            compress(sourceFile,zos,sourceFile.getName(),KeepDirStructure);
            long end = System.currentTimeMillis();
            System.out.println("压缩完成,耗时:" + (end - start) +" ms");
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtils",e);
        }finally{
            if(zos != null){
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }


    }



        /**
         * 压缩成ZIP 方法
         * @param srcFiles 需要压缩的文件列表
         * @param out           压缩文件输出流
         * @throws RuntimeException 压缩失败会抛出运行时异常
         */

    public static void toZip(List<File> srcFiles , OutputStream out)throws RuntimeException {

        long start = System.currentTimeMillis();
        ZipOutputStream zos = null ;
        try {
            zos = new ZipOutputStream(out);
            for (File srcFile : srcFiles) {
                byte[] buf = new byte[BUFFER_SIZE];
                zos.putNextEntry(new ZipEntry(srcFile.getName()));
                int len;
                FileInputStream in = new FileInputStream(srcFile);
                while ((len = in.read(buf)) != -1){
                    zos.write(buf, 0, len);
                }
                zos.closeEntry();
                in.close();
            }
            long end = System.currentTimeMillis();
            System.out.println("压缩完成,耗时:" + (end - start) +" ms");
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtils",e);
        }finally{
            if(zos != null){
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }



        /**
         * 递归压缩方法
         * @param sourceFile 源文件
         * @param zos        zip输出流
         * @param name       压缩后的名称
         * @param KeepDirStructure  是否保留原来的目录结构,true:保留目录结构;
         *                          false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
         * @throws Exception
         */
    private static void compress(File sourceFile, ZipOutputStream zos, String name,
                                 boolean KeepDirStructure) throws Exception{
        byte[] buf = new byte[BUFFER_SIZE];
        if(sourceFile.isFile()){
            // 向zip输出流中添加一个zip实体,构造器中name为zip实体的文件的名字
            zos.putNextEntry(new ZipEntry(name));
            // copy文件到zip输出流中
            int len;
            FileInputStream in = new FileInputStream(sourceFile);

            while ((len = in.read(buf)) != -1){
                zos.write(buf, 0, len);
            }

            // Complete the entry
            zos.closeEntry();
            in.close();
        } else {
            File[] listFiles = sourceFile.listFiles();
            if(listFiles == null || listFiles.length == 0){
                // 需要保留原来的文件结构时,需要对空文件夹进行处理
                if(KeepDirStructure){
                    // 空文件夹的处理
                    zos.putNextEntry(new ZipEntry(name + "/"));
                    // 没有文件,不需要文件的copy
                    zos.closeEntry();
                }
            }else {
                for (File file : listFiles) {
                    // 判断是否需要保留原来的文件结构
                    if (KeepDirStructure) {
                        // 注意:file.getName()前面需要带上父文件夹的名字加一斜杠,
                        // 不然最后压缩包中就不能保留原来的文件结构,即:所有文件都跑到压缩包根目录下了
                        compress(file, zos, name + "/" + file.getName(),KeepDirStructure);
                    } else {
                        compress(file, zos, file.getName(),KeepDirStructure);
                    }
                }
            }

        }

    }


}

**

四、ResponseUtil

**

package com.example.house.SYSDBA.util;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;

public class ResponseUtil {
    public static void responseBrowser(File file, HttpServletResponse response){
        //下面的是进行响应客户端的测试代码
        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            //设置Http响应头告诉浏览器下载这个附件,下载的文件名也是在这里设置的

            System.out.println("file.getName()   >>>"+file.getName());

            response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(file.getName(), "UTF-8"));
            System.out.println("file.getPath() >>>>>>"+file.getPath());

            OutputStream outputStream = null;

                outputStream = response.getOutputStream();

            byte[] bytes = new byte[2048];
            int len = 0;
            while ((len = fileInputStream.read(bytes))>0){
                outputStream.write(bytes,0,len);
            }
                fileInputStream.close();
                outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("文件响应出错,读写出错");
        }

    }
}

controller控制器层

package com.example.house.SYSDBA.controller;

import com.example.house.SYSDBA.util.FTPUtil;
import com.example.house.SYSDBA.util.ResponseUtil;
import com.example.house.SYSDBA.util.ZipUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

import static com.example.house.SYSDBA.util.FTPUtil.*;

@RestController
@RequestMapping("/FTP")
public class FTPController {

    // http://localhost:8080/FTP/uploadFile
    //这个方法用于上传文件或文件夹 但是上传文件夹的参数还不知道用什么接收 未实现
    @RequestMapping("/uploadFile")
    public void uploadFileOrDirectory(@RequestParam(value = "File") MultipartFile file) throws IOException {
        System.out.println("-----------------------应用启动------------------------");

        String basePath = "/用户名-私人/备份文件/哈哈";  //这个基础路径是服务器的路径,如果不存在则会进行创建,会将文件上传到这个路径下面

        //将MultipartFile转换为File,会在项目中生成临时文件
        File file2 = new File(file.getOriginalFilename());

        System.out.println("file.getOriginalFilename()  >>>"+file.getOriginalFilename());
        FileUtils.copyInputStreamToFile(file.getInputStream(), file2);
        System.out.println("file.getAbsolutePath()>>>>>>"+file2.getAbsolutePath());

        FTPClient ftpClient = FTPUtil.connectFtpServer("ip", "用户名", "密码", "utf-8");

        uploadFiles(ftpClient, file2,basePath);

        closeFTPConnect(ftpClient);

        //将文件的在服务器的完整路径及文件名保存下来 方便存入数据库
        String filePath = basePath+"/"+file2.getName();
        System.out.println("filePath  >>>" +filePath);

        //将项目中的临时文件删除
        file2.delete();

        System.out.println("-----------------------应用关闭------------------------");
    }



    // http://localhost:8080/FTP/downloadFile
    //这个方法用于下载文件,不会在本地下载文件,直接通过流传输,返回给浏览器
    @RequestMapping("/downloadFile")
    public void downloadFiles(HttpServletResponse response) throws Exception {

        System.out.println("-----------------------应用启动------------------------");

        String relativePath ="/用户名-私人/备份文件/哈哈/cors.png";

        //他会删到你传路径的上一级,也就是会把gxg下的全部删了,包括gxg

        FTPClient ftpClient = FTPUtil.connectFtpServer("ip", "用户名", "密码", "utf-8");

        downloadFile(ftpClient,relativePath,response);

        closeFTPConnect(ftpClient);

        System.out.println("-----------------------应用关闭------------------------");

    }




    // http://localhost:8080/FTP/downloadFileLocal
    //这个方法用于下载文件,会在服务器本地下载文件,然后通过页面的超链接访问此文件
    @RequestMapping("/downloadFileLocal")
    public void downloadFileLocal(HttpServletResponse response) throws Exception {

        System.out.println("-----------------------应用启动------------------------");

        //不支持excel
        //支持jpg,png,图片
        //支持pdf文件的在线预览,自带下载的按钮
        //String relativePath ="/用户名-私人/备份文件/哈哈/新目录/test.jpg";
        String relativePath ="/用户名-私人/备份文件/哈哈/新目录/test.pdf";

        //这个是存到本地的文件夹路径,不存在会自动生成文件夹   下载的文件和压缩包都在这个文件夹下面
        String absoluteLocalDirectory = "F:\\gxg\\ftpDownload";


        //他会删到你传路径的上一级,也就是会把gxg下的全部删了,包括gxg

        FTPClient ftpClient = FTPUtil.connectFtpServer("ip", "用户名", "密码", "utf-8");

        //下面两个参数之所以填一样的,是因为下载文件的两个路径直接进行比较,找出最后的文件名拼接上绝对路径
        File file = downloadSingleFile(ftpClient,absoluteLocalDirectory,relativePath,relativePath);

        //实现PDF在线预览
        if (file.exists()){
            byte[] data = null;
            try {
                FileInputStream input = new FileInputStream(file);
                data = new byte[input.available()];
                input.read(data);
                response.getOutputStream().write(data);
                input.close();
            } catch (Exception e) {
                System.out.println(e);
            }

        }else{
            System.out.println("文件不存在");
        }

        //删除下载到本机的文件
        file.delete();

        closeFTPConnect(ftpClient);

        //这里得到的就是本地的这个文件的绝对路径,可以保存到数据库
        System.out.println("file.getPath()   >>>>"+file.getPath());

        System.out.println("-----------------------应用关闭------------------------");

    }





    // http://localhost:8080/FTP/downloadDirectory
    //这个方法用于下载文件夹,会在本地下载文件夹,然后压缩,返回给浏览器,最后将本地文件夹删掉
    @RequestMapping("/downloadDirectory")
    public void downloadDirectory(HttpServletResponse response) throws IOException {
        System.out.println("-----------------------应用启动------------------------");
        FTPClient ftpClient = FTPUtil.connectFtpServer("ip", "用户名", "密码", "utf-8");
        List<String> relativePathList = new ArrayList<>();

        //这个是要在服务器的下载的文件夹路径  后面将文件夹的最后一个文件夹拼接到本地的绝对路径
        String relativeDirectoryPath = "/用户名-私人/备份文件/哈哈";
        //这个是存到本地的文件夹路径,不存在会自动生成文件夹   下载的文件和压缩包都在这个文件夹下面
        String absoluteLocalDirectory = "F:\\gxg\\ftpDownload";
        //这个是压缩包的后缀
        String suffix =".zip";

        //这个是存到本地的文件夹的完整路径 "F:\\gxg\\ftpDownload\\哈哈"   最后将哈哈删掉
        String localDirectory = absoluteLocalDirectory+"\\"+relativeDirectoryPath.substring(relativeDirectoryPath.lastIndexOf("/")+1);

        //这个是本地压缩包的完整的绝对路径  "F:\\gxg\\ftpDownload\\哈哈.zip"
        String localFile = localDirectory+suffix;

        //先将这个  哈哈  的文件夹的内容清空,否则之前的文件也会下载过来
        deleteDir(localDirectory);

        //下载文件夹以及里面的文件
        relativePathList = loopServerPath(ftpClient, relativeDirectoryPath, relativePathList);
        for (String relativePath : relativePathList) {
            //下面的relativePath 是各个文件的相对路径
            downloadSingleFile(ftpClient, absoluteLocalDirectory, relativePath , relativeDirectoryPath);
        }

        //创建一个本地的压缩包文件
        File file = new File(localFile);

        //创建一个文件输出流
        FileOutputStream fos = new FileOutputStream(file);
        //将本地文件进行压缩,结果在本地的压缩包
        ZipUtils.toZip(localDirectory,fos,true);

        ResponseUtil.responseBrowser(file,response);

        closeFTPConnect(ftpClient);

        //看情况删除本地的文件夹包括里面的压缩包
        //deleteDir(deleteDir);
        //"F:\\gxg\\ftpDownload\\哈哈.zip"   最后将压缩包删掉
        //file.delete();

        System.out.println("-----------------------应用关闭------------------------");

    }

}



你可能感兴趣的:(java,spring,boot,ftp)