技术要点

  1. 将 logback 日志输出到 Swing 组件上。
  2. 使 jsch.jar 包连接 sftp 服务器、实现文件的上传下载。
  3. 最终是为了实现文件自动同步。

步骤一、将logback日志输出到Swing组件上

Stack Overflow上解决方案(因为自己也通过一些 csdn 的文章对于 AppenderBase 进行继承,但并未实现效果 ,后面直接去Stack Overflow上搜索就一次性解决了)

gitee源代码

温馨提示:

因为我们同步工具是需要部署在 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 Feeltry {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çãoSWINGCLIENT = 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 sizeframe.setBounds(0, 0, 1000, 800);// Set the default window closing methodframe.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);// Set the window to the center of the screenframe.setLocationRelativeTo(null);contentPane = new JPanel(new BorderLayout());contentPane.setBackground(Color.WHITE);jTextPane = new JTextPane();// Settings are not editablejTextPane.setEditable(false);// show with ScrollPanelogScrollPane = 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() {@Overridepublic void actionPerformed(ActionEvent arg0) {btnStart.setEnabled(false);btnStop.setEnabled(true);startSwing();}});btnStop.setEnabled(false);btnStop.addActionListener(new ActionListener() {@Overridepublic 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 {// ERRORERROR_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));// WARNWARN_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));// INFOINFO_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));// DEBUGDEBUG_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));// TRACETRACE_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));// RESTORESTO_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));}@Overridepublic 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();}@Overrideprotected void append(ILoggingEvent event) {/* Formata mensagem do log */String formattedMsg = patternLayout.doLayout(event);// Forma segura de atualizar o JTextpaneSwingUtilities.invokeLater(() -> {// Alias for JTextPhone JScrollPane in the applicationJTextPane 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 linhasint 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 logif (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 positionlogScrollPane.getVerticalScrollBar().setValue(logScrollPane.getVerticalScrollBar().getMaximum());} catch (BadLocationException e) {log.error("error: {}", e.getMessage());}// Vai para a última linhatextPane.setCaretPosition(textPane.getDocument().getLength());});}/*** Código copiado do {@link JTextArea#getLineCount()}* @param textPane de onde quero as linhas contadas* @return quantidade de linhas &gt; 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 &gt;= 0* @return the offset &gt;= 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 documentreturn ((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 &gt;= 0* @param end the end position &gt;= 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 blocke.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;}
}

效果图

使用 JAVA Swing 构建 Sftp 桌面连接工具相关推荐

  1. Java Swing图书管理系统桌面软件附源码

    Java Swing图书管理系统桌面软件附源码,亲测可运行. 功能界面如下: 登录界面,默认账号admin密码admin 主功能界面: 添加图书功能界面: 部分源码: 完整源码下载地址: JavaSw ...

  2. java swing 悬浮_[Java教程]JAVA Swing窗口在桌面上浮动_星空网

    JAVA Swing窗口在桌面上浮动 2012-02-15 0 1 class Util { 2 private Toolkit tool; 3 private int width; 4 privat ...

  3. Java Swing中的聊天气泡

    本文将向您解释"如何在Java swing应用程序中绘制聊天气泡?" 聊天气泡与呼出气泡或思想气泡相同. 今天,大多数聊天应用程序都以这种格式显示转换,因此本文将帮助您在用Java ...

  4. java swing 聊天气泡_Java Swing中的聊天气泡

    本文将向您解释"如何在Java swing应用程序中绘制聊天气泡?" 聊天气泡与呼出气泡或思想气泡相同. 今天,大多数聊天应用程序都以这种格式显示转换,因此本文将帮助您在用Java ...

  5. Java swing酒店管理系统

    Java酒店管理系统 以前在学校急忙写的一个小项目,不完善,但该有的功能基本都有,排版马马虎虎,利用java基础和java swing构建的一个酒店管理系统,管理人员能够查看房间状态,用户则可以查看剩 ...

  6. Java Swing快速构建窗体应用程序

    以前接触java感觉其在桌面开发上,总是不太方便,没有一个好的拖拽界面布局工具,可以快速构建窗体. 最近学习了一下NetBeans IDE 8.1,感觉其窗体设计工具还是很不错的 , 就尝试一下做了一 ...

  7. JNoteHelper 给你的java swing或桌面程序提供一双翅膀

    JNoteHelper 使用swing构建的java程序, 主要基于miglayout,swingx,flatlatf. 开发得初衷,只是打算作为个人笔记的助手, 因为基于java swing开发, ...

  8. java swing 文件选择,设置默认文件选择路径,桌面路径

    在上传文件,选择文件的时候,往往会遇到路径选择的问题,比如,一般上传的默认路径是 我的文档,而我们恰好需要默认在桌面,那怎么办呢? 下面的内容也许会帮到你! 首先,看java swing 方面,使用 ...

  9. Java Swing中JFreeChart构建双纵轴(双Y轴)图表的使用纪要

    背景 项目应用中整理纪要,用于参数说明.后抽部分简码以用例,特此纪要! 问题 Java Swing中JFreeChart如何构建双纵轴(双Y轴)图表 说明 JFreeChart是一个工厂类,是Swin ...

最新文章

  1. SAP Spartacus organization unit list抬头显示所有unit的标题实现
  2. 微机原理——移位指令
  3. sum(x) over( partition by y ORDER BY z ) 分析
  4. 操作系统之计算机系统概述:1、操作系统概述(定义、功能、作用)
  5. linux下安装TensorFlow(centos)
  6. 实用必备xp框架模块_两款实用工具类软件,是你的日常必备!
  7. 【ElasticSearch】es Elasticsearch压测实战 II esrally 进阶实战 笔记
  8. Docker部署MySQL5.7主从复制结构
  9. 在html中写三角,css3怎么写三角形?
  10. 蚂蚁S9矿卡ddr型号确认方法
  11. 【codeforces】【01字符串匹配】Equalize【Manthan, Codefest 18 (rated, Div. 1 + Div. 2)】
  12. JS让网页字体大小随窗口大小改变而改变
  13. KOG注释[Ubuntu 15.10系统]
  14. P4208 [JSOI2008]最小生成树计数
  15. 搭建自己的Linux根文件系统
  16. 小 tips:删除word表格下面多余的空白页
  17. uni-app,文本实现展开、收起全文
  18. 信息学奥赛一本通1278:复制书稿(evd)
  19. php办公网聊天室,使用phpFreeChat在您的网站上运行聊天室 | MOS86
  20. sublime快捷键!+tab键失效

热门文章

  1. 亚太元宇宙新纪元峰会于1月12日在上海淳大万丽酒店盛大召开
  2. insetSelective 和insert的区别
  3. FastDFS,Redis,Solr,ActiveMQ核心技术整合五
  4. 关于 Spring AOP (AspectJ) 你该知晓的一切
  5. splatter包安装总结
  6. 图森计划裁员25%/ 特斯拉被曝将冻结招聘/ 天才黑客Geohot从推特辞职…今日更多新鲜事在此...
  7. GO+Selenium批量关注各大网站实战 2 (今日头条,批量关注)
  8. 《王道计算机组成原理》学习笔记和总目录导航
  9. 计算机网络学习笔记:基础知识
  10. 为什么建议大家使用 Linux 开发?爽++