• 这问题属于覆盖率范畴之外的通用功能了,用代码调用 sftp 下载、执行 scp 或者 xcopy 之类的命令,都可以。
    给你个样例吧,这是从一个 linux 远程服务器上下载的,根据自己需要去调整,里面有的从远程服务器下载目录的时候 finally 里的 close 我给注释掉了,因为我认为下载目录不需要每个文件都执行重连,不是很规范,这个你自己调整吧。

    当然里面还有一些本地代码,你想办法自己定义一下,或者删除掉就好了,这代码里的本地和远程,代指的是当前系统的服务器 (本地),测试服务器 (远程)。
    (如果这里面涉及到下载文件的安全问题啥的,可以自己调一下)

    import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.InetAddress;
    import java.net.UnknownHostException;
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import com.administrator.platform.exception.base.BusinessValidationException;
    
    import ch.ethz.ssh2.Connection;
    import ch.ethz.ssh2.SCPClient;
    import ch.ethz.ssh2.SCPInputStream;
    import ch.ethz.ssh2.SCPOutputStream;
    import ch.ethz.ssh2.SFTPv3Client;
    import ch.ethz.ssh2.SFTPv3DirectoryEntry;
    import ch.ethz.ssh2.SFTPv3FileAttributes;
    import ch.ethz.ssh2.Session;
    import ch.ethz.ssh2.StreamGobbler;
    
    public class GanymedSshClient {
        private static GanymedSshClient instance;
    
        private Session session;
        private ServerInfo serverInfo;
        private Connection connection;
    
        private SCPClient scpClient;
        private SFTPv3Client sftPv3Client;
    
        private static final String MODE = "0644";
        private static final int MKDIR_POSIX_PERMISSION = 0755;
    
        private static final long LENGTH = 1024;
        private static final String REMOTE_PATH_SEPARATOR = "/";
        private static final String NO_SUCH_FILE_ERROR_MESSAGE = "No such file (SSH_FX_NO_SUCH_FILE:";
    
        private final List<SFTPv3DirectoryEntry> childrenList = new ArrayList<>();
    
        private static final Logger LOGGER = LoggerFactory
                .getLogger(GanymedSshClient.class);
    
        private GanymedSshClient() {
    
        }
    
        public Session getSession() {
            return session;
        }
    
        public void setSession(Session session) {
            this.session = session;
        }
    
        public ServerInfo getServerInfo() {
            return serverInfo;
        }
    
        public void setServerInfo(ServerInfo serverInfo) {
            this.serverInfo = serverInfo;
        }
    
        public GanymedSshClient(ServerInfo serverInfo) {
            this.serverInfo = serverInfo;
        }
    
        public GanymedSshClient(String host, int port, String username,
                String password) {
            this.serverInfo = new ServerInfo(host, port, username, password);
        }
    
        public GanymedSshClient(String host, String username, String password) {
            this.serverInfo = new ServerInfo(host, username, password);
        }
    
        public GanymedSshClient(String host) {
            this.serverInfo = new ServerInfo(host);
        }
    
        /**
         * 初始化session
         * 
         * @see :
         * @param :
         * @return : void
         */
        private void initConnection() {
            if (null == this.connection
                    || !this.connection.isAuthenticationComplete()) {
                this.connection = new Connection(this.serverInfo.getHost());
                LOGGER.debug("开始初始化连接");
                try {
                    this.connection.connect();
                    boolean authed = connection.authenticateWithPassword(
                            this.serverInfo.getUsername(),
                            this.serverInfo.getPassword());
    
                    if (!authed) {
                        throw new BusinessValidationException("初始化ssh连接失败,认证异常");
                    }
    
                    LOGGER.debug("CONNECTION 初始化完成");
                } catch (IOException e) {
                    LOGGER.error("初始化连接失败,失败原因:{}", e.getMessage());
                    throw new BusinessValidationException("初始化连接失败");
                }
            }
        }
    
        private Connection getConnection() {
            initConnection();
            return this.connection;
        }
    
        /**
         * 初始化session
         * 
         * @see :
         * @param :
         * @return : void
         */
        private void initSession() {
            if (null == this.session) {
                LOGGER.debug("初始化session");
                try {
                    this.session = getConnection().openSession();
    
                    if (null != this.session) {
                        LOGGER.debug("session初始化完成");
                    }
                } catch (IOException e) {
                    LOGGER.error("获取会话失败,失败原因:{}", e.getMessage());
                }
    
            }
        }
    
        private Session getCurrentSession() {
            initSession();
    
            return this.session;
        }
    
        private SCPClient getScpClient() {
            if (null == this.scpClient) {
                this.scpClient = new SCPClient(getConnection());
            }
    
            return this.scpClient;
    
        }
    
        private void close() {
            if (null != this.connection) {
                this.connection.close();
                this.connection = null;
                this.scpClient = null;
            }
    
            if (null != this.session) {
                this.session.close();
                this.session = null;
            }
    
            if (null != this.sftPv3Client) {
                this.sftPv3Client.close();
                this.sftPv3Client = null;
            }
        }
    
        /**
         * 上传文件
         * 
         * @see :
         * @param :
         * @return : void
         * @param localFile
         * @param remoteFileName
         * @param remoteFolder
         */
        public void uploadFile(String localFile, String remoteFolder) {
            LOGGER.debug("上传文件:{},到:{}", localFile, remoteFolder);
            try (SCPOutputStream os = getScpOutputStream(localFile, remoteFolder);
                    FileInputStream fis = new FileInputStream(localFile);) {
                byte[] b = new byte[4096];
                int i;
                while ((i = fis.read(b)) != -1) {
                    os.write(b, 0, i);
                }
                os.flush();
            } catch (IOException e) {
                LOGGER.error("scp 上传文件失败:{}", e.getMessage());
                throw new BusinessValidationException("上传文件失败");
            } finally {
                close();
            }
        }
    
        /**
         * 获取文件输出流失败
         * 
         * @see :
         * @param :
         * @return : SCPOutputStream
         * @param localFile
         * @param remoteFolder
         * @return
         */
        private SCPOutputStream getScpOutputStream(String localFile,
                String remoteFolder) {
            try {
    
                File file = new File(localFile);
                return getScpClient().put(file.getName(), file.length(),
                        remoteFolder, MODE);
            } catch (IOException e) {
                LOGGER.error("获取文件输出流失败:{}", e.getMessage());
                throw new BusinessValidationException("获取文件输出流失败");
            }
        }
    
        /**
         * 批量上传文件
         * 
         * @see :
         * @param :
         * @return : void
         * @param localFiles
         * @param remoteFolder
         */
        public void uploadFiles(String[] localFiles, String remoteFolder) {
            for (String localFile : localFiles) {
                File thisFile = new File(localFile);
    
                if (thisFile.exists() && thisFile.isDirectory()) {
                    File[] files = thisFile.listFiles();
    
                    String dirName = remoteFolder + REMOTE_PATH_SEPARATOR
                            + thisFile.getName();
                    mkdir(dirName);
                    uploadFiles(files, dirName);
    
                } else if (thisFile.exists() && thisFile.isFile()) {
                    uploadFile(localFile, remoteFolder);
                }
            }
        }
    
        /**
         * 上传本地文件到远程服务器端,即将本地的文件localFile上传到远程Linux服务器中的remoteTargetDirectory目录下
         * 
         * @param localFileList
         * @param remoteTargetDirectory
         */
        public void uploadFiles(List<String> localFileList,
                String remoteTargetDirectory) {
            uploadFiles(localFileList.toArray(new String[] {}),
                    remoteTargetDirectory);
        }
    
        /**
         * 上传本地文件到远程服务器端,即将本地的文件localFile上传到远程Linux服务器中的remoteTargetDirectory目录下
         * 
         * @param localFileList
         * @param remoteTargetDirectory
         */
        public void uploadFiles(File[] localFileList,
                String remoteTargetDirectory) {
    
            String[] filePaths = new String[localFileList.length];
    
            for (int i = 0; i < localFileList.length; i++) {
                filePaths[i] = localFileList[i].getAbsolutePath();
            }
            uploadFiles(filePaths, remoteTargetDirectory);
        }
    
        /**
         * 上传文件
         * 
         * @see :
         * @param :
         * @return : void
         * @param localFolder
         * @param remoteFolder
         */
        public void uploadFolder(String localFolder, String remoteFolder) {
            File localFileFolder = new File(localFolder);
            File[] files = localFileFolder.listFiles();
            uploadFiles(files, remoteFolder);
        }
    
        /**
         * 下载单个文件
         * 
         * @see :
         * @param :
         * @return : void
         * @param remoteFile
         * @param destFile
         */
        public boolean downloadFile(String remoteFile, String remoteDir,
                String destFileFolder) {
            File destFile = new File(destFileFolder);
            if (!destFile.exists()) {
                destFile.mkdirs();
            }
    
            String localFile = remoteFile;
    
            /**
             * 2019年8月12日 20:38:22 modified by 孙留平
             * 
             * @see: 当远程文件中有$符号的时候,会被转义,因此远程的时候,需要换上传义字符,尤其是java的内部类被编译出来的class都是带有$符号的
             * 
             */
            if (remoteFile.contains("$")) {
                remoteFile = remoteFile.replace("$", "\\$");
    
            }
    
            try (SCPInputStream scpInputStream = getRemoteFileInputStream(
                    remoteFile, remoteDir);
                    FileOutputStream fos = new FileOutputStream(
                            new File(destFileFolder, localFile));) {
                String message = String.format("下载文件:%s/%s,到:%s", remoteDir,
                        remoteFile, destFileFolder);
    
                byte[] b = new byte[4096];
                int i;
                while ((i = scpInputStream.read(b)) != -1) {
                    fos.write(b, 0, i);
                }
                fos.flush();
                LOGGER.debug("{}成功", message);
                return true;
            } catch (IOException e) {
                LOGGER.error("下载文件失败,原因:{}", e.getMessage());
                return false;
            }
            // finally {
            // close();
            // }
        }
    
        /**
         * 获取远程流
         * 
         * @see :
         * @param :
         * @return : SCPInputStream
         * @param remoteFile
         * @param remoteDir
         * @return
         */
        private SCPInputStream getRemoteFileInputStream(String remoteFile,
                String remoteDir) {
            SCPInputStream scpInputStream;
            try {
                scpInputStream = getScpClient()
                        .get(remoteDir + REMOTE_PATH_SEPARATOR + remoteFile);
    
                return scpInputStream;
            } catch (IOException e) {
                LOGGER.error("获取目录:{}下的文件:{}流失败,失败原因{}", remoteDir, remoteFile,
                        e.getMessage());
                throw new BusinessValidationException("获取输入流失败");
            }
        }
    
        /**
         * 下载单个文件
         * 
         * @see :
         * @param :
         * @return : void
         * @param remoteFiles
         * @param destFile
         */
        public boolean downloadFiles(String[] remoteFiles, String remoteFileFolder,
                String destFileFolder) {
            File destFile = new File(destFileFolder);
            if (destFile.isFile()) {
                destFileFolder = destFile.getParent();
            }
    
            for (String string : remoteFiles) {
                downloadFile(string, remoteFileFolder, destFileFolder);
            }
    
            return true;
        }
    
        /**
         * 下载目录
         * 
         * @see :
         * @param :
         * @return : void
         * @param remoteFileFolder
         * @param destFileFolder
         */
        public void downloadFolder(String remoteFileFolder, String destFileFolder) {
            // listRemoteDir(remoteFileFolder);
            List<SFTPv3DirectoryEntry> descendantsFiles = getChildren(
                    remoteFileFolder);
            File destFolderFile = new File(destFileFolder);
            if (!destFolderFile.exists()) {
                destFolderFile.mkdirs();
            }
            LOGGER.debug("开始下载:从{}到:{}", remoteFileFolder, destFileFolder);
            for (SFTPv3DirectoryEntry sftPv3DirectoryEntry : descendantsFiles) {
                // 如果是文件夹,则下载文件夹
                if (sftPv3DirectoryEntry.attributes.isDirectory()) {
                    downloadFolder(
                            remoteFileFolder + REMOTE_PATH_SEPARATOR
                                    + sftPv3DirectoryEntry.filename,
                            destFileFolder + File.separator
                                    + sftPv3DirectoryEntry.filename);
                } else if (sftPv3DirectoryEntry.attributes.isRegularFile()) {
                    downloadFile(sftPv3DirectoryEntry.filename, remoteFileFolder,
                            destFileFolder);
                }
            }
        }
    
        /**
         * 下载目录,从远程数组,到本地文件夹,意思把数组中的远程目录都下载到同一个文件夹下,小心文件覆盖
         * 
         * @see :
         * @param :
         * @return : void
         * @param remoteFileFolder
         * @param destFileFolder
         */
        public void downloadFolders(String[] remoteFileFolder,
                String destFileFolder) {
            for (String string : remoteFileFolder) {
                downloadFolder(string, destFileFolder);
            }
        }
    
        /**
         * 下载目录,从远程数组、到本地数组,一一对应
         * 
         * @see :
         * @param :
         * @return : void
         * @param remoteFileFolder
         * @param destFileFolder
         */
        public void downloadFolders(String[] remoteFileFolder,
                String[] destFileFolder) {
    
            if (null == remoteFileFolder || null == destFileFolder) {
                throw new BusinessValidationException("远程文件夹和本地文件夹都不能为null");
            }
    
            if (remoteFileFolder.length == 0 || destFileFolder.length == 0) {
                throw new BusinessValidationException("远程文件夹和本地文件夹都不能为空");
            }
    
            if (remoteFileFolder.length != destFileFolder.length) {
                throw new BusinessValidationException("远程文件夹数组长度,和本地文件夹数组长度,必须得一样");
            }
            for (int i = 0; i < destFileFolder.length; i++) {
                downloadFolder(remoteFileFolder[i], destFileFolder[i]);
            }
        }
    
        /**
         * 执行命令
         * 
         * @see :
         * @param :
         * @return : void
         * @param command
         */
        public String executeShell(String command) {
            LOGGER.debug("执行命令:{}", command);
            Session currentSession = getCurrentSession();
    
            try (InputStream stdStream = new StreamGobbler(
                    currentSession.getStdout());
                    InputStream stdErrStream = new StreamGobbler(
                            currentSession.getStderr());
    
                    BufferedReader bReader = new BufferedReader(
                            new InputStreamReader(stdStream));
    
                    BufferedReader bufferedErrorReader = new BufferedReader(
                            new InputStreamReader(stdErrStream))) {
                currentSession.execCommand(command);
                StringBuilder outputBuilder = new StringBuilder();
                String line = null;
                while (true) {
                    line = bReader.readLine();
                    if (null == line) {
                        break;
                    }
                    outputBuilder.append(line).append("\n");
                }
    
                String errorLine = null;
                while ((errorLine = bufferedErrorReader.readLine()) != null) {
                    outputBuilder.append(errorLine).append("\n");
                }
    
                LOGGER.debug("命令执行结果:\n{}", outputBuilder);
                return outputBuilder.toString();
            } catch (IOException e1) {
                LOGGER.error("执行命令失败:{}", e1.getMessage());
                throw new BusinessValidationException("执行命令失败");
            } finally {
                close();
            }
        }
    
        /**
         * 删除远程文件或者目录
         * 
         * @see :
         * @param :
         * @return : void
         * @param remoteFileOrFolder
         */
        public boolean deleteRemoteFile(String remoteFile) {
            try {
                LOGGER.debug("尝试删除文件:{}", remoteFile);
                getSftpV3Client().rm(remoteFile);
                LOGGER.debug("删除文件成功:{}", remoteFile);
    
                return true;
            } catch (IOException e) {
                LOGGER.error("删除文件失败:{}", e.getMessage());
    
                if (e.getMessage().contains(NO_SUCH_FILE_ERROR_MESSAGE)) {
                    LOGGER.debug("文件夹不存在,不需要删除");
    
                    return true;
                }
    
                return false;
            } finally {
                close();
            }
        }
    
        /**
         * 删除远程文件或者目录
         * 
         * @see :
         * @param :
         * @return : void
         * @param remoteFileOrFolder
         */
        public boolean deleteRemoteFile(String[] remoteFiles) {
            try {
    
                LOGGER.debug("尝试删除一组文件");
                for (String remoteFile : remoteFiles) {
                    getSftpV3Client().rm(remoteFile);
                    LOGGER.debug("删除文件成功:{}", remoteFile);
                }
                LOGGER.debug("批量删除文件成功");
                return true;
            } catch (IOException e) {
                LOGGER.error("删除文件失败:{}", e.getMessage());
                if (e.getMessage().contains(NO_SUCH_FILE_ERROR_MESSAGE)) {
                    LOGGER.debug("文件不存在,不需要删除");
    
                    return true;
                }
    
                return false;
            } finally {
                close();
            }
        }
    
        /**
         * 删除远程目录
         * 
         * @see :
         * @param :
         * @return : void
         * @param remoteFileOrFolder
         */
        public boolean deleteRemoteFileFolder(String remoteFileFolder) {
            try {
                LOGGER.debug("尝试删除文件夹:{}", remoteFileFolder);
                getSftpV3Client().rmdir(remoteFileFolder);
                LOGGER.debug("删除文件夹成功:{}", remoteFileFolder);
    
                return true;
            } catch (IOException e) {
                LOGGER.error("删除文件夹失败:{}", e.getMessage());
    
                if (e.getMessage().contains(NO_SUCH_FILE_ERROR_MESSAGE)) {
                    LOGGER.debug("文件夹不存在,不需要删除");
    
                    return true;
                }
    
                if (e.getMessage().contains("Failure (SSH_FX_FAILURE:")) {
                    return deleteNoneEmptyRemoteFolder(remoteFileFolder);
                }
    
                return false;
            } finally {
                close();
            }
        }
    
        /**
         * 删除非空文件夹
         * 
         * @see :
         * @param :
         * @return : boolean
         * @param remoteFolder
         * @return
         */
        private boolean deleteNoneEmptyRemoteFolder(String remoteFolder) {
            LOGGER.debug("删除非空文件夹:{}", remoteFolder);
            String command = "rm -rf " + remoteFolder;
            String deleteNoneEmptyFolder = executeShell(command);
            LOGGER.debug("删除非空文件夹结果:{}", deleteNoneEmptyFolder);
            return true;
    
        }
    
        /**
         * 在远端linux上创建文件夹
         * 
         * @param dirName
         *            文件夹名称
         * @param posixPermissions
         *            目录或者文件夹的权限
         */
        public boolean mkdir(String dirName, int posixPermissions) {
            try {
                LOGGER.debug("创建文件夹:{}", dirName);
                getSftpV3Client().mkdir(dirName, posixPermissions);
                LOGGER.debug("创建文件夹:{}成功", dirName);
                return true;
            } catch (IOException e) {
                LOGGER.error("创建文件夹失败:{}", e.getMessage());
                return false;
            }
        }
    
        /**
         * 在远端linux上创建文件夹
         * 
         * @param dirName
         *            文件夹名称
         */
        public boolean mkdir(String dirName) {
            return mkdir(dirName, MKDIR_POSIX_PERMISSION);
        }
    
        /**
         * 在远程Linux服务器端移动文件或者文件夹到新的位置
         * 
         * @param oldPath
         * @param newPath
         */
        public boolean moveFileOrDir(String oldPath, String newPath) {
            try {
                LOGGER.debug("把文件或者文件夹从:{},移动到:{}", oldPath, newPath);
                getSftpV3Client().mv(oldPath, newPath);
                LOGGER.debug("把文件或者文件夹从:{},移动到:{}成功", oldPath, newPath);
                return true;
            } catch (Exception e) {
                LOGGER.error("移动文件失败:从何{}到:{}", oldPath, newPath);
                return false;
            }
        }
    
        /**
         * 获取sftpclient
         * 
         * @see :
         * @param :
         * @return : SFTPv3Client
         * @return
         */
        private SFTPv3Client getSftpV3Client() {
            try {
                if (null == this.sftPv3Client) {
                    LOGGER.debug("初始化SFTPv3Client");
                    this.sftPv3Client = new SFTPv3Client(getConnection());
                    LOGGER.debug("初始化SFTPv3Client完成");
                }
                return this.sftPv3Client;
            } catch (IOException e) {
                LOGGER.error("获取SFTPv3Client失败,失败原因:{}", e.getMessage());
                throw new BusinessValidationException("获取SFTPv3Client失败");
            }
        }
    
        /**
         * 列举远程目录文件
         * 
         * @see :
         * @param :
         * @return : List<File>
         * @param remoteDir
         * @return
         */
        private void listRemoteDir(String remoteDir) {
            List<SFTPv3DirectoryEntry> children = null;
            try {
                children = getSftpV3Client().ls(remoteDir);
                if (children.isEmpty()) {
                    return;
                }
    
                Iterator iterator = children.iterator();
                while (iterator.hasNext()) {
                    SFTPv3DirectoryEntry thisChild = (SFTPv3DirectoryEntry) iterator
                            .next();
    
                    SFTPv3FileAttributes attributes = thisChild.attributes;
                    if (!".".equals(thisChild.filename)
                            && !"..".equals(thisChild.filename)) {
                        String childFolder = remoteDir + "/" + thisChild.filename;
                        if (attributes.isDirectory()) {
                            listRemoteDir(childFolder);
                        }
                        this.childrenList.add(thisChild);
                    }
                }
            } catch (IOException e) {
                LOGGER.error("获取子孙文件或者文件夹失败:{}", e.getMessage());
                throw new BusinessValidationException("获取文件夹下的内容失败");
            }
        }
    
        /**
         * 列举远程目录文件
         * 
         * @see :
         * @param :
         * @return : List<File>
         * @param remoteDir
         * @return
         */
        private List<SFTPv3DirectoryEntry> getChildren(String remoteDir) {
            List<SFTPv3DirectoryEntry> children = null;
            List<SFTPv3DirectoryEntry> finalChildren = new ArrayList<>();
            try {
                children = getSftpV3Client().ls(remoteDir);
                Iterator iterator = children.iterator();
                while (iterator.hasNext()) {
                    SFTPv3DirectoryEntry thisChild = (SFTPv3DirectoryEntry) iterator
                            .next();
                    if (!".".equals(thisChild.filename)
                            && !"..".equals(thisChild.filename)) {
                        finalChildren.add(thisChild);
                    }
                }
                return finalChildren;
            } catch (IOException e) {
                LOGGER.error("获取子孙文件或者文件夹失败:{}", e.getMessage());
                throw new BusinessValidationException("获取文件夹下的内容失败");
            }
            // finally {
            // close();
            // }
        }
    
        /**
         * 单例模式
         * 懒汉式
         * 线程安全
         * 
         * @return
         */
        public static GanymedSshClient getInstance() {
            if (null == instance) {
                synchronized (GanymedSshClient.class) {
                    if (null == instance) {
                        instance = new GanymedSshClient();
                    }
                }
            }
            return instance;
        }
    
        /**
         * 获取实例
         * 
         * @see :
         * @param :
         * @return : GanymedSshClient
         * @param ip
         * @param port
         * @param name
         * @param password
         * @return
         */
        public static GanymedSshClient getInstance(String ip, int port, String name,
                String password) {
            if (null == instance) {
                synchronized (GanymedSshClient.class) {
                    if (null == instance) {
                        instance = new GanymedSshClient(ip, port, name, password);
                    }
                }
            }
            return instance;
        }
    
        /**
         * 获取实例
         * 
         * @see :
         * @param :
         * @return : GanymedSshClient
         * @param ip
         * @param port
         * @param name
         * @param password
         * @return
         */
        public static GanymedSshClient getInstance(String ip, String name,
                String password) {
            if (null == instance) {
                synchronized (GanymedSshClient.class) {
                    if (null == instance) {
                        instance = new GanymedSshClient(ip, name, password);
                    }
                }
            }
            return instance;
        }
    
        /**
         * 获取实例
         * 
         * @see :
         * @param :
         * @return : GanymedSshClient
         * @param ip
         * @param port
         * @param name
         * @param password
         * @return
         */
        public static GanymedSshClient getInstance(String ip) {
            if (null == instance) {
                synchronized (GanymedSshClient.class) {
                    if (null == instance) {
                        instance = new GanymedSshClient(ip);
                    }
                }
            }
            return instance;
        }
    
        /**
         * 获取实例
         * 
         * @see :
         * @param :
         * @return : GanymedSshClient
         * @param ip
         * @param port
         * @param name
         * @param password
         * @return
         */
        public static GanymedSshClient getInstance(ServerInfo serverInfo) {
            if (null == instance) {
                synchronized (GanymedSshClient.class) {
                    if (null == instance) {
                        instance = new GanymedSshClient(serverInfo);
                    }
                }
            }
            return instance;
        }
    
        /**
         * 判断服务器是否可认证
         * 
         * @see :
         * @param :
         * @return : boolean
         * @return
         */
        public boolean serverCanBeAuthed() {
            try {
                initConnection();
                return true;
            } catch (Exception e) {
                LOGGER.error("认证授权失败,原因:{}", e.getMessage());
                return false;
            }
        }
    
        /**
         * 判断服务器是否可连通
         * 
         * @see :
         * @param :
         * @return : boolean
         * @return
         */
        public boolean serverIpCanBeConnected() {
            try {
                return InetAddress.getByName(this.serverInfo.getHost())
                        .isReachable(3000);
            } catch (UnknownHostException e) {
                LOGGER.error("测试连接服务器失败,失败原因:{}", e.getMessage());
                return false;
            } catch (IOException e) {
                LOGGER.error("连接服务器IO异常,失败原因:{}", e.getMessage());
                return false;
            }
    
        }
    
        public static void main(String[] args) {
            GanymedSshClient ganymedSshClient = new GanymedSshClient(
                    "192.168.110.31", "admin", "admin");
            String destFolder = "E:\\test3\\tq-datamanagement";
            ganymedSshClient.downloadFolder(
                    "/home/admin/codecoverge/tq-datamanagement", destFolder);
        }
    }
    
    
  • 应该是最好独立的,只需要提供 class 文件就可以解析

  • 这个没明白具体什么意思,可以描述清晰一些?或者直接上图~

  • 你这样做的目的是什么呢?
    如果是单项目多工程,那一般也只有一个属于启动项目,其他属于 jar 包引入,这时候需要一个 jacoco 服务也就够了;
    如果是多项目多工程,那项目之间也不需要组合统计吧?一个 jacoco 服务,对着多个系统服务,即使能启动起来不报端口占用错误,那也没什么太大意义吧。
    你统计数据的时候还是要按照不同的项目或者不同的系统服务来分开,但是一个 jacoco 的话,多个系统的东西都堆到这一个服务里面,覆盖率的执行数据也是同一时间保存的,那不是纯粹增加解析的工作量么。

    难道仅仅是为了给服务器节省一个 jacoco 服务的启动端口?

    不知道我有没有说明白~~

    (关于端口占用的问题,他只是启动的时候一个提示,你可以关注一下即使报了这个端口占用错误,系统还是不时能起来的,我印象中这个不影响应用的启动,只是显示启动这个 jacoco 的 tcp 服务失败,但实际上这个 jacoco 服务 (就是之前被另一个系统启动起来的那个) 能不能统计你这个新系统的覆盖数据,你可以尝试一下,看看拉去出来的覆盖率数据有没有你的新系统的代码,这个底层细节我没有深入去看。)

  • 当然可以

  • 你这个应该问题不大吧,即使你用脚本启动,也可以给 shell 脚本传端口参数吧。
    不太清楚你 jenkins 去打包部署的时候是怎么做的,如果同一个项目起多个服务,那你用 jenkins 做也是 java 命令后面跟-Dspring.profile.active 这个参数或者是直接指定 port 吧,那应该直接把 jacoco 的配置传过去应该是类似的?

    就是不知道我有没有准确理解你的意图

  • 咦,书都到我手里一周了 ,你现在才来文章😂
    在里面看到了茹老师和恒捷兄的推荐序哈哈,祝一切安好!!!

  • 妥。我修修

  • 仅楼主可见
  • 😂 我先去解个题哈哈哈。简历先不急

  • 😂 介不介意被裁的来占个楼

  • 1 at 2020年04月03日

    那肯定是要想办法改正的;
    关于要提升的东西,也在慢慢思考,一言难尽

  • 没有呢,暂时没涉及到 kotlin 的项目需求😂

  • 一些招聘心得 at 2020年03月30日

    本人没有招聘经验,面试经验也不多,但将心比心,面试官和面试者在心理上很多地方也是互通的。

    • 其实作为面试者,有一条挺重要的点就是理解面试官的意图,一个动作、一个表情、一个词语、一个姿势变化、一个语气的升降调,那些面霸除了基本功扎实以外,这一点做的也都不差 (可惜我不行,我也不喜欢去往这方面钻。以前就曾经载在一个稍微 “狡猾” 的问题上,虽然至今为止我也不认为我的回答有问题,只不过考虑的点不同而已)。奈何世界上多数人还是普通人,能做到可以清晰洞察面试官意图的人,还是少数,可怜的我是多数那一批。

    • 如前面有哥们所说的,我也很喜欢"味道"这个词,其实味道对不对、三观互相合不合,几个问题问下来,基本上面试官和面试者都基本清楚了。再后面的就是即使三观或者味道不是百分之百合,如果有一方可以在一定程度上降低期望,那也是有可能完成合作的。

    • 个人感觉,找工作跟谈恋爱是一样的,需要三观合又可以互相对对方起到积极作用。有的人喜欢一见钟情、有的人喜欢日久生情,但到最后一般都是三观合、互相扶持、又愿意互相欠就的人,走的长久。

    • 一个人一生也就那么点追求

      • 找到合适的人过一生、
      • 找到合适的工作好好忙几年,多者几十年,至于中间碰到不合适的或者说不那么合适的,那些经历也不是完全无用。

    有点啰嗦,共勉😂

  • 😂 啥情况

  • 2019 最喜欢的话题,可惜还没有机会去深入研究,等有时间了一定好好分析下,他们的实现估计短时间内不会开源,所以需要自己摸索吧😃

  • 杭州人民又无缘😁

  • 占楼,已填😛

  • 感谢提问,其实我对 jacoco 的理解也是有限的,很多原理性的东西或者更深入的东西我也不会😂
    看完描述,我首先想到的是贤弟 (妹) 可能没有读完这个文章的内容,这个怪我文笔不好写的太啰嗦了,道个歉哈。
    本身没有搞清楚你这四个工程是个怎样的结构,我姑且以为你这个四个工程属于父子工程(隶属一个 git url,而不是项目间交互),以此为基准,下面就聊聊你提到的几个点:


    问点 1: 项目是用 dubbo 框架

    答: 首先你提到 dubbo 框架让我心里一虚,因为我对 dubbo 的理解只停留在表面,所以我只能按照我理解的来说 (当然可能会错,这个需要你识别一下哈。)
    dubbo 本身是基于接口进行的调用,所以你对 dubbo 的调用覆盖,势必要到 provider 端 (因为实现都在 provider 端),一般来说,dubbo 项目会专门提供一个 client 端的 jar 包或者 GAV(通常是纯接口),供 consumer 端调用,这至少在我理解的我们公司项目是这样。就比如你的 A B C D 四个项目,可能你对 A 项目发起的请求,它内部通过 dubbo 调用了 D(provider) 项目的接口,那么相当于你对 A 项目进行测试,那么这一部分测试一方面覆盖率你 A 项目的部分代码,另一部分也覆盖到了 D 项目的代码实现,虽然你在 A 项目中有用到一部分 D 项目的代码,但是可能它提供的都是接口,你 (应该) 是没办法统计到这些 D 项目中的具体实现。这个要计划清楚


    问点 2: 但 182 上面三个应用没有 tomcat 怎么拿到覆盖率

    答: jacoco 的本质工作机制,并不依赖于哪个 web 容器,它需要的是一个动态的或者静态字节码插桩,就是你这个应用或者 java 代码不管将来怎么运行,这并不重要,重要的是要给它插桩的时机 (也就是 class 的生成和运行)。这个点理清楚了,你下一个问题也就不是问题了。所以你这个问题就追溯到上面提到工作机制的 java -jar,如果你最后没有收到覆盖率数据,那我猜可能是启动命令写错了或者端口没有规划好,或者去参考下文章说的那些覆盖率报告为 0 的场景,有没有命中。


    问点 3: 我参(尝?)试了在 java -jar 启动参数里面增加 JavaAgent,开启了三个监控端口,不知道是否可行

    答: 肯定是可行的 ,完全支持这种机制。


    问点 4: 这 4 个应用的覆盖率报告如何合并然后显示在 Jenkins 里面?

    答前半句 (能否合并):

    • 肯定是 OK 的,具体去看下这个文章的主题 (十二),覆盖率数据是可以合并的。

    答后半句 (能否在 Jenkins 上运行):

    • 我猜你想集成到 jenkins 上。这个我没法肯定回答,因为我暂时没有用过 jenkins 去统计这个覆盖率,我一直以为 jenkins 只支持单元测试的覆盖率统计 (可能显得很没文化,孤陋寡闻了哈)。 这个建议先去了解下你希望用 jenkins 能做到什么地步,如果它支持自定义覆盖率数据 (就是 exec 或者 ec 文件),那么结合源码和 class 文件目录,那你这个目标应该是没问题的,上面覆盖率数据合并之后丢给 jenkins 去统计就行了。 如果 jenkins 只是支持像执行单元测试那样才能统计,那我暂时没有好的建议,你或许可以发帖求助下社区。
  • 😛 说的都是大实话,哈哈。
    俗话说的好,提出问题和现象并不难,难的是给出解决方案并得以实现。
    期待大佬新的解决方案出现

  • 哇。阅文集团,斗破、全职、星辰粉怒赞😛

  • 一次 Logback 发现的隐患 at 2019年11月20日

    先赞后看。
    😺 好都地方点了跳转都是 403 错误

  • 谢阅。
    影响范围这个话题是比较大的,也很难一句两句说清楚。不过按照我当前能力的理解,对测试来说,最粗粒度的可以做到检测出变更的方法或者类,出现在哪些请求的调用链上,也就是说被哪些类的哪些方法调用了,这才是测试应该关注的。如果其他的请求链路,没有出现变更方法的调用,那从一定程度上一定范围内是安全的。所以你可以按照这个思路来想想可以做点什么,至于用到的技术和一些,我有些建议,但我没有完全试验过,所以可以看看能否参考:

    • 1.你可以通过解析新版本的字节码,把整个应用的调用链路给解析出来,当然了这个调用链路多数情况下会有多颗树组成,对整个应用来说,最终会是一个由多个调用树组成的森林。这个用 ASM 字节码解析,应该可以做到,但数据量可能比较大,怎么存储怎么展示,可能需要细想,在 java 层可以想办法以 hashmap 来存。
    • 2. 上面的森林拿到之后,可以根据新代码的源码和基线代码的源码,来解析变更类和方法,看你怎么存储,差不多可以拿到一个变更方法的列表 (当然也会有该方法的类信息存在)。
    • 3. 结合上面的调用关系森林和变更方法的列表,可以初步判断在调用森林里,哪些树中的哪些路径是包含了变更方法的,针对这些请求链路,可以看看怎么补充测试。整个调用关系的存储,之前请教过思寒,可以尝试用 neo4j 或者 kibana 来处理,或者你有其他的思路也可以尝试下

    当然了,以上只是我的设想,我之前也做过尝试,只简单做到了 1 和 2,至于 3 嘛,存储和展示都需要一定的技术和资源,我并没有深入研究下去,当然可能会有一定难度,但我之前想到的也就是这个思路。我这想的都比较浅,不知道会不会对你有所帮助。

  • 😁 好机会都在北上广深,心余力亏。
    怒吼一声,杭州何在!!!😛

  • 这个看起来就是你的构建 jdk 和你运行时的 jdk 不一致,最好是要保持一致。

    在哪儿构建,就在哪儿运行,如果不能做到这样,那就干脆生成报告的时候,class 文件、源代码、和 exec 文件直接从运行时的机器上复制。

    你可以看下我上面的回复,生成报告,要想准确,就得源码、class 文件、exec 最好是同一批构建的产物,当然不排除有时候分开也可以,但最好是要保证同一批~~

    希望可以帮到你😌

    你可以了解下 class 文件的构造,开头有一串东西 (好像叫魔数啥的),是用来区分 jdk 相关的东西的,差别过大,可能就会被认为不匹配了。