使用sftp操作文件并添加事务管理
2019-03-08 18:56 QQ~sunshine 阅读(797) 评论(0) 编辑 收藏 举报本文主要针对文件操作的事务管理,即写文件和删除文件并且能保证事务的一致性,可与数据库联合使用,比如需要在服务器存文件,相应的记录存放在数据库,那么数据库的记录和服务器的文件数一定是要一一对应的,该部分代码可以保证大多数情况下的文件部分的事务要求(特殊情况下面会说),和数据库保持一致的话需要自行添加数据库部分,比较简单。
基本原理就是,添加文件时先在目录里添加一个临时的文件,如果失败或者数据库插入部分失败直接回滚,即删除该文件,如果成功则提交事务,即将该文件重命名为你需要的正式文件名字(重命名基本不会失败,如果失败了比如断电,那就是特殊情况了)。同理删除文件是先将文件重命名做一个临时文件而不是直接删除,然后数据库部分删除失败的话回滚事务,即将该文件重命名成原来的,如果成功则提交事务,即删除临时文件。
和数据库搭配使用异常的逻辑判断需要谨慎,比如删除文件应先对数据库操作进行判断,如果先对文件操作进行判断,加入成功了直接提交事务即删除了临时文件,数据库部分失败了文件是没办法回滚的。
我这里用的是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 private String 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,到这里已经完了,之前有需要写文件事务管理的时候只找到一个谷歌的包可以完成(包名一时半会忘记了),但是与实际功能还有些差别,所以就根据那个源码自己改了改,代码写的可能很一般,主要也是怕以后自己用忘记,就记下来,如果刚好能帮到有需要的人,那就更好。哪位大神如果有更好的方法也请不要吝啬,传授一下。(抱拳)