前言
版本:
SpringBoot 2.4
camel 3.5.0
最近在做跟一个第三方系统的对接,主要流程就是对方生成XML格式的报文,需要我方将其报文发送到海关申报,然后将申报完的数据再组装成XML报文格式发回到对方的FTP服务器。功能其实挺简单,用Apache的camel-ftp很容易就能实现,下面看看具体如何做吧。
一、引入camel依赖:
<!-- camel-spring-boot-starter -->
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
<!-- camel ftp组件-->
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-ftp</artifactId>
<version>3.5.0</version>
</dependency>
二、camel配置:
# camel配置
camel:
# camel ftp服务路由地址
route:
id: XMLRoute
ftp:
server: sftp://119.22.77.152:22/Export/Pre-CDF?username=root&password=123456&passiveMode=true&move=backup&moveFailed=error&delay=5000&exceptionHandler=#errorExceptionHandler&onCompletionExceptionHandler=#errorExceptionHandler
# server: file:e:/test?recursive=true&move=.backup&moveFailed=.error&exceptionHandler=#errorExceptionHandler&onCompletionExceptionHandler=#errorExceptionHandler
file:
download:
local:
# camel 文件内容下载到本地地址(配置中文乱码)
address: file:/home/platform/ex-dec/receive/
# camel 文件下载到本地备份地址
backup:
address: file:/home/platform/ex-dec/receive/backup/
- [IP][端口]后面的/Export/Pre-CDF表示监听/Export/Pre-CDF目录。
- passiveMode=true:被动模式true
- move=backup:将源目录下的源文件移动到backup目录下,如果不设置默认是移动到.camel目录中。
- moveFailed=error:将异常导致失败的源文件移动到error目录中。
- delay=5000:表示5秒监听一次。
- exceptionHandler:自定义的异常处理器。
三、新建文件过滤类FtpDownloadFileFilter,继承camel的Predicate
package com.yorma.ex.camel.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.camel.Exchange;
import org.apache.camel.Predicate;
import org.apache.camel.component.file.GenericFileMessage;
import org.springframework.stereotype.Component;
import java.io.RandomAccessFile;
/**
* camel过滤器
*
* @author ZHANGCHAO
* @date 2021/6/8 14:27
* @since 1.0.0
*/
@Slf4j
@Component
public class FtpDownloadFileFilter implements Predicate {
@Override
public boolean matches(Exchange exchange) {
boolean bMatches;
GenericFileMessage<RandomAccessFile> inFileMessage = (GenericFileMessage<RandomAccessFile>) exchange.getIn();
String fileName = inFileMessage.getGenericFile().getFileName();
// 只需要处理.xml格式的文件
bMatches = fileName.endsWith(".xml");
log.info("fileName:"+fileName+", matches:"+bMatches);
return bMatches;
}
}
四、自定义异常处理类ErrorExceptionHandler,继承camel的ExceptionHandler
package com.yorma.ex.camel.handle;
import lombok.extern.slf4j.Slf4j;
import org.apache.camel.Exchange;
import org.apache.camel.spi.ExceptionHandler;
import org.apache.camel.spring.SpringCamelContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* 错误处理
*
* @author ZHANGCHAO
* @date 2021/6/9 8:44
* @since 1.0.0
*/
@Slf4j
@Component
public class ErrorExceptionHandler implements ExceptionHandler {
/**
* camel路由ID
*/
@Value("${camel.route.id}")
private String routeId;
@Override
public void handleException(Throwable exception) {
this.handleException(null, null, exception);
}
@Override
public void handleException(String message, Throwable exception) {
this.handleException(message, null, exception);
}
@Override
public void handleException(String message, Exchange exchange, Throwable exception) {
log.info("系统在处理下行数据时出现异常,获取Exchange对象是否为空:{},异常信息是:", null != exchange ? "不为空" : "为空", exception);
if (null != exchange) {
if (exchange.getContext() instanceof SpringCamelContext) {
SpringCamelContext springCamelContext = (SpringCamelContext) exchange.getContext();
// 关闭路由
stop(springCamelContext);
// 启动路由
start(springCamelContext);
} else {
log.info("系统在处理下行数据时出现异常,获取getContext不是SpringCamelContext");
exchange.getFromEndpoint().stop();
exchange.getContext().stop();
}
}
}
/**
* 关闭路由
*
* @param springCamelContext SpringCamelContext对象
*/
private void stop(SpringCamelContext springCamelContext) {
try {
log.info("路由ID:{},即将关闭", routeId);
springCamelContext.stopRoute(routeId);
log.info("路由ID:{},关闭完成", routeId);
} catch (Exception e) {
log.error("路由ID:{},在关闭路由时出现异常,异常信息是:", routeId, e);
}
}
/**
* 启动路由
*
* @param springCamelContext SpringCamelContext对象
*/
private void start(SpringCamelContext springCamelContext) {
// 获取当前时间
long currentTime = System.currentTimeMillis();
// 设置1小时后执行
currentTime += 60 * 60 * 1000;
Date date = new Date(currentTime);
log.info("路由ID:{},系统将在:{},启动此路由", routeId, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
// 启动路由
start(springCamelContext, date);
}
/**
* 指定时间启动路由
*
* @param springCamelContext SpringCamelContext对象
* @param startDate 启动时间
*/
private void start(SpringCamelContext springCamelContext, Date startDate) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
try {
log.info("路由ID:{},即将启动", routeId);
springCamelContext.startRoute(routeId);
log.info("路由ID:{},启动完成", routeId);
} catch (Exception e) {
log.error("路由ID:{},在启动路由时出现异常,异常信息是:", routeId, e);
}
}
}, startDate);
}
}
五、核心代码,创建camel的路由
package com.yorma.ex.camel.route;
import com.yorma.ex.camel.filter.FtpDownloadFileFilter;
import com.yorma.ex.camel.process.DataProcessor;
import org.apache.camel.builder.RouteBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class CamelRouteBuilder extends RouteBuilder {
/**
* FTP服务器地址
*/
@Value("${camel.route.ftp.server}")
private String ftpServer;
/**
* 文件下载根路径
*/
@Value("${camel.file.download.local.address}")
private String basePath;
/**
* 备份目录根目录
*/
@Value("${camel.file.download.local.backup.address}")
private String backupBasePath;
@Autowired
private DataProcessor dataProcessor;
@Autowired
private FtpDownloadFileFilter ftpDownloadFileFilter;
@Override
public void configure() {
try {
from(ftpServer)
.log("原始xml文件: ${file:name}开始处理")
.filter(ftpDownloadFileFilter)
.toD(basePath)
// .toD(backupBasePath)
.log("原始xml文件: ${file:name}下载成功")
.process(dataProcessor);
} catch (Exception e) {
log.info("系统路由在执行任务时发生异常,异常信息是:", e);
}
}
}
六、新增数据处理类DataProcessor实现Processor ,并重写void process(Exchange exchange) 方法,用于camel取回文件后的后续处理。
package com.yorma.ex.camel.process;
import cn.hutool.core.io.IoUtil;
import com.yorma.dcl.api.DecApi;
import com.yorma.dcl.entity.DecHead;
import com.yorma.entity.YmMsg;
import com.yorma.ex.utils.JSONObject;
import com.yorma.ex.utils.XML;
import com.yorma.util.RequestKit;
import lombok.extern.slf4j.Slf4j;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import static cn.hutool.core.util.ObjectUtil.isNotEmpty;
import static cn.hutool.core.util.StrUtil.isBlank;
import static com.yorma.ex.utils.XmlUtils.setDecFromXML;
/**
* 文件处理
*
* @author Administrator
*/
@Slf4j
@Component
public class DataProcessor implements Processor {
@Autowired
private DecApi decApi;
/**
* 文件下载根路径
*/
@Value("${camel.file.download.local.address}")
private String basePath;
@Override
public void process(Exchange exchange) throws IOException {
// 获取文件名称
String fileName = String.valueOf(exchange.getIn().getHeader(Exchange.FILE_NAME));
if (isBlank(fileName)) {
log.info("获取文件名称为空或空字符串,系统终止操作");
return;
}
File file = new File(basePath.replaceFirst("file:", "") + fileName);
if (!file.exists()) {
log.info("未查找到下载的本地报文!");
return;
}
InputStream in = new FileInputStream(file);
String xml = IoUtil.read(in, StandardCharsets.UTF_8);
in.close();
JSONObject jsonObject = XML.toJSONObject(xml);
System.out.println("jsonObject==> " + jsonObject);
DecHead decHead = new DecHead();
setDecFromXML(decHead, jsonObject);
RequestKit.addHeader("apiKey", "461277322-943d-4b2f-b9b6-3f860d746ffd");
RequestKit.addHeader("apiUser", "exDecUser");
RequestKit.addHeader("Tenant-Id", "1310053879995457537");
RequestKit.addHeader("Customer-Id", "1310053881811587074");
YmMsg<DecHead> decHeadYmMsg = decApi.saveDecForEx(decHead);
if (isNotEmpty(decHeadYmMsg.getData())) {
DecHead resultHead = decHeadYmMsg.getData();
log.info("===> 已生成报关单草单" + resultHead.toString());
}
}
}
思路:将文件转为JSONObject,然后从里面get取值。
主要取值逻辑代码:
package com.yorma.ex.utils;
import cn.hutool.core.collection.CollUtil;
import cn