天天看點

關于JSch的使用,執行ssh指令,檔案上傳和下載下傳以及連接配接方式

最近在做一個SAAS服務的項目,SAAS就是軟體即服務,具體可以去問度娘,然後底層呢需要遠端執行SSH指令來進行支援,最後就選擇了JSch來完成這個工作。

JSch是SSH2的一個純JAVA實作。它允許你連接配接到一個sshd伺服器,使用端口轉發,X11轉發,檔案傳輸等等。

大緻需求就是能夠用java代碼來實作對伺服器的一系列操控,其實就是執行一個業務流程的指令。

因為很多的環境配置,系統指令等都已經寫好了腳本,我們用java代碼要實作的就是,連上伺服器,執行指令,上傳、下載下傳檔案,執行腳本等一系列操作。。。

我的設想:

關于對伺服器操作的對外提供三個主要方法;

1、執行指令方法;

2、檔案上傳方法;

3、檔案下載下傳方法。

由于檔案上傳和下載下傳又涉及到進度問題,是以又提供了4個對外擷取檔案上傳和下載下傳情況檢視的方法:

1、擷取檔案大小方法;

2、擷取檔案已傳輸大小方法;

3、判斷檔案是否已經傳輸完成方法;

4、擷取檔案傳輸百分比方法。

下面說下具體實作,一步步來說吧!

一、引入JSch的jar包(我是在POM添加的)

我引入的是JSch最新的包,附上:

<!-- https://mvnrepository.com/artifact/com.jcraft/jsch -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.53</version>
</dependency>
           

二、建工具類

public class SSHUtil{
	
	private static final Logger logger = LoggerFactory.getLogger(SSHUtil.class);

}
           

三、定義初始化常量(後面再說具體作用)

//擷取緩存對象(這個是我們項目的緩存系統,根據命名空間去取存放的資料)
	private BaseCache<HashMap<String, Long>> cache;
	//命名空間(用于從緩存中存放和取出資料)
	private static final String CACHE_NAMESPACE = "syncssh";
	
	//預設session通道存活時間(我這裡定義的是5分鐘)
private static int SESSION_TIMEOUT = 300000;
//預設connect通道存活時間
private static int CONNECT_TIMEOUT = 1000;
//預設端口号
private static int DEFULT_PORT = 22;
    
//初始化對象
private JSch jsch = null;
private Session session = null;
//用于讀取的唯一ID(這個是用于讀取某個檔案上傳或者下載下傳的進度,都放在緩存空間,我肯定需要一個ID來找到是調用者是想找到哪個檔案的傳輸進度)
private String PROCESSID;
           

四、擷取連接配接

這裡要說明一下,擷取Session連接配接的方式有兩種,一種是直接賬号、密碼、IP位址、端口号就能連接配接,另外一種就是秘鑰方式連接配接(免密連接配接);

我們的SAAS服務叢集是基于一個SAAS管理伺服器,所有的子伺服器都是通過該伺服器進行管理,是以要操控子伺服器進行練級,就要在主伺服器上生成一個秘鑰對;

可以使用指令:     ssh-keygen -t rsa  來生成秘鑰;   生成的秘鑰是根據目前登入使用者的賬号生成的,也就是說跟目前登入使用者的賬号是綁定的,

然後就可以在 ssh檔案夾中看到有一個  id_rsa  id_rsa.pub  兩個檔案(也有自定義名稱方法,具體可以查一下);

id_rsa是私鑰,id_rsa.pub是公鑰,接下來要做的就是把這個公鑰拷貝到子伺服器的ssh檔案夾下

然後調用指令 : cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys   

這一步就是把該公鑰作為子伺服器的信任清單中;

要明白這樣做的意義就要先明白免密連接配接的具體步驟:

1.在A上生成公鑰私鑰。 

2.将公鑰拷貝給server B,要重命名成authorized_keys(從英文名就知道含義了) 

3.Server A向Server B發送一個連接配接請求。 

4.Server B得到Server A的資訊後,在authorized_key中查找,如果有相應的使用者名和IP,則随機生成一個字元串,并用Server A的公鑰加密,發送給Server A。 

5.Server A得到Server B發來的消息後,使用私鑰進行解密,然後将解密後的字元串發送給Server B。Server B進行和生成的對比,如果一緻,則允許免登入。 

那麼現在,子伺服器上就有有了主伺服器上的一個公鑰,而主伺服器本身存了一份私鑰。

下面講一下我對這兩個登入方式的定義:

該工具類的構造方法是不對外的,隻提供兩個擷取執行個體的方法 ,分别是:

/**
     * 根據伺服器IP、賬号、密碼擷取jschUtil執行個體
     * @param user  	伺服器賬号
     * @param password	伺服器密碼
     * @param host		伺服器ip
     * @param port		端口号 (傳null預設22端口)
     * @return  賬号、密碼、IP不允許為null 有為null傳回null
     * @throws JSchException
     * @author wangyijie
     * @data 2017年12月7日
     */
    public static SSHUtil getInstance(String user, String password, String host, Integer port) throws JSchException {
    	if (StringUtils.isBlank(user) || StringUtils.isBlank(password) || StringUtils.isBlank(host)) {
    		return null;
    	}
    	if (port == null) {
    		port = DEFULT_PORT;//這個是上面初始化的端口号22
    	}
        SSHInfo sshInfo = new SSHInfo(user, password, host, port);
        return new SSHUtil(sshInfo);
    }
    
    /**
     * 根據伺服器IP、賬号、秘鑰位址、秘鑰密碼擷取jschUtil執行個體
     * @param user  伺服器賬号
     * @param host	伺服器位址
     * @param port	端口号 (傳null預設22端口)
     * @param privateKey	秘鑰位址(本地存放的私鑰位址)
     * @param passphrase	秘鑰密碼
     * @return	賬号、IP、秘鑰位址不允許為null 有為null傳回null
     * @throws JSchException
     * @author wangyijie
     * @data 2017年12月8日
     */
    public static SSHUtil getInstance( String user, String host, Integer port ,String privateKey ,String passphrase) throws JSchException{
    	if (StringUtils.isBlank(user) || StringUtils.isBlank(host) || StringUtils.isBlank(privateKey)) {
    		return null;
    	}
    	if (port == null) {
    		port = DEFULT_PORT;//這個是上面初始化的端口号22

    	}
    	SSHInfo sshInfo = new SSHInfo(user, host, port, privateKey, passphrase);
    	return new SSHUtil(sshInfo);
    }
           

這裡我們看到了我new了一個SSHInfo的對象

這個對象是擷取Session連接配接的重要對象,來看一下這個對象,這個類我是直接在工具類中定義的;(後面的需要的類也都是在該工具類中定義的,因為别的地方用不到)

private static class SSHInfo{
        private String user;		//伺服器賬号
        private String password;	//伺服器密碼
        private String host;		//位址
        private int port;		//端口号
        private String privateKey;  //秘鑰檔案路徑(本地存放的私鑰位址)
        private String passphrase;	//秘鑰的密碼(如果秘鑰進行過加密則需要)
        
        /**
         * 賬号密碼方式構造
         * @param user 賬号
         * @param password 密碼
         * @param host  IP位址
         * @param port  端口号
         */
        public SSHInfo(String user, String password, String host, int port) {
        	this.user = user;
        	this.password = password;
        	this.host = host;
        	this.port = port;
        }
        
        /**
         * 秘鑰方式構造
         * @param user  賬号
         * @param host	IP位址
         * @param port	端口号
         * @param privateKey  秘鑰位址(本地存放的私鑰位址)
         * @param passphrase  秘鑰密碼(如果秘鑰被加密過則需要)
         */
        public SSHInfo(String user, String host, int port, String privateKey, String passphrase) {
        	this.user = user;
        	this.host = host;
        	this.port = port;
        	this.privateKey = privateKey;
        	this.passphrase = passphrase;
        }
        
        public String getPrivateKey() {
	return privateKey;
	}
	public void setPrivateKey(String privateKey) {
	this.privateKey = privateKey;
	}
	public String getPassphrase() {
		return passphrase;
	}
	public void setPassphrase(String passphrase) {
		this.passphrase = passphrase;
	}
        public String getUser() {
            return user;
        }
        public String getPassword() {
            return password;
        }
        public String getHost() {
            return host;
        }
        public int getPort() {
            return port;
        }
    }
           

這個類我也提供了兩個構造函數,一個賬号密碼方式構造,一個秘鑰連接配接方式構造

passphrase這個字段的意思就是當初生成秘鑰對的時候是否對秘鑰加密過,如果加密過,就需要進行解密,這個字段就是對秘鑰加密的密碼是什麼;

這個類看了之後我們再回到之前的擷取執行個體方法上,那麼最後這個SSHInfo對象都被傳到SSHUtil的構造函數中,下面看一下SSHUtil的構造函數:

private SSHUtil(SSHInfo sshInfo) throws JSchException {
    	//初始化緩存
    	cache = SpringContext.getBean("localCache");
    	//設定緩存生命周期為1天
    	cache.set(CACHE_NAMESPACE, TimeUnit.DAYS.toSeconds(1));
    	
    	//執行個體化工具類的時候開啟Session通道
        jsch =new JSch();
        //秘鑰方式連接配接
        if (StringUtils.isNotBlank(sshInfo.getPrivateKey())) {
            if (StringUtils.isNotBlank(sshInfo.getPassphrase())) {  
            	//設定帶密碼的密鑰  
                jsch.addIdentity(sshInfo.getPrivateKey(), sshInfo.getPassphrase());  
            } else {  
            	//設定不帶密碼的密鑰   
                jsch.addIdentity(sshInfo.getPrivateKey());  
            }  
        }
        //擷取session連接配接
        session = jsch.getSession(sshInfo.getUser(),sshInfo.getHost(),sshInfo.getPort());
        //連接配接失敗
        if (session == null) {
        	if (StringUtils.isNotBlank(sshInfo.getPrivateKey())) {
        		logger.error("JSCH秘鑰方式開啟Session通道——失敗,伺服器賬号:{},秘鑰位址:{},秘鑰密碼:{},IP位址:{},端口号:{}",sshInfo.getUser(),
        				sshInfo.getPrivateKey(), sshInfo.getPassphrase(), sshInfo.getHost(), sshInfo.getPort());
        	} else {
        		logger.error("JSCH賬号密碼方式開啟Session通道——失敗,伺服器賬号:{},秘鑰:{},IP位址:{},端口号:{}",sshInfo.getUser(),
                		sshInfo.getPassword(),sshInfo.getHost(),sshInfo.getPort());
        	}
        }
        //如果密碼方式連接配接  session傳入密碼
        if (StringUtils.isNotBlank(sshInfo.getPassword())) {
        	session.setPassword(sshInfo.getPassword());
        }
        session.setUserInfo(new MyUserInfo());
        //設定session通道最大開啟時間  預設5分鐘  可調用close()方法關閉該通道
        session.connect(SESSION_TIMEOUT);
        if (StringUtils.isNotBlank(sshInfo.getPrivateKey())) {
        	logger.info("JSCH秘鑰方式開啟Session通道——成功,伺服器賬号:{},秘鑰位址:{},秘鑰密碼:{},IP位址:{},端口号:{}",sshInfo.getUser(),
    				sshInfo.getPrivateKey(), sshInfo.getPassphrase(), sshInfo.getHost(), sshInfo.getPort());
        } else {
        	 logger.info("JSCH開啟Session通道——成功,伺服器賬号:{},秘鑰:{},IP位址:{},端口号:{}",sshInfo.getUser(),
             		sshInfo.getPassword(),sshInfo.getHost(),sshInfo.getPort());
        }
    }
           

記住!這個方法要的私鑰位址是本地存放的位址!本地!   如果要測試的話,比如windows系統,隻需要把那個生成的私鑰下載下傳到你的電腦上,然後把路徑指向這個私鑰就行了!!!   (我就被這玩意坑了一下~~~)

那麼看到這裡大緻這個Session連接配接就有了

我上面定義Session對象的執行個體的時候是沒有定義成靜态的,是以每個人調用這個方法的時候擷取Session是不共用的,線程是不共享的。

另外,在擷取Session的時候我new了一個MyUserInfo對象對吧,先來看一下這個内部類:

/*
     * 自定義UserInfo
     */
     private static class MyUserInfo implements UserInfo{
        @Override 
        public String getPassphrase() {
        	return null;
        }
        @Override 
        public String getPassword() {
        	return null;
        }
        @Override 
        public boolean promptPassword(String s) {
        	return false;
        }
        @Override 
        public boolean promptPassphrase(String s) { 
        	 return false; 
        }
        @Override
        public boolean promptYesNo(String s) {
            return true;
        }
        @Override 
        public void showMessage(String s) { }
     }
     
           

說實話,我當時也沒具體研究這個類的左右,我隻知道在promptYesNo方法中return true;就不會在連接配接的時候詢問是否确定要連接配接,還有一種方法可以直接确認這個詢問,我這裡就不多說了;

Session連接配接已經有了,下面就是具體的執行方法實作了。

五、指令執行方法

話不多說,先貼代碼:

/**
     * 執行指令
     * @param cmd  要執行的指令
     * <ol>
     * 比如:
     * <li>ls</li>
     * <li>cd opt/</li>
     * </ol>
     * <ol>
     * 多個連續指令可用 && 連接配接
     * <li>cd /opt/softinstaller && chmod u+x *.sh && ./installArg.sh java</li>
     * </ol>
     * @return  成功執行傳回true   連接配接因為錯誤異常斷開傳回false
     * @throws IOException
     * @throws JSchException
     * @throws InterruptedException
     * @author wangyijie
     * @data 2017年12月7日
     */
    public boolean exec(String cmd) throws IOException, JSchException, InterruptedException {
    	logger.warn("JSCH執行系統指令:{}",cmd);
    	//開啟exec通道
        ChannelExec channelExec = (ChannelExec)session.openChannel( "exec" );
        if (channelExec == null) {
        	logger.error("JSCH打開exec通道失敗,需要執行的系統指令:{}",cmd);
        }
        channelExec.setCommand( cmd );
        channelExec.setInputStream( null );
        channelExec.setErrStream( System.err );
        //擷取伺服器輸出流
        InputStream in = channelExec.getInputStream();
        channelExec.connect();
 
        int res = -1;
        StringBuffer buf = new StringBuffer( 1024 );
        byte[] tmp = new byte[ 1024 ];
        while ( true ) {
            while ( in.available() > 0 ) {
                int i = in.read( tmp, 0, 1024 );
                if ( i < 0 ) {
                	break;
                }
                buf.append( new String( tmp, 0, i ) );
            }
            if ( channelExec.isClosed() ) {
                res = channelExec.getExitStatus();
                break;
            }
            TimeUnit.MILLISECONDS.sleep(100);
        }
        logger.warn("系統指令:{},執行結果:{}", cmd, buf);
        //關閉通道
        channelExec.disconnect();
        if (res == IConstant.TRUE) {
        	return true;
        }
        return false;
    }
           

這裡用到的就是JSch的exec通道,通過之前我們擷取的session打開這個通道,然後把指令放進去,通過getInputStream()方法,擷取一個輸入流,這個輸入流是用來讀取該指令執行後,伺服器的執行結果,比如:執行ls,那麼伺服器本身肯定會有回報的,這裡就是把這個回報讀出來。

我這裡隻是把回報寫在了日志中,而傳回結果隻是給了調用成功或者失敗。

六、檔案上傳和下載下傳方法

代碼開路:

/**
     * 上傳檔案到伺服器(上傳傳到伺服器後的檔案名與上傳的檔案同名)
     * @param uploadPath  要上傳到伺服器的路徑
     * @param filePath  本地檔案的存儲路徑
     * @param processid 唯一ID(用于檢視上傳的進度,多個地方調用請勿重複)
     * @param 例如:.sftpUpload("/opt", "F:\\softinstaller.zip");
     * @throws Exception
     * @author wangyijie
     * @data 2017年12月8日
     */
    public void sftpUpload(String uploadPath, String filePath, String processid){  
        Channel channel = null;  
        try {  
        	logger.warn("JSCH開啟sftp通道上傳到伺服器檔案————————上傳到伺服器位置uploadPath={},檔案所在路徑filePath={}",
        			uploadPath, filePath);
            //建立sftp通信通道  
            channel = (Channel) session.openChannel("sftp");
            if (channel == null) {
            	logger.error("JSCH開啟sftp通道上傳到伺服器檔案失敗————————上傳到伺服器位置uploadPath={},檔案所在路徑filePath={}",
            			uploadPath, filePath);
            	return;
            }
            //指定通道存活時間
            channel.connect(CONNECT_TIMEOUT);
            ChannelSftp sftp = (ChannelSftp) channel;  
            //設定檢視進度的ID(隻對該線程有效)
            PROCESSID = processid;
            cache.put(CACHE_NAMESPACE, processid, new HashMap<String, Long>());
            //這個對象是為了檢視進度
            Monitor monitor = new Monitor();
            //開始複制檔案
            sftp.put(filePath, uploadPath, monitor);
            logger.warn("JSCH關閉sftp通道上傳到伺服器檔案—————————上傳到伺服器位置uploadPath={},檔案所在路徑filePath={}",
        			uploadPath, filePath);
        } catch (Exception e) {  
        	 logger.warn("sftp通道上傳到伺服器檔案錯誤—————————上傳到伺服器位置uploadPath={},檔案所在路徑filePath={}",
         			uploadPath, filePath);
        }
    }
           

這裡檔案上傳用的是sftp,代碼注釋應該能看懂了吧~~~~

這裡我要說明的一點就是關于檢視進度,sftp複制檔案有很多種方法,我這裡隻是其中的一種,具體可百度查詢,但是想要檢視進度,就必須要涉及到一個對象Monitor ;

這個對象我下面再細說,這裡我将調用該方法的人傳入的唯一ID跟該檔案上傳的進度綁定在了一起。

下面直接貼上下載下傳的方法:

/**
     * 下載下傳伺服器指定路徑的指定檔案
     * @param fileName   伺服器上的檔案名
     * @param downloadPath	要下載下傳的檔案在伺服器上的路徑
     * @param filePath	要存在本地的位置
     * @param processid  唯一ID(用于檢視下載下傳的進度,多個地方調用請勿重複)
     * @throws Exception
     * @author wangyijie
     * @data 2017年12月11日
     */
    public void sftpDownload(String fileName, String downloadPath, String filePath, String processid) {
    	 Channel channel = null;  
         try {  
         	logger.warn("JSCH開啟sftp通道下載下傳伺服器檔案————————檔案名fileName={},下載下傳的檔案位于伺服器位置downloadPath={},檔案下載下傳到本地的路徑filePath={}",
         			fileName, downloadPath, filePath);
             //建立sftp通信通道  
             channel = (Channel) session.openChannel("sftp");
             if (channel == null) {
            	 logger.error("JSCH開啟sftp通道下載下傳伺服器檔案失敗————————檔案名fileName={},下載下傳的檔案位于伺服器位置downloadPath={},檔案下載下傳到本地的路徑filePath={}",
              			fileName, downloadPath, filePath);
            	 return;
             }
             //指定通道存活時間
             channel.connect(CONNECT_TIMEOUT);
             ChannelSftp sftp = (ChannelSftp) channel;  
             //進入伺服器指定的檔案夾  
             sftp.cd(downloadPath);  
             //該對象用于檢視進度
             Monitor monitor = new Monitor();
             sftp.get(fileName, filePath, monitor);
             logger.warn("JSCH關閉sftp通道下載下傳伺服器檔案————————檔案名fileName={},下載下傳的檔案位于伺服器位置downloadPath={},檔案下載下傳到本地的路徑filePath={}",
            		 fileName, downloadPath, filePath);
         } catch (Exception e) {  
        	 logger.error("sftp通道下載下傳伺服器檔案錯誤————————檔案名fileName={},下載下傳的檔案位于伺服器位置downloadPath={},檔案下載下傳到本地的路徑filePath={}",
            		 fileName, downloadPath, filePath);
         }
    }
    
           

看完這個其實跟上傳差不多,隻不過一個是get,一個是put,方法底層都寫好了,我們隻需要調用傳入參數就行了。

好了,然後就直接開始說這個進度問題吧!

七、進度檢視

先貼上代碼:

/**
 	 * 用于檔案上傳或者下載下傳的進度檢視
 	 * @author wangyijie
 	 * @date 2017年12月8日
 	 * @version 1.0
 	 */
    private class Monitor implements SftpProgressMonitor {
    	private long COUNT = 0;
    	/**
    	 * 檔案開始上傳執行方法
    	 */
    	@Override
    	public void init(int op, String src, String dest, long max) {
    		HashMap<String, Long> map = new HashMap<String, Long>();  //根據命名空間和唯一ID去系統緩存子產品中取得緩存對象  下面一樣的道理
    		if (map != null) {
    			map.put("maxsize", max); //檔案大小   機關/B
    			map.put("count", COUNT); //已經傳輸的大小(目前是0)機關/B   下面一樣的
    			map.put("isend", 0L); //是否傳輸完成   下面一樣的
    			cache.put(CACHE_NAMESPACE, PROCESSID, map);
    		}
    	}
    	/**
    	 * 檔案每傳送一個資料包執行方法
    	 */
    	@Override
    	public boolean count(long count) {
    		COUNT = COUNT + count; //沒傳輸完成一個資料包就加到已經傳輸的大小上
    	 	HashMap<String, Long> map = cache.get(CACHE_NAMESPACE, PROCESSID);
    		if (map != null) {
    			map.put("count", COUNT);
    			map.put("isend", 0L);
    			cache.put(CACHE_NAMESPACE, PROCESSID, map);
    		}
    		return true;
    	}
    	/**
    	 * 檔案傳輸完成執行方法
    	 */
    	@Override
    	public void end() {
       	HashMap<String, Long> map = cache.get(CACHE_NAMESPACE, PROCESSID);
    		if (map != null) {
    			map.put("isend", 1L);
    			cache.put(CACHE_NAMESPACE, PROCESSID, map);
    		}
    	} 
    }
           

這個類就是我上面說的用于檢視進度的類,當我們調用sftp的get、put方法時可以傳入該類,然後該類實作了SftpProgressMonitor 接口,實作了接口的三個方法,

我注釋上已經标出這三個方法的執行時間。

我在SSHUtil中定義了一個用于檢視進度的唯一ID,然後每個調用者擷取執行個體的時候,調用上傳或者下載下傳方法都會給這個ID,當上傳或下載下傳執行的時候,那麼緩存中就動态的存儲了目前檔案傳輸的情況,然後我對外提供了擷取進度的方法,這樣調用者就可以擷取進度,也就是說檔案傳輸的線程來更新進度,另外的線程用來擷取進度。

然後我們來看一下提供的方法:

/**
     * 擷取檔案大小
     * @param processid  檔案上傳或下載下傳方法傳入的唯一ID
     * @return
     * @author wangyijie
     * @data 2017年12月8日
     */
    public Long getFileSize (String processid) {
    	HashMap<String, Long> map = cache.get(CACHE_NAMESPACE, PROCESSID); //根據命名空間和唯一ID擷取緩存對象    下面幾個方法一樣的  把另一個線程put的值取出來
    	if (map == null) {
    		return null;
    	}
    	return map.get("maxsize");
    }
    
    /**
     * 擷取已經傳輸的檔案大小
     * @param processid 檔案上傳或下載下傳方法傳入的唯一ID
     * @return
     * @author wangyijie
     * @data 2017年12月8日
     */
    public Long getCount (String processid) {
    	HashMap<String, Long> map = cache.get(CACHE_NAMESPACE, PROCESSID);
    	if (map == null) {
    		return null;
    	}
    	return map.get("count");
    }
    
    /**
     * 檔案是傳輸完成
     * @param processid 檔案上傳或下載下傳方法傳入的唯一ID
     * @return 完成傳回1 、  未完成傳回0
     * @author wangyijie
     * @data 2017年12月8日
     */
    public Integer isEnd (String processid) {
    	HashMap<String, Long> map = cache.get(CACHE_NAMESPACE, PROCESSID);
    	if (map == null) {
    		return null;
    	}
		return map.get("isend").intValue();
    }
    
    /**
     * 擷取檔案傳輸的百分比
     * @param processid 檔案上傳或下載下傳方法傳入的唯一ID
     * @param 格式  比如   :  #.## 表示精确到小數點後2位   (為空預設為小數點後2位)
     * @return  比如:  12.28%
     * @author wangyijie
     * @data 2017年12月11日
     */
    public String getPercentage(String processid, String formate) {
    	Long max = getFileSize(processid);
    	Long count = getCount(processid);
    	
    	double d = ((double)count * 100)/(double)max;
    	DecimalFormat df = null;
    	if (StringUtils.isBlank(formate)) {
    		df = new DecimalFormat("#.##");
    	} else {
    		df = new DecimalFormat(formate);
    	}
        return df.format(d) + "%";
    }
           

八、關閉方法

Session擷取後,記得關掉它

/**
     * 關閉session通道
     * 
     * @author wangyijie
     * @data 2017年12月8日
     */
    public void close(){
    	session.disconnect();
        logger.warn("Session通道已關閉");
    }
           

沒看懂的可以留言問我~~

所學尚淺,見笑~,但有所知,言無不盡,見諒!