使用 JAVA Swing 构建 Sftp 桌面连接工具
技术要点
- 将 logback 日志输出到 Swing 组件上。
- 使 jsch.jar 包连接 sftp 服务器、实现文件的上传下载。
- 最终是为了实现文件自动同步。
步骤一、将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 > 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 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 >= 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 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 桌面连接工具相关推荐
- Java Swing图书管理系统桌面软件附源码
Java Swing图书管理系统桌面软件附源码,亲测可运行. 功能界面如下: 登录界面,默认账号admin密码admin 主功能界面: 添加图书功能界面: 部分源码: 完整源码下载地址: JavaSw ...
- java swing 悬浮_[Java教程]JAVA Swing窗口在桌面上浮动_星空网
JAVA Swing窗口在桌面上浮动 2012-02-15 0 1 class Util { 2 private Toolkit tool; 3 private int width; 4 privat ...
- Java Swing中的聊天气泡
本文将向您解释"如何在Java swing应用程序中绘制聊天气泡?" 聊天气泡与呼出气泡或思想气泡相同. 今天,大多数聊天应用程序都以这种格式显示转换,因此本文将帮助您在用Java ...
- java swing 聊天气泡_Java Swing中的聊天气泡
本文将向您解释"如何在Java swing应用程序中绘制聊天气泡?" 聊天气泡与呼出气泡或思想气泡相同. 今天,大多数聊天应用程序都以这种格式显示转换,因此本文将帮助您在用Java ...
- Java swing酒店管理系统
Java酒店管理系统 以前在学校急忙写的一个小项目,不完善,但该有的功能基本都有,排版马马虎虎,利用java基础和java swing构建的一个酒店管理系统,管理人员能够查看房间状态,用户则可以查看剩 ...
- Java Swing快速构建窗体应用程序
以前接触java感觉其在桌面开发上,总是不太方便,没有一个好的拖拽界面布局工具,可以快速构建窗体. 最近学习了一下NetBeans IDE 8.1,感觉其窗体设计工具还是很不错的 , 就尝试一下做了一 ...
- JNoteHelper 给你的java swing或桌面程序提供一双翅膀
JNoteHelper 使用swing构建的java程序, 主要基于miglayout,swingx,flatlatf. 开发得初衷,只是打算作为个人笔记的助手, 因为基于java swing开发, ...
- java swing 文件选择,设置默认文件选择路径,桌面路径
在上传文件,选择文件的时候,往往会遇到路径选择的问题,比如,一般上传的默认路径是 我的文档,而我们恰好需要默认在桌面,那怎么办呢? 下面的内容也许会帮到你! 首先,看java swing 方面,使用 ...
- Java Swing中JFreeChart构建双纵轴(双Y轴)图表的使用纪要
背景 项目应用中整理纪要,用于参数说明.后抽部分简码以用例,特此纪要! 问题 Java Swing中JFreeChart如何构建双纵轴(双Y轴)图表 说明 JFreeChart是一个工厂类,是Swin ...
最新文章
- SAP Spartacus organization unit list抬头显示所有unit的标题实现
- 微机原理——移位指令
- sum(x) over( partition by y ORDER BY z ) 分析
- 操作系统之计算机系统概述:1、操作系统概述(定义、功能、作用)
- linux下安装TensorFlow(centos)
- 实用必备xp框架模块_两款实用工具类软件,是你的日常必备!
- 【ElasticSearch】es Elasticsearch压测实战 II esrally 进阶实战 笔记
- Docker部署MySQL5.7主从复制结构
- 在html中写三角,css3怎么写三角形?
- 蚂蚁S9矿卡ddr型号确认方法
- 【codeforces】【01字符串匹配】Equalize【Manthan, Codefest 18 (rated, Div. 1 + Div. 2)】
- JS让网页字体大小随窗口大小改变而改变
- KOG注释[Ubuntu 15.10系统]
- P4208 [JSOI2008]最小生成树计数
- 搭建自己的Linux根文件系统
- 小 tips:删除word表格下面多余的空白页
- uni-app,文本实现展开、收起全文
- 信息学奥赛一本通1278:复制书稿(evd)
- php办公网聊天室,使用phpFreeChat在您的网站上运行聊天室 | MOS86
- sublime快捷键!+tab键失效
热门文章
- 亚太元宇宙新纪元峰会于1月12日在上海淳大万丽酒店盛大召开
- insetSelective 和insert的区别
- FastDFS,Redis,Solr,ActiveMQ核心技术整合五
- 关于 Spring AOP (AspectJ) 你该知晓的一切
- splatter包安装总结
- 图森计划裁员25%/ 特斯拉被曝将冻结招聘/ 天才黑客Geohot从推特辞职…今日更多新鲜事在此...
- GO+Selenium批量关注各大网站实战 2 (今日头条,批量关注)
- 《王道计算机组成原理》学习笔记和总目录导航
- 计算机网络学习笔记:基础知识
- 为什么建议大家使用 Linux 开发?爽++