Spring Integration运行时错误:没有可用的请求通道。无法发送请求消息

人工智能 2026-07-08

我在将SpringBoot 3迁移到SpringBoot 4之后遇到了一个问题。尝试通过SFTP发送文件时,使用 MPUT 提示没有通道:

2026-06-16T10:15:10.331217825Z ERROR [] --- [cdb-job-thread3] com.harry.potter.job.MyExportRestrictionsTask: ExportRestrictions failed! Exception: {} org.springframework.messaging.MessagingException: No request channel available. Cannot send request message.
        at org.springframework.integration.gateway.MessagingGatewaySupport.sendAndReceive(MessagingGatewaySupport.java:591)
        at org.springframework.integration.gateway.MessagingGatewaySupport.sendAndReceive(MessagingGatewaySupport.java:578)
        at org.springframework.integration.gateway.GatewayProxyFactoryBean.sendOrSendAndReceive(GatewayProxyFactoryBean.java:653)
        at org.springframework.integration.gateway.GatewayProxyFactoryBean.invokeGatewayMethod(GatewayProxyFactoryBean.java:587)
        at org.springframework.integration.gateway.GatewayProxyFactoryBean.doInvoke(GatewayProxyFactoryBean.java:553)
        at org.springframework.integration.gateway.GatewayProxyFactoryBean.invoke(GatewayProxyFactoryBean.java:544)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:222)
        at jdk.proxy2/jdk.proxy2.$Proxy146.mput(Unknown Source)
        at com.harry.potter.job.service.JobService.putExportFiles(JobService.java:234)
        at com.harry.potter.job.service.JobService.task(JobService.java:84)
        at com.harry.potter.job.MyExportRestrictionsTask.runAsScheduledTask(MyExportRestrictionsTask.java:82)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:158)
        at net.javacrumbs.shedlock.core.DefaultLockingTaskExecutor.executeWithLock(DefaultLockingTaskExecutor.java:74)
        at net.javacrumbs.shedlock.spring.aop.MethodProxyScheduledLockAdvisor$LockingInterceptor.invoke(MethodProxyScheduledLockAdvisor.java:86)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:719)
        at com.harry.potter.job.MyExportRestrictionsTask$$SpringCGLIB$$0.runAsScheduledTask(<generated>)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:128)
        at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$1(ScheduledMethodRunnable.java:122)
        at io.micrometer.observation.Observation.observe(Observation.java:569)
        at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:122)
        at org.springframework.scheduling.config.Task$OutcomeTrackingRunnable.run(Task.java:88)
        at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
        at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:94)
        at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
        at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
        at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
        at java.base/java.lang.Thread.run(Unknown Source)

我的SFTP配置如下:channel用于定义SFTP连接配置,context定义在传输中要使用的文件名和路径名,sessionFactory用于创建传输会话,以及带有 MPUT 命令路由的集成流(第二条路由尚未启用)和消息SFTP命令网关:

@Configuration
public class MySftpConfiguration {
    private static final Logger LOG = LoggerFactory.getLogger(MySftpConfiguration.class);

    @Bean
    public TransferChannel myChannel() {
        LOG.debug("myChannel");
        TransferChannel channel = new TransferChannel();
        channel.setHost(myEnv.getSftpHost());
        channel.setPort(myEnv.getSftpPort());
        channel.setUser(myEnv.getSftpUser());
        channel.setPrivateKey(myEnv.getSftpPrivateKey());
        channel.setPassword(myEnv.getSftpPassword());
        return channel;
    }

    @Bean
    public TransferContext myContext(TransferChannel myChannel) {
        LOG.debug("myContext");
        TransferContext context = new TransferContext();
        context.setEnabled(myEnv.isEnabled());
        context.setChannel(myChannel);
        context.setPreserveTimestamp(true);
        context.setLocalDir(myEnv.getLocalDir());
        context.setLocalFilename(myEnv.getLocalFilename());
        context.setRemoteDir(myEnv.getRemoteDir());
        return context;
    }

    @Bean
    public SessionFactory<DirEntry> mySessionFactory(TransferChannel myChannel) {
        LOG.debug("mySessionFactory");
        DefaultSftpSessionFactory sf = new DefaultSftpSessionFactory(false);
        sf.setHost(myChannel.getHost());
        sf.setPort(myChannel.getPort());
        sf.setUser(myChannel.getUser());
        if (Channel.getPrivateKey() != null) {
            sf.setPrivateKey(myChannel.getPrivateKey());
        } else {
            sf.setPassword(myChannel.getPassword());
        }
        sf.setAllowUnknownKeys(true);
        return new CachingSessionFactory<DirEntry>(sf);
    }

    /**
     * The Integration Flow "mySftpCmdGate" is the messaging end-point of
     * the SFTP command gate "MessagingGateway".
     * This command gate was said just by declaring it
     * to encapsulate the method which was called into a message header
     * named "method".
     * With that, "mySftpCmdGate" end-point receives messages of two types: 
     * 1. message with header["method"] = "getRequest"
     * 2. message with header["method"] = "mput"
     * These messages are routed with two routings f1,f2 to the appropriate handler,
     * two SFTP OutboundGateways which perform the required command
     * on the file - given as message payload - on the SFTP side.
     * 
     * @param mySessionFactory the SFTP session factory
     * @param myContext the SFTP context
     * @return the message end-point as {@link IntegrationFlow}
     */
    @Bean
    public IntegrationFlow mySftpFlow(SessionFactory<DirEntry> mySessionFactory, TransferContext myContext) {
        LOG.debug("mySftpFlow");
        return IntegrationFlow.from(mySftpCmdGate.class, g -> g
                        .header("method", args -> args.method().getName()))
                .log()
                .route(Message.class, m -> m.getHeaders().get("method", String.class),
                        r -> r
    //                          .subFlowMapping("getRequest", f1 -> f1
    //                                      .handle(Sftp.outboundGateway(mySessionFactory, Command.GET, "payload")
    //                                              .localDirectory(new File(myContext.getLocalDir())))
    //                                      .transform(new FileToStringTransformer())
    //                                      .transform(Transformers.fromJson(myRequest.class))
    //                              )
                            .subFlowMapping("mput", f2 -> f2
                                        .handle(Sftp.outboundGateway(mySessionFactory, Command.MPUT, "payload")
                                                    .autoCreateDirectory(false)
                                                    .useTemporaryFileName(false)
                                                    .remoteDirectoryExpression(myContext.getRemoteDir()))
                                ))
                .get();
    }

    @MessagingGateway
    public interface mySftpCmdGate {
    //      myRequest getRequest(File remoteFile);
        List<String> mput(File localDir);
    }
}

出了什么问题?有什么线索吗?

** 更新1 **

为了将SpringBoot-3.5.5迁移到SpringBoot-4.0.6(Spring迁移到Spring-7.0.7;SpringIntegration-6.5.5迁移到SpringIntegration-7.0.4),让我在迁移后的项目无错误编译所需做的事情,是:

  • 将import org.springframework.integration.sftp.gateway.SftpOutboundGateway 修改为 org.springframework.integration.sftp.outbound.SftpOutboundGateway
  • 在应用主类中添加 @IntegrationComponentScan("com.harry.potter.job.config")
  • @MessagingGateway 标注命令网关
  • 将使用命令网关的服务类中的构造注入限定为 @Qualified("mySftpConfiguration.MySftpCmdGate")
  • 在使用命令网关的服务类中的引用从 com.harry.potter.job.config.MySftpCmdGate 改为 com.harry.potter.job.config.MySftpConfiguration.MySftpCmdGate

在这些改动之后,SFTP传输再也无法正常工作,并显示了上述异常栈。

有什么想法吗?

解决方案

我又让AI将 SpringBoot-3.5.5迁移到SpringBoot-4.0.6。AI比我懒多了,没动SFTP命令网关。当时我进行迁移时,曾困惑JVM不再识别 MySftpCmdGate.class

原因在于,在SpringBoot 4中,应用需要同时被 @EnableIntegration@IntegrationComponentScan(...) 注解。以前这可能是可选的,只靠找到Spring Integration依赖就能工作。一旦这两个注解就位,项目就能无错误地编译。

所以我把SFTP命令网关改回到原来的状态:

    public interface mySftpCmdGate {
        List<String> mput(File localDir);
    }

回滚后,SFTP传输又能正常工作。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章