前言
前面我们实现的文件上传下载是在本地进行的,本文我们将实现基于 SFTP 来实现连接远程 SFTP 服务器进行文件上传下载。
实现
在此项目中,我们将实现如下功能:
- 上传单个文件;
- 上传多个文件;
- 上传整个文件夹(如果含子文件夹则递归上传);
- 下载单个文件;
- 下载文件夹内所有文件(不含子文件夹);
- 下载文件夹内所有文件(如果含子文件夹则递归下载)。
搭建SFTP服务器
可参考本文,使用 Docker 来搭建一个 SFTP 服务器。
pom.xml
我们创建一个新的 SpringBoot 项目,然后在 pom.xml 中引入依赖:
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.54</version>
</dependency>
jsch(Java Secure Channel)
是一个用于在 Java 应用程序中实现SSH(Secure Shell)
连接和文件传输的库。它提供了一组 API 和功能,使开发者能够在其应用程序中执行以下操作:
SSH连接:通过 jsch,可以建立到 SSH 服务器的安全连接。从而实现执行各种远程操作,例如远程命令执行和文件传输。
远程命令执行:jsch 允许在远程 SSH 服务器上执行命令。
文件传输:可以使用 jsch 库来实现文件的上传和下载,以及对远程文件系统的操作。这对于备份、同步文件以及远程文件管理非常有用。
端口转发:jsch 支持 SSH 端口转发,允许建立本地端口和远程服务器之间的安全通道,以便在不直接暴露服务的情况下访问远程服务。
SFTP支持:jsch 包含对
SFTP(SSH文件传输协议)
的支持,这是一个用于安全文件传输的协议,类似于 FTP,但使用 SSH 进行安全加密。密钥管理:jsch 支持 SSH 密钥管理,包括公钥和私钥的生成、加载和使用。
jsch 是一个功能强大的库,广泛用于开发需要与远程服务器进行通信和文件传输的 Java 应用程序。它提供了安全性和加密,适用于各种用例,包括自动化、远程管理和文件传输。
除此之外,还需要引入如下依赖:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
application.properties
在 properties 配置文件中添加如下配置项:
sftp.host=192.168.91.128
sftp.port=2222
sftp.username=langjialing
sftp.password=123456
sftp.timeout=1000
sftp.remoteRootPath=/upload/招标线索信息
sftp.directory=/upload/招标线索信息
sftp.saveFile=./files/
踩坑记录
如果文件路径中含有中文,需要先设置文件编码格式,然后再添加如上设置项,否则中文路径会乱码。
如果设置后仍然乱码,那可能是设置未对配置文件生效,可以在确保设置了编码后把 properties 配置文件删除重新再建一个。
SftpConfig.java实体配置类
创建 SftpConfig.java
实体配置类:
package com.langjialing.springbootsftp.config;
import javax.annotation.Resource;
/**
* @author 郎家岭伯爵
* @time 2023/10/16 11:23
*/
public class SftpConfig {
private String hostname;
private Integer port;
private String username;
private String password;
private Integer timeout;
private Resource privateKey;
private String remoteRootPath;
private String fileSuffix;
public String getHostname() {
return hostname;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getTimeout() {
return timeout;
}
public void setTimeout(Integer timeout) {
this.timeout = timeout;
}
public Resource getPrivateKey() {
return privateKey;
}
public void setPrivateKey(Resource privateKey) {
this.privateKey = privateKey;
}
public String getRemoteRootPath() {
return remoteRootPath;
}
public void setRemoteRootPath(String remoteRootPath) {
this.remoteRootPath = remoteRootPath;
}
public String getFileSuffix() {
return fileSuffix;
}
public void setFileSuffix(String fileSuffix) {
this.fileSuffix = fileSuffix;
}
public SftpConfig(String hostname, Integer port, String username, String password,
Integer timeout, Resource privateKey, String remoteRootPath, String fileSuffix) {
this.hostname = hostname;
this.port = port;
this.username = username;
this.password = password;
this.timeout = timeout;
this.privateKey = privateKey;
this.remoteRootPath = remoteRootPath;
this.fileSuffix = fileSuffix;
}
public SftpConfig(String hostname, Integer port, String username, String password,
Integer timeout, String remoteRootPath) {
this.hostname = hostname;
this.port = port;
this.username = username;
this.password = password;
this.timeout = timeout;
this.remoteRootPath = remoteRootPath;
}
public SftpConfig() {
}
}
SftpUtil.java工具类
创建 SftpUtil.java
工具类:
package com.langjialing.springbootsftp.config;
import com.jcraft.jsch.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.*;
/**
* @author 郎家岭伯爵
* @time 2023/10/16 11:25
*/
public class SftpUtil {
private long count;
/**
* 已经连接次数
*/
private long count1 = 0;
private long sleepTime;
private static final Logger logger = LoggerFactory.getLogger(SftpUtil.class);
/**
* 连接sftp服务器
*
* @return
*/
public ChannelSftp connect(SftpConfig sftpConfig) {
ChannelSftp sftp = null;
try {
JSch jsch = new JSch();
jsch.getSession(sftpConfig.getUsername(), sftpConfig.getHostname(), sftpConfig.getPort());
Session sshSession = jsch.getSession(sftpConfig.getUsername(), sftpConfig.getHostname(), sftpConfig.getPort());
logger.info("Session created ... UserName=" + sftpConfig.getUsername() + ";host=" + sftpConfig.getHostname() + ";port=" + sftpConfig.getPort());
sshSession.setPassword(sftpConfig.getPassword());
Properties sshConfig = new Properties();
sshConfig.put("StrictHostKeyChecking", "no");
sshSession.setConfig(sshConfig);
sshSession.connect();
logger.info("Session connected ...");
logger.info("Opening Channel ...");
Channel channel = sshSession.openChannel("sftp");
channel.connect();
sftp = (ChannelSftp) channel;
logger.info("登录成功");
} catch (Exception e) {
try {
count1 += 1;
if (count == count1) {
throw new RuntimeException(e);
}
Thread.sleep(sleepTime);
logger.info("重新连接....");
connect(sftpConfig);
} catch (InterruptedException e1) {
throw new RuntimeException(e1);
}
}
return sftp;
}
/**
* 上传文件。
*
* @param directory 上传的目录
* @param uploadFile 要上传的文件
* @param sftpConfig
*/
public void upload(String directory, String uploadFile, SftpConfig sftpConfig) {
ChannelSftp sftp = connect(sftpConfig);
try {
sftp.cd(directory);
} catch (SftpException e) {
try {
sftp.mkdir(directory);
sftp.cd(directory);
} catch (SftpException e1) {
throw new RuntimeException("ftp创建文件路径失败" + directory);
}
}
File file = new File(uploadFile);
InputStream inputStream=null;
try {
inputStream = new FileInputStream(file);
sftp.put(inputStream, file.getName());
} catch (Exception e) {
throw new RuntimeException("sftp异常" + e);
} finally {
disConnect(sftp);
closeStream(inputStream,null);
}
}
/**
* 同时上传多个文件。
* @param directory 上传的目录
* @param uploadFiles 要上传的文件
* @param sftpConfig
*/
public void uploadMultipleFiles(String directory, List<String> uploadFiles, SftpConfig sftpConfig) {
ChannelSftp sftp = connect(sftpConfig);
try {
sftp.cd(directory);
} catch (SftpException e) {
try {
sftp.mkdir(directory);
sftp.cd(directory);
} catch (SftpException e1) {
throw new RuntimeException("ftp创建文件路径失败" + directory);
}
}
for (String uploadFile : uploadFiles) {
File file = new File(uploadFile);
if (file.exists()) {
try (InputStream inputStream = new FileInputStream(file)) {
sftp.put(inputStream, file.getName());
} catch (Exception e) {
throw new RuntimeException("sftp异常" + e);
}
} else {
throw new RuntimeException("文件不存在: " + uploadFile);
}
}
disConnect(sftp);
}
/**
* 上传文件夹。
* @param directory 上传的目录
* @param localDirectory 要上传的文件
* @param sftpConfig
*/
public void uploadDirectory(String directory, String localDirectory, SftpConfig sftpConfig) {
ChannelSftp sftp = connect(sftpConfig);
try {
sftp.cd(directory);
} catch (SftpException e) {
try {
sftp.mkdir(directory);
sftp.cd(directory);
} catch (SftpException e1) {
throw new RuntimeException("ftp创建文件路径失败" + directory);
}
}
File localDir = new File(localDirectory);
if (localDir.isDirectory()) {
uploadDirectoryRecursively(sftp, localDir);
}
disConnect(sftp);
}
/**
* 上传文件。如果文件夹内含多级下级文件夹,则递归创建。
* @param sftp
* @param localDir 本地文件夹。
*/
private void uploadDirectoryRecursively(ChannelSftp sftp, File localDir) {
for (File file : Objects.requireNonNull(localDir.listFiles())) {
if (file.isDirectory()) {
String newRemotePath = file.getName() + "/";
try {
sftp.mkdir(newRemotePath);
sftp.cd(newRemotePath);
uploadDirectoryRecursively(sftp, file);
sftp.cd("..");
} catch (SftpException e) {
logger.info("ftp创建文件路径失败:" + e.getMessage());
throw new RuntimeException("ftp创建文件路径失败:" + newRemotePath);
}
} else if (file.isFile()) {
try (InputStream inputStream = new FileInputStream(file)) {
sftp.put(inputStream, file.getName());
} catch (Exception e) {
throw new RuntimeException("sftp异常" + e);
}
}
}
}
/**
* 下载文件。
*
* @param directory 下载目录
* @param downloadFile 下载的文件
* @param saveFile 存在本地的路径
* @param sftpConfig
*/
public void download(String directory, String downloadFile, String saveFile, SftpConfig sftpConfig) {
OutputStream output = null;
try {
File localDirFile = new File(saveFile);
// 判断本地目录是否存在,不存在需要新建各级目录
if (!localDirFile.exists()) {
localDirFile.mkdirs();
}
if (logger.isInfoEnabled()) {
logger.info("开始获取远程文件:[{}]---->[{}]", new Object[]{directory, saveFile});
}
ChannelSftp sftp = connect(sftpConfig);
sftp.cd(directory);
if (logger.isInfoEnabled()) {
logger.info("打开远程文件:[{}]", new Object[]{directory});
}
output = new FileOutputStream(new File(saveFile.concat(File.separator).concat(downloadFile)));
sftp.get(downloadFile, output);
if (logger.isInfoEnabled()) {
logger.info("文件下载成功");
}
disConnect(sftp);
} catch (Exception e) {
if (logger.isInfoEnabled()) {
logger.info("文件下载出现异常,[{}]", e);
}
throw new RuntimeException("文件下载出现异常,[{}]", e);
} finally {
closeStream(null,output);
}
}
/**
* 下载远程文件夹下的所有文件。
* @param remoteFilePath 要下载的文件路径。
* @param localDirPath 本地保存位置。
* @throws Exception
*/
public void downloadDirFile(String remoteFilePath, String localDirPath, SftpConfig sftpConfig) throws Exception {
File localDirFile = new File(localDirPath);
// 判断本地目录是否存在,不存在需要新建各级目录
if (!localDirFile.exists()) {
localDirFile.mkdirs();
}
if (logger.isInfoEnabled()) {
logger.info("sftp文件服务器文件夹[{}],下载到本地目录[{}]", new Object[]{remoteFilePath, localDirFile});
}
ChannelSftp channelSftp = connect(sftpConfig);
Vector<ChannelSftp.LsEntry> lsEntries = channelSftp.ls(remoteFilePath);
if (logger.isInfoEnabled()) {
logger.info("远程目录下的文件为[{}]", lsEntries);
}
for (ChannelSftp.LsEntry entry : lsEntries) {
String fileName = entry.getFilename();
if (checkFileName(fileName)) {
continue;
}
String remoteFileName = getRemoteFilePath(remoteFilePath, fileName);
channelSftp.get(remoteFileName, localDirPath);
}
disConnect(channelSftp);
}
/**
* 下载远程文件夹下的所有文件(如果文件夹包含子文件夹,则递归下载)。
* @param remoteDirPath 要下载的目录路径。
* @param localDirPath 本地保存位置。
* @param sftpConfig
* @throws Exception
*/
public void downloadDirectory(String remoteDirPath, String localDirPath, SftpConfig sftpConfig) throws Exception {
File localDirFile = new File(localDirPath);
if (!localDirFile.exists()) {
localDirFile.mkdirs();
}
if (logger.isInfoEnabled()) {
logger.info("SFTP文件服务器文件夹[{}],下载到本地目录[{}]", remoteDirPath, localDirFile);
}
ChannelSftp channelSftp = connect(sftpConfig);
downloadRemoteDirectory(channelSftp, remoteDirPath, localDirPath);
disConnect(channelSftp);
}
/**
* 下载文件夹(如果文件夹含多级文件夹,则递归下载)。
* @param channelSftp
* @param remoteDirPath 要下载的目录路径。
* @param localDirPath 本地保存位置。
* @throws SftpException
*/
private void downloadRemoteDirectory(ChannelSftp channelSftp, String remoteDirPath, String localDirPath) throws SftpException {
Vector<ChannelSftp.LsEntry> lsEntries = channelSftp.ls(remoteDirPath);
for (ChannelSftp.LsEntry entry : lsEntries) {
String fileName = entry.getFilename();
if (checkFileName(fileName)) {
continue;
}
String remoteFilePath = remoteDirPath + "/" + fileName;
String localFilePath = localDirPath + File.separator + fileName;
if (entry.getAttrs().isDir()) {
// 如果是目录,递归下载子目录
File localDirFile = new File(localFilePath);
if (!localDirFile.exists()) {
localDirFile.mkdirs();
}
downloadRemoteDirectory(channelSftp, remoteFilePath, localFilePath);
} else {
// 如果是文件,下载文件
channelSftp.get(remoteFilePath, localFilePath);
}
}
}
/**
* 关闭流
* @param outputStream
*/
private void closeStream(InputStream inputStream,OutputStream outputStream) {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private boolean checkFileName(String fileName) {
if (".".equals(fileName) || "..".equals(fileName)) {
return true;
}
return false;
}
private String getRemoteFilePath(String remoteFilePath, String fileName) {
if (remoteFilePath.endsWith("/")) {
return remoteFilePath.concat(fileName);
} else {
return remoteFilePath.concat("/").concat(fileName);
}
}
/**
* 删除文件
*
* @param directory 要删除文件所在目录
* @param deleteFile 要删除的文件
* @param sftp
*/
public void delete(String directory, String deleteFile, ChannelSftp sftp) {
try {
sftp.cd(directory);
sftp.rm(deleteFile);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 列出目录下的文件
*
* @param directory 要列出的目录
* @param sftpConfig
* @return
* @throws SftpException
*/
public List<String> listFiles(String directory, SftpConfig sftpConfig) throws SftpException, UnsupportedEncodingException {
ChannelSftp sftp = connect(sftpConfig);
List fileNameList = new ArrayList();
try {
sftp.cd(directory);
} catch (SftpException e) {
return fileNameList;
}
sftp.setFilenameEncoding("UTF-8");
Vector<?> vector = sftp.ls(directory);
for (int i = 0; i < vector.size(); i++) {
if (vector.get(i) instanceof ChannelSftp.LsEntry) {
ChannelSftp.LsEntry lsEntry = (ChannelSftp.LsEntry) vector.get(i);
String fileName = lsEntry.getFilename();
if (".".equals(fileName) || "..".equals(fileName)) {
continue;
}
fileNameList.add(fileName);
}
}
disConnect(sftp);
return fileNameList;
}
/**
* 断掉连接
*/
public void disConnect(ChannelSftp sftp) {
try {
sftp.disconnect();
sftp.getSession().disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
public SftpUtil(long count, long sleepTime) {
this.count = count;
this.sleepTime = sleepTime;
}
public SftpUtil() {
}
}
注:
- 在此工具类中使用了
try-with-resources
语法,可参考此处。 - 在
@GetMapping("/download")
下载单个文件接口里有一个handExcelData(saveFile + "202310/" + targetFileName)
方法,它是用于处理 Excel 文件的。在此项目中可忽略。
SftpController.java控制器类
创建 SftpController.java
控制器类,以便后续使用 POSTMAN 调用接口。
package com.langjialing.springbootsftp.controller;
import com.jcraft.jsch.SftpException;
import com.langjialing.springbootsftp.config.SftpConfig;
import com.langjialing.springbootsftp.config.SftpUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
/**
* @author 郎家岭伯爵
* @time 2023/10/16 15:30
*/
@RestController
@RequestMapping("/sftp")
@Slf4j
public class SftpController {
@Value("${sftp.host}")
private String host;
@Value("${sftp.port}")
private int port;
@Value("${sftp.username}")
private String username;
@Value("${sftp.password}")
private String password;
@Value("${sftp.timeout}")
private int timeout;
@Value("${sftp.remoteRootPath}")
private String remoteRootPath;
@Value("${sftp.directory}")
private String directory;
@Value("${sftp.saveFile}")
private String saveFile;
@GetMapping("/t")
public void sftp() {
SftpUtil ftp = new SftpUtil(3, 6000);
SftpConfig sftpConfig = new SftpConfig("192.168.91.128", 2222,
"langjialing", "123456", 1000, "/upload/excel_20230912.xlsx");
try {
List<String> list = ftp.listFiles("/upload", sftpConfig);
log.info("文件上传下载详情{}" , new Object[]{list});
} catch (SftpException | UnsupportedEncodingException e) {
log.error("文件上传下载异常:{}" , e.getMessage());
}
}
@GetMapping("/upload")
public void upload(@RequestParam String uploadFile){
SftpUtil ftp = new SftpUtil(3, 6000);
SftpConfig sftpConfig = new SftpConfig(host, port, username, password, timeout, remoteRootPath);
ftp.upload(directory, uploadFile, sftpConfig);
}
@PostMapping("/uploadMultipleFiles")
public void uploadMultipleFiles(@RequestBody List<String> uploadFiles){
SftpUtil ftp = new SftpUtil(3, 6000);
SftpConfig sftpConfig = new SftpConfig(host, port, username, password, timeout, remoteRootPath);
ftp.uploadMultipleFiles(directory, uploadFiles, sftpConfig);
}
@GetMapping("/uploadDirectory")
public void uploadDirectory(@RequestParam String uploadDirectory){
SftpUtil ftp = new SftpUtil(3, 6000);
SftpConfig sftpConfig = new SftpConfig(host, port, username, password, timeout, remoteRootPath);
ftp.uploadDirectory(directory, uploadDirectory, sftpConfig);
}
@GetMapping("/download")
public void download(@RequestParam String targetFileName) throws SftpException, UnsupportedEncodingException {
SftpUtil ftp = new SftpUtil(3, 6000);
SftpConfig sftpConfig = new SftpConfig(host, port, username, password, timeout, remoteRootPath);
// 列出远程目录中的文件列表
List<String> remoteFileList = ftp.listFiles(directory, sftpConfig);
System.out.println("文件列表为:" + remoteFileList);
if (remoteFileList.contains(targetFileName)) {
// 文件存在,可以进行下载操作
System.out.println("文件存在,可以进行下载操作");
ftp.download(directory, targetFileName, saveFile + "202310", sftpConfig);
handExcelData(saveFile + "202310/" + targetFileName);
} else {
// 文件不存在,进行相应的处理
System.out.println("文件不存在:" + targetFileName);
}
ftp.download(directory, targetFileName, saveFile + "202310", sftpConfig);
handExcelData(saveFile + "202310/" + targetFileName);
}
public void handExcelData(String excelFilePath){
// 按单元格数据类型处理数据
try (FileInputStream fis = new FileInputStream(excelFilePath);
Workbook workbook = new XSSFWorkbook(fis)) {
// 获取第一个工作表
Sheet sheet = workbook.getSheetAt(0);
for (Row row : sheet) {
for (Cell cell : row) {
switch (cell.getCellType()) {
case STRING:
System.out.print(cell.getStringCellValue());
break;
case NUMERIC:
System.out.print(cell.getNumericCellValue());
break;
case BOOLEAN:
System.out.print(cell.getBooleanCellValue());
break;
default:
System.out.print("");
}
// 列之间用制表符分隔
System.out.print("\t");
}
// 换行处理下一行
System.out.println();
}
} catch (IOException e) {
e.printStackTrace();
}
// 把单元格内容全部识别为String类型,使用嵌套增强for循环进行遍历
try (FileInputStream fis = new FileInputStream(excelFilePath);
Workbook workbook = new XSSFWorkbook(fis)) {
// 获取第一个工作表
Sheet sheet = workbook.getSheetAt(0);
// 格式化数据
DataFormatter dataFormatter = new DataFormatter();
for (Row row : sheet) {
for (Cell cell : row) {
String cellValue = dataFormatter.formatCellValue(cell);
System.out.print(cellValue);
// 列之间用制表符分隔
System.out.print("\t");
}
// 换行处理下一行
System.out.println();
}
} catch (IOException e) {
e.printStackTrace();
}
// 把单元格内容全部识别为String类型,使用嵌套普通for循环进行遍历,可读取指定位置的单元格
try (FileInputStream fis = new FileInputStream(excelFilePath);
Workbook workbook = new XSSFWorkbook(fis)) {
// 获取第一个工作表
Sheet sheet = workbook.getSheetAt(0);
// 格式化数据
DataFormatter dataFormatter = new DataFormatter();
// 遍历行
for (int rowIndex = 0; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
Row row = sheet.getRow(rowIndex);
// 遍历列
for (int columnIndex = 0; columnIndex < row.getLastCellNum(); columnIndex++) {
Cell cell = row.getCell(columnIndex);
String cellValue = dataFormatter.formatCellValue(cell);
System.out.print(cellValue);
// 列之间用制表符分隔
System.out.print("\t");
}
// 换行处理下一行
System.out.println();
}
} catch (IOException e) {
e.printStackTrace();
}
}
@GetMapping("/downloadDirFile")
public void downloadDirFile(@RequestParam String path) throws Exception {
SftpUtil ftp = new SftpUtil(3, 6000);
SftpConfig sftpConfig = new SftpConfig(host, port, username, password, timeout, remoteRootPath);
// 列出远程目录中的文件列表
List<String> remoteFileList = ftp.listFiles(directory, sftpConfig);
System.out.println("文件列表为:" + remoteFileList);
ftp.downloadDirFile(path, saveFile + "202310", sftpConfig);
}
@GetMapping("/downloadDirectory")
public void downloadDirectory(@RequestParam String path) throws Exception {
SftpUtil ftp = new SftpUtil(3, 6000);
SftpConfig sftpConfig = new SftpConfig(host, port, username, password, timeout, remoteRootPath);
ftp.downloadDirectory(path, saveFile + "202310", sftpConfig);
}
}
POSTMAN调用测试
启动项目后,使用 POSTMAN 调用接口进行测试。
总结
SpringBoot 实现使用 SFTP 远程上传/下载文件。