技术要点
- 将 logback 日志输出到 Swing 组件上。
- 使 jsch.jar 包连接 sftp 服务器、实现文件的上传下载。
- 最终是为了实现文件自动同步。
步骤一、将logback日志输出到Swing组件上
Stack Overflow上解决方案(因为自己也通过一些 csdn 的文章对于 AppenderBase 进行继承,但并未实现效果 ,后面直接去Stack Overflow上搜索就一次性解决了)
温馨提示:
因为我们同步工具是需要部署在 windows 服务器上,所以才有将 logback 日志输出到 Swing 组件上,如果你们用的是 linux服务器,可以自行修改。
我在此基础上根据自己的需要进行了一些更改。下面是关键部分的代码
SwingClient
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
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<ILoggingEvent> {
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)}<br>
*
* 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.<br>
*
* @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 version="1.0" encoding="UTF-8"?>
<configuration debug="true" scan="true" scanPeriod="1 seconds">
<contextName>logback</contextName>
<!--定义参数,后面可以通过${app.name}使用-->
<property name="app.name" value="logback_test"/>
<!--ConsoleAppender 用于在屏幕上输出日志-->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<!--定义了一个过滤器,在LEVEL之下的日志输出不会被打印出来-->
<!--这里定义了DEBUG,也就是控制台不会输出比ERROR级别小的日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<!-- encoder 默认配置为PatternLayoutEncoder -->
<!--定义控制台输出格式-->
<encoder>
<pattern>%d [%thread] %-5level %logger{36} [%file : %line] - %msg%n</pattern>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--定义日志输出的路径-->
<!--这里的scheduler.manager.server.home 没有在上面的配置中设定,所以会使用java启动时配置的值-->
<!--比如通过 java -Dscheduler.manager.server.home=/path/to XXXX 配置该属性-->
<file>${scheduler.manager.server.home}/logs/${app.name}.log</file>
<!--定义日志滚动的策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--定义文件滚动时的文件名的格式-->
<fileNamePattern>${scheduler.manager.server.home}/logs/${app.name}.%d{yyyy-MM-dd.HH}.log.gz
</fileNamePattern>
<!--60天的时间周期,日志量最大20GB-->
<maxHistory>60</maxHistory>
<!-- 该属性在 1.1.6版本后 才开始支持-->
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<!--每个日志文件最大100MB-->
<maxFileSize>100MB</maxFileSize>
</triggeringPolicy>
<!--定义输出格式-->
<encoder>
<pattern>%d [%thread] %-5level %logger{36} [%file : %line] - %msg%n</pattern>
</encoder>
</appender>
<appender name="sftpAppender" class="com.blackdragon.sftp.logger.Appender" />
<!--root是默认的logger 这里设定输出级别是debug-->
<root level="trace">
<!--定义了两个appender,日志会通过往这两个appender里面写-->
<appender-ref ref="stdout"/>
<appender-ref ref="sftpAppender"/>
</root>
</configuration>
步骤二:使用 jsch 连接 sftp 服务器
SftpConfig
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
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<SftpFile>
* @throws SftpException
*/
public List<SftpFile> listFiles(String directory) throws SftpException {
List<SftpFile> 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;
}
}
效果图