# SFTPTool **Repository Path**: s2689763871_admin/sftptool ## Basic Information - **Project Name**: SFTPTool - **Description**: sftp同步工具 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2022-10-03 - **Last Updated**: 2025-02-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 使用 JAVA Swing 构建 Sftp 桌面连接工具 **技术要点** 1. 将 logback 日志输出到 Swing 组件上。 2. 使 jsch.jar 包连接 sftp 服务器、实现文件的上传下载。 3. 最终是为了实现文件自动同步。 **步骤一、将logback日志输出到Swing组件上** [Stack Overflow上解决方案](https://stackoverflow.com/questions/9395358/how-link-log-from-logback-in-swing-application)(因为自己也通过一些 csdn 的文章对于 AppenderBase 进行继承,但并未实现效果 ,后面直接去Stack Overflow上搜索就一次性解决了) [gitee源代码]() ***温馨提示:*** *因为我们同步工具是需要部署在 windows 服务器上,所以才有将 logback 日志输出到 Swing 组件上,如果你们用的是 linux服务器,可以自行修改。* 我在此基础上根据自己的需要进行了一些更改。下面是关键部分的代码 **SwingClient** ```java package com.blackdragon.sftp.swing; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.*; import javax.swing.plaf.nimbus.NimbusLookAndFeel; import com.blackdragon.sftp.schedule.SftpSchedule; import com.blackdragon.sftp.utils.SFTPUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Component(value = "swingClient") public class SwingClient { private static Logger logger = LoggerFactory.getLogger(SwingClient.class); public static final SwingClient SWINGCLIENT; public SftpSchedule sftpSchedule; public SFTPUtil sftpUtil; static { // Look and Feel try { UIManager.setLookAndFeel(new NimbusLookAndFeel()); } catch (UnsupportedLookAndFeelException e) { logger.error("Erro ao configurar NimbusLookAndFeel"); } // Esse painel do form principal está sendo usando em outros lugares da aplicação SWINGCLIENT = new SwingClient(); } public JFrame frame; public JPanel contentPane; public JPanel headPane; public JTextPane jTextPane; public JScrollPane logScrollPane; public JLabel lableApplicationStatus; /** * Create the application. */ public SwingClient() { initialize(); } public void initialize() { frame = new JFrame("v1.0 Balck_Dragon SFTP"); // set window size frame.setBounds(0, 0, 1000, 800); // Set the default window closing method frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // Set the window to the center of the screen frame.setLocationRelativeTo(null); contentPane = new JPanel(new BorderLayout()); contentPane.setBackground(Color.WHITE); jTextPane = new JTextPane(); // Settings are not editable jTextPane.setEditable(false); // show with ScrollPane logScrollPane = new JScrollPane(); logScrollPane.setBounds(30, 50, 900, 500); logScrollPane.setViewportView(jTextPane); frame.setContentPane(contentPane); final JButton btnStart = new JButton("启动-Start"); final JButton btnStop = new JButton("停止-Stop"); btnStart.setBounds(30, 15, 100, 30); btnStop.setBounds(150, 15, 100, 30); btnStart.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { btnStart.setEnabled(false); btnStop.setEnabled(true); startSwing(); } }); btnStop.setEnabled(false); btnStop.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { btnStart.setEnabled(true); btnStop.setEnabled(false); stopSwing(); } }); headPane = new JPanel(new FlowLayout(1,10,1)); headPane.add("North", btnStart); headPane.add("North", btnStop); contentPane.add("North", headPane); contentPane.add("Center", logScrollPane); lableApplicationStatus = new JLabel("SftpSchedule Status : Stopped!", JLabel.CENTER); lableApplicationStatus.setFont(new Font("Calibri", Font.PLAIN, 15)); lableApplicationStatus.setBounds(0, 100, 20, 15); contentPane.add("South", lableApplicationStatus); } private void startSwing() { sftpSchedule.start(sftpUtil); lableApplicationStatus.setText("SftpSchedule Status : Running!"); } public void stopSwing() { sftpSchedule.stop(); lableApplicationStatus.setText("SftpSchedule Status : Stopped!"); } public JTextPane getTextPane() { return jTextPane; } public SFTPUtil getSftpUtil() { return sftpUtil; } public SftpSchedule getSftpSchedule() { return sftpSchedule; } public void setSftpSchedule(SftpSchedule sftpSchedule) { this.sftpSchedule = sftpSchedule; } public void setSftpUtil(SFTPUtil sftpUtil) { this.sftpUtil = sftpUtil; } public JFrame getFrame() { return frame; } public void setFrame(JFrame frame) { this.frame = frame; } } ``` **Appender** ```java package com.blackdragon.sftp.logger; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.PatternLayout; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; import com.blackdragon.sftp.swing.SwingClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import javax.swing.text.*; import java.awt.*; /** * @author Rodrigo Garcia Lima (email: rodgarcialima@gmail.com | github: rodgarcialima) * @see ch.qos.logback.core.AppenderBase */ public class Appender extends AppenderBase { private final static Logger log = LoggerFactory.getLogger(Appender.class); /** * Utilizo para formatar a mensagem de log */ private PatternLayout patternLayout; /** * Cada nível de log tem um estilo próprio */ private static SimpleAttributeSet ERROR_ATT, WARN_ATT, INFO_ATT, DEBUG_ATT, TRACE_ATT, RESTO_ATT; /** * Definição dos estilos de log */ static { // ERROR ERROR_ATT = new SimpleAttributeSet(); ERROR_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.TRUE); ERROR_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.FALSE); ERROR_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(153, 0, 0)); // WARN WARN_ATT = new SimpleAttributeSet(); WARN_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.FALSE); WARN_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.FALSE); WARN_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(153, 76, 0)); // INFO INFO_ATT = new SimpleAttributeSet(); INFO_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.FALSE); INFO_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.FALSE); INFO_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(0, 0, 153)); // DEBUG DEBUG_ATT = new SimpleAttributeSet(); DEBUG_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.FALSE); DEBUG_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.TRUE); DEBUG_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(64, 64, 64)); // TRACE TRACE_ATT = new SimpleAttributeSet(); TRACE_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.FALSE); TRACE_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.TRUE); TRACE_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(153, 0, 76)); // RESTO RESTO_ATT = new SimpleAttributeSet(); RESTO_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.FALSE); RESTO_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.TRUE); RESTO_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(0, 0, 0)); } @Override public void start() { patternLayout = new PatternLayout(); patternLayout.setContext(getContext()); patternLayout.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"); patternLayout.start(); super.start(); } @Override protected void append(ILoggingEvent event) { /* Formata mensagem do log */ String formattedMsg = patternLayout.doLayout(event); // Forma segura de atualizar o JTextpane SwingUtilities.invokeLater(() -> { // Alias for JTextPhone JScrollPane in the application JTextPane textPane = SwingClient.SWINGCLIENT.jTextPane; JScrollPane logScrollPane = SwingClient.SWINGCLIENT.logScrollPane; try { // Trunca linhas para economizar memória // Quando atingir 2000 linhas, eu quero que // apague as 500 primeiras linhas int limite = 1000; int apaga = 200; if (textPane.getDocument().getDefaultRootElement().getElementCount() > limite) { int end = getLineEndOffset(textPane, apaga); replaceRange(textPane, null, 0, end); } // Decide qual atributo (estilo) devo usar de acordo com o nível o log if (event.getLevel() == Level.ERROR) { textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, ERROR_ATT); } else if (event.getLevel() == Level.WARN) { textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, WARN_ATT); } else if (event.getLevel() == Level.INFO) { textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, INFO_ATT); } else if (event.getLevel() == Level.DEBUG) { textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, DEBUG_ATT); } else if (event.getLevel() == Level.TRACE) { textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, TRACE_ATT); } else { textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, RESTO_ATT); } // Set scrollbar position logScrollPane.getVerticalScrollBar().setValue(logScrollPane.getVerticalScrollBar().getMaximum()); } catch (BadLocationException e) { log.error("error: {}", e.getMessage()); } // Vai para a última linha textPane.setCaretPosition(textPane.getDocument().getLength()); }); } /** * Código copiado do {@link JTextArea#getLineCount()} * @param textPane de onde quero as linhas contadas * @return quantidade de linhas > 0 */ private int getLineCount(JTextPane textPane) { return textPane.getDocument().getDefaultRootElement().getElementCount(); } /** * Código copiado do {@link JTextArea#getLineEndOffset(int)} * @param textPane de onde quero o offset * @param line the line >= 0 * @return the offset >= 0 * @throws BadLocationException Thrown if the line is * less than zero or greater or equal to the number of * lines contained in the document (as reported by * getLineCount) */ private int getLineEndOffset(JTextPane textPane, int line) throws BadLocationException { int lineCount = getLineCount(textPane); if (line < 0) { throw new BadLocationException("Negative line", -1); } else if (line >= lineCount) { throw new BadLocationException("No such line", textPane.getDocument().getLength()+1); } else { Element map = textPane.getDocument().getDefaultRootElement(); Element lineElem = map.getElement(line); int endOffset = lineElem.getEndOffset(); // hide the implicit break at the end of the document return ((line == lineCount - 1) ? (endOffset - 1) : endOffset); } } /** * Código copiado do {@link JTextArea#replaceRange(String, int, int)}
* * Replaces text from the indicated start to end position with the * new text specified. Does nothing if the model is null. Simply * does a delete if the new string is null or empty.
* * @param textPane de onde quero substituir o texto * @param str the text to use as the replacement * @param start the start position >= 0 * @param end the end position >= start * @exception IllegalArgumentException if part of the range is an invalid position in the model */ private void replaceRange(JTextPane textPane, String str, int start, int end) throws IllegalArgumentException { if (end < start) { throw new IllegalArgumentException("end before start"); } Document doc = textPane.getDocument(); if (doc != null) { try { if (doc instanceof AbstractDocument) { ((AbstractDocument)doc).replace(start, end - start, str, null); } else { doc.remove(start, end - start); doc.insertString(start, str, null); } } catch (BadLocationException e) { throw new IllegalArgumentException(e.getMessage()); } } } } ``` **logback.xml** ```xml logback INFO %d [%thread] %-5level %logger{36} [%file : %line] - %msg%n ${scheduler.manager.server.home}/logs/${app.name}.log ${scheduler.manager.server.home}/logs/${app.name}.%d{yyyy-MM-dd.HH}.log.gz 60 20GB 100MB %d [%thread] %-5level %logger{36} [%file : %line] - %msg%n ``` **步骤二:使用 jsch 连接 sftp 服务器** **SftpConfig** ```java package com.blackdragon.sftp.config; import com.blackdragon.sftp.common.constant.Constants; import com.blackdragon.sftp.utils.SFTPUtil; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import java.util.Objects; /** * @author: Black_Dragon * @date: 2022/8/31 */ @Configuration public class SftpConfig { @Value("${sftp.host}") private String host; @Value("${sftp.port}") private int port; @Value("${sftp.username}") private String username; @Value("${sftp.password}") private String password; @Value("${sftp.privateKey}") private String privateKey; @Value("${sftp.authMethod}") private Integer authMethod; public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getPrivateKey() { return privateKey; } public void setPrivateKey(String privateKey) { this.privateKey = privateKey; } public SFTPUtil getSftpUtil(){ if(Objects.equals(authMethod, Constants.KEY_VERIFICATION)){ return new SFTPUtil(username, host, port, privateKey); } return new SFTPUtil(username, password, host, port); } } ``` **SFTPUtil** ````java package com.blackdragon.sftp.utils; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.*; import com.blackdragon.sftp.domain.SftpFile; import com.jcraft.jsch.Channel; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import com.jcraft.jsch.SftpException; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author: Black_Dragon * @date: 2022/8/30 */ public class SFTPUtil { private final static Logger log = LoggerFactory.getLogger(SFTPUtil.class); private ChannelSftp sftp; private Session session; /** FTP 登录用户名*/ private String username; /** FTP 登录密码*/ private String password; /** 私钥 */ private String privateKey; /** FTP 服务器地址IP地址*/ private String host; /** FTP 端口*/ private int port; /** * 构造基于密码认证的sftp对象 * @param username * @param password * @param host * @param port */ public SFTPUtil(String username, String password, String host, int port) { this.username = username; this.password = password; this.host = host; this.port = port; login(); } /** * 构造基于秘钥认证的sftp对象 * @param username * @param host * @param port * @param privateKey */ public SFTPUtil(String username, String host, int port, String privateKey) { this.username = username; this.host = host; this.port = port; this.privateKey = privateKey; login(); } /** * 连接sftp服务器 * @throws JSchException * * @throws Exception */ private void login(){ try { JSch jsch = new JSch(); if (privateKey != null) { jsch.addIdentity(privateKey);// 设置私钥 log.info("sftp connect,path of private key file:{}" , privateKey); } log.info("sftp connect by host:{} username:{}",host,username); session = jsch.getSession(username, host, port); log.info("Session is build"); if (password != null) { session.setPassword(password); } Properties config = new Properties(); config.put("StrictHostKeyChecking", "no"); session.setConfig(config); session.connect(); log.info("Session is connected"); Channel channel = session.openChannel("sftp"); channel.connect(); log.info("channel is connected"); sftp = (ChannelSftp) channel; log.info(String.format("sftp server host:[%s] port:[%s] is connect successfull", host, port)); } catch (JSchException e) { log.error("Cannot connect to specified sftp server : {}:{} \n Exception message is: {}", new Object[]{host, port, e.getMessage()}); // throw e; } } /** * 关闭连接 server */ public void logout(){ if (sftp != null) { if (sftp.isConnected()) { sftp.disconnect(); log.info("sftp is closed already"); } } if (session != null) { if (session.isConnected()) { session.disconnect(); log.info("sshSession is closed already"); } } } /** * 将输入流的数据上传到sftp作为文件 * * @param directory * 上传到该目录 * @param sftpFileName * sftp端文件名 * @param input * 输入流 * @throws SftpException * @throws Exception */ public void upload(String directory, String sftpFileName, InputStream input) throws SftpException{ log.info("file:{} begin upload" , sftpFileName); try { sftp.cd(directory); } catch (SftpException e) { log.warn("{}, directory is not exist,{}", directory, e.getMessage()); sftp.mkdir(directory); sftp.cd(directory); } sftp.put(input, sftpFileName); sftp.cd(".."); try { input.close(); //必须关闭资源,不然无法删除文件 } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } log.info("file:{} is upload successful" , sftpFileName); } /** * 上传单个文件 * * @param directory * 上传到sftp目录 * @param uploadFile * 要上传的文件,包括路径 * @throws FileNotFoundException * @throws SftpException * @throws Exception */ public void upload(String directory, String uploadFile) throws FileNotFoundException, SftpException{ File file = new File(uploadFile); upload(directory, file.getName(), new FileInputStream(file)); } /** * 将byte[]上传到sftp,作为文件。注意:从String生成byte[]是,要指定字符集。 * * @param directory * 上传到sftp目录 * @param sftpFileName * 文件在sftp端的命名 * @param byteArr * 要上传的字节数组 * @throws SftpException * @throws Exception */ public void upload(String directory, String sftpFileName, byte[] byteArr) throws SftpException{ upload(directory, sftpFileName, new ByteArrayInputStream(byteArr)); } /** * 将字符串按照指定的字符编码上传到sftp * * @param directory * 上传到sftp目录 * @param sftpFileName * 文件在sftp端的命名 * @param dataStr * 待上传的数据 * @param charsetName * sftp上的文件,按该字符编码保存 * @throws UnsupportedEncodingException * @throws SftpException * @throws Exception */ public void upload(String directory, String sftpFileName, String dataStr, String charsetName) throws UnsupportedEncodingException, SftpException{ upload(directory, sftpFileName, new ByteArrayInputStream(dataStr.getBytes(charsetName))); } /** * 下载文件 * * @param directory * 下载目录 * @param downloadFile * 下载的文件 * @param saveFile * 存在本地的路径 * @throws SftpException * @throws FileNotFoundException * @throws Exception */ public void download(String directory, String downloadFile, String saveFile) throws SftpException, FileNotFoundException{ if (directory != null && !"".equals(directory)) { sftp.cd(directory); } File file = new File(saveFile); sftp.get(downloadFile, new FileOutputStream(file)); log.info("file:{} is download successful" , downloadFile); } /** * 下载文件 * @param directory 下载目录 * @param downloadFile 下载的文件名 * @return 字节数组 * @throws SftpException * @throws IOException * @throws Exception */ public byte[] download(String directory, String downloadFile) throws SftpException, IOException{ if (directory != null && !"".equals(directory)) { sftp.cd(directory); } InputStream is = sftp.get(downloadFile); byte[] fileData = IOUtils.toByteArray(is); log.info("file:{} is download successful" , downloadFile); return fileData; } /** * 删除文件 * * @param directory * 要删除文件所在目录 * @param deleteFile * 要删除的文件 * @throws SftpException * @throws Exception */ public void delete(String directory, String deleteFile) throws SftpException{ sftp.cd(directory); sftp.rm(deleteFile); } /** * 列出目录下的文件 * * @param directory * @return List * @throws SftpException */ public List listFiles(String directory) throws SftpException { List sftpFileList = new ArrayList<>(); sftp.ls(directory).forEach(vector -> { SftpFile sftpFile = new SftpFile(); ChannelSftp.LsEntry lsEntry = (ChannelSftp.LsEntry) vector; sftpFile.setFilename(lsEntry.getFilename()); sftpFile.setLongname(lsEntry.getLongname()); sftpFile.setSize(lsEntry.getAttrs().getSize()); sftpFile.setAtime(lsEntry.getAttrs().getATime()); sftpFile.setMtime(lsEntry.getAttrs().getMTime()); sftpFile.setFlags(lsEntry.getAttrs().getFlags()); sftpFile.setGid(lsEntry.getAttrs().getGId()); sftpFileList.add(sftpFile); }); return sftpFileList; } } ```` **效果图** ![img.png](img.png) ![img_1.png](img_1.png)