近期工作过程中,遇到了一个需要解析压缩包的功能开发。对于这个功能,如果频繁的解压压缩包,在性能上多少有些损耗,且解压文件,会产生大量中间文件,不适合后续管理。特别是现在都是基于容器化部署,如果文件操作处理不当,导致删除失败,将很快打满容器磁盘容量,引起容器崩溃重启。
综合考虑之后,采用ArchiveInputStream
,在不解压文件的前提下,解析压缩包。
具体实践如下。
整体流程
- 从SFTP获取压缩包
- 读取压缩包文件内容
- 删除本地压缩包文件
从SFTP获取压缩包文件
添加项目依赖,使用jcraft包来连接SFTP服务器。
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.54</version>
</dependency>
引入依赖后,需要实现一个工具类来连接SFTP。此处只列出关键代码,如果详细工具类,可以网上找一下,有很多。
public class SFTPUtil {
/**
* 通过SFTP连接服务器
*/
public void connect() {
try {
JSch jsch = new JSch();
jsch.getSession(username, host, port);
sshSession = jsch.getSession(username, host, port);
if (log.isInfoEnabled()) {
log.info("Session created.");
}
sshSession.setPassword(password);
Properties sshConfig = new Properties();
sshConfig.put("StrictHostKeyChecking", "no");
sshSession.setConfig(sshConfig);
sshSession.connect();
if (log.isInfoEnabled()) {
log.info("Session connected.");
}
Channel channel = sshSession.openChannel("sftp");
channel.connect();
if (log.isInfoEnabled()) {
log.info("Opening Channel.");
}
sftp = (ChannelSftp) channel;
if (log.isInfoEnabled()) {
log.info("Connected to " + host + ".");
}
}
catch (Exception e) {
log.error("Connected to " + host + "failed.", e);
}
}
/**
* 关闭连接
*/
public void disconnect() {
if (this.sftp != null) {
if (this.sftp.isConnected()) {
this.sftp.disconnect();
if (log.isInfoEnabled()) {
log.info("sftp is closed already");
}
}
}
if (this.sshSession != null) {
if (this.sshSession.isConnected()) {
this.sshSession.disconnect();
if (log.isInfoEnabled()) {
log.info("sshSession is closed already");
}
}
}
}
/**
* 下载单个文件
*
* @param remotePath:远程下载目录(以路径符号结束)
* @param remoteFileName:下载文件名
* @param localPath:本地保存目录(以路径符号结束)
* @param localFileName:保存文件名
* @return
*/
public boolean downloadFile(String remotePath, String remoteFileName, String localPath, String localFileName) {
FileOutputStream fieloutput = null;
if (!remotePath.endsWith("/")) {
remotePath += "/";
}
if (!localPath.endsWith("/")) {
localPath += "/";
}
String remoteFilePath = remotePath + remoteFileName;
try {
String filePath = localPath + localFileName;
SftpATTRS attrs = sftp.lstat(remoteFilePath);
if (attrs.isFifo()){
}
// sftp.cd(remotePath);
mkdirs(localPath);
File file = new File(filePath);
fieloutput = new FileOutputStream(file);
sftp.get(remoteFilePath, fieloutput);
if (log.isInfoEnabled()) {
log.info("===DownloadFile:" + remoteFileName + " success from sftp.");
}
return true;
} catch (FileNotFoundException e) {
log.warn("file not exists ,file name is " + remoteFilePath);
} catch ( SftpException | BaseAppException e){
log.error("download file failed.file name is " + remoteFilePath, e);
} finally {
if (null != fieloutput) {
try {
fieloutput.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
}
从压缩包中获取文件
核心的代码就是下面这段
public final class TarFileUtil {
/**
* 通过文件在压缩包中的路径和文件名称来模糊匹配文件,
* 并返回第一个匹配上的文件
*/
public static byte[] getBytesFromTarFile(File tarFile, String targetFilePath, String targetFileName) {
ArchiveInputStream archiveInputStream = null;
try {
archiveInputStream = getArchiveInputStream(tarFile);
TarArchiveEntry entry = null;
while ((entry = (TarArchiveEntry) archiveInputStream.getNextEntry()) != null) {
if (entry.getSize() <= 0) {
continue;
}
if (!StringUtils.isEmpty(targetFilePath) && !entry.getName().startsWith(targetFilePath)) {
continue;
}
if (!StringUtils.isEmpty(targetFileName) && !entry.getName().endsWith(targetFileName)) {
continue;
}
return FileUtil.getContent(archiveInputStream);
}
} catch (Exception e) {
LOGGER.error("获取压缩包文件失败!", e);
} finally {
if (null != archiveInputStream) {
try {
archiveInputStream.close();
} catch (IOException e) {
LOGGER.error("file close error!", e);
}
}
}
return null;
}
private static ArchiveInputStream getArchiveInputStream(File tarFile) throws IOException, ArchiveException {
if (StringUtils.endsWithIgnoreCase(tarFile.getName(), ".gz")) {
return new ArchiveStreamFactory()
.createArchiveInputStream("tar", new GZIPInputStream(new BufferedInputStream(new FileInputStream(tarFile))));
} else {
return new ArchiveStreamFactory()
.createArchiveInputStream("tar", new BufferedInputStream(new FileInputStream(tarFile)));
}
}
}
通过个方法还可以做一些文件操作的其他方法实现。比如我在工具类中加入了
// 将目标文件的内容转化为String,并返回
public static String readTarFileToStr(File tarFile, String targetFilePath, String targetFileName);
// 将目标文件内容读取出来,写入到本地文件,并返回
public static File readTarFileToFile(File tarFile, String targetFilePath, String targetFileName);
// 列举指定目录下,匹配上文件名的所有文件列表
public static List<String> listFilesInPath(File tarFile, String targetFilePath, String targetFileName);
// 将文件按行读出,并返回内容列表
public static List<String> getLinesFromTarFile(File tarFile, String targetFilePath, String targetFileName);
你也可以根据自己的需要,新增其他方法。因为这些方法实现逻辑类似,此处不再赘述。
注意:ArchiveInputStream
的读取和关闭,应保证在一个方法体里,尽量不要将ArchiveInputStream
对象作为结果返回出去,在外层做关闭操作,以免文件流关闭失败,导致临时文件无法删除。
删除临时文件
压缩包解析完成后,切记删除下载的压缩包。删除前,确认所有的文件流都关闭了。
public class ResolveHandel{
public void resolveTarFileMethod() {
try{
sftpUtil.connect();
sftpUtil.downloadFile(filePath + fileName);
// do something to resolve package
} catch (Exception e){
// deal with exception
} finally {
// 删除本地缓存文件
sftpUtil.deleteFile(filePath + fileName);
// 断开连接
sftpUtil.disconnect();
}
}
}
此处指列出了关键代码,如有不明白的地方,可以留言交流。