SpringBoot集成SFTP客戶(hù)端實(shí)現(xiàn)文件上傳下載實(shí)例
背景
在項(xiàng)目開(kāi)發(fā)中,一般文件存儲(chǔ)很少再使用SFTP服務(wù),但是也不排除合作伙伴使用SFTP來(lái)存儲(chǔ)項(xiàng)目中的文件或者通過(guò)SFTP來(lái)實(shí)現(xiàn)文件數(shù)據(jù)的交互。
我遇到的項(xiàng)目中,就有銀行和保險(xiǎn)公司等合作伙伴通過(guò)SFTP服務(wù)來(lái)實(shí)現(xiàn)與我們項(xiàng)目的文件數(shù)據(jù)的交互。
為了能夠順利地完成與友商的SFTP服務(wù)的連通,我們需要在自己的項(xiàng)目中實(shí)現(xiàn)一套SFTP客戶(hù)端工具。一般我們會(huì)采用Jsch來(lái)實(shí)現(xiàn)SFTP客戶(hù)端。
依賴(lài)
<!--執(zhí)行遠(yuǎn)程操作--> <dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> <version>0.1.55</version> </dependency> <!--鏈接池--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.11.1</version> </dependency>
首先我們一定要引入jsch
依賴(lài),這個(gè)是我們實(shí)現(xiàn)SFTP客戶(hù)端的基石;其次我們引入了鏈接池工具,為了避免每次執(zhí)行SFTP命令都要重新創(chuàng)建鏈接,我們使用池化的方式優(yōu)化了比較消耗資源的創(chuàng)建操作。
創(chuàng)建工具類(lèi)
為了更好的使用SFTP工具,我們把jsch
中關(guān)于SFTP的相關(guān)功能提煉出來(lái),做了一次簡(jiǎn)單的封裝,做成了我們可以直接使用的工具類(lèi)。
里面只有兩類(lèi)方法:
1.創(chuàng)建Session與開(kāi)啟Session;
session創(chuàng)建好后,還不能創(chuàng)建channel,需要開(kāi)啟session后才能創(chuàng)建channel;
2.創(chuàng)建channel與開(kāi)啟channel;
channel也是一樣,創(chuàng)建好的channel需要開(kāi)啟后才能真正地執(zhí)行命令;
public class JschUtil { /** * 創(chuàng)建session * * @param userName 用戶(hù)名 * @param password 密碼 * @param host 域名 * @param port 端口 * @param privateKeyFile 密鑰文件 * @param passphrase 口令 * @return * @throws AwesomeException */ public static Session createSession(String userName, String password, String host, int port, String privateKeyFile, String passphrase) throws AwesomeException { return createSession(new JSch(), userName, password, host, port, privateKeyFile, passphrase); } /** * 創(chuàng)建session * * @param jSch * @param userName 用戶(hù)名 * @param password 密碼 * @param host 域名 * @param port 端口 * @param privateKeyFile 密鑰 * @param passphrase 口令 * @return * @throws AwesomeException */ public static Session createSession(JSch jSch, String userName, String password, String host, int port, String privateKeyFile, String passphrase) throws AwesomeException { try { if (!StringUtils.isEmpty(privateKeyFile)) { // 使用密鑰驗(yàn)證方式,密鑰可以是有口令的密鑰,也可以是沒(méi)有口令的密鑰 if (!StringUtils.isEmpty(passphrase)) { jSch.addIdentity(privateKeyFile, passphrase); } else { jSch.addIdentity(privateKeyFile); } } // 獲取session Session session = jSch.getSession(userName, host, port); if (!StringUtils.isEmpty(password)) { session.setPassword(password); } // 不校驗(yàn)域名 session.setConfig("StrictHostKeyChecking", "no"); return session; } catch (Exception e) { throw new AwesomeException(500, "create session fail"); } } /** * 創(chuàng)建session * * @param jSch * @param userName 用戶(hù)名 * @param password 密碼 * @param host 域名 * @param port 端口 * @return * @throws AwesomeException */ public static Session createSession(JSch jSch, String userName, String password, String host, int port) throws AwesomeException { return createSession(jSch, userName, password, host, port, StringUtils.EMPTY, StringUtils.EMPTY); } /** * 創(chuàng)建session * * @param jSch * @param userName 用戶(hù)名 * @param host 域名 * @param port 端口 * @return * @throws AwesomeException */ private Session createSession(JSch jSch, String userName, String host, int port) throws AwesomeException { return createSession(jSch, userName, StringUtils.EMPTY, host, port, StringUtils.EMPTY, StringUtils.EMPTY); } /** * 開(kāi)啟session鏈接 * * @param jSch * @param userName 用戶(hù)名 * @param password 密碼 * @param host 域名 * @param port 端口 * @param privateKeyFile 密鑰 * @param passphrase 口令 * @param timeout 鏈接超時(shí)時(shí)間 * @return * @throws AwesomeException */ public static Session openSession(JSch jSch, String userName, String password, String host, int port, String privateKeyFile, String passphrase, int timeout) throws AwesomeException { Session session = createSession(jSch, userName, password, host, port, privateKeyFile, passphrase); try { if (timeout >= 0) { session.connect(timeout); } else { session.connect(); } return session; } catch (Exception e) { throw new AwesomeException(500, "session connect fail"); } } /** * 開(kāi)啟session鏈接 * * @param userName 用戶(hù)名 * @param password 密碼 * @param host 域名 * @param port 端口 * @param privateKeyFile 密鑰 * @param passphrase 口令 * @param timeout 鏈接超時(shí)時(shí)間 * @return * @throws AwesomeException */ public static Session openSession(String userName, String password, String host, int port, String privateKeyFile, String passphrase, int timeout) throws AwesomeException { Session session = createSession(userName, password, host, port, privateKeyFile, passphrase); try { if (timeout >= 0) { session.connect(timeout); } else { session.connect(); } return session; } catch (Exception e) { throw new AwesomeException(500, "session connect fail"); } } /** * 開(kāi)啟session鏈接 * * @param jSch * @param userName 用戶(hù)名 * @param password 密碼 * @param host 域名 * @param port 端口 * @param timeout 鏈接超時(shí)時(shí)間 * @return * @throws AwesomeException */ public static Session openSession(JSch jSch, String userName, String password, String host, int port, int timeout) throws AwesomeException { return openSession(jSch, userName, password, host, port, StringUtils.EMPTY, StringUtils.EMPTY, timeout); } /** * 開(kāi)啟session鏈接 * * @param userName 用戶(hù)名 * @param password 密碼 * @param host 域名 * @param port 端口 * @param timeout 鏈接超時(shí)時(shí)間 * @return * @throws AwesomeException */ public static Session openSession(String userName, String password, String host, int port, int timeout) throws AwesomeException { return openSession(userName, password, host, port, StringUtils.EMPTY, StringUtils.EMPTY, timeout); } /** * 開(kāi)啟session鏈接 * * @param jSch * @param userName 用戶(hù)名 * @param host 域名 * @param port 端口 * @param timeout 鏈接超時(shí)時(shí)間 * @return * @throws AwesomeException */ public static Session openSession(JSch jSch, String userName, String host, int port, int timeout) throws AwesomeException { return openSession(jSch, userName, StringUtils.EMPTY, host, port, StringUtils.EMPTY, StringUtils.EMPTY, timeout); } /** * 開(kāi)啟session鏈接 * * @param userName 用戶(hù)名 * @param host 域名 * @param port 端口 * @param timeout 鏈接超時(shí)時(shí)間 * @return * @throws AwesomeException */ public static Session openSession(String userName, String host, int port, int timeout) throws AwesomeException { return openSession(userName, StringUtils.EMPTY, host, port, StringUtils.EMPTY, StringUtils.EMPTY, timeout); } /** * 創(chuàng)建指定通道 * * @param session * @param channelType * @return * @throws AwesomeException */ public static Channel createChannel(Session session, ChannelType channelType) throws AwesomeException { try { if (!session.isConnected()) { session.connect(); } return session.openChannel(channelType.getValue()); } catch (Exception e) { throw new AwesomeException(500, "open channel fail"); } } /** * 創(chuàng)建sftp通道 * * @param session * @return * @throws AwesomeException */ public static ChannelSftp createSftp(Session session) throws AwesomeException { return (ChannelSftp) createChannel(session, ChannelType.SFTP); } /** * 創(chuàng)建shell通道 * * @param session * @return * @throws AwesomeException */ public static ChannelShell createShell(Session session) throws AwesomeException { return (ChannelShell) createChannel(session, ChannelType.SHELL); } /** * 開(kāi)啟通道 * * @param session * @param channelType * @param timeout * @return * @throws AwesomeException */ public static Channel openChannel(Session session, ChannelType channelType, int timeout) throws AwesomeException { Channel channel = createChannel(session, channelType); try { if (timeout >= 0) { channel.connect(timeout); } else { channel.connect(); } return channel; } catch (Exception e) { throw new AwesomeException(500, "connect channel fail"); } } /** * 開(kāi)啟sftp通道 * * @param session * @param timeout * @return * @throws AwesomeException */ public static ChannelSftp openSftpChannel(Session session, int timeout) throws AwesomeException { return (ChannelSftp) openChannel(session, ChannelType.SFTP, timeout); } /** * 開(kāi)啟shell通道 * * @param session * @param timeout * @return * @throws AwesomeException */ public static ChannelShell openShellChannel(Session session, int timeout) throws AwesomeException { return (ChannelShell) openChannel(session, ChannelType.SHELL, timeout); } enum ChannelType { SESSION("session"), SHELL("shell"), EXEC("exec"), X11("x11"), AGENT_FORWARDING("auth-agent@openssh.com"), DIRECT_TCPIP("direct-tcpip"), FORWARDED_TCPIP("forwarded-tcpip"), SFTP("sftp"), SUBSYSTEM("subsystem"); private final String value; ChannelType(String value) { this.value = value; } public String getValue() { return this.value; } } }
SFTP鏈接池化
我們通過(guò)實(shí)現(xiàn)BasePooledObjectFactory
類(lèi)來(lái)池化通道ChannelSftp
。這并不是真正池化的代碼,下面的代碼只是告知池化管理器如何創(chuàng)建對(duì)象和銷(xiāo)毀對(duì)象。
static class SftpFactory extends BasePooledObjectFactory<ChannelSftp> implements AutoCloseable { private Session session; private SftpProperties properties; // 初始化SftpFactory // 里面主要是創(chuàng)建目標(biāo)session,后續(xù)可用通過(guò)這個(gè)session不斷地創(chuàng)建ChannelSftp。 SftpFactory(SftpProperties properties) throws AwesomeException { this.properties = properties; String username = properties.getUsername(); String password = properties.getPassword(); String host = properties.getHost(); int port = properties.getPort(); String privateKeyFile = properties.getPrivateKeyFile(); String passphrase = properties.getPassphrase(); session = JschUtil.createSession(username, password, host, port, privateKeyFile, passphrase); } // 銷(xiāo)毀對(duì)象,主要是銷(xiāo)毀ChannelSftp @Override public void destroyObject(PooledObject<ChannelSftp> p) throws Exception { p.getObject().disconnect(); } // 創(chuàng)建對(duì)象ChannelSftp @Override public ChannelSftp create() throws Exception { int timeout = properties.getTimeout(); return JschUtil.openSftpChannel(this.session, timeout); } // 包裝創(chuàng)建出來(lái)的對(duì)象 @Override public PooledObject<ChannelSftp> wrap(ChannelSftp channelSftp) { return new DefaultPooledObject<>(channelSftp); } // 驗(yàn)證對(duì)象是否可用 @Override public boolean validateObject(PooledObject<ChannelSftp> p) { return p.getObject().isConnected(); } // 銷(xiāo)毀資源,關(guān)閉session @Override public void close() throws Exception { if (Objects.nonNull(session)) { if (session.isConnected()) { session.disconnect(); } session = null; } } }
為了實(shí)現(xiàn)真正的池化操作,我們還需要以下代碼:
1.我們需要在SftpClient對(duì)象中創(chuàng)建一個(gè)GenericObjectPool
對(duì)象池,這個(gè)才是真正的池子,它負(fù)責(zé)創(chuàng)建和存儲(chǔ)所有的對(duì)象。
2.我們還需要提供資源銷(xiāo)毀的功能,也就是實(shí)現(xiàn)AutoCloseable
,在服務(wù)停止時(shí),需要把相關(guān)的資源銷(xiāo)毀。
public class SftpClient implements AutoCloseable { private SftpFactory sftpFactory; GenericObjectPool<ChannelSftp> objectPool; // 構(gòu)造方法1 public SftpClient(SftpProperties properties, GenericObjectPoolConfig<ChannelSftp> poolConfig) throws AwesomeException { this.sftpFactory = new SftpFactory(properties); objectPool = new GenericObjectPool<>(this.sftpFactory, poolConfig); } // 構(gòu)造方法2 public SftpClient(SftpProperties properties) throws AwesomeException { this.sftpFactory = new SftpFactory(properties); SftpProperties.PoolConfig config = properties.getPool(); // 默認(rèn)池化配置 if (Objects.isNull(config)) { objectPool = new GenericObjectPool<>(this.sftpFactory); } else { // 自定義池化配置 GenericObjectPoolConfig<ChannelSftp> poolConfig = new GenericObjectPoolConfig<>(); poolConfig.setMaxIdle(config.getMaxIdle()); poolConfig.setMaxTotal(config.getMaxTotal()); poolConfig.setMinIdle(config.getMinIdle()); poolConfig.setTestOnBorrow(config.isTestOnBorrow()); poolConfig.setTestOnCreate(config.isTestOnCreate()); poolConfig.setTestOnReturn(config.isTestOnReturn()); poolConfig.setTestWhileIdle(config.isTestWhileIdle()); poolConfig.setBlockWhenExhausted(config.isBlockWhenExhausted()); poolConfig.setMaxWait(Duration.ofMillis(config.getMaxWaitMillis())); poolConfig.setTimeBetweenEvictionRuns(Duration.ofMillis(config.getTimeBetweenEvictionRunsMillis())); objectPool = new GenericObjectPool<>(this.sftpFactory, poolConfig); } } // 銷(xiāo)毀資源 @Override public void close() throws Exception { // 銷(xiāo)毀鏈接池 if (Objects.nonNull(this.objectPool)) { if (!this.objectPool.isClosed()) { this.objectPool.close(); } } this.objectPool = null; // 銷(xiāo)毀sftpFactory if (Objects.nonNull(this.sftpFactory)) { this.sftpFactory.close(); } } }
SFTP鏈接池的使用
我們已經(jīng)對(duì)鏈接池進(jìn)行了初始化,下面我們就可以從鏈接池中獲取我們需要的ChannelSftp
來(lái)實(shí)現(xiàn)文件的上傳下載了。
下面實(shí)現(xiàn)了多種文件上傳和下載的方式:
1.直接把本地文件上傳到SFTP服務(wù)器的指定路徑;
2.把InputStream輸入流提交到SFTP服務(wù)器指定路徑中;
3.可以針對(duì)以上兩種上傳方式進(jìn)行進(jìn)度的監(jiān)測(cè);
4.把SFTP服務(wù)器中的指定文件下載到本地機(jī)器上;
5.把SFTP服務(wù)器˙中的文件寫(xiě)入指定的輸出流;
6.針對(duì)以上兩種下載方式,監(jiān)測(cè)下載進(jìn)度;
/** * 上傳文件 * * @param srcFilePath * @param targetDir * @param targetFileName * @return * @throws AwesomeException */ public boolean uploadFile(String srcFilePath, String targetDir, String targetFileName) throws AwesomeException { return uploadFile(srcFilePath, targetDir, targetFileName, null); } /** * 上傳文件 * * @param srcFilePath * @param targetDir * @param targetFileName * @param monitor * @return * @throws AwesomeException */ public boolean uploadFile(String srcFilePath, String targetDir, String targetFileName, SftpProgressMonitor monitor) throws AwesomeException { ChannelSftp channelSftp = null; try { // 從鏈接池獲取對(duì)象 channelSftp = this.objectPool.borrowObject(); // 如果不存在目標(biāo)文件夾 if (!exist(channelSftp, targetDir)) { mkdirs(channelSftp, targetDir); } channelSftp.cd(targetDir); // 上傳文件 if (Objects.nonNull(monitor)) { channelSftp.put(srcFilePath, targetFileName, monitor); } else { channelSftp.put(srcFilePath, targetFileName); } return true; } catch (Exception e) { throw new AwesomeException(500, "upload file fail"); } finally { if (Objects.nonNull(channelSftp)) { // 返還對(duì)象給鏈接池 this.objectPool.returnObject(channelSftp); } } } /** * 上傳文件到目標(biāo)文件夾 * * @param in * @param targetDir * @param targetFileName * @return * @throws AwesomeException */ public boolean uploadFile(InputStream in, String targetDir, String targetFileName) throws AwesomeException { return uploadFile(in, targetDir, targetFileName, null); } /** * 上傳文件,添加進(jìn)度監(jiān)視器 * * @param in * @param targetDir * @param targetFileName * @param monitor * @return * @throws AwesomeException */ public boolean uploadFile(InputStream in, String targetDir, String targetFileName, SftpProgressMonitor monitor) throws AwesomeException { ChannelSftp channelSftp = null; try { channelSftp = this.objectPool.borrowObject(); // 如果不存在目標(biāo)文件夾 if (!exist(channelSftp, targetDir)) { mkdirs(channelSftp, targetDir); } channelSftp.cd(targetDir); if (Objects.nonNull(monitor)) { channelSftp.put(in, targetFileName, monitor); } else { channelSftp.put(in, targetFileName); } return true; } catch (Exception e) { throw new AwesomeException(500, "upload file fail"); } finally { if (Objects.nonNull(channelSftp)) { this.objectPool.returnObject(channelSftp); } } } /** * 下載文件 * * @param remoteFile * @param targetFilePath * @return * @throws AwesomeException */ public boolean downloadFile(String remoteFile, String targetFilePath) throws AwesomeException { return downloadFile(remoteFile, targetFilePath, null); } /** * 下載目標(biāo)文件到本地 * * @param remoteFile * @param targetFilePath * @return * @throws AwesomeException */ public boolean downloadFile(String remoteFile, String targetFilePath, SftpProgressMonitor monitor) throws AwesomeException { ChannelSftp channelSftp = null; try { channelSftp = this.objectPool.borrowObject(); // 如果不存在目標(biāo)文件夾 if (!exist(channelSftp, remoteFile)) { // 不用下載了 return false; } File targetFile = new File(targetFilePath); try (FileOutputStream outputStream = new FileOutputStream(targetFile)) { if (Objects.nonNull(monitor)) { channelSftp.get(remoteFile, outputStream, monitor); } else { channelSftp.get(remoteFile, outputStream); } } return true; } catch (Exception e) { throw new AwesomeException(500, "upload file fail"); } finally { if (Objects.nonNull(channelSftp)) { this.objectPool.returnObject(channelSftp); } } } /** * 下載文件 * * @param remoteFile * @param outputStream * @return * @throws AwesomeException */ public boolean downloadFile(String remoteFile, OutputStream outputStream) throws AwesomeException { return downloadFile(remoteFile, outputStream, null); } /** * 下載文件 * * @param remoteFile * @param outputStream * @param monitor * @return * @throws AwesomeException */ public boolean downloadFile(String remoteFile, OutputStream outputStream, SftpProgressMonitor monitor) throws AwesomeException { ChannelSftp channelSftp = null; try { channelSftp = this.objectPool.borrowObject(); // 如果不存在目標(biāo)文件夾 if (!exist(channelSftp, remoteFile)) { // 不用下載了 return false; } if (Objects.nonNull(monitor)) { channelSftp.get(remoteFile, outputStream, monitor); } else { channelSftp.get(remoteFile, outputStream); } return true; } catch (Exception e) { throw new AwesomeException(500, "upload file fail"); } finally { if (Objects.nonNull(channelSftp)) { this.objectPool.returnObject(channelSftp); } } } /** * 創(chuàng)建文件夾 * * @param channelSftp * @param dir * @return */ protected boolean mkdirs(ChannelSftp channelSftp, String dir) { try { String pwd = channelSftp.pwd(); if (StringUtils.contains(pwd, dir)) { return true; } String relativePath = StringUtils.substringAfter(dir, pwd); String[] dirs = StringUtils.splitByWholeSeparatorPreserveAllTokens(relativePath, "/"); for (String path : dirs) { if (StringUtils.isBlank(path)) { continue; } try { channelSftp.cd(path); } catch (SftpException e) { channelSftp.mkdir(path); channelSftp.cd(path); } } return true; } catch (Exception e) { return false; } } /** * 判斷文件夾是否存在 * * @param channelSftp * @param dir * @return */ protected boolean exist(ChannelSftp channelSftp, String dir) { try { channelSftp.lstat(dir); return true; } catch (Exception e) { return false; } }
集成到SpringBoot中
我們可以通過(guò)java config
的方式,把我們已經(jīng)實(shí)現(xiàn)好的SftpClient
類(lèi)實(shí)例化到Spring IOC
容器中來(lái)管理,以便讓開(kāi)發(fā)人員在整個(gè)項(xiàng)目中通過(guò)@Autowired
的方式就可以直接使用。
配置
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; /** * @author zouwei * @className SftpProperties * @date: 2022/8/19 下午12:12 * @description: */ @Data @Configuration @ConfigurationProperties(prefix = "sftp.config") public class SftpProperties { // 用戶(hù)名 private String username; // 密碼 private String password; // 主機(jī)名 private String host; // 端口 private int port; // 密鑰 private String privateKeyFile; // 口令 private String passphrase; // 通道鏈接超時(shí)時(shí)間 private int timeout; // 鏈接池配置 private PoolConfig pool; @Data public static class PoolConfig { //最大空閑實(shí)例數(shù),空閑超過(guò)此值將會(huì)被銷(xiāo)毀淘汰 private int maxIdle; // 最小空閑實(shí)例數(shù),對(duì)象池將至少保留2個(gè)空閑對(duì)象 private int minIdle; //最大對(duì)象數(shù)量,包含借出去的和空閑的 private int maxTotal; //對(duì)象池滿(mǎn)了,是否阻塞獲?。╢alse則借不到直接拋異常) private boolean blockWhenExhausted; // BlockWhenExhausted為true時(shí)生效,對(duì)象池滿(mǎn)了阻塞獲取超時(shí),不設(shè)置則阻塞獲取不超時(shí),也可在borrowObject方法傳遞第二個(gè)參數(shù)指定本次的超時(shí)時(shí)間 private long maxWaitMillis; // 創(chuàng)建對(duì)象后是否驗(yàn)證對(duì)象,調(diào)用objectFactory#validateObject private boolean testOnCreate; // 借用對(duì)象后是否驗(yàn)證對(duì)象 validateObject private boolean testOnBorrow; // 歸還對(duì)象后是否驗(yàn)證對(duì)象 validateObject private boolean testOnReturn; // 定時(shí)檢查期間是否驗(yàn)證對(duì)象 validateObject private boolean testWhileIdle; //定時(shí)檢查淘汰多余的對(duì)象, 啟用單獨(dú)的線(xiàn)程處理 private long timeBetweenEvictionRunsMillis; //jmx監(jiān)控,和springboot自帶的jmx沖突,可以選擇關(guān)閉此配置或關(guān)閉springboot的jmx配置 private boolean jmxEnabled; } }
java Bean注入
import com.example.awesomespring.exception.AwesomeException; import com.example.awesomespring.sftp.SftpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author zouwei * @className SftpConfig * @date: 2022/8/19 下午12:12 * @description: */ @Configuration public class SftpConfig { @Autowired private SftpProperties properties; // 創(chuàng)建SftpClient對(duì)象 @Bean(destroyMethod = "close") @ConditionalOnProperty(prefix = "sftp.config") public SftpClient sftpClient() throws AwesomeException { return new SftpClient(properties); } }
通過(guò)以上代碼,我們就可以在項(xiàng)目的任何地方直接使用SFTP客戶(hù)端來(lái)上傳和下載文件了。
更多關(guān)于SpringBoot SFTP文件上傳下載的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java?String類(lèi)和StringBuffer類(lèi)的區(qū)別介紹
這篇文章主要介紹了Java?String類(lèi)和StringBuffer類(lèi)的區(qū)別,?關(guān)于java的字符串處理我們一般使用String類(lèi)和StringBuffer類(lèi)有什么不同呢,下面我們一起來(lái)看看詳細(xì)介紹吧2022-03-03Java+Selenium調(diào)用JavaScript的方法詳解
這篇文章主要為大家講解了java在利用Selenium操作瀏覽器網(wǎng)站時(shí)候,有時(shí)會(huì)需要用的JavaScript的地方,代碼該如何實(shí)現(xiàn)呢?快跟隨小編一起學(xué)習(xí)一下吧2023-01-01SpringBoot啟動(dòng)后啟動(dòng)內(nèi)嵌瀏覽器的方法
這篇文章主要介紹了SpringBoot啟動(dòng)后啟動(dòng)內(nèi)嵌瀏覽器的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12ReentrantLock實(shí)現(xiàn)原理詳解
本文將對(duì)ReentrantLock實(shí)現(xiàn)原理進(jìn)行詳細(xì)的介紹,具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-02-02SpringBoot最簡(jiǎn)潔的國(guó)際化配置
這篇文章主要介紹了SpringBoot最簡(jiǎn)潔的國(guó)際化配置,Spring Boot是一個(gè)用于構(gòu)建獨(dú)立的、生產(chǎn)級(jí)別的Spring應(yīng)用程序的框架,國(guó)際化是一個(gè)重要的功能,它允許應(yīng)用程序根據(jù)用戶(hù)的語(yǔ)言和地區(qū)顯示不同的內(nèi)容,在Spring Boot中,實(shí)現(xiàn)國(guó)際化非常簡(jiǎn)單,需要的朋友可以參考下2023-10-10Springboot項(xiàng)目異常處理及返回結(jié)果統(tǒng)一
這篇文章主要介紹了Springboot項(xiàng)目異常處理及返回結(jié)果統(tǒng)一,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-08-08