FTP上传、下载(递归下载)、删除(递归删除)之Java工具类

FTP/SFTP 专栏收录该内容
8 篇文章 1 订阅

声明:本工具类中所有的路径,均采用绝对路径。

声明:不论FTP是否限制chroot_local_user、chroot_list_enable,本工具类均适用(即:不论FTP
           用户刚进FTP时pwd得到的是“/”还是形如“/var/ftp/user_a”这样的,本工具类都适用)。


目录

测试样例(图片版)

上传测试

单文件下载测试

指定目录下所有文件下载(不包括:该目录下的文件夹及其内部内容)测试

指定目录下所有文件、文件夹下载(还包括:该目录下的文件夹及其内部内容)测试

单文件删除测试

指定目录删除(包括:删除该目录下的文件夹及其内部内容)测试

正文 > 工具类代码(文字版)

准备工作:在pom.xml中引入依赖

FTP工具类

辅助类FTPTimestampParserImplExZH

辅助类UnixFTPEntryParser


测试样例(图片版)

声明:本人的FTP搭建在虚拟机CentOS7上,在本地(Windows)上进行的程序测试。

上传测试

注意:.uploadFile(String remoteDir, String remoteFileName, File file)中,remoteDir统一写(以“/”分割的)
           绝对路径;remoteDir可以是不存在的目录(会自动创建);更多详情见代码注释!

运行测试方法,可看到上传成功(本人直接以浏览器浏览FTP):

多次上传,得到现有目录:

说明:/abc/下有a、b、c这三个目录(每个目录下都有各自的文件);还有My备忘录.txt、JustryDeng.html、
           nonsuffixFile这三个文件了。
由此可见:上传成功!

单文件下载测试:

注:.downloadFile(String remoteDirOrRemoteFile, String localDir)中,remoteDirOrRemoteFile统一
        写(以“/”分割的)绝对路径;localDir可以是不存在的目录(会自动创建);更多详情见代码注释!

注:如果remoteDirOrRemoteFile不存在,那么不会下载下来任何东西。

运行测试方法,可看到下载成功:

注:下载下来的文件的内容也是正确无乱码的。

注:.recursiveDownloadFile(String remoteDirOrRemoteFile, String localDir)方法也支持单文件下载。

由此可见:单文件下载成功!

指定目录下所有文件下载(不包括:该目录下的文件夹及其内部内容)测试:

注:.downloadFile(String remoteDirOrRemoteFile, String localDir)中remoteDirOrRemoteFile统
        一写(以“/”分割的)绝对路径;localDir可以是不存在的目录(会自动创建);更多详情见代码注释!

运行测试方法,可看到下载成功:

注:可以看到,只把FTP 的/abc/目录下的文件下载下来了,并没有下载/abc/目录下的a、b、c文件夹及里面的内容。

由此可见:指定目录下所有文件下载成功!

指定目录下所有文件、文件夹下载(还包括:该目录下的文件夹及其内部内容)测试:

注:.recursiveDownloadFile(String remoteDirOrRemoteFile, String localDir)中,
        remoteDirOrRemoteFile统一写(以“/”分割的)绝对路径;localDir可以是不存在
        的目录(会自动创建);更多详情见代码注释!

运行测试方法,可看到下载成功:

注:可以看到,不仅把FTP 的/abc/目录下的有My备忘录.txt、JustryDeng.html、nonsuffixFile这三个文件下载下
       来了,还把/abc/目录下的a、b、c文件夹(以及每个文件夹里面的文件、子文件夹)都下载下来了。

由此可见:指定目录下所有文件、文件夹(递归)下载成功!

单文件删除测试:

注:.deleteBlankDirOrFile(String deletedBlankDirOrFile)中,deletedBlankDirOrFile统一写(以“/”分割的)
        绝对路径,可以是一个明确的要删除的文件全路径;也可以是一个空的文件夹路径;更多详情见代码注释!

运行测试方法,可看到删除成功:

注:可以看到,FTP 的/test/目录下的JustryDeng.html文件已经被删除了。

由此可见:单文件删除成功!

指定目录删除(包括:删除该目录下的文件夹及其内部内容)测试:

删除前,FTP是有/abc/目录的,且其内容为:

调用FTP工具类删除:

运行测试方法,可以看见:

FTP目录下找不到abc目录了,即:删除abc文件夹(及里面的内容)成功!
由此可见:递归删除文件夹(及里面的文件、文件夹)成功!


正文 > 工具类代码(文字版)

准备工作:在pom.xml中引入依赖

<!-- 
    Apache Commons Net library contains a collection of network utilities 
	and protocol implementations. Supported protocols include: Echo, Finger, 
	FTP, NNTP, NTP, POP3(S), SMTP(S), Telnet, Whois 
-->
<dependency>
	<groupId>commons-net</groupId>
	<artifactId>commons-net</artifactId>
	<version>3.6</version>
</dependency>

FTP工具类

import org.apache.commons.net.ftp.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * FTP上传、下载、删除 工具类
 * 约定:统一使用绝对路径
 *
 * @author JustryDeng
 * @date 2018年9月26日 上午10:38:46
 */
public class FtpUtil {

    private final Logger LOGGER = LoggerFactory.getLogger(FtpUtil.class);

    /** 路径分隔符 */
    private final String SEPARATOR_STR = "/";

    /** 点 */
    private final String DOT_STR = ".";

    /** ftp服务器地址 */
    private String hostname;

    /** 端口号 */
    private Integer port;

    /** ftp登录账号 */
    private String username;

    /** ftp登录密码 */
    private String password;

    /**
     * 命令 语句 编码(控制发出去的命令的编码)
     * 如:在删除时,发出去的指令由于此处的编码不对应的原因,乱码了;(找不到目标文件)导致删除失败
     * 如:在下载时,发出去的指令由于此处的编码不对应的原因,乱码了;(找不到目标文件)导致下载失败
     * 如:在上传时,发出去的指令由于此处的编码不对应的原因,乱码了;导致上传到FTP的文件的文件名乱码
     *
     * 注:根据不同的(Server/Client)情况,这里灵活设置
     */
    private String sendCommandStringEncoding = "UTF-8";

    /**
     * 下载文件,文件名encode编码
     *
     * 注:根据不同的(Server/Client)情况,这里灵活设置
     */
    private String downfileNameEncodingParam1 = "UTF-8";

    /**
     * 下载文件,文件名decode编码
     *
     * 注:根据不同的(Server/Client)情况,这里灵活设置
     */
    private String downfileNameDecodingParam2 = "UTF-8";

    /**
     * 设置文件传输形式(使用FTP类静态常量赋值即可)
     *
     * 注:根据要下载上传的文件情况,这里灵活设置
     */
    private Integer transportFileType = FTP.BINARY_FILE_TYPE;

    /**
     * 用户FTP对应的根目录, 形如 /var/ftpusers/justry_deng_root
     * 注:初始化之后,其值不可能为 null
     *
     */
    private String userRootDir;

    /** FTP客户端 */
    private FTPClient ftpClient;

    private FtpUtil(String hostname, Integer port, String username, String password) {
        super();
        this.hostname = hostname;
        this.port = port;
        this.username = username;
        this.password = password;
    }


    /**
     * 设置下载时,文件名的编码
     * 即:new String(file.getName().getBytes(param1), param2) 中的param1
     * 注:根据不同的(Server/Client)情况,这里灵活设置
     *
     * @date 2018年9月26日 下午7:34:26
     */
    public void setDownfileNameEncodingParam1(String downfileNameEncodingParam1) {
        this.downfileNameEncodingParam1 = downfileNameEncodingParam1;
    }

    /**
     * 设置下载时,文件名的编码
     * 即:new String(file.getName().getBytes(param1), param2) 中的param2
     * 注:根据不同的(Server/Client)情况,这里灵活设置
     *
     * @date 2018年9月26日 下午7:34:26
     */
    public void setDownfileNameDecodingParam2(String downfileNameDecodingParam2) {
        this.downfileNameDecodingParam2 = downfileNameDecodingParam2;
    }


    /**
     * 设置文件传输形式 -> 二进制
     * 根据自己的时机情况,选择FTP.BINARY_FILE_TYPE或FTP.ASCII_FILE_TYPE等即可
     * 注:根据不同的文件情况,这里灵活设置
     *
     * @date 2018年9月27日 上午9:48:51
     */
    public void setTransportFileType(Integer transportFileType) {
        if( transportFileType != null) {
            this.transportFileType = transportFileType;
        }
    }

    /**
     * FTP的上传、下载、删除,底层还是 发送得命令语句; 这里就设置发送的命令语句的编码
     * 如:在删除时,发出去的指令由于此处的编码不对应的原因,乱码了;(找不到目标文件)导致删除失败
     * 如:在下载时,发出去的指令由于此处的编码不对应的原因,乱码了;(找不到目标文件)导致下载失败
     * 如:在上传时,发出去的指令由于此处的编码不对应的原因,乱码了;导致上传到FTP的文件的文件名乱码
     *
     *  Saves the character encoding to be used by the FTP control connection.
     *  Some FTP servers require that commands be issued in a non-ASCII
     *  encoding like UTF-8 so that filenames with multi-byte character
     *  representations (e.g, Big 8) can be specified.
     */
    public void setSendCommandStringEncoding(String sendCommandStringEncoding) {
        this.sendCommandStringEncoding = sendCommandStringEncoding;
    }

    /**
     * @param hostname
     *            FTPServer ip
     * @param port
     *            FTPServer 端口
     * @param username
     *            用户名
     * @param password
     *            密码
     * @return FtpUtil实例
     * @date 2018年9月26日 下午4:39:02
     */
    public static FtpUtil getFtpUtilInstance(String hostname, Integer port, String username, String password) {
        return new FtpUtil(hostname, port, username, password);
    }

    /**
     * 初始化FTP服务器
     *
     * @throws IOException IO异常
     * @date 2018年9月26日 下午1:37:14
     */
    private void initFtpClient() throws IOException {
        if(ftpClient == null){
            ftpClient = new FTPClient();
        }
        // 设置编码(其是影响是否乱码的主要因素,在ftpClient.connect(hostname, port)连接前就要设置,否者不一定生效)
        ftpClient.setControlEncoding(sendCommandStringEncoding);
        // Returns the integer value of the reply code of the last FTP reply.
        int replyCode = ftpClient.getReplyCode();
        // 221表示 退出了网络,那么需要重新连接
        int ftpDisconnectionStatusCode = 221;
        // Determine if a reply code is a positive completion response.
        // FTPReply.isPositiveCompletion(replyCode)可用于验证是否连接FTP
        if (FTPReply.isPositiveCompletion(replyCode) && replyCode != ftpDisconnectionStatusCode) {
            LOGGER.info(" FtpUtil -> alreadly connected FTPServer !");
            return;
        } else {
            LOGGER.info(" FtpUtil -> connecting FTPServer -> {} : {}", this.hostname, this.port);
            // 连接ftp服务器
            ftpClient.connect(hostname, port);
            // 登录ftp服务器
            ftpClient.login(username, password);
            LOGGER.info(" FtpUtil -> connect FTPServer success!");
            // 初始化ftp用户根目录, 形如 /var/ftpusers/justry_deng_root
            userRootDir = ftpClient.printWorkingDirectory();
            if (userRootDir == null || "".equals(userRootDir.trim()) || SEPARATOR_STR.equals(userRootDir)) {
                userRootDir = "";
            }
        }
        // 设置文件传输形式
        ftpClient.setFileType(transportFileType);
        // 设置文件传输形式
        ftpClient.setFileType(transportFileType);
        // 设置FTP客户端(即本地)模式为被动模式
        ftpClient.enterLocalPassiveMode();

        /* TODO 此配置主要解决: 操作Linux下的FTP,.listFiles(xxx)方法不能获取到指定文件夹下 的 文件(夹)问题
         *      引入此配置后,反而可能导致  操作Windows下的FTP时,.listFiles(xxx)方法不能获取到指定文件夹下 的 文件(夹),
         *      所以,如果是操作Windows下的FTP,操作失败时,可考虑将 此配置注释掉
         */
        // 由于apache不支持中文语言环境,通过定制类解析中文日期类型
        ftpClient.configure(new FTPClientConfig("com.aspire.util.UnixFTPEntryParser"));
    }

    /**
     * 上传文件至FTP
     * 注:若有同名文件,那么原文件会被覆盖
     *
     * @param remoteDir
     *            上传到指定目录(FTP用户pwd时的绝对路径)
     * @param remoteFileName
     *            上传到FTP,该文件的文件名
     * @param file
     *            要上传的本地文件
     *
     * @return 上传结果
     * @throws IOException IO异常
     * @date 2018年9月26日 下午1:35:27
     */
    @SuppressWarnings("unused")
    public boolean uploadFile(String remoteDir, String remoteFileName, File file) throws IOException{
        boolean result;
        remoteDir = handleRemoteDir(remoteDir);
        try(InputStream inputStream = new FileInputStream(file)){
            // 初始化
            initFtpClient();
            createDirecroty(remoteDir);
            ftpClient.changeWorkingDirectory(remoteDir);
            result = ftpClient.storeFile(remoteFileName, inputStream);
        }
        LOGGER.info(" FtpUtil -> uploadFile boolean result is -> {}", result);
        return result;
    }

    /**
     * 从FTP下载文件
     * 注:如果remoteDirOrRemoteFile不存在,则不会下载下来任何东西
     * 注:如果remoteDirOrRemoteFile不存在,localDir也不存在;再不会下载下来任何东西,
     *    也不会在本地创建localDir目录
     *
     * @param remoteDirOrRemoteFile
     *            FTP中的某一个目录(此时下载该目录下的所有文件,该目录下的文件夹不会被下载);
     *            或  FTP中的某一个文件全路径名(此时下载该文件)
     * @param localDir
     *            本地用于保存下载下来的文件的文件夹
     *
     * @return 下载了的文件个数
     * @throws IOException IO异常
     * @date 2018年9月26日 下午7:24:11
     */
    public int downloadFile(String remoteDirOrRemoteFile, String localDir) throws IOException{
        remoteDirOrRemoteFile = handleRemoteDir(remoteDirOrRemoteFile);
        int successSum = 0;
        int failSum = 0;
        initFtpClient();
        // 根据remoteDirOrRemoteFile是文件还是目录,来切换changeWorkingDirectory
        if (!remoteDirOrRemoteFile.contains(DOT_STR)) {
            // 切换至要下载的文件所在的目录,否者下载下来的文件大小为0
            boolean flag = ftpClient.changeWorkingDirectory(remoteDirOrRemoteFile);
            // 不排除那些 没有后缀名的文件 存在的可能;
            // 如果切换至该目录失败,那么其可能是没有后缀名的文件,那么尝试着下载该文件
            if (!flag) {
                return downloadNonsuffixFile(remoteDirOrRemoteFile, localDir);
            }
        } else {
            String tempWorkingDirectory;
            int index = remoteDirOrRemoteFile.lastIndexOf(SEPARATOR_STR);
            if (index > 0) {
                tempWorkingDirectory = remoteDirOrRemoteFile.substring(0, index);
            } else {
                tempWorkingDirectory = SEPARATOR_STR;
            }
            // 切换至要下载的文件所在的目录,否者下载下来的文件大小为0
            ftpClient.changeWorkingDirectory(tempWorkingDirectory);
        }
        File localFileDir = new File(localDir);
        // 获取remoteDirOrRemoteFile目录下所有 文件以及文件夹   或  获取指定的文件
        FTPFile[] ftpFiles = ftpClient.listFiles(remoteDirOrRemoteFile);
        for (FTPFile file : ftpFiles) {
            // 如果是文件夹,那么不下载 (因为:直接下载文件夹的话,是无效文件)
            if (file.isDirectory()) {
                continue;
            }
            //如果文件夹不存在则创建    
            if (!localFileDir.exists()) {
                boolean result = localFileDir.mkdirs();
                LOGGER.info(" {} is not exist, create this Dir! create result -> {}!",
                        localFileDir, result);
            }
            String name = new String(file.getName().getBytes(this.downfileNameEncodingParam1),
                    this.downfileNameDecodingParam2);
            String tempLocalFile = localDir.endsWith(SEPARATOR_STR) ?
                    localDir + name :
                    localDir + SEPARATOR_STR + name;
            File localFile = new File(tempLocalFile);
            try (OutputStream os = new FileOutputStream(localFile)) {
                boolean result = ftpClient.retrieveFile(file.getName(), os);
                if (result) {
                    successSum++;
                    LOGGER.info(" already download normal file -> {}", name);
                } else {
                    failSum++;
                }
            }
        }
        LOGGER.info(" FtpUtil -> downloadFile success download file total -> {}", successSum);
        LOGGER.info(" FtpUtil -> downloadFile fail download file total -> {}", failSum);
        return successSum;
    }

    /**
     * downloadFile的升级版 -> 其功能如下:
     *     1.remoteDirOrRemoteFile可为FTP上某一个文件的全路径名(绝对路径)
     *       -> 下载该文件,此处与downloadFile功能一致
     *
     *     2.remoteDirOrRemoteFile可为FTP上某一个文件目录名
     *       -> 下载该目录下的所有文件、文件夹(包括该文件夹中的所有文件文件夹并以此类推)
     *           注:对比downloadFile方法可知,downloadFile只能下载该目录下的所有文件,不能递归下载
     *
     * @date 2018年9月26日 下午7:26:22
     */
    public int recursiveDownloadFile(String remoteDirOrRemoteFile, String localDir) throws IOException {
        remoteDirOrRemoteFile = handleRemoteDir(remoteDirOrRemoteFile);
        int successSum = 0;
        // remoteDirOrRemoteFile是一个明确的文件  还是  一个目录
        if (remoteDirOrRemoteFile.contains(DOT_STR)) {
            successSum = downloadFile(remoteDirOrRemoteFile, localDir);
        } else {
            /// 初步组装数据,调用递归方法;查询给定FTP目录以及其所有子孙目录,进而得到FTP目录与本地目录的对应关系Map
            // 有序存放FTP remote文件夹路径
            // 其实逻辑是:先往alreadyQueriedDirList里面存,再进行的查询。此处可以这么处理。
            List<String> alreadyQueryDirList = new ArrayList<>(16);
            alreadyQueryDirList.add(remoteDirOrRemoteFile);
            // 有序存放FTP remote文件夹路径
            List<String> requiredQueryDirList = new ArrayList<>(16);
            requiredQueryDirList.add(remoteDirOrRemoteFile);
            // 记录FTP目录与 本地目录对应关系
            Map<String, String> storeDataMap = new HashMap<>(16);
            storeDataMap.put(remoteDirOrRemoteFile, localDir);
            queryFTPAllChildrenDirectory(storeDataMap, alreadyQueryDirList, requiredQueryDirList);
            String tempPath;
            // 循环调用downloadFile()方法,进行嵌套下载
            for(String str : alreadyQueryDirList) {
                // 将FTP用户的pwd的绝对路径转换为,用户输入的路径(因为 downloadFile方法会将用户输入的路径转化为pwd路径)
                // 提示:用户刚登陆进FTP时,输入pwd,得到的不一定是"/",有可能时FTP对应的Linux上的文件夹路径,
                //     这与FTP的设置有关,可详见《程序员成长笔记(四)》搭建FTP服务器相关章节
                tempPath = str.length() > userRootDir.length() ?
                        str.substring(userRootDir.length()) :
                        SEPARATOR_STR;
                int thiscount = downloadFile(tempPath, storeDataMap.get(str));
                successSum += thiscount;
            }
        }
        System.out.println(" FtpUtil -> recursiveDownloadFile(excluded created directories) "
                + " success download file total -> " + successSum);
        return successSum;
    }

    /**
     * 删除文件 或 删除空的文件夹
     * 注:删除不存在的目录或文件  会导致删除失败
     * 注: 出于保护措施,输入的“/”时,不可删除,直接返回false
     *
     * @param deletedBlankDirOrFile
     *            要删除的文件的全路径名  或  要删除的空文件夹全路径名
     *        统一:路径分割符 用“/”,而不用“\”;
     *
     * @return 删除成功与否
     * @throws IOException IO异常
     * @date 2018年9月26日 下午9:12:07
     */
    public boolean deleteBlankDirOrFile(String deletedBlankDirOrFile) throws IOException{
        if(deletedBlankDirOrFile == null || SEPARATOR_STR.equals(deletedBlankDirOrFile)) {
            return false;
        }
        deletedBlankDirOrFile = handleRemoteDir(deletedBlankDirOrFile);
        boolean flag;
        initFtpClient();
        // 根据remoteDirOrRemoteFile是文件还是目录,来切换changeWorkingDirectory
        if (deletedBlankDirOrFile.lastIndexOf(DOT_STR) < 0) {
            // 出于保护机制:如果当前文件夹中是空的,那么才能删除成功
            flag = ftpClient.removeDirectory(deletedBlankDirOrFile);
            // 不排除那些 没有后缀名的文件 存在的可能;
            // 如果删除空文件夹失败,那么其可能是没有后缀名的文件,那么尝试着删除文件
            if (!flag) {
                flag = ftpClient.deleteFile(deletedBlankDirOrFile);
            }
            // 如果是文件,那么直接删除该文件
        } else {
            String tempWorkingDirectory;
            int index = deletedBlankDirOrFile.lastIndexOf(SEPARATOR_STR);
            if (index > 0) {
                tempWorkingDirectory = deletedBlankDirOrFile.substring(0, index);
            } else {
                tempWorkingDirectory = SEPARATOR_STR;
            }
            // 切换至要下载的文件所在的目录,否者下载下来的文件大小为0
            ftpClient.changeWorkingDirectory(tempWorkingDirectory);
            flag = ftpClient.deleteFile(deletedBlankDirOrFile.substring(index + 1));
        }
        LOGGER.info(" FtpUtil -> deleteBlankDirOrFile [{}] boolean result is -> {}",
                deletedBlankDirOrFile, flag);
        return flag;
    }


    /**
     * deleteBlankDirOrFile的加强版 -> 可删除文件、空文件夹、非空文件夹
     * 注: 出于保护措施,输入的“/”时,不可删除,直接返回false
     *
     * @param deletedBlankDirOrFile
     *            要删除的文件路径或文件夹
     *
     * @return 删除成功与否
     * @throws IOException IO异常
     * @date 2018年9月27日 上午1:25:16
     */
    public boolean recursiveDeleteBlankDirOrFile(String deletedBlankDirOrFile) throws IOException{
        if(deletedBlankDirOrFile == null || SEPARATOR_STR.equals(deletedBlankDirOrFile)) {
            return false;
        }
        String realDeletedBlankDirOrFile = handleRemoteDir(deletedBlankDirOrFile);
        boolean result = true;
        initFtpClient();
        if(!destDirExist(realDeletedBlankDirOrFile)) {
            LOGGER.info(" {} maybe is a  non-suffix file!, try delete!", realDeletedBlankDirOrFile);
            boolean flag = deleteBlankDirOrFile(deletedBlankDirOrFile);
            String flagIsTrue = " FtpUtil -> recursiveDeleteBlankDirOrFile "
                    + realDeletedBlankDirOrFile + " -> success!";
            String flagIsFalse = " FtpUtil -> recursiveDeleteBlankDirOrFile "
                    + realDeletedBlankDirOrFile + " -> target file is not exist!";
            LOGGER.info(flag ? flagIsTrue : flagIsFalse);
            return true;
        }
        // remoteDirOrRemoteFile是一个明确的文件  还是  一个目录
        if (realDeletedBlankDirOrFile.contains(DOT_STR) || !ftputilsChangeWorkingDirectory(realDeletedBlankDirOrFile)) {
            result = deleteBlankDirOrFile(deletedBlankDirOrFile);
        } else {
            /// 初步组装数据,调用递归方法;查询给定FTP目录以及其所有子孙目录、子孙文件        (含其自身)
            // 存放  文件夹路径
            // 其实逻辑是:先往alreadyQueriedDirList里面存,再进行的查询。此处可以这么处理。
            List<String> alreadyQueriedDirList = new ArrayList<>(16);
            alreadyQueriedDirList.add(realDeletedBlankDirOrFile);
            // 存放  文件路径
            List<String> alreadyQueriedFileList = new ArrayList<>(16);
            // 存放 文件夹路径
            List<String> requiredQueryDirList = new ArrayList<>(16);
            requiredQueryDirList.add(realDeletedBlankDirOrFile);
            queryAllChildrenDirAndChildrenFile(alreadyQueriedDirList,
                    alreadyQueriedFileList,
                    requiredQueryDirList);
            String tempPath;
            // 循环调用deleteBlankDirOrFile()方法,删除文件
            for (String filePath : alreadyQueriedFileList) {
                tempPath = filePath.length() > userRootDir.length() ?
                           filePath.substring(userRootDir.length()) :
                           SEPARATOR_STR;
                deleteBlankDirOrFile(tempPath);
            }
            // 对alreadyQueriedDirList进行排序,以保证等下删除时,先删除的空文件夹是 最下面的
            String[] alreadyQueriedDirArray = new String[alreadyQueriedDirList.size()];
            alreadyQueriedDirArray = alreadyQueriedDirList.toArray(alreadyQueriedDirArray);
            sortArray(alreadyQueriedDirArray);
            // 循环调用deleteBlankDirOrFile()方法,删除空的文件夹
            for (String str : alreadyQueriedDirArray) {
                tempPath = str.length() > userRootDir.length() ?
                           str.substring(userRootDir.length()) :
                           SEPARATOR_STR;
                boolean isSuccess = deleteBlankDirOrFile(tempPath);
                if (!isSuccess) {
                    result = false;
                }
            }
        }
        LOGGER.info(" FtpUtil -> recursiveDeleteBlankDirOrFile {} boolean result is -> {}",
                realDeletedBlankDirOrFile, result);
        return result;
    }

    /**
     * 根据数组元素的长度,来进行排序(字符串长的,排在前面)
     * 数组元素不能为null
     *
     * @date 2018年9月27日 上午12:54:03
     */
    private void sortArray(String[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            for(int j = 0; j < array.length - 1 - i; j++) {
                if (array[j].length() - array[j+1].length() < 0) {
                    String flag=array[j];
                    array[j] = array[j+1];
                    array[j+1] = flag;
                }
            }
        }
    }

    /**
     *
     * 根据给出的FTP目录、对应本地目录; 查询该FTP目录的所有子目录 , 以及获得与每一个子
     * 目录对应的本地目录(含其自身以及与其自身对应的本地目录)
     *
     * @param storeDataMap
     *            存储FTP目录与本地目录的对应关系;key -> FTP目录, value -> 与key对应的本地目录
     * @param alreadyQueriedDirList
     *            所有已经查询过了的FTP目录,即:key集合
     * @param requiredQueryDirList
     *            还需要查询的FTP目录
     *
     * @throws IOException IO异常
     * @date 2018年9月26日 下午7:17:52
     */
    private void queryFTPAllChildrenDirectory(Map<String, String> storeDataMap,
                                              List<String> alreadyQueriedDirList,
                                              List<String> requiredQueryDirList) throws IOException {
        List<String> newRequiredQueryDirList = new ArrayList<>(16);
        if(requiredQueryDirList.size() == 0) {
            return;
        }
        for (String str : requiredQueryDirList) {
            String rootLocalDir = storeDataMap.get(str);
            // 获取rootRemoteDir目录下所有 文件以及文件夹(或  获取指定的文件)
            FTPFile[] ftpFiles = ftpClient.listFiles(str);
            for(FTPFile file : ftpFiles){
                if (file.isDirectory()) {
                    String tempName = file.getName();
                    String ftpChildrenDir = str.endsWith(SEPARATOR_STR) ?
                            str + tempName :
                            str + SEPARATOR_STR + tempName;
                    String localChildrenDir = rootLocalDir.endsWith(SEPARATOR_STR) ?
                            rootLocalDir + tempName :
                            rootLocalDir + SEPARATOR_STR + tempName;
                    alreadyQueriedDirList.add(ftpChildrenDir);
                    newRequiredQueryDirList.add(ftpChildrenDir);
                    storeDataMap.put(ftpChildrenDir, localChildrenDir);
                }
            }
        }
        this.queryFTPAllChildrenDirectory(storeDataMap, alreadyQueriedDirList, newRequiredQueryDirList);
    }

    /**
     * 根据给出的FTP目录,查询其所有子目录以及子文件(含其自身)
     *
     * @param alreadyQueriedDirList
     *            所有已经查询出来了的目录
     * @param alreadyQueriedFileList
     *            所有已经查询出来了的文件
     * @param requiredQueryDirList
     *            还需要查询的FTP目录
     * @throws IOException IO异常
     * @date 2018年9月27日 上午12:12:53
     */
    private void queryAllChildrenDirAndChildrenFile(List<String> alreadyQueriedDirList,
                                                    List<String> alreadyQueriedFileList,
                                                    List<String> requiredQueryDirList) throws IOException {
        List<String> newRequiredQueryDirList = new ArrayList<>(16);
        if (requiredQueryDirList.size() == 0) {
            return;
        }
        initFtpClient();
        for (String dirPath : requiredQueryDirList) {
            // 获取dirPath目录下所有 文件以及文件夹(或  获取指定的文件)
            FTPFile[] ftpFiles = ftpClient.listFiles(dirPath);
            for (FTPFile file : ftpFiles) {
                String tempName = file.getName();
                String ftpChildrenName = dirPath.endsWith(SEPARATOR_STR) ?
                        dirPath + tempName :
                        dirPath + SEPARATOR_STR + tempName;
                if (file.isDirectory()) {
                    alreadyQueriedDirList.add(ftpChildrenName);
                    newRequiredQueryDirList.add(ftpChildrenName);
                } else {
                    alreadyQueriedFileList.add(ftpChildrenName);
                }
            }

        }
        this.queryAllChildrenDirAndChildrenFile(alreadyQueriedDirList, alreadyQueriedFileList, newRequiredQueryDirList);
    }


    /**
     * 创建指定目录(注:如果要创建的目录已经存在,那么返回false)
     *
     * @param dir
     *            目录路径,绝对路径,如: /abc 或  /abc/ 可以
     *                   相对路径,如:  sss 或    sss/ 也可以
     *                  注:相对路径创建的文件夹所在位置时,相对于当前session所处目录位置。
     *                  提示: .changeWorkingDirectory() 可切换当前session所处目录位置
     * @return 创建成功与否
     * @throws IOException IO异常
     * @date 2018年9月26日 下午3:42:20
     */
    private boolean makeDirectory(String dir) throws IOException {
        boolean flag;
        flag = ftpClient.makeDirectory(dir);
        if (flag) {
            LOGGER.info(" FtpUtil -> makeDirectory -> create Dir [{}] success!", dir);
        } else {
            LOGGER.info(" FtpUtil -> makeDirectory -> create Dir [{}] fail!", dir);
        }
        return flag;
    }

    /**
     * 在FTP服务器上创建remoteDir目录(不存在,则创建;存在,则不创建)
     *
     * @param directory
     *            要创建的目录
     *            注:此目录指的是FTP用户pwd时获取到的目录(这也与FTP服务器设置是否允许用户切出上级目录有关)
     *            注:pwdRemoteDir不能为null或“”,pwdRemoteDir必须是绝对路径
     *
     * @throws IOException IO异常
     * @date 2018年9月26日 下午2:19:37
     */
    private void createDirecroty(String directory) throws IOException {
        if (!directory.equals(userRootDir) && !ftpClient.changeWorkingDirectory(directory)) {
            if (!directory.endsWith(SEPARATOR_STR)) {
                directory = directory  + SEPARATOR_STR;
            }
            // 获得每一个节点目录的起始位置
            int start = userRootDir.length() + 1;
            int end = directory.indexOf(SEPARATOR_STR, start);
            // 循环创建目录
            String dirPath = userRootDir;
            String subDirectory;
            boolean result;
            while (end >= 0) {
                subDirectory = directory.substring(start, end);
                dirPath = dirPath + SEPARATOR_STR + subDirectory;
                if (!ftpClient.changeWorkingDirectory(dirPath)) {
                    result = makeDirectory(dirPath);
                    LOGGER.info(" FtpUtil -> createDirecroty -> invoke makeDirectory got retrun -> {}!", result);
                }
                start = end + 1;
                end = directory.indexOf(SEPARATOR_STR, start);
            }
        }
    }


    /**
     * 避免在代码中频繁 initFtpClient、logout、disconnect;
     * 这里包装一下FTPClient的.changeWorkingDirectory(String pathname)方法
     *
     * @param pathname
     *            要切换(session)到FTP的哪一个目录下
     * @date 2018年9月27日 上午11:24:25
     */
    private boolean ftputilsChangeWorkingDirectory(String pathname) throws IOException{
        boolean result;
        initFtpClient();
        result = ftpClient.changeWorkingDirectory(pathname);
        return result;
    }

    /**
     * 判断FTP上某目录是否存在
     *
     * @param pathname
     *            要判断的路径(文件名全路径、文件夹全路径都可以)
     *            注:此路径应从根目录开始
     * @date 2018年9月27日 上午11:24:25
     */
    private boolean destDirExist(String pathname) throws IOException{
        boolean result;
        if (pathname.contains(DOT_STR)) {
            int index = pathname.lastIndexOf(SEPARATOR_STR);
            if (index != 0) {
                pathname = pathname.substring(0, index);
            } else {
                return true;
            }
        }
        result = ftpClient.changeWorkingDirectory(pathname);
        return result;
    }

    /**
     * 处理用户输入的 FTP路径
     * 注:这主要是为了 兼容  FTP(对是否允许用户切换到上级目录)的设置
     *
     * @param remoteDirOrFile
     *            用户输入的FTP路径
     * @return  处理后的路径
     * @date 2019/2/1 14:00
     */
    private String handleRemoteDir(String remoteDirOrFile) throws IOException {
        initFtpClient();
        if(remoteDirOrFile == null
                || "".equals(remoteDirOrFile.trim())
                || SEPARATOR_STR.equals(remoteDirOrFile)) {
            remoteDirOrFile = userRootDir + SEPARATOR_STR;
        } else if(remoteDirOrFile.startsWith(SEPARATOR_STR)) {
            remoteDirOrFile = userRootDir + remoteDirOrFile;
        } else {
            remoteDirOrFile = userRootDir + SEPARATOR_STR + remoteDirOrFile;
        }
        return remoteDirOrFile;
    }

    /**
     * 下载 无后缀名的文件
     *
     * @param remoteDirOrFile
     *            经过handleRemoteDir()方法处理后的   FTP绝对路径
     *
     * @return  成功条数
     * @throws IOException IO异常
     * @date 2019/2/1 14:23
     */
    private int downloadNonsuffixFile(String remoteDirOrFile, String localDir) throws IOException {
        int successSum = 0;
        int failSum = 0;
        File localFileDir = new File(localDir);
        String tempWorkingDirectory;
        String tempTargetFileName;
        int index = remoteDirOrFile.lastIndexOf(SEPARATOR_STR);
        tempTargetFileName = remoteDirOrFile.substring(index + 1);
        if(tempTargetFileName.length() > 0) {
            if (index > 0) {
                tempWorkingDirectory = remoteDirOrFile.substring(0, index);
            }else {
                tempWorkingDirectory = SEPARATOR_STR;
            }
            ftpClient.changeWorkingDirectory(tempWorkingDirectory);
            // 获取tempWorkingDirectory目录下所有 文件以及文件夹   或  获取指定的文件
            FTPFile[] ftpFiles = ftpClient.listFiles(tempWorkingDirectory);
            for(FTPFile file : ftpFiles){
                String name = new String(file.getName().getBytes(this.downfileNameEncodingParam1),
                        this.downfileNameDecodingParam2);
                // 如果不是目标文件,那么不下载
                if(!tempTargetFileName.equals(name)) {
                    continue;
                }
                //如果文件夹不存在则创建    
                if (!localFileDir.exists()) {
                    boolean result = localFileDir.mkdirs();
                    LOGGER.info(" {} is not exist, create this Dir! create result -> {}!",
                            localFileDir, result);
                }
                String tempLocalFile = localDir.endsWith(SEPARATOR_STR) ?
                        localDir + name :
                        localDir + SEPARATOR_STR + name;
                File localFile = new File(tempLocalFile);
                try (OutputStream os = new FileOutputStream(localFile)) {
                    boolean result = ftpClient.retrieveFile(file.getName(), os);
                    if (result) {
                        successSum++;
                        LOGGER.info(" already download nonsuffixname file -> {}", name);
                    } else {
                        failSum++;
                    }
                }
                LOGGER.info(" FtpUtil -> downloadFile success download item count -> {}", successSum);
                LOGGER.info(" FtpUtil -> downloadFile fail download item count -> {}", failSum);
            }
        }
        return successSum;
    }

    /**
     * 释放资源
     *
     * 注:考虑到 递归下载、递归删除,如果将释放资源写在下载、删除逻辑里面,那么
     *    当文件较多时,就不频繁的连接FTP、断开FTP;那么久非常影响效率;
     *    所以干脆提供一个方法,在使用FTP结束后,需要主动调用此方法 释放资源
     *
     *
     */
    public void releaseResource() throws IOException {
        if (ftpClient == null) {
            return;
        }
        try {
            ftpClient.logout();
        } catch (IOException e) {
            // 连接未打开的话
            // 忽略 java.io.IOException: Connection is not open
        }
        if (ftpClient.isConnected()) {
            ftpClient.disconnect();
        }
        ftpClient = null;
    }
}

辅助类FTPTimestampParserImplExZH:

import org.apache.commons.net.ftp.parser.FTPTimestampParserImpl;

import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

/**
 * 解决apache ftp中文语言环境下,FTPClient.listFiles()为空的bug
 *
 * @author JustryDeng
 * @date 2019/2/1 15:36
 */
@SuppressWarnings("all")
class FTPTimestampParserImplExZH extends FTPTimestampParserImpl {

    private SimpleDateFormat defaultDateFormat = new SimpleDateFormat("mm d hh:mm");
    private SimpleDateFormat recentDateFormat = new SimpleDateFormat("yyyy mm d");

    /**
     * @author hzwei206 将中文环境的时间格式进行转换
     */
    private String formatDate_Zh2En(String timeStrZh) {
        if (timeStrZh == null) {
            return "";
        }

        int len = timeStrZh.length();
        StringBuffer sb = new StringBuffer(len);
        char ch = ' ';
        for (int i = 0; i < len; i++) {
            ch = timeStrZh.charAt(i);
            if ((ch >= '0' && ch <= '9') || ch == ' ' || ch == ':') {
                sb.append(ch);
            }
        }

        return sb.toString();
    }

    /**
     * Implements the one { FTPTimestampParser#parseTimestamp(String) method} in the { FTPTimestampParser
     * FTPTimestampParser} interface according to this algorithm: If the recentDateFormat member has been defined, try
     * to parse the supplied string with that. If that parse fails, or if the recentDateFormat member has not been
     * defined, attempt to parse with the defaultDateFormat member. If that fails, throw a ParseException.
     *
     * @see org.apache.commons.net.ftp.parser.FTPTimestampParser#parseTimestamp(String)
     */
    @Override
    public Calendar parseTimestamp(String timestampStr) throws ParseException {
        timestampStr = formatDate_Zh2En(timestampStr);
        Calendar now = Calendar.getInstance();
        now.setTimeZone(this.getServerTimeZone());

        Calendar working = Calendar.getInstance();
        working.setTimeZone(this.getServerTimeZone());
        ParsePosition pp = new ParsePosition(0);

        Date parsed = null;
        if (this.recentDateFormat != null) {
            parsed = recentDateFormat.parse(timestampStr, pp);
        }
        if (parsed != null && pp.getIndex() == timestampStr.length()) {
            working.setTime(parsed);
            working.set(Calendar.YEAR, now.get(Calendar.YEAR));
            if (working.after(now)) {
                working.add(Calendar.YEAR, -1);
            }
        } else {
            pp = new ParsePosition(0);
            parsed = defaultDateFormat.parse(timestampStr, pp);
            // note, length checks are mandatory for us since
            // SimpleDateFormat methods will succeed if less than
            // full string is matched. They will also accept,
            // despite "leniency" setting, a two-digit number as
            // a valid year (e.g. 22:04 will parse as 22 A.D.)
            // so could mistakenly confuse an hour with a year,
            // if we don't insist on full length parsing.
            if (parsed != null && pp.getIndex() == timestampStr.length()) {
                working.setTime(parsed);
            } else {
                throw new ParseException("Timestamp could not be parsed with older or recent DateFormat", pp.getIndex());
            }
        }
        return working;
    }
}

辅助类UnixFTPEntryParser:

import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.parser.ConfigurableFTPFileEntryParserImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.ParseException;
import java.util.Calendar;

/**
 * common-net-1.4.1.jar源码,修改对于日期中文格式的支持,从而解决FTPClient.listFiles()返回为空问题
 */
@SuppressWarnings("all")
public class UnixFTPEntryParser extends ConfigurableFTPFileEntryParserImpl {

    private static Logger logger = LoggerFactory.getLogger(UnixFTPEntryParser.class);
    /**
     * months abbreviations looked for by this parser.  Also used
     * to determine which month is matched by the parser
     */
    private static final String DEFAULT_MONTHS =
            "(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)";

    static final String DEFAULT_DATE_FORMAT
            = "MMM d yyyy"; //Nov 9 2001

    static final String DEFAULT_RECENT_DATE_FORMAT
            = "MMM d HH:mm"; //Nov 9 20:06

    static final String NUMERIC_DATE_FORMAT
            = "yyyy-MM-dd HH:mm"; //2001-11-09 20:06

    /**
     * Some Linux distributions are now shipping an FTP server which formats
     * file listing dates in an all-numeric format:
     * <code>"yyyy-MM-dd HH:mm</code>.
     * This is a very welcome development,  and hopefully it will soon become
     * the standard.  However, since it is so new, for now, and possibly
     * forever, we merely accomodate it, but do not make it the default.
     * <p>
     * For now end users may specify this format only via
     * <code>UnixFTPEntryParser(FTPClientConfig)</code>.
     * Steve Cohen - 2005-04-17
     */
    public static final FTPClientConfig NUMERIC_DATE_CONFIG =
            new FTPClientConfig(
                    FTPClientConfig.SYST_UNIX,
                    NUMERIC_DATE_FORMAT,
                    null, null, null, null);

    /**
     * this is the regular expression used by this parser.
     * <p>
     * Permissions:
     * r   the file is readable
     * w   the file is writable
     * x   the file is executable
     * -   the indicated permission is not granted
     * L   mandatory locking occurs during access (the set-group-ID bit is
     * on and the group execution bit is off)
     * s   the set-user-ID or set-group-ID bit is on, and the corresponding
     * user or group execution bit is also on
     * S   undefined bit-state (the set-user-ID bit is on and the user
     * execution bit is off)
     * t   the 1000 (octal) bit, or sticky bit, is on [see chmod(1)], and
     * execution is on
     * T   the 1000 bit is turned on, and execution is off (undefined bit-
     * state)
     */
    private static final String REGEX =
            "([bcdlfmpSs-])"
                    + "(((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-])))\\+?\\s+"
                    + "(\\d+)\\s+"
                    + "(\\S+)\\s+"
                    + "(?:(\\S+)\\s+)?"
                    + "(\\d+)\\s+"

        /*
          numeric or standard format date
        */
                    //问题出在此处,这个匹配只匹配2中形式:
                    //(1)2008-08-03
                    //(2)Jan  9或4月 26
                    //而出错的hp机器下的显示为 8月20日(没有空格分开)
                    //故无法匹配而报错
                    //将下面字符串改为:
                    + "((?:\\d+[-/]\\d+[-/]\\d+)|(?:\\S+\\s+\\S+)|(?:\\S+))\\s+"
                    //+ "((?:\\d+[-/]\\d+[-/]\\d+)|(?:\\S+\\s+\\S+))\\s+"

        /*
           year (for non-recent standard format)
           or time (for numeric or recent standard format
        */
                    + "(\\d+(?::\\d+)?)\\s+"

                    + "(\\S*)(\\s*.*)";


    /**
     * The default constructor for a UnixFTPEntryParser object.
     *
     * @throws IllegalArgumentException
     *         Thrown if the regular expression is unparseable.  Should not be seen
     *         under normal conditions.  It it is seen, this is a sign that
     *         <code>REGEX</code> is  not a valid regular expression.
     */
    public UnixFTPEntryParser() {
        this(null);
    }

    /**
     * This constructor allows the creation of a UnixFTPEntryParser object with
     * something other than the default configuration.
     *
     * @param config
     *         The {@link FTPClientConfig configuration} object used to
     *         configure this parser.
     * @throws IllegalArgumentException
     *         Thrown if the regular expression is unparseable.  Should not be seen
     *         under normal conditions.  It it is seen, this is a sign that
     *         <code>REGEX</code> is  not a valid regular expression.
     * @since 1.4
     */
    public UnixFTPEntryParser(FTPClientConfig config) {
        super(REGEX);
        configure(config);
    }

    /**
     * Parses a line of a unix (standard) FTP server file listing and converts
     * it into a usable format in the form of an <code> FTPFile </code>
     * instance.  If the file listing line doesn't describe a file,
     * <code> null </code> is returned, otherwise a <code> FTPFile </code>
     * instance representing the files in the directory is returned.
     * <p>
     *
     * @param entry
     *         A line of text from the file listing
     * @return An FTPFile instance corresponding to the supplied entry
     */
    @Override
    public FTPFile parseFTPEntry(String entry) {
        FTPFile file = new FTPFile();
        file.setRawListing(entry);
        int type;
        boolean isDevice = false;
        if (matches(entry)) {
            String typeStr = group(1);
            String hardLinkCount = group(15);
            String usr = group(16);
            String grp = group(17);
            String filesize = group(18);
            String datestr = group(19) + " " + group(20);
            String name = group(21);
            String endtoken = group(22);
            try {
                //file.setTimestamp(super.parseTimestamp(datestr));
                FTPTimestampParserImplExZH Zh2En = new FTPTimestampParserImplExZH();
                file.setTimestamp(Zh2En.parseTimestamp(datestr));
            } catch (ParseException e) {
                //logger.error(e, e);
                //return null;  // this is a parsing failure too.
                //logger.info(entry+":修改日期重置为当前时间");
                file.setTimestamp(Calendar.getInstance());
            }
            // bcdlfmpSs-
            switch (typeStr.charAt(0)) {
                case 'd':
                    type = FTPFile.DIRECTORY_TYPE;
                    break;
                case 'l':
                    type = FTPFile.SYMBOLIC_LINK_TYPE;
                    break;
                case 'b':
                case 'c':
                    isDevice = true;
                    // break; - fall through
                case 'f':
                case '-':
                    type = FTPFile.FILE_TYPE;
                    break;
                default:
                    type = FTPFile.UNKNOWN_TYPE;
            }
            file.setType(type);
            int g = 4;
            for (int access = 0; access < 3; access++, g += 4) {
                // Use != '-' to avoid having to check for suid and sticky bits
                file.setPermission(access, FTPFile.READ_PERMISSION,
                        (!"-".equals(group(g))));
                file.setPermission(access, FTPFile.WRITE_PERMISSION,
                        (!"-".equals(group(g + 1))));

                String execPerm = group(g + 2);
                if (!"-".equals(execPerm) && !Character.isUpperCase(execPerm.charAt(0))) {
                    file.setPermission(access, FTPFile.EXECUTE_PERMISSION, true);
                } else {
                    file.setPermission(access, FTPFile.EXECUTE_PERMISSION, false);
                }
            }
            if (!isDevice) {
                try {
                    file.setHardLinkCount(Integer.parseInt(hardLinkCount));
                } catch (NumberFormatException e) {
                    // intentionally do nothing
                }
            }
            file.setUser(usr);
            file.setGroup(grp);
            try {
                file.setSize(Long.parseLong(filesize));
            } catch (NumberFormatException e) {
                // intentionally do nothing
            }
            if (null == endtoken) {
                file.setName(name);
            } else {
                // oddball cases like symbolic links, file names
                // with spaces in them.
                name += endtoken;
                if (type == FTPFile.SYMBOLIC_LINK_TYPE) {

                    int end = name.indexOf(" -> ");
                    // Give up if no link indicator is present
                    if (end == -1) {
                        file.setName(name);
                    } else {
                        file.setName(name.substring(0, end));
                        file.setLink(name.substring(end + 4));
                    }
                } else {
                    file.setName(name);
                }
            }
            return file;
        } else {
            logger.info("matches(entry) failure:" + entry);
        }
        return null;
    }
    /**
     * Defines a default configuration to be used when this class is
     * instantiated without a {@link  FTPClientConfig  FTPClientConfig}
     * parameter being specified.
     *
     * @return the default configuration for this parser.
     */
    @Override
    protected FTPClientConfig getDefaultConfiguration() {
        return new FTPClientConfig(
                FTPClientConfig.SYST_UNIX,
                DEFAULT_DATE_FORMAT,
                DEFAULT_RECENT_DATE_FORMAT,
                null, null, null);
    }

}

 

笔者寄语:

  • 如果看此文后还是不太熟悉,可直接去我的GitHub上下载测试项目(链接在下面)。

^_^ 如有不当之处,欢迎指正

^_^ 参考链接
               https://www.cnblogs.com/weihbs/p/7760552.html

^_^ 代码(工具类、以及测试代码)托管链接 
               
https://github.com/JustryDeng/PublicRepository

^_^ 本文已经被收录进《程序员成长笔记(第四部)》,笔者JustryDeng

  • 3
    点赞
  • 5
    评论
  • 9
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:书香水墨 设计师:CSDN官方博客 返回首页

打赏作者

justry_deng

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值