Java基于JSch操作SFTP

young 545 2021-11-04

依赖

<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.55</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.5.6</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.5.6</version>
    <scope>test</scope>
</dependency>

目录结构

+---java
|   \---com
|       \---example
|           \---jsch
|               |   JschApplication.java
|               |
|               +---dto
|               |       DownloadDto.java
|               |       UploadDto.java
|               |
|               +---sftp
|               |       SftpConfig.java
|               |       SftpConfiguration.java
|               |       SftpUtil.java
|               |
|               \---util
|                       BeanFactory.java
|
\---resources
    |   application.yaml

配置类

@Data
@Configuration
@ConfigurationProperties(prefix = "sftp")
class SftpConfiguration {
    private String host;
    private String userName;
    private String password;
    private int port = 22;
    private Integer timeout;
}

application.yaml

sftp:
  userName: xxxx
  password: xxxxxxxx
  port: 22
  timeout: 60000
  host: xxxxxxxxx

工具类

BeanFactory

@Configuration
public class BeanFactory implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    public static <T> T getBean(Class<T> clazz){
        return applicationContext.getBean(clazz);
    }

    public static <T> T getBean(String beanName){
        return (T) applicationContext.getBean(beanName);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

SftpConfig

@Slf4j
class SftpConfig {
    final String SFTP_CHANNEL_TYPE = "sftp";
    final String CONNECT_FAIL_MESSAGE = "连接SFTP失败,host:【%s】,port:【%s】,userName:【%s】,请检查相关配置,网络,以及防火墙情况";
    private Session session = null;
    private Channel channel = null;
    private ChannelSftp channelSftp = null;

    public ChannelSftp getChannelSftp() {
        return channelSftp;
    }

    public void disconnect() {
        if (channelSftp != null) {
            channelSftp.disconnect();
            channelSftp.exit();
            channelSftp = null;
        }
        if (channel != null) {
            channel.disconnect();
            channel = null;
        }
        if (session != null) {
            session.disconnect();
            session = null;
        }
    }

    public static SftpConfig createSftpConfig() {
	// 能拿到配置就行,不一定用Spring
        SftpConfiguration sftpConfiguration = BeanFactory.getBean(SftpConfiguration.class);
        SftpConfig sftpConfig = new SftpConfig(sftpConfiguration);
        return sftpConfig;
    }

    private SftpConfig(SftpConfiguration sftpConfiguration) {
        JSch jSch = new JSch();
        connectSession(sftpConfiguration, jSch);
        connectChannel(sftpConfiguration);
    }

    private void connectChannel(SftpConfiguration sftpConfiguration) {
        try {
            this.channel = session.openChannel(SFTP_CHANNEL_TYPE);
            this.channel.connect();
        } catch (JSchException e) {
            log.error("channel连接失败", e);
            if (this.channel != null) {
                channel.disconnect();
            }
            throw buildConnectFailException(e, sftpConfiguration);
        }
        this.channelSftp = (ChannelSftp) channel;
    }

    private void connectSession(SftpConfiguration sftpConfiguration, JSch jSch) {
        try {
            this.session = jSch.getSession(sftpConfiguration.getUserName(), sftpConfiguration.getHost(), sftpConfiguration.getPort());
        } catch (JSchException e) {
            log.error("获取Session失败", e);
            if (session != null) {
                session.disconnect();
            }
            throw buildConnectFailException(e, sftpConfiguration);
        }
        this.session.setPassword(sftpConfiguration.getPassword());
        Properties config = new Properties();
        // 不验证 HostKey
        config.put("StrictHostKeyChecking", "no");
        session.setConfig(config);
        try {
            if (sftpConfiguration.getTimeout() == null) {
                this.session.connect();
            } else {
                this.session.connect(sftpConfiguration.getTimeout());
            }
        } catch (JSchException e) {
            log.error("session连接失败", e);
            if (this.session != null) {
                session.disconnect();
            }
            throw buildConnectFailException(e, sftpConfiguration);
        }
    }

    public RuntimeException buildConnectFailException(JSchException e, SftpConfiguration sftpConfiguration) {
        return new RuntimeException(String.format(CONNECT_FAIL_MESSAGE, sftpConfiguration.getHost(), sftpConfiguration.getPort(), sftpConfiguration.getUserName()), e);
    }
}

SftpUtil

public static void upload(String uploadDir, String fileName, InputStream inputStream) {
        UploadDto uploadDto = new UploadDto();
        uploadDto.setUploadDir(uploadDir);
        UploadDto.UploadInfo uploadInfo = new UploadDto.UploadInfo(inputStream, fileName);
        uploadDto.setUploadInfoList(Collections.singletonList(uploadInfo));
        upload(uploadDto);
    }

    public static void upload(UploadDto uploadDto) {
        process(uploadDto, upload());
    }

    public static void download(String dir, String fileName, OutputStream outputStream) {
        process(new DownloadDto(dir,fileName,outputStream), download());
    }

    public static void download(DownloadDto dto) {
        process(dto, download());
    }

    private static <T> void process(T dto, BiConsumer<ChannelSftp, T> consumer) {
        SftpConfig sftpConfig = SftpConfig.createSftpConfig();
        ChannelSftp sftp = sftpConfig.getChannelSftp();
        try {
            consumer.accept(sftp, dto);
        } finally {
            sftpConfig.disconnect();
        }
    }


    private static BiConsumer<ChannelSftp, UploadDto> upload() {
        return (sftp, uploadDto) -> {
            String uploadDir = uploadDto.getUploadDir();
            cdToDir(sftp, uploadDir);
            Collection<UploadDto.UploadInfo> uploadInfoList = uploadDto.getUploadInfoList();
            for (UploadDto.UploadInfo uploadInfo : uploadInfoList) {
                doUpload(sftp, uploadDir, uploadInfo);
            }
        };
    }

    private static BiConsumer<ChannelSftp, DownloadDto> download() {
        return (sftp, dto) -> {
            String dir = dto.getDir();
            try {
                sftp.cd(dir);
            } catch (SftpException e) {
                log.error("路径【{}】不存在", dir);
                throw new RuntimeException("路径【" + dir + "】不存在", e);
            }
            String fileName = dto.getFileName();
            try {
                sftp.get(fileName, dto.getOutputStream());
            } catch (SftpException e) {
                log.error("下载路径【{}】的文件【{}】失败", dir, fileName);
                throw new RuntimeException("下载路径【" + dir + "】的文件【" + fileName + "】失败", e);
            }
        };
    }




    private static void cdToDir(ChannelSftp sftp, String uploadDir) {
        try {
            //进入目录
            sftp.cd(uploadDir);
        } catch (SftpException exception) {
            try {
                //指定上传路径不存在
                if (ChannelSftp.SSH_FX_NO_SUCH_FILE == exception.id) {
                    //创建目录
                    sftp.mkdir(uploadDir);
                    //进入目录
                    sftp.cd(uploadDir);
                }
            } catch (SftpException e) {
                log.error("创建目录【" + uploadDir + "】失败", e);
                throw new RuntimeException("创建目录【" + uploadDir + "】失败", e);
            }
        }
    }

    private static void doUpload(ChannelSftp sftp, String uploadDir, UploadDto.UploadInfo uploadInfo) {
        String fileName = uploadInfo.getFileName();
        try {
            sftp.put(uploadInfo.getInputStream(), fileName);
            log.info("上传文件【{}】至目录【{}】成功", fileName, uploadDir);
        } catch (SftpException e) {
            log.error("上传文件【{}】至目录【{}】失败", fileName, uploadDir);
            throw new RuntimeException("上传文件【" + fileName + "】至目录【" + uploadDir + "】失败", e);
        }
    }
}

DTO

DownloadDto

@Data
@AllArgsConstructor
@NoArgsConstructor
public class DownloadDto {
    private String dir;
    private String fileName;
    private OutputStream outputStream;
}

UploadDto

@Data
public class UploadDto {
    private String uploadDir;
    private Collection<UploadInfo> uploadInfoList;

    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    public static class UploadInfo{
        private InputStream inputStream;
        private String fileName;
    }
}

测试

@SpringBootTest(classes = JschApplication.class)
class JschApplicationTests {

    @Test
    void upload() throws FileNotFoundException {
        SftpUtil.upload("/app/testupload/","test.png",new FileInputStream("E:/young/spi-test.png"));
    }

    @Test
    void download() throws FileNotFoundException {
        SftpUtil.download("/app/testupload/","test.png",new FileOutputStream("E:/young/spi-test1.png"));
    }

}