使用sftp操作檔案並新增事務管理
本文主要針對檔案操作的事務管理,即寫檔案和刪除檔案並且能保證事務的一致性,可與資料庫聯合使用,比如需要在伺服器存檔案,相應的記錄存放在資料庫,那麼資料庫的記錄和伺服器的檔案數一定是要一一對應的,該部分程式碼可以保證大多數情況下的檔案部分的事務要求(特殊情況下面會說),和資料庫保持一致的話需要自行新增資料庫部分,比較簡單。
基本原理就是,新增檔案時先在目錄裡新增一個臨時的檔案,如果失敗或者資料庫插入部分失敗直接回滾,即刪除該檔案,如果成功則提交事務,即將該檔案重新命名為你需要的正式檔名字(重新命名基本不會失敗,如果失敗了比如斷電,那就是特殊情況了)。同理刪除檔案是先將檔案重新命名做一個臨時檔案而不是直接刪除,然後資料庫部分刪除失敗的話回滾事務,即將該檔案重新命名成原來的,如果成功則提交事務,即刪除臨時檔案。
和資料庫搭配使用異常的邏輯判斷需要謹慎,比如刪除檔案應先對資料庫操作進行判斷,如果先對檔案操作進行判斷,加入成功了直接提交事務即刪除了臨時檔案,資料庫部分失敗了檔案是沒辦法回滾的。
我這裡用的是spriingBoot,如果用的別的看情況做修改即可,這裡需要四個類:
SftpProperties : 這個是sftp連線檔案伺服器的各項屬性,各屬性需要配置到springBoot配置檔案中,也可以換種方法獲取到即可。
1 import org.springframework.beans.factory.annotation.Value; 2 import org.springframework.stereotype.Component; 3 4 @Component 5 public class SftpProperties { 6@Value("${spring.sftp.ip}") 7private String ip; 8@Value("${spring.sftp.port}") 9private int port; 10@Value("${spring.sftp.username}") 11private String username; 12@Value("${spring.sftp.password}") 13private String password; 14 15public String getIp() { 16return ip; 17} 18 19public void setIp(String ip) { 20this.ip = ip; 21} 22 23public int getPort() { 24return port; 25} 26 27public void setPort(int port) { 28this.port = port; 29} 30 31public String getUsername() { 32return username; 33} 34 35public void setUsername(String username) { 36this.username = username; 37} 38 39public String getPassword() { 40return password; 41} 42 43public void setPassword(String password) { 44this.password = password; 45} 46 47@Override 48public String toString() { 49return "SftpConfig{" + 50"ip='" + ip + '\'' + 51", port=" + port + 52", username='" + username + '\'' + 53", password='******'}"; 54} 55 }
SftpClient:這個主要通過sftp連線檔案伺服器並讀取資料。
1 import com.jcraft.jsch.*; 2 import org.slf4j.Logger; 3 import org.slf4j.LoggerFactory; 4 import org.springframework.stereotype.Component; 5 6 import java.io.*; 7 8 @Component 9 public class SftpClient implements AutoCloseable { 10private static final Logger logger = LoggerFactory.getLogger(SftpClient.class); 11private Session session; 12 13//通過sftp連線伺服器 14public SftpClient(SftpProperties config) throws JSchException { 15JSch.setConfig("StrictHostKeyChecking", "no"); 16session = new JSch().getSession(config.getUsername(), config.getIp(), config.getPort()); 17session.setPassword(config.getPassword()); 18session.connect(); 19} 20 21public Session getSession() { 22return session; 23} 24 25public ChannelSftp getSftpChannel() throws JSchException { 26ChannelSftp channel = (ChannelSftp) session.openChannel("sftp"); 27channel.connect(); 28return channel; 29} 30 31/** 32* 讀取檔案內容 33* @param destFm 檔案絕對路徑 34* @return 35* @throws JSchException 36* @throws IOException 37* @throws SftpException 38*/ 39public byte[] readBin(String destFm) throws JSchException, IOException, SftpException { 40ChannelSftp channel = (ChannelSftp) session.openChannel("sftp"); 41channel.connect(); 42try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { 43channel.get(destFm, outputStream); 44return outputStream.toByteArray(); 45} finally { 46channel.disconnect(); 47} 48} 49 50/** 51* 退出登入 52*/ 53@Override 54public void close() throws Exception { 55try { 56this.session.disconnect(); 57} catch (Exception e) { 58//ignore 59} 60} 61 }
SftpTransaction:這個主要是對檔案的操作
1 import com.jcraft.jsch.ChannelSftp; 2 import com.jcraft.jsch.JSchException; 3 import org.apache.commons.lang.StringUtils; 4 import org.apache.commons.lang3.tuple.Pair; 5 import org.slf4j.Logger; 6 import org.slf4j.LoggerFactory; 7 import org.springframework.stereotype.Component; 8 9 import java.io.ByteArrayInputStream; 10 import java.util.ArrayList; 11 import java.util.List; 12 import java.util.UUID; 13 14 @Component 15 public class SftpTransaction { 16private static final Logger LOGGER = LoggerFactory.getLogger(SftpTransaction.class); 17private final String transactionId;// 事務唯一id 18private final ChannelSftp channelSftp; 19private int opType = -1;// 檔案操作標識 1 新增檔案2 刪除檔案 20private List<String> opFiles = new ArrayList<>(5); 21 22public SftpTransaction(SftpClient client) throws JSchException { 23this.transactionId = StringUtils.replace(UUID.randomUUID().toString(), "-", ""); 24this.channelSftp = client.getSftpChannel(); 25} 26 27// 根據檔名和事務id建立臨時檔案 28private String transactionFilename(String transactionId, String filename, String path) { 29return String.format("%stransact-%s-%s", path, transactionId, filename); 30} 31 32// 根據路徑反推檔名 33private String unTransactionFilename(String tfm, String path) { 34return path + StringUtils.split(tfm, "-", 3)[2]; 35} 36 37/** 38* 新增檔案 39* @param contents 存放檔案內容 40* @param path 檔案絕對路徑(不包含檔名) 41* @throws Exception 42*/ 43public void create(List<Pair<String, byte[]>> contents, String path) throws Exception { 44if (this.opType == -1) { 45this.opType = 1; 46} else { 47throw new IllegalStateException(); 48} 49for (Pair<String, byte[]> content : contents) { 50// 獲取content裡的資料 51try (ByteArrayInputStream stream = new ByteArrayInputStream(content.getValue())) { 52// 拼接一個檔名做臨時檔案 53String destFm = this.transactionFilename(this.transactionId, content.getKey(), path); 54this.channelSftp.put(stream, destFm); 55this.opFiles.add(destFm); 56} 57} 58} 59 60/** 61* 刪除檔案 62* @param contents 存放要刪除的檔名 63* @param path 檔案的絕對路徑(不包含檔名) 64* @throws Exception 65*/ 66public void delete(List<String> contents, String path) throws Exception { 67if (this.opType == -1) { 68this.opType = 2; 69} else { 70throw new IllegalStateException(); 71} 72for (String name : contents) { 73String destFm = this.transactionFilename(this.transactionId, name, path); 74this.channelSftp.rename(path+name, destFm); 75this.opFiles.add(destFm); 76} 77} 78 79/** 80* 提交事務 81* @param path 絕對路徑(不包含檔名) 82* @throws Exception 83*/ 84public void commit(String path) throws Exception { 85switch (this.opType) { 86case 1: 87for (String fm : this.opFiles) { 88String destFm = this.unTransactionFilename(fm, path); 89//將之前的臨時檔案命名為真正需要的檔名 90this.channelSftp.rename(fm, destFm); 91} 92break; 93case 2: 94for (String fm : opFiles) { 95//刪除這個檔案 96this.channelSftp.rm(fm); 97} 98break; 99default: 100throw new IllegalStateException(); 101} 102this.channelSftp.disconnect(); 103} 104 105/** 106* 回滾事務 107* @param path 絕對路徑(不包含檔名) 108* @throws Exception 109*/ 110public void rollback(String path) throws Exception { 111switch (this.opType) { 112case 1: 113for (String fm : opFiles) { 114// 刪除這個檔案 115this.channelSftp.rm(fm); 116} 117break; 118case 2: 119for (String fm : opFiles) { 120String destFm = this.unTransactionFilename(fm, path); 121// 將檔案回滾 122this.channelSftp.rename(fm, destFm); 123} 124break; 125default: 126throw new IllegalStateException(); 127} 128this.channelSftp.disconnect(); 129} 130 }
SftpTransactionManager:這個是對事務的操作。
1 import org.springframework.beans.factory.annotation.Autowired; 2 import org.springframework.stereotype.Component; 3 4 @Component 5 public class SftpTransactionManager { 6@Autowired 7private SftpClient client; 8 9//開啟事務 10public SftpTransaction startTransaction() throws Exception { 11return new SftpTransaction(client); 12} 13 14/** 15* 提交事務 16* @param transaction 17* @param path 絕對路徑(不包含檔名) 18* @throws Exception 19*/ 20public void commitTransaction(SftpTransaction transaction, String path) throws Exception { 21transaction.commit(path); 22} 23 24/** 25* 回滾事務 26* @param transaction 27* @param path 絕對路徑(不包含檔名) 28* @throws Exception 29*/ 30public void rollbackTransaction(SftpTransaction transaction, String path) throws Exception { 31transaction.rollback(path); 32} 33 }
SftpTransactionTest:這是一個測試類,使用之前可以先行測試是否可行,有問題可以評論
1 import com.springcloud.utils.sftpUtil.SftpTransaction; 2 import com.springcloud.utils.sftpUtil.SftpTransactionManager; 3 import org.apache.commons.lang3.tuple.ImmutablePair; 4 import org.apache.commons.lang3.tuple.Pair; 5 import org.junit.Test; 6 7 import java.util.ArrayList; 8 import java.util.List; 9 10 /** 11*測試檔案事務管理 12*/ 13 public class SftpTransactionTest { 14 15//建立檔案 16@Test 17public static void createFile() throws Exception { 18// 定義一個存放檔案的絕對路徑 19String targetPath = "/data/file/"; 20//建立一個事務管理例項 21SftpTransactionManager manager = new SftpTransactionManager(); 22SftpTransaction sftpTransaction = null; 23try { 24//開啟事務並返回一個事務例項 25sftpTransaction = manager.startTransaction(); 26//建立一個存放要操作檔案的集合 27List<Pair<String, byte[]>> contents = new ArrayList<>(); 28ImmutablePair aPair = new ImmutablePair<>("file_a", "data_a".getBytes());//file_a是檔案a的名字,data_a是檔案a的內容 29ImmutablePair bPair = new ImmutablePair<>("file_b", "data_b".getBytes()); 30ImmutablePair cPair = new ImmutablePair<>("file_c", "data_c".getBytes()); 31contents.add(aPair); 32contents.add(bPair); 33contents.add(cPair); 34// 將內容進行事務管理 35sftpTransaction.create(contents, targetPath); 36// 事務提交 37manager.commitTransaction(sftpTransaction, targetPath); 38}catch (Exception e) { 39if (sftpTransaction != null) { 40// 發生異常事務回滾 41manager.rollbackTransaction(sftpTransaction, targetPath); 42} 43throw e; 44} 45} 46 47//刪除檔案 48@Test 49public void deleteFile() throws Exception { 50// 定義一個存放檔案的絕對路徑 51String targetPath = "/data/file/"; 52//建立一個事務管理例項 53SftpTransactionManager manager = new SftpTransactionManager(); 54SftpTransaction sftpTransaction = null; 55try { 56//開啟事務並返回一個事務例項 57sftpTransaction = manager.startTransaction(); 58List<String> contents = new ArrayList<>(); 59contents.add("file_a");// file_a要刪除的檔名 60contents.add("file_b"); 61contents.add("file_c"); 62sftpTransaction.delete(contents, targetPath); 63manager.commitTransaction(sftpTransaction, targetPath); 64} catch (Exception e) { 65//回滾事務 66if (sftpTransaction != null) { 67manager.rollbackTransaction(sftpTransaction, targetPath); 68} 69throw e; 70} 71} 72 }
這是對於sftp檔案操作的依賴,其他的依賴應該都挺好。
1 <dependency> 2 <groupId>com.jcraft</groupId> 3 <artifactId>jsch</artifactId> 4 </dependency>
ok,到這裡已經完了,之前有需要寫檔案事務管理的時候只找到一個谷歌的包可以完成(包名一時半會忘記了),但是與實際功能還有些差別,所以就根據那個原始碼自己改了改,程式碼寫的可能很一般,主要也是怕以後自己用忘記,就記下來,如果剛好能幫到有需要的人,那就更好。哪位大神如果有更好的方法也請不要吝嗇,傳授一下。(抱拳)