java通过sftp形式连接主机下载文件(附项目与主机编码不一致解决方法)
最近接了一个文件下载接口需求,需要采用sftp形式与对端主机连接,进行文件传输。
首先引入java操作sftp的工具类包:
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.53</version>
</dependency>
编写文件工具类(包含sftp连接、断开、下载文件等方法)
/**
* sftp形式下载文件
*
* @author wangshuai
*
*/
@Slf4j
public class SFTPUtil {
private ChannelSftp sftp = new ChannelSftp();
private Session session;
/**
* SFTP 登录用户名
*/
private String username;
/**
* SFTP 登录密码
*/
private String password;
/**
* 私钥
*/
private String privateKey;
/**
* SFTP 服务器地址IP地址
*/
private String host;
/**
* SFTP 端口
*/
private int port;
/**
* 构造基于密码认证的sftp对象
*/
public SFTPUtil(String username, String password, String host, int port) {
this.username = username;
this.password = password;
this.host = host;
this.port = port;
}
/**
* 构造基于秘钥认证的sftp对象
*/
public SFTPUtil(String username, String host, int port, String privateKey) {
this.username = username;
this.host = host;
this.port = port;
this.privateKey = privateKey;
}
public SFTPUtil() {
}
/**
* 连接sftp服务器
*/
public void login() {
try {
JSch jsch = new JSch();
if (privateKey != null) {
jsch.addIdentity(privateKey);// 设置私钥
}
session = jsch.getSession(username, host, port);
if (password != null) {
session.setPassword(password);
}
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.connect();
Channel channel = session.openChannel("sftp");
channel.connect();
sftp = (ChannelSftp) channel;
} catch (JSchException e) {
e.printStackTrace();
log.error("login.faild",e);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (SftpException e) {
e.printStackTrace();
}
}
/**
* 关闭连接 server
*/
public void logout() {
if (sftp != null) {
if (sftp.isConnected()) {
sftp.disconnect();
}
}
if (session != null) {
if (session.isConnected()) {
session.disconnect();
}
}
}
public boolean isExist(String serverPath, String folderName) {
try {
sftp.cd(serverPath);
log.info("登录到当前路径:"+serverPath);
SftpATTRS attrs = null;
attrs = sftp.stat(folderName);
if (attrs != null) {
return true;
}
} catch (Exception e) {
e.getMessage();
return false;
}
return false;
}
public boolean isConnect() {
if (null != session) {
return session.isConnected();
}
return false;
}
public InputStream download(String directory) throws SftpException {
return sftp.get(directory);
}
}
编写对应controller控制层代码
@RestController
@RequestMapping(value = "XXXXXXX")
@Slf4j
public class StatementShowController {
@Value(value = "${statementshow.ftpHost}")
String ftpHost;
@Value(value = "${statementshow.ftpUserName}")
String ftpUserName;
@Value(value = "${statementshow.ftpPort}")
int ftpPort;
@Value(value = "${statementshow.ftpPassword}")
String ftpPassword;
@Value(value = "${statementshow.hostDirectory}")
String hostDirectory;
@GetMapping("/downloadfile")
public DataResult<Boolean> downloadFile (@RequestParam("fdate") String fdate, @RequestParam("fname") String fname,
HttpServletRequest request, HttpServletResponse response){
if (StringUtils.isBlank(fdate) || StringUtils.isBlank(fname)) {
return new DataResult<>(403, "fdate,fileName不能为空.", false);
}
log.info("downloadfile--sftpInfo:"+ftpHost+","+ftpUserName+","+ftpPort+","+ftpPassword+","+hostDirectory+","+fdate+","+fname);
String filePath =hostDirectory+"/"+fdate;
byte[] buffer = new byte[1024 * 10];
InputStream fis = null;
SFTPUtil sftp = null;
OutputStream out = null;
try {
//sftp登录
log.info("sftplogin:begin");
sftp = new SFTPUtil(ftpUserName, ftpPassword, ftpHost,ftpPort);
sftp.login();
log.info("sftplogin:success");
//判断有无对应文件
if (!sftp.isExist(filePath, fname)) {
log.info("sftp:文件或路径未找到");
throw new ServiceException("文件未找到");
}
response.setContentType("application/force-download;charset=UTF-8");
final String userAgent = request.getHeader("USER-AGENT");
try {
String fileName = null;
if (org.apache.commons.lang3.StringUtils.contains(userAgent, "MSIE") || org.apache.commons.lang3.StringUtils.contains(userAgent, "Edge")) {
// IE浏览器
fileName = URLEncoder.encode(fname, "UTF8");
} else if (org.apache.commons.lang3.StringUtils.contains(userAgent, "Mozilla")) {
// google,火狐浏览器
fileName = new String(fname.getBytes(), "ISO8859-1");
} else {
// 其他浏览器
fileName = URLEncoder.encode(fname, "UTF8");
}
response.setHeader("Content-disposition", "attachment; filename=" + fileName);
} catch (UnsupportedEncodingException e) {
log.error(e.getMessage(), e);
return null;
}
fis = sftp.download(fname);
out = response.getOutputStream();
//读取文件流
int len = 0;
while ((len = fis.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return new DataResult<>(200, "文件成功下载.", true);
} catch (Exception e) {
e.printStackTrace();
log.error("文件下载失败",e);
return new DataResult<>(403, "文件下载失败.请检查文件路径", false);
} finally {
sftp.logout();
if (out != null) {
try {
out.close();
} catch (IOException e) {
out = null;
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
fis = null;
}
}
}
}
}
此时代码已经初步完毕,只要你的项目编码与sftp目标主机编码一致就完全没问题。
问题是我的项目编码是utf-8,目标主机用的是gbk编码。此时可以下载不带中文的文件。带中文名称的报找不到对应文件的错误、。
此时可以设置ChannelSftp的编码来解决问题,调用setFilenameEncoding("GBK")即可。
问题是调用这个方法以后项目运行到这一行报错了,报错信息是:The encoding can not be changed for this sftp server.
debug进这个方法:
发现他的version是3,而version在3与5之间是不可以设置编码的。
一开始我尝试换该jar包的版本,但是换了好几个发现这个version依旧是3.
此时只能通过反射来修改这个属性的值,进而使setFilenameEncoding("GBK")生效。
在login方法最后加以下代码:
Class<?> cl = ChannelSftp.class;
Field f =cl.getDeclaredField("server_version");
f.setAccessible(true);
f.set(sftp, 2);
sftp.setFilenameEncoding("GBK");
OK~,成功下载。