背景
在项目开发中,一般文件存储很少再使用SFTP服务,但是也不排除合作伙伴使用SFTP来存储项目中的文件或者通过SFTP来实现文件数据的交互。我遇到的项目中,就有银行和保险公司等合作伙伴通过SFTP服务来实现与我们项目的文件数据的交互。
为了能够顺利地完成与友商的SFTP服务的连通,我们需要在自己的项目中实现一套SFTP客户端工具。一般我们会采用Jsch来实现SFTP客户端。
依赖
<!--执行远程操作--><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
依赖,这个是我们实现SFTP客户端的基石;其次我们引入了链接池工具,为了避免每次执行SFTP命令都要重新创建链接,我们使用池化的方式优化了比较消耗资源的创建操作。
创建工具类
为了更好的使用SFTP工具,我们把jsch
中关于SFTP的相关功能提炼出来,做了一次简单的封装,做成了我们可以直接使用的工具类。
里面只有两类方法:
1.创建Session与开启Session;
session创建好后,还不能创建channel,需要开启session后才能创建channel;
2.创建channel与开启channel;
channel也是一样,创建好的channel需要开启后才能真正地执行命令;
publicclassJschUtil{/***创建session**@paramuserName用户名*@parampassword密码*@paramhost域名*@paramport端口*@paramprivateKeyFile密钥文件*@parampassphrase口令*@return*@throwsAwesomeException*/publicstaticSessioncreateSession(StringuserName,Stringpassword,Stringhost,intport,StringprivateKeyFile,Stringpassphrase)throwsAwesomeException{returncreateSession(newJSch(),userName,password,host,port,privateKeyFile,passphrase);}/***创建session**@paramjSch*@paramuserName用户名*@parampassword密码*@paramhost域名*@paramport端口*@paramprivateKeyFile密钥*@parampassphrase口令*@return*@throwsAwesomeException*/publicstaticSessioncreateSession(JSchjSch,StringuserName,Stringpassword,Stringhost,intport,StringprivateKeyFile,Stringpassphrase)throwsAwesomeException{try{if(!StringUtils.isEmpty(privateKeyFile)){//使用密钥验证方式,密钥可以是有口令的密钥,也可以是没有口令的密钥if(!StringUtils.isEmpty(passphrase)){jSch.addIdentity(privateKeyFile,passphrase);}else{jSch.addIdentity(privateKeyFile);}}//获取sessionSessionsession=jSch.getSession(userName,host,port);if(!StringUtils.isEmpty(password)){session.setPassword(password);}//不校验域名session.setConfig("StrictHostKeyChecking","no");returnsession;}catch(Exceptione){thrownewAwesomeException(500,"createsessionfail");}}/***创建session**@paramjSch*@paramuserName用户名*@parampassword密码*@paramhost域名*@paramport端口*@return*@throwsAwesomeException*/publicstaticSessioncreateSession(JSchjSch,StringuserName,Stringpassword,Stringhost,intport)throwsAwesomeException{returncreateSession(jSch,userName,password,host,port,StringUtils.EMPTY,StringUtils.EMPTY);}/***创建session**@paramjSch*@paramuserName用户名*@paramhost域名*@paramport端口*@return*@throwsAwesomeException*/privateSessioncreateSession(JSchjSch,StringuserName,Stringhost,intport)throwsAwesomeException{returncreateSession(jSch,userName,StringUtils.EMPTY,host,port,StringUtils.EMPTY,StringUtils.EMPTY);}/***开启session链接**@paramjSch*@paramuserName用户名*@parampassword密码*@paramhost域名*@paramport端口*@paramprivateKeyFile密钥*@parampassphrase口令*@paramtimeout链接超时时间*@return*@throwsAwesomeException*/publicstaticSessionopenSession(JSchjSch,StringuserName,Stringpassword,Stringhost,intport,StringprivateKeyFile,Stringpassphrase,inttimeout)throwsAwesomeException{Sessionsession=createSession(jSch,userName,password,host,port,privateKeyFile,passphrase);try{if(timeout>=0){session.connect(timeout);}else{session.connect();}returnsession;}catch(Exceptione){thrownewAwesomeException(500,"sessionconnectfail");}}/***开启session链接**@paramuserName用户名*@parampassword密码*@paramhost域名*@paramport端口*@paramprivateKeyFile密钥*@parampassphrase口令*@paramtimeout链接超时时间*@return*@throwsAwesomeException*/publicstaticSessionopenSession(StringuserName,Stringpassword,Stringhost,intport,StringprivateKeyFile,Stringpassphrase,inttimeout)throwsAwesomeException{Sessionsession=createSession(userName,password,host,port,privateKeyFile,passphrase);try{if(timeout>=0){session.connect(timeout);}else{session.connect();}returnsession;}catch(Exceptione){thrownewAwesomeException(500,"sessionconnectfail");}}/***开启session链接**@paramjSch*@paramuserName用户名*@parampassword密码*@paramhost域名*@paramport端口*@paramtimeout链接超时时间*@return*@throwsAwesomeException*/publicstaticSessionopenSession(JSchjSch,StringuserName,Stringpassword,Stringhost,intport,inttimeout)throwsAwesomeException{returnopenSession(jSch,userName,password,host,port,StringUtils.EMPTY,StringUtils.EMPTY,timeout);}/***开启session链接**@paramuserName用户名*@parampassword密码*@paramhost域名*@paramport端口*@paramtimeout链接超时时间*@return*@throwsAwesomeException*/publicstaticSessionopenSession(StringuserName,Stringpassword,Stringhost,intport,inttimeout)throwsAwesomeException{returnopenSession(userName,password,host,port,StringUtils.EMPTY,StringUtils.EMPTY,timeout);}/***开启session链接**@paramjSch*@paramuserName用户名*@paramhost域名*@paramport端口*@paramtimeout链接超时时间*@return*@throwsAwesomeException*/publicstaticSessionopenSession(JSchjSch,StringuserName,Stringhost,intport,inttimeout)throwsAwesomeException{returnopenSession(jSch,userName,StringUtils.EMPTY,host,port,StringUtils.EMPTY,StringUtils.EMPTY,timeout);}/***开启session链接**@paramuserName用户名*@paramhost域名*@paramport端口*@paramtimeout链接超时时间*@return*@throwsAwesomeException*/publicstaticSessionopenSession(StringuserName,Stringhost,intport,inttimeout)throwsAwesomeException{returnopenSession(userName,StringUtils.EMPTY,host,port,StringUtils.EMPTY,StringUtils.EMPTY,timeout);}/***创建指定通道**@paramsession*@paramchannelType*@return*@throwsAwesomeException*/publicstaticChannelcreateChannel(Sessionsession,ChannelTypechannelType)throwsAwesomeException{try{if(!session.isConnected()){session.connect();}returnsession.openChannel(channelType.getValue());}catch(Exceptione){thrownewAwesomeException(500,"openchannelfail");}}/***创建sftp通道**@paramsession*@return*@throwsAwesomeException*/publicstaticChannelSftpcreateSftp(Sessionsession)throwsAwesomeException{return(ChannelSftp)createChannel(session,ChannelType.SFTP);}/***创建shell通道**@paramsession*@return*@throwsAwesomeException*/publicstaticChannelShellcreateShell(Sessionsession)throwsAwesomeException{return(ChannelShell)createChannel(session,ChannelType.SHELL);}/***开启通道**@paramsession*@paramchannelType*@paramtimeout*@return*@throwsAwesomeException*/publicstaticChannelopenChannel(Sessionsession,ChannelTypechannelType,inttimeout)throwsAwesomeException{Channelchannel=createChannel(session,channelType);try{if(timeout>=0){channel.connect(timeout);}else{channel.connect();}returnchannel;}catch(Exceptione){thrownewAwesomeException(500,"connectchannelfail");}}/***开启sftp通道**@paramsession*@paramtimeout*@return*@throwsAwesomeException*/publicstaticChannelSftpopenSftpChannel(Sessionsession,inttimeout)throwsAwesomeException{return(ChannelSftp)openChannel(session,ChannelType.SFTP,timeout);}/***开启shell通道**@paramsession*@paramtimeout*@return*@throwsAwesomeException*/publicstaticChannelShellopenShellChannel(Sessionsession,inttimeout)throwsAwesomeException{return(ChannelShell)openChannel(session,ChannelType.SHELL,timeout);}enumChannelType{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");privatefinalStringvalue;ChannelType(Stringvalue){this.value=value;}publicStringgetValue(){returnthis.value;}}}
SFTP链接池化
我们通过实现BasePooledObjectFactory
类来池化通道ChannelSftp
。这并不是真正池化的代码,下面的代码只是告知池化管理器如何创建对象和销毁对象。
staticclassSftpFactoryextendsBasePooledObjectFactory<ChannelSftp>implementsAutoCloseable{privateSessionsession;privateSftpPropertiesproperties;//初始化SftpFactory//里面主要是创建目标session,后续可用通过这个session不断地创建ChannelSftp。SftpFactory(SftpPropertiesproperties)throwsAwesomeException{this.properties=properties;Stringusername=properties.getUsername();Stringpassword=properties.getPassword();Stringhost=properties.getHost();intport=properties.getPort();StringprivateKeyFile=properties.getPrivateKeyFile();Stringpassphrase=properties.getPassphrase();session=JschUtil.createSession(username,password,host,port,privateKeyFile,passphrase);}//销毁对象,主要是销毁ChannelSftp@OverridepublicvoiddestroyObject(PooledObject<ChannelSftp>p)throwsException{p.getObject().disconnect();}//创建对象ChannelSftp@OverridepublicChannelSftpcreate()throwsException{inttimeout=properties.getTimeout();returnJschUtil.openSftpChannel(this.session,timeout);}//包装创建出来的对象@OverridepublicPooledObject<ChannelSftp>wrap(ChannelSftpchannelSftp){returnnewDefaultPooledObject<>(channelSftp);}//验证对象是否可用@OverridepublicbooleanvalidateObject(PooledObject<ChannelSftp>p){returnp.getObject().isConnected();}//销毁资源,关闭session@Overridepublicvoidclose()throwsException{if(Objects.nonNull(session)){if(session.isConnected()){session.disconnect();}session=null;}}}
为了实现真正的池化操作,我们还需要以下代码:
1.我们需要在SftpClient对象中创建一个
GenericObjectPool
对象池,这个才是真正的池子,它负责创建和存储所有的对象。2.我们还需要提供资源销毁的功能,也就是实现
AutoCloseable
,在服务停止时,需要把相关的资源销毁。
publicclassSftpClientimplementsAutoCloseable{privateSftpFactorysftpFactory;GenericObjectPool<ChannelSftp>objectPool;//构造方法1publicSftpClient(SftpPropertiesproperties,GenericObjectPoolConfig<ChannelSftp>poolConfig)throwsAwesomeException{this.sftpFactory=newSftpFactory(properties);objectPool=newGenericObjectPool<>(this.sftpFactory,poolConfig);}//构造方法2publicSftpClient(SftpPropertiesproperties)throwsAwesomeException{this.sftpFactory=newSftpFactory(properties);SftpProperties.PoolConfigconfig=properties.getPool();//默认池化配置if(Objects.isNull(config)){objectPool=newGenericObjectPool<>(this.sftpFactory);}else{//自定义池化配置GenericObjectPoolConfig<ChannelSftp>poolConfig=newGenericObjectPoolConfig<>();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=newGenericObjectPool<>(this.sftpFactory,poolConfig);}}//销毁资源@Overridepublicvoidclose()throwsException{//销毁链接池if(Objects.nonNull(this.objectPool)){if(!this.objectPool.isClosed()){this.objectPool.close();}}this.objectPool=null;//销毁sftpFactoryif(Objects.nonNull(this.sftpFactory)){this.sftpFactory.close();}}}
SFTP链接池的使用
我们已经对链接池进行了初始化,下面我们就可以从链接池中获取我们需要的ChannelSftp
来实现文件的上传下载了。
下面实现了多种文件上传和下载的方式:
1.直接把本地文件上传到SFTP服务器的指定路径;
2.把InputStream输入流提交到SFTP服务器指定路径中;
3.可以针对以上两种上传方式进行进度的监测;
4.把SFTP服务器中的指定文件下载到本地机器上;
5.把SFTP服务器˙中的文件写入指定的输出流;
6.针对以上两种下载方式,监测下载进度;
/***上传文件**@paramsrcFilePath*@paramtargetDir*@paramtargetFileName*@return*@throwsAwesomeException*/publicbooleanuploadFile(StringsrcFilePath,StringtargetDir,StringtargetFileName)throwsAwesomeException{returnuploadFile(srcFilePath,targetDir,targetFileName,null);}/***上传文件**@paramsrcFilePath*@paramtargetDir*@paramtargetFileName*@parammonitor*@return*@throwsAwesomeException*/publicbooleanuploadFile(StringsrcFilePath,StringtargetDir,StringtargetFileName,SftpProgressMonitormonitor)throwsAwesomeException{ChannelSftpchannelSftp=null;try{//从链接池获取对象channelSftp=this.objectPool.borrowObject();//如果不存在目标文件夹if(!exist(channelSftp,targetDir)){mkdirs(channelSftp,targetDir);}channelSftp.cd(targetDir);//上传文件if(Objects.nonNull(monitor)){channelSftp.put(srcFilePath,targetFileName,monitor);}else{channelSftp.put(srcFilePath,targetFileName);}returntrue;}catch(Exceptione){thrownewAwesomeException(500,"uploadfilefail");}finally{if(Objects.nonNull(channelSftp)){//返还对象给链接池this.objectPool.returnObject(channelSftp);}}}/***上传文件到目标文件夹**@paramin*@paramtargetDir*@paramtargetFileName*@return*@throwsAwesomeException*/publicbooleanuploadFile(InputStreamin,StringtargetDir,StringtargetFileName)throwsAwesomeException{returnuploadFile(in,targetDir,targetFileName,null);}/***上传文件,添加进度监视器**@paramin*@paramtargetDir*@paramtargetFileName*@parammonitor*@return*@throwsAwesomeException*/publicbooleanuploadFile(InputStreamin,StringtargetDir,StringtargetFileName,SftpProgressMonitormonitor)throwsAwesomeException{ChannelSftpchannelSftp=null;try{channelSftp=this.objectPool.borrowObject();//如果不存在目标文件夹if(!exist(channelSftp,targetDir)){mkdirs(channelSftp,targetDir);}channelSftp.cd(targetDir);if(Objects.nonNull(monitor)){channelSftp.put(in,targetFileName,monitor);}else{channelSftp.put(in,targetFileName);}returntrue;}catch(Exceptione){thrownewAwesomeException(500,"uploadfilefail");}finally{if(Objects.nonNull(channelSftp)){this.objectPool.returnObject(channelSftp);}}}/***下载文件**@paramremoteFile*@paramtargetFilePath*@return*@throwsAwesomeException*/publicbooleandownloadFile(StringremoteFile,StringtargetFilePath)throwsAwesomeException{returndownloadFile(remoteFile,targetFilePath,null);}/***下载目标文件到本地**@paramremoteFile*@paramtargetFilePath*@return*@throwsAwesomeException*/publicbooleandownloadFile(StringremoteFile,StringtargetFilePath,SftpProgressMonitormonitor)throwsAwesomeException{ChannelSftpchannelSftp=null;try{channelSftp=this.objectPool.borrowObject();//如果不存在目标文件夹if(!exist(channelSftp,remoteFile)){//不用下载了returnfalse;}FiletargetFile=newFile(targetFilePath);try(FileOutputStreamoutputStream=newFileOutputStream(targetFile)){if(Objects.nonNull(monitor)){channelSftp.get(remoteFile,outputStream,monitor);}else{channelSftp.get(remoteFile,outputStream);}}returntrue;}catch(Exceptione){thrownewAwesomeException(500,"uploadfilefail");}finally{if(Objects.nonNull(channelSftp)){this.objectPool.returnObject(channelSftp);}}}/***下载文件**@paramremoteFile*@paramoutputStream*@return*@throwsAwesomeException*/publicbooleandownloadFile(StringremoteFile,OutputStreamoutputStream)throwsAwesomeException{returndownloadFile(remoteFile,outputStream,null);}/***下载文件**@paramremoteFile*@paramoutputStream*@parammonitor*@return*@throwsAwesomeException*/publicbooleandownloadFile(StringremoteFile,OutputStreamoutputStream,SftpProgressMonitormonitor)throwsAwesomeException{ChannelSftpchannelSftp=null;try{channelSftp=this.objectPool.borrowObject();//如果不存在目标文件夹if(!exist(channelSftp,remoteFile)){//不用下载了returnfalse;}if(Objects.nonNull(monitor)){channelSftp.get(remoteFile,outputStream,monitor);}else{channelSftp.get(remoteFile,outputStream);}returntrue;}catch(Exceptione){thrownewAwesomeException(500,"uploadfilefail");}finally{if(Objects.nonNull(channelSftp)){this.objectPool.returnObject(channelSftp);}}}/***创建文件夹**@paramchannelSftp*@paramdir*@return*/protectedbooleanmkdirs(ChannelSftpchannelSftp,Stringdir){try{Stringpwd=channelSftp.pwd();if(StringUtils.contains(pwd,dir)){returntrue;}StringrelativePath=StringUtils.substringAfter(dir,pwd);String[]dirs=StringUtils.splitByWholeSeparatorPreserveAllTokens(relativePath,"/");for(Stringpath:dirs){if(StringUtils.isBlank(path)){continue;}try{channelSftp.cd(path);}catch(SftpExceptione){channelSftp.mkdir(path);channelSftp.cd(path);}}returntrue;}catch(Exceptione){returnfalse;}}/***判断文件夹是否存在**@paramchannelSftp*@paramdir*@return*/protectedbooleanexist(ChannelSftpchannelSftp,Stringdir){try{channelSftp.lstat(dir);returntrue;}catch(Exceptione){returnfalse;}}
集成到SpringBoot中
我们可以通过java config
的方式,把我们已经实现好的SftpClient
类实例化到Spring IOC
容器中来管理,以便让开发人员在整个项目中通过@Autowired
的方式就可以直接使用。
配置
importlombok.Data;importorg.springframework.boot.context.properties.ConfigurationProperties;importorg.springframework.context.annotation.Configuration;/***@authorzouwei*@classNameSftpProperties*@date:2022/8/19下午12:12*@description:*/@Data@Configuration@ConfigurationProperties(prefix="sftp.config")publicclassSftpProperties{//用户名privateStringusername;//密码privateStringpassword;//主机名privateStringhost;//端口privateintport;//密钥privateStringprivateKeyFile;//口令privateStringpassphrase;//通道链接超时时间privateinttimeout;//链接池配置privatePoolConfigpool;@DatapublicstaticclassPoolConfig{//最大空闲实例数,空闲超过此值将会被销毁淘汰privateintmaxIdle;//最小空闲实例数,对象池将至少保留2个空闲对象privateintminIdle;//最大对象数量,包含借出去的和空闲的privateintmaxTotal;//对象池满了,是否阻塞获取(false则借不到直接抛异常)privatebooleanblockWhenExhausted;//BlockWhenExhausted为true时生效,对象池满了阻塞获取超时,不设置则阻塞获取不超时,也可在borrowObject方法传递第二个参数指定本次的超时时间privatelongmaxWaitMillis;//创建对象后是否验证对象,调用objectFactory#validateObjectprivatebooleantestOnCreate;//借用对象后是否验证对象validateObjectprivatebooleantestOnBorrow;//归还对象后是否验证对象validateObjectprivatebooleantestOnReturn;//定时检查期间是否验证对象validateObjectprivatebooleantestWhileIdle;//定时检查淘汰多余的对象,启用单独的线程处理privatelongtimeBetweenEvictionRunsMillis;//jmx监控,和springboot自带的jmx冲突,可以选择关闭此配置或关闭springboot的jmx配置privatebooleanjmxEnabled;}}
java Bean注入
importcom.example.awesomespring.exception.AwesomeException;importcom.example.awesomespring.sftp.SftpClient;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.autoconfigure.condition.ConditionalOnProperty;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;/***@authorzouwei*@classNameSftpConfig*@date:2022/8/19下午12:12*@description:*/@ConfigurationpublicclassSftpConfig{@AutowiredprivateSftpPropertiesproperties;//创建SftpClient对象@Bean(destroyMethod="close")@ConditionalOnProperty(prefix="sftp.config")publicSftpClientsftpClient()throwsAwesomeException{returnnewSftpClient(properties);}}
通过以上代码,我们就可以在项目的任何地方直接使用SFTP客户端来上传和下载文件了。