一、Jsch简介
Jsch 是SSH2的一个纯Java实现。它允许你连接到一个sshd 服务器,使用端口转发,X11转发,文件传输等等。你可以将它的功能集成到你自己的 程序中。
二、 实现原理
1. 根据远程主机的IP地址,用户名和端口,建立会话(Session);
2. 设置用户信息(包括密码和Userinfo),然后连接session;
getSession()只是创建一个session,需要设置必要的认证信息之后,调用connect()才能建立连接。
3. 在session上建立指定类型的通道(Channel);
4. 设置channel上需要远程执行的Shell脚本,连接channel,就可以远程执行该Shell脚本;
调用openChannel(String type) 可以在session上打开指定类型的channel。该channel只是被初始化,使用前需要先调用 connect()进行连接。
5. 可以读取远程执行Shell脚本的输出,然后依次断开channel和session的连接;
三、Channel的类型可以为如下类型:
1)shell - ChannelShell
2)exec - ChannelExec
3)direct-tcpip - ChannelDirectTCPIP
4)sftp - ChannelSftp
5)subsystem - ChannelSubsystem
四、使用
公司有个需求需要删除服务器上历史不用的文件,第一次执行的时候会有上百万的数据需要删除。我们知道Jsch可以用来连接服务器实现sftp操作,当然用它来操作。不能直接执行rm -r *,因为只删除历史数据,保留近期的数据,所有要对文件的日期进行判断。
1:maven引入:
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
2:创建连接:
getCurrentSession方法是获取当前线程的session,封装了一个内部类LocalSession,Biz为项目中封装配置属性的。这个类主要操作
sftp协议的,创建的是ChannelSftp 类来操作服务器。
channel.cd(path); //用来切换目录
channel.put(String src, String dst, int mode) ;//方法用来操作上传数据。
channel.rm(str) //用来操作删除文件。
需求用到的是下面这个方法。
重点说下:Vector files= channel.ls(path); //这个方法用来列出目录下所有的文件信息、包括文件夹。注意返回的是Vector
public class SftpNew {
private final static Logger logger = Logger.getLogger(SftpNew.class);
private JSch jsch = new JSch();
private ThreadLocal<LocalSession> current = new ThreadLocal<SftpNew.LocalSession>();
private class LocalSession {
Session session;
ChannelSftp channel;
public void close() {
if (this.channel != null) {
this.channel.exit();
this.channel.disconnect();
this.channel = null;
}
if (this.session != null) {
this.session.disconnect();
this.session = null;
}
}
}
/**
* 上传pdf
*
* @param src 本地pdf路径
* @param dest sftp上传路径
* @throws Exception
*/
public boolean upload(String src, String dest) {
LocalSession s = this.getCurrentSession();
if (s == null) {
return false;
}
ChannelSftp channel = s.channel;
boolean b = true;
try {
logger.info("上传pdf:" + src);
if (src == null) {
b = false;
}
channel.put(src, dest, ChannelSftp.OVERWRITE);
} catch (Exception e) {
logger.error("sftp文件上传失败,错误信息是:" + Tools.getStackTrace(e));
b = false;
this.close();
}
return b;
}
/**
* 删除sftp上的pdf文件
*
* @param path 存放目录
* @param pdfPath pdf文件名
* @return
*/
public boolean delete(String path, String pdfPath) {
LocalSession s = this.getCurrentSession();
if (s == null) {
return false;
}
ChannelSftp channel = s.channel;
try {
logger.info("删除pdf:" + path);
channel.cd(pdfPath); // 打开pdf存放目录
channel.rm(path); // 删除PDF
} catch (Exception e) {
if (!e.getMessage().toLowerCase().contains("no such file")) {
logger.error("PDF删除失败,请检查");
logger.error("错误信息是:" + e.getMessage());
this.close();
} else {
logger.info("文件不存在,不用删除了");
return true;
}
return false;
}
return true;
}
private LocalSession getCurrentSession() {
LocalSession s = this.current.get();
if (s == null) {
s = new LocalSession();
logger.info("create new sftp channel");
try {
s.session = this.jsch.getSession(Biz.getUser(), Biz.getIp(),
Integer.valueOf(Biz.getPort()));
s.session.setPassword(Biz.getPwd());
s.session.setConfig("StrictHostKeyChecking", "no");
s.session.setTimeout(30000);
s.session.connect();
s.channel = (ChannelSftp) s.session.openChannel("sftp");
s.channel.connect();
this.current.set(s);
} catch (JSchException e) {
logger.error("sftp error: " + e.getMessage());
return null;
}
} else {
if (!s.channel.isConnected()) {
logger.info("the sftp is not sonnected");
// 如果已经失败,则重新连接一下
s.close();
this.current.remove();
return this.getCurrentSession();
}
}
return s;
}
}
正常情况下我们要操作某个目录下文件,我们直接channel.ls(path)返回文件集合就可以了,但是现在的情况时,第一次去ls的时候会返回上百万的文件信息,如果直接用集合接收不是要炸了。
我们点进去看下它里面是如何操作的:
它直接创建了一个Vector集合和实现了一个内部接口LsEntrySelector对象,这个selector接口会在调用
ls(String path, LsEntrySelector selector)的时候传进去。这个接口里面只有一个select(LsEntry entry)方法,看方法描述为:
此方法会在ls方法遍历每个文件实例的时候调用,如果此方法返回BREAK,则ls方法会取消。
再看ls(path)方法里是怎么操作的,它直接重写了selector方法,把每个文件实例LsEntry不做处理直接添加进集合里面,然后返回CONTINUE。
在ls(String path, LsEntrySelector selector)方法中会调用 cancel = selector.select(new LsEntry(f, l, attrs)); 新建的文件实例直接传入,然后添加进Vector集合。返回的一直为CONTINUE.
因此默认情况下,我们调用ls(path) 方法,默认实现的LsEntrySelector 类中的select方法不会返回BREAK,Jsch会把当前目录下所有文件都遍历完添加进Vector中。
显然默认情况下的ls方法并不是我们需要的,我们需要自定义LsEntrySelector实现类,来控制ls什么时候结束,这才是我们需要的。
我采取的方法是,ls每次返回2048个文件实例,删除完这2048个文件之后再去ls,循环删除。
如果条件允许,可以把循环得到的文件实例集合交给多线程处理,主线程只关注读取文件实例,不做删除,我这里生产环境不方便操作多线程。
方法如下:
/**
*
* @param deletCount #每次循环删除多少条数据
* @param path #定期删除文件目录
* @param days #删除多久之前的数据,单位天
* @return
*/
public boolean deleteFilesByOrd( final int deletCount, String path, final String days) {
long t1 = System.currentTimeMillis();
LocalSession s = this.getCurrentSession();
if (s == null) {
return false;
}
boolean needContinue = false;
ChannelSftp channel = s.channel;
final long currentTime = System.currentTimeMillis();
try {
channel.cd(path); // 打开存放目录
final List<String> needDeletFileList = new ArrayList<String>(2048);
ChannelSftp.LsEntrySelector selector = new ChannelSftp.LsEntrySelector() {
long dayLong = Long.valueOf(days);
int count = 0;
public int select(ChannelSftp.LsEntry entry) {
if (count > deletCount) {
count = 0;
return BREAK;
}
if (".".equals(entry.getFilename()) || "..".equals(entry.getFilename())) {
return CONTINUE;
}
//得到文件的最近修改时间,单位是秒,可以参见entry.getAttrs().getMTimeString()得知
long fileDate = ((long) entry.getAttrs().getMTime()) * 1000L;
long exDays = (currentTime - fileDate) / (1000 * 60 * 60 * 24);
if (exDays >= dayLong) {
needDeletFileList.add(entry.getFilename());
count++;
}
return CONTINUE;
}
};
channel.ls(path, selector);
do {
if (needDeletFileList.size() >= deletCount) {
needContinue = true;
} else {
needContinue = false;
}
for (String filename:needDeletFileList
) {
channel.rm(path+"/"+filename);
}
needDeletFileList.clear();
if (needContinue) {
channel.ls(path, selector);
}
} while (needContinue);
long t2 = System.currentTimeMillis();
logger.info("删除文件花费时间:" + (t2 - t1) / (1000*60)+"min");
} catch (Exception e) {
logger.error("定时文件删除失败==={}", e);
return false;
}finally {
close();
}
return true;
}
文件实例类也是ChannelSftp的内部类:
通过它可以得到文件名,文件属性,其他的属性相关信息在SftpATTRS类中。
public class LsEntry implements Comparable{
private String filename;
private String longname;
private SftpATTRS attrs;
LsEntry(String filename, String longname, SftpATTRS attrs){
setFilename(filename);
setLongname(longname);
setAttrs(attrs);
}
public String getFilename(){return filename;};
void setFilename(String filename){this.filename = filename;};
public String getLongname(){return longname;};
void setLongname(String longname){this.longname = longname;};
public SftpATTRS getAttrs(){return attrs;};
void setAttrs(SftpATTRS attrs) {this.attrs = attrs;};
public String toString(){ return longname; }
public int compareTo(Object o) throws ClassCastException{
if(o instanceof LsEntry){
return filename.compareTo(((LsEntry)o).getFilename());
}
throw new ClassCastException("a decendent of LsEntry must be given.");
}
}
有不清楚的地方可以留言。