1. MFT介绍

Managed File Transfer (“MFT”)是一种安全的数据传输软件,是通过网络从一台计算机到另一台计算机的数据传输。

大文件传输是一种安全的数据传输软件,是通过网络从一台计算机到另一台计算机的数据传输。MFT软件通常基于FTP网路协议。而且,MFT也可弥补FTP的缺陷。

2. 业务背景

2.1 业务流程

做的项目需要与A银行系统对接,在文件传输这块,A银行用的是MFT这种方式,并且做了一定程度的封装。大致流程是:

基于MFT文件上传和下载_下载文件

2.2 文件上传

  1. 我们需要在我们服务器上搭建MFT的客户端,A银行会给我们分配一个 USERID ,这个UESRID会指向A银行在自己MFT服务器上为我们分配的文件目录;
  2. 在本地服务器上创建上传目录,将要上传的文件放在该目录下;
  3. 执行A银行提供的shell脚本,上传脚本内容如下/home/Axway/Synchrony/SecureClient/sclient script /home/Axway/Synchrony/SecureClient/put
  • 1
  1. 前面脚本都是固定的(MFT客户端安装目录,可配置成环境变量),核心在于put文件(路径可自定义),put文件示例:
    open ABank lcd /home/qy_work/cebfile/upload cd USERID newjob put 20181107.txt jobsubmit close
    其中 /home/qy_work/cebfile/upload 是文件上传目录,USERID为银行分配的唯一标识,20181107.txt是需要上传的文件,这里可以上传多个文件;
  2. 上传后返回信息:
    Checking transfer engine status on port 1717: is running open cebbank Connected lchdir /home/qy_work/cebfile/upload current working directory: /home/qy_work/cebfile/upload chdir USERID Directory changed newjob put 20181107.txt inserted in current job jobsubmit Job submitted: 20 Close Disconnected
    这里需要注意的是Job submitted: 20,这里称为JOBID,用来查询上传状态;
  3. 查询上传文件状态信息:/home/Axway/Synchrony/SecureClient/sclientadm displayjob -id JOBID
  • 1
  1. 查询上传文件状态返回信息:
    Ident = 20 Description = Site Alias = ABank Creation Date = 2018/11/07 16:25:59 Start Date = 2018/11/07 16:25:59 End Date = 2018/11/07 16:25:59 Status = Finished Error Message = Percent = 100 % Total size = 20 B (20 bytes) Tasks count = 1 Current Task = 0 Connection Retry Count = 0 Transfer Retry Count = 0 Stop On Error = false Update Frequency = 5000
    重点在于 statuspercent两个字段对应信息,前者标识文件上传状态,后者标识文件上传百分比。
    至此,基于MFT文件上传的流程全部描述完。接下来说说文件下载的流程;

2.3 文件下载

  1. 文件下载同样也需要执行A银行提供的脚本,下载脚本如下:/home/Axway/Synchrony/SecureClient/sclient script /home/Axway/Synchrony/SecureClient/get
  • 1
  1. 前面脚本都是固定的(MFT客户端安装目录,可配置成环境变量),核心在于get文件(路径可自定义),get文件示例:
    open cebbank lcd /home/qy_work/cebfile/download cd USERID newjob get 2018110702.txt jobsubmit close
    可以发现,和put脚本区别主要在于putget命令区别,其中 /home/qy_work/cebfile/download是文件下载目录,USERID为银行分配的唯一标识,20181107.txt是需要下载的文件,这里可以下载多个文件;
  2. 下载后返回信息:
    Checking transfer engine status on port 1717: is running open ABank Connected lchdir /home/qy_work/cebfile/download current working directory: /home/qy_work/cebfile/download chdir USERID Directory changed newjob get 2018110702.txt inserted in current job jobsubmit Job submitted: 23 Close Disconnected
    这里需要注意的是Job submitted: 20,这里称为JOBID,用来查询下载状态;
  3. 查询上传文件状态信息:/home/Axway/Synchrony/SecureClient/sclientadm displayjob -id JOBID
  • 1
  1. 查询上传下载状态返回信息:
    Ident = 23 Description = Site Alias = ABank Creation Date = 2018/11/07 17:30:14 Start Date = 2018/11/07 17:30:14 End Date = 2018/11/07 17:30:14 Status = Finished Error Message = Percent = 100 % Total size = 20 B (20 bytes) Tasks count = 1 Current Task = 0 Connection Retry Count = 0 Transfer Retry Count = 0 Stop On Error = false Update Frequency = 5000
    重点在于 statuspercent两个字段对应信息,前者标识文件下载状态,后者标识文件下载百分比。
    到这里整个文件上传和下载的流程以及整个上传下载的过程已经全部走通。但是这是基于Linux下执行脚本来上传和下载文件。现在需要把这些过程转换成java代码。

3. 业务难点

  • put、get脚本不是固定的,需要传入文件路径、文件名称以及用户ID,其中用户ID可能是唯一的,文件路径和文件名称需要灵活配置;
  • java操作shell脚本;
  • java接收shell脚本执行后的流,并转成字符流,截取相应的上传、下载状态来判定文件上传、下载的最终状态;

4. 技术方案

4.1 脚本生成

对于put、get脚本生成,这里采用静态模板​​Velocity​​技术,动态生成put/get脚本。

4.1.1 添加依赖

 <!-- 模板引擎 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>


4.1.2 脚本模板

MFTModel.vm

open cebbank
lcd $!{dirPath}
cd $!{userId}
newjob
$!{manageType} $!{fileName}
jobsubmit
close


4.1.3 生成脚本的工具类

package com.ceb.mental.util;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

import java.io.*;


public class VelocityUtil {
private final static String CHARSET = "utf8";

/**
* 生成MFT 上传、下载脚本
*
* @param manageType 上传或下载 put/get
* @param dirPath 上传文件所在文件夹或下载文件存放文件夹
* @param fileName 上传文件名称或下载文件名称
* @param dest 生成脚本存放路径(文件夹+文件名称)
* @param userId 银行分配的id
* @param encode 编码格式
*/
public static String createMFTCommandFile(String manageType,
String dirPath,
String fileName,
String dest,
String userId,
String encode) {
FileOutputStream outStream = null;
BufferedWriter sw = null;
try {
VelocityEngine ve = new VelocityEngine();
ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
ve.init();
Template t = ve.getTemplate("MFTModel.vm", encode);
VelocityContext ctx = new VelocityContext();
ctx.put("dirPath", dirPath);
ctx.put("userId", userId);
ctx.put("fileName", fileName);
ctx.put("manageType", manageType);
//确定静态文档在共享文件目录的完整存储路径
File file = new File(dest);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
outStream = new FileOutputStream(file);
OutputStreamWriter oswriter = new OutputStreamWriter(outStream, encode);
sw = new BufferedWriter(oswriter);
t.merge(ctx, sw);
sw.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (sw != null) {
sw.close();
}
if (outStream != null) {
outStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return dest;
}

/**
* 生成MFT 上传脚本
*
* @param manageType 上传或下载 put/get
* @param dirPath 上传文件所在文件夹或下载文件存放文件夹
* @param fileName 上传文件名称或下载文件名称
* @param dest 生成脚本存放路径(文件夹+文件名称)
* @param userId 银行分配的id
*/
public static String createMFTCommandFile(String manageType,
String dirPath,
String fileName,
String dest,
String userId) {
return createMFTCommandFile(manageType, dirPath, fileName, dest, userId, CHARSET);
}

public static void main(String[] args) throws IOException {
System.out.println(createMFTCommandFile("put", "/home/qy_work/cebfile/upload",
"20181107.txt", "D:/file/put01", "USERID"));
}
}


4.2 执行脚本

4.2.1 java执行Shell命令介绍

每个Java应用程序都有一个Runtime类实例,使应用程序能够与其运行的环境相连接。可以通过getRuntime方法获取当前运行时环境。 ​​java执行shell命令介绍​

应用程序不能创建自己的Runtime类实例。

介绍几个主要方法:

  • Process exec(String command)
    ​ 在单独的进程中执行指定的字符串命令。
  • Process exec(String command, String[] envp)
    ​ 在指定环境的单独进程中执行指定的字符串命令。
  • Process exec(String command, String[] envp, File dir)
    ​ 在有指定环境和工作目录的独立进程中执行指定的字符串命令。
  • Process exec(String[] cmdarray)
    ​ 在单独的进程中执行指定命令和变量。
  • Process exec(String[] cmdarray, String[] envp)
    ​ 在指定环境的独立进程中执行指定命令和变量。
  • Process exec(String[] cmdarray, String[] envp, File dir)
    ​ 在指定环境和工作目录的独立进程中执行指定的命令和变量。
    ​command​​:一条指定的系统命令。
    ​envp​​:环境变量字符串数组,其中每个环境变量的设置格式为name=value;如果子进程应该继承当前进程的环境,则该参数为null。
    ​dir​​:子进程的工作目录;如果子进程应该继承当前进程的工作目录,则该参数为null。
    ​cmdarray​​:包含所调用命令及其参数的数组。

4.2.2 执行shell命令后会返回inputStream

例如:

public class Test {  
public static void main(String[] args){
InputStream in = null;
try {
Process pro = Runtime.getRuntime().exec(new String[]{"sh",
"/home/test/test.sh","select admin from M_ADMIN",
"/home/test/result.txt"});
pro.waitFor();
in = pro.getInputStream();
BufferedReader read = new BufferedReader(new InputStreamReader(in));
String result = read.readLine();
System.out.println("INFO:"+result);
} catch (Exception e) {
e.printStackTrace();
}
}
}


此时通过​​pro.getInputStream();​​ 获取​​inputstream​​,再将流转成字符流,此时便能输出执行shell命令后返回的信息;

4.2.3 结合业务场景的工具类

package com.ceb.mental.util;


import org.apache.commons.lang.StringUtils;

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class RunShell {


private final static String BASH_PATH_PUT = "/home/Axway/Synchrony/SecureClient/sclient script ";
private final static String BASH_PATH_STATUS = "/home/Axway/Synchrony/SecureClient/sclientadm displayjob -id ";
private final static String GET = "get";
private final static String PUT = "put";

/**
* MFT上传文件
*
* @param dirPath 上传文件目录
* @param fileName 上传文件名称
* @param dest 上传脚本存放路径
* @param userId 光大分配id
* @return
*/
public static boolean putMFTFile(String dirPath, String fileName, String dest, String userId) {
return manageMFTFile(dirPath, fileName, dest, userId, PUT);
}

/**
* MFT下载文件
*
* @param dirPath 下载文件存放的目录
* @param fileName 下载文件名称
* @param dest 下载脚本存放路径
* @param userId 光大分配id
* @return
*/
public static boolean getMFTFile(String dirPath, String fileName, String dest, String userId) {
return manageMFTFile(dirPath, fileName, dest, userId, GET);
}

public static boolean manageMFTFile(String dirPath, String fileName, String dest, String userId, String manageType) {
//生成put文件
String commandFile = VelocityUtil.createMFTCommandFile(manageType, dirPath, fileName, dest, userId);
//组装put脚本
String command = BASH_PATH_PUT + commandFile;
System.out.println("==========>执行脚本:" + command);
//执行put上传命令
String result = runCommand(command, 12);
if (StringUtils.isNotBlank(command)) {
//截取jobId
String jobId = result.substring(result.lastIndexOf(":") + 1).trim();
System.out.println("截取的jobId:" + jobId);
//查询状态
String statusCommand = BASH_PATH_STATUS + jobId;
System.out.println("==========>查询文件状态脚本:" + statusCommand);
String statusResult = runCommand(statusCommand, 7);
String status = statusResult.substring(statusResult.lastIndexOf("=") + 1).trim();
System.out.println("上传结果:" + status);
if (StringUtils.isNotBlank(status) && status.equals("Finished")) {
return true;
}
}
return false;
}

/**
* 运行脚本,并读取特定行信息
*
* @param commond shell脚本
* @param readLine 从第几行读取
* @return
*/
private static String runCommand(String commond, int readLine) {
String result = null;
try {
Process ps = Runtime.getRuntime().exec(commond);
ps.waitFor();
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
int iLine = 1;
String line;
while ((line = br.readLine()) != null) {
if (iLine == readLine) {
sb.append(line).append("\n");
}
iLine++;
}
result = sb.toString();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

}


4.2.4 业务测试

编写controller.

package com.ceb.mental.controller;

import com.ceb.mental.util.RunShell;
import com.ceb.mental.util.VelocityUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/mft")
public class MFTController {

@RequestMapping("/upload")
public void putFile(String fileName){
RunShell.putMFTFile("/home/qy_work/cebfile/upload", fileName,
"/home/qy_work/cebfile/upload/put", "USERID");
}

@RequestMapping("/download")
public void getFile(String fileName){
RunShell.getMFTFile("/home/qy_work/cebfile/download", fileName,
"/home/qy_work/cebfile/upload/get", "USERID");
}

}


将项目部署到MTF的客户端,访问相应的URL,

4.2.5 检查是否上传成功

/home/Axway/Synchrony/SecureClient/sclient cebbank list USERID


  • 1

这里查询的是银行MTF端上传文件的列表。查询结果,如图:

基于MFT文件上传和下载_上传_02

5. 总结

刚开始都不知道MFT是啥,然后从搭建客户端,在客户端一点点尝试操作,查阅相关资料,摸清原理,理清业务及其中的技术点,评估技术方案,积跬步、解决bug,最终完成了这个看似很难的需求。

生命不息、战斗不止!