使用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}") 7 private String ip; 8 @Value("${spring.sftp.port}") 9 private int port; 10 @Value("${spring.sftp.username}") 11 private String username; 12 @Value("${spring.sftp.password}") 13 privateString password; 14 15 public String getIp() { 16 return ip; 17 } 18 19 public void setIp(String ip) { 20 this.ip = ip; 21 } 22 23 public int getPort() { 24 return port; 25 } 26 27 public void setPort(int port) { 28 this.port = port; 29 } 30 31 public String getUsername() { 32 return username; 33 } 34 35 public void setUsername(String username) { 36 this.username = username; 37 } 38 39 public String getPassword() { 40 return password; 41 } 42 43 public void setPassword(String password) { 44 this.password = password; 45 } 46 47 @Override 48 public String toString() { 49 return "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 { 10 private static final Logger logger = LoggerFactory.getLogger(SftpClient.class); 11 private Session session; 12 13 //通過sftp連接服務器 14 public SftpClient(SftpProperties config) throws JSchException { 15 JSch.setConfig("StrictHostKeyChecking", "no"); 16 session = new JSch().getSession(config.getUsername(), config.getIp(), config.getPort()); 17 session.setPassword(config.getPassword()); 18 session.connect(); 19 } 20 21 public Session getSession() { 22 return session; 23 } 24 25 public ChannelSftp getSftpChannel() throws JSchException { 26 ChannelSftp channel = (ChannelSftp) session.openChannel("sftp"); 27 channel.connect(); 28 return channel; 29 } 30 31 /** 32 * 讀取文件內容 33 * @param destFm 文件絕對路徑 34 * @return 35 * @throws JSchException 36 * @throws IOException 37 * @throws SftpException 38 */ 39 public byte[] readBin(String destFm) throws JSchException, IOException, SftpException { 40 ChannelSftp channel = (ChannelSftp) session.openChannel("sftp"); 41 channel.connect(); 42 try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { 43 channel.get(destFm, outputStream); 44 return outputStream.toByteArray(); 45 } finally { 46 channel.disconnect(); 47 } 48 } 49 50 /** 51 * 退出登錄 52 */ 53 @Override 54 public void close() throws Exception { 55 try { 56 this.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 { 16 private static final Logger LOGGER = LoggerFactory.getLogger(SftpTransaction.class); 17 private final String transactionId; // 事務唯一id 18 private final ChannelSftp channelSftp; 19 private int opType = -1; // 文件操作標識 1 添加文件 2 刪除文件 20 private List<String> opFiles = new ArrayList<>(5); 21 22 public SftpTransaction(SftpClient client) throws JSchException { 23 this.transactionId = StringUtils.replace(UUID.randomUUID().toString(), "-", ""); 24 this.channelSftp = client.getSftpChannel(); 25 } 26 27 // 根據文件名和事務id創建臨時文件 28 private String transactionFilename(String transactionId, String filename, String path) { 29 return String.format("%stransact-%s-%s", path, transactionId, filename); 30 } 31 32 // 根據路徑反推文件名 33 private String unTransactionFilename(String tfm, String path) { 34 return path + StringUtils.split(tfm, "-", 3)[2]; 35 } 36 37 /** 38 * 添加文件 39 * @param contents 存放文件內容 40 * @param path 文件絕對路徑(不包含文件名) 41 * @throws Exception 42 */ 43 public void create(List<Pair<String, byte[]>> contents, String path) throws Exception { 44 if (this.opType == -1) { 45 this.opType = 1; 46 } else { 47 throw new IllegalStateException(); 48 } 49 for (Pair<String, byte[]> content : contents) { 50 // 獲取content裏的數據 51 try (ByteArrayInputStream stream = new ByteArrayInputStream(content.getValue())) { 52 // 拼接一個文件名做臨時文件 53 String destFm = this.transactionFilename(this.transactionId, content.getKey(), path); 54 this.channelSftp.put(stream, destFm); 55 this.opFiles.add(destFm); 56 } 57 } 58 } 59 60 /** 61 * 刪除文件 62 * @param contents 存放要刪除的文件名 63 * @param path 文件的絕對路徑(不包含文件名) 64 * @throws Exception 65 */ 66 public void delete(List<String> contents, String path) throws Exception { 67 if (this.opType == -1) { 68 this.opType = 2; 69 } else { 70 throw new IllegalStateException(); 71 } 72 for (String name : contents) { 73 String destFm = this.transactionFilename(this.transactionId, name, path); 74 this.channelSftp.rename(path+name, destFm); 75 this.opFiles.add(destFm); 76 } 77 } 78 79 /** 80 * 提交事務 81 * @param path 絕對路徑(不包含文件名) 82 * @throws Exception 83 */ 84 public void commit(String path) throws Exception { 85 switch (this.opType) { 86 case 1: 87 for (String fm : this.opFiles) { 88 String destFm = this.unTransactionFilename(fm, path); 89 //將之前的臨時文件命名為真正需要的文件名 90 this.channelSftp.rename(fm, destFm); 91 } 92 break; 93 case 2: 94 for (String fm : opFiles) { 95 //刪除這個文件 96 this.channelSftp.rm(fm); 97 } 98 break; 99 default: 100 throw new IllegalStateException(); 101 } 102 this.channelSftp.disconnect(); 103 } 104 105 /** 106 * 回滾事務 107 * @param path 絕對路徑(不包含文件名) 108 * @throws Exception 109 */ 110 public void rollback(String path) throws Exception { 111 switch (this.opType) { 112 case 1: 113 for (String fm : opFiles) { 114 // 刪除這個文件 115 this.channelSftp.rm(fm); 116 } 117 break; 118 case 2: 119 for (String fm : opFiles) { 120 String destFm = this.unTransactionFilename(fm, path); 121 // 將文件回滾 122 this.channelSftp.rename(fm, destFm); 123 } 124 break; 125 default: 126 throw new IllegalStateException(); 127 } 128 this.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 7 private SftpClient client; 8 9 //開啟事務 10 public SftpTransaction startTransaction() throws Exception { 11 return new SftpTransaction(client); 12 } 13 14 /** 15 * 提交事務 16 * @param transaction 17 * @param path 絕對路徑(不包含文件名) 18 * @throws Exception 19 */ 20 public void commitTransaction(SftpTransaction transaction, String path) throws Exception { 21 transaction.commit(path); 22 } 23 24 /** 25 * 回滾事務 26 * @param transaction 27 * @param path 絕對路徑(不包含文件名) 28 * @throws Exception 29 */ 30 public void rollbackTransaction(SftpTransaction transaction, String path) throws Exception { 31 transaction.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 17 public static void createFile() throws Exception { 18 // 定義一個存放文件的絕對路徑 19 String targetPath = "/data/file/"; 20 //創建一個事務管理實例 21 SftpTransactionManager manager = new SftpTransactionManager(); 22 SftpTransaction sftpTransaction = null; 23 try { 24 //開啟事務並返回一個事務實例 25 sftpTransaction = manager.startTransaction(); 26 //創建一個存放要操作文件的集合 27 List<Pair<String, byte[]>> contents = new ArrayList<>(); 28 ImmutablePair aPair = new ImmutablePair<>("file_a", "data_a".getBytes()); //file_a是文件a的名字,data_a是文件a的內容 29 ImmutablePair bPair = new ImmutablePair<>("file_b", "data_b".getBytes()); 30 ImmutablePair cPair = new ImmutablePair<>("file_c", "data_c".getBytes()); 31 contents.add(aPair); 32 contents.add(bPair); 33 contents.add(cPair); 34 // 將內容進行事務管理 35 sftpTransaction.create(contents, targetPath); 36 // 事務提交 37 manager.commitTransaction(sftpTransaction, targetPath); 38 }catch (Exception e) { 39 if (sftpTransaction != null) { 40 // 發生異常事務回滾 41 manager.rollbackTransaction(sftpTransaction, targetPath); 42 } 43 throw e; 44 } 45 } 46 47 //刪除文件 48 @Test 49 public void deleteFile() throws Exception { 50 // 定義一個存放文件的絕對路徑 51 String targetPath = "/data/file/"; 52 //創建一個事務管理實例 53 SftpTransactionManager manager = new SftpTransactionManager(); 54 SftpTransaction sftpTransaction = null; 55 try { 56 //開啟事務並返回一個事務實例 57 sftpTransaction = manager.startTransaction(); 58 List<String> contents = new ArrayList<>(); 59 contents.add("file_a"); // file_a要刪除的文件名 60 contents.add("file_b"); 61 contents.add("file_c"); 62 sftpTransaction.delete(contents, targetPath); 63 manager.commitTransaction(sftpTransaction, targetPath); 64 } catch (Exception e) { 65 //回滾事務 66 if (sftpTransaction != null) { 67 manager.rollbackTransaction(sftpTransaction, targetPath); 68 } 69 throw e; 70 } 71 } 72 }
這是對於sftp文件操作的依賴,其他的依賴應該都挺好。
1 <dependency> 2 <groupId>com.jcraft</groupId> 3 <artifactId>jsch</artifactId> 4 </dependency>
ok,到這裏已經完了,之前有需要寫文件事務管理的時候只找到一個谷歌的包可以完成(包名一時半會忘記了),但是與實際功能還有些差別,所以就根據那個源碼自己改了改,代碼寫的可能很一般,主要也是怕以後自己用忘記,就記下來,如果剛好能幫到有需要的人,那就更好。哪位大神如果有更好的方法也請不要吝嗇,傳授一下。(抱拳)
使用sftp操作文件並添加事務管理