You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java Apache Camel REST API首次FTP下载PDF耗时久,二次快,求优化

Apache Camel FTP首次下载耗时过长优化方案

问题背景

用Java+Apache Camel开发的REST API,通过请求参数拼接文件名从FTP目录下载PDF。首次调用耗时超20秒,二次及后续调用仅需5秒内,需要优化首次调用速度至5秒以内。

API示例地址:

http://server3343.erp.local:5464/api/rest/invoices/receipts/pdf?companyNumber=45&accountNumber=000333&documentType=FC&transactionNumber=BG534530SF

当前实现代码:

package ar.com.newsan.esb.route;

import ar.com.company.camel.common.route.RestRouteBuilder;
import ar.com.company.esb.Constants;
import org.apache.camel.LoggingLevel;
import org.apache.camel.model.rest.RestParamType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.ws.rs.core.MediaType;

@Component
public class GetReceiptsPDFRoute extends RestRouteBuilder {

    @Value("${timeout.FTP}")
    private Long timeoutFTP;

    @Override
    public void configure() {
        super.configure();
        rest(restApiPath)
            .get("/receipts/pdf?companyNumber={companyNumber}&accountNumber={accountNumber}&documentType={documentType}&transactionNumber={transactionNumber}").id("receipts PDF")
            .param().name("companyNumber").type(RestParamType.query).dataType("String").required(true).endParam()
            .param().name("accountNumber").type(RestParamType.query).dataType("String").required(true).endParam()
            .param().name("documentType").type(RestParamType.query).dataType("String").required(true).endParam()
            .param().name("transactionNumber").type(RestParamType.query).dataType("String").required(true).endParam()
            .consumes(MediaType.APPLICATION_JSON)
            .produces("application/pdf")
            .to("direct:receiptsPDF");

        /**
         *  set name and location to look for the PDF in FTP DIRECTORY
         */
        from("direct:receiptsPDF").routeId("ReceiptsPDF")
            .setProperty("currentOperation", constant("ReceiptsPDF"))
            .log(LoggingLevel.INFO,"ReceiptsPDF", "[Inicia ${property.currentOperation}]")
            .log(LoggingLevel.INFO, "company Number: ${header.companyNumber}")
            .log(LoggingLevel.INFO, "account Number: ${header.accountNumber}")
            .log(LoggingLevel.INFO, "document Type: ${header.documentType}")
            .log(LoggingLevel.INFO, "transaction Number: ${header.transactionNumber}")
            .doTry()
                .choice()
                    .when(header("documentType").isEqualTo(Constants.FILE))
                        .log(LoggingLevel.INFO, "ReceiptsPDF","[${property.currentOperation}] Find FILE")
                        .setHeader("TARGET_DIR", simple("{{source.ebs.FILEs}}"))
                .endChoice()
                .otherwise()
                    .log(LoggingLevel.INFO, "ReceiptsPDF","[${property.currentOperation}] Find FILES")
                    .setHeader("TARGET_DIR", simple("{{source.ebs.FILEs}}"))
                .end()
                .setProperty("fileName", simple("${header.companyNumber}${header.accountNumber}${header.documentType}${header.transactionNumber}.pdf"))
                .to("direct:findPdfInFTP")
                .log(LoggingLevel.INFO,"ReceiptsPDF", "[Ending ${property.currentOperation}]")
            .endDoTry()
            .doCatch(Exception.class)
                .log(LoggingLevel.ERROR, "ReceiptsPDF", "[ERROR: ${exception.message}")
        .end();

        /**
         *  search in FTP DIRECTORY
         */
        from("direct:findPdfInFTP").routeId("find file in ftp")
            .doTry()
                .to("direct:searchInFTP")
                .choice()
                    .when(simple("${body} == null"))
                        .log(LoggingLevel.INFO, "findPdfInFTP", "Search in processed sub-direcotries")
                        .setHeader("TARGET_DIR", simple("${header.TARGET_DIR}/Procesados"))
                        .to("direct:searchInFTP")
                .otherwise()
                .log(LoggingLevel.INFO,"findPdfInFTP","FILE OBTAINED: ${headers.CamelFileName}")
            .endDoTry()
            .doCatch(Exception.class)
                .log(LoggingLevel.ERROR, "findPdfInFTP", "[ERROR: ${exception.message}")
            .end();

        from("direct:searchInFTP").routeId("searchInFTP")
            .doTry()
                .log(LoggingLevel.INFO, "searchInFTP","Search: {{ebs.sftp}}/${header.TARGET_DIR}/${property.fileName}")
                .pollEnrich().simple("{{ebs.sftp}}/${header.TARGET_DIR}?password={{ebs.sftp.password}}&fileName=${property.fileName}&disconnect=true&useList=false&ignoreFileNotFoundOrPermissionError=true&useUserKnownHostsFile=false").timeout(timeoutFTP)
            .endDoTry()
            .doCatch(Exception.class)
                .log(LoggingLevel.ERROR, "searchInFTP", "[ERROR: ${exception.message}")
            .end();
    }
}

核心问题分析

首次调用慢的主要原因是FTP连接的初始化开销:

  1. 当前代码设置了disconnect=true,每次请求后强制断开连接,导致每次请求都要重新建立TCP连接、完成认证流程,这部分首次操作耗时极长
  2. 没有启用连接池,无法复用已建立的连接
  3. 二次调用时连接可能被系统层面缓存复用,所以速度明显提升

优化措施

1. 启用FTP连接池,复用连接

修改searchInFTP路由中的SFTP端点配置,移除disconnect=true,添加连接池相关参数:

from("direct:searchInFTP").routeId("searchInFTP")
    .doTry()
        .log(LoggingLevel.INFO, "searchInFTP","Search: {{ebs.sftp}}/${header.TARGET_DIR}/${property.fileName}")
        // 启用连接池,移除disconnect=true
        .pollEnrich().simple("{{ebs.sftp}}/${header.TARGET_DIR}?password={{ebs.sftp.password}}&fileName=${property.fileName}&useList=false&ignoreFileNotFoundOrPermissionError=true&useUserKnownHostsFile=false&poolSize=5&maxWait=1000&soTimeout=5000").timeout(timeoutFTP)
    .endDoTry()
    .doCatch(Exception.class)
        .log(LoggingLevel.ERROR, "searchInFTP", "[ERROR: ${exception.message}")
    .end();

参数说明:

  • poolSize=5:设置连接池大小,根据业务并发量调整
  • maxWait=1000:获取连接的最大等待时间(毫秒),避免长时间阻塞
  • soTimeout=5000:Socket超时时间,快速终止无响应的连接尝试

2. 预初始化FTP连接

在应用启动时提前建立连接池中的连接,避免首次请求时的连接建立开销。添加如下路由到configure方法中:

// 应用启动时初始化FTP连接池
from("timer:initFTP?repeatCount=1")
    .routeId("initFTPConnection")
    .log(LoggingLevel.INFO, "Initializing FTP connection pool")
    .to("{{ebs.sftp}}/{{source.ebs.FILEs}}?password={{ebs.sftp.password}}&fileName=test.pdf&useList=false&ignoreFileNotFoundOrPermissionError=true&poolSize=5")
    .log(LoggingLevel.INFO, "FTP connection pool initialized");

这个路由仅在应用启动时执行一次,提前触发连接建立,后续请求直接复用连接池中的连接。

3. 优化超时配置

调整timeout.FTP参数为合理值,比如5000毫秒(5秒),避免首次连接时无意义的等待:

timeout.FTP=5000

4. 可选:优化目录搜索逻辑

当前代码会先后搜索主目录和Procesados子目录,两次FTP操作会增加耗时。如果业务上有明确的文件存放优先级,可以调整搜索顺序,优先搜索文件更可能存在的目录,减少不必要的FTP请求。

5. 网络层面优化

  • 尝试使用FTP服务器的IP地址代替域名,减少DNS解析开销
  • 如果使用密码认证,可改为密钥认证,降低认证环节的耗时
  • 检查防火墙、代理等网络设备是否对首次连接有额外的延迟规则

内容的提问来源于stack exchange,提问作者JustToKnow

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 05:40:29