From 495ae6673979c48065198db4971a5e0de11c9ebb Mon Sep 17 00:00:00 2001 From: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:12:11 +0900 Subject: [PATCH] GH-3203 Skip standalone bindings in supplierInitializer An output-only binding has no backing Supplier/Function bean, but since gh-3166 stopped null-ing out BindableFunctionProxyFactory.getFunctionDefinition(), its function definition is now the binding name. In supplierInitializer that name is looked up in the function catalog, the plain output channel is wrapped, isSupplier() reports true and a SourcePollingChannelAdapter is built on top of the channel. The first poll then casts the channel to Supplier and throws a ClassCastException, breaking output-only bindings that worked in 5.0.1. Guard the supplier initialization with proxyFactory.isFunctionExist() so that standalone bindings (functionExist == false) are skipped, restoring the 5.0.1 behavior without reverting the getFunctionDefinition() change required by gh-3166. Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> --- .../function/FunctionConfiguration.java | 8 +++ .../OutputOnlyBindingSupplierTests.java | 68 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/function/OutputOnlyBindingSupplierTests.java diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/FunctionConfiguration.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/FunctionConfiguration.java index 0868808c72..2c0e9bec0f 100644 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/FunctionConfiguration.java +++ b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/FunctionConfiguration.java @@ -190,6 +190,14 @@ InitializingBean supplierInitializer(FunctionCatalog functionCatalog, StreamFunc return () -> { for (BindableFunctionProxyFactory proxyFactory : proxyFactories) { + // see https://github.com/spring-cloud/spring-cloud-stream/issues/3203 + // A standalone output-/input-binding has no backing function, yet its + // function definition is now the binding name (since gh-3166 stopped + // null-ing it out in getFunctionDefinition()). Skip such bindings here so + // that a plain output channel is never resolved and treated as a Supplier. + if (!proxyFactory.isFunctionExist()) { + continue; + } FunctionInvocationWrapper functionWrapper = functionCatalog.lookup(proxyFactory.getFunctionDefinition()); if (functionWrapper != null && functionWrapper.isSupplier()) { // gather output content types diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/function/OutputOnlyBindingSupplierTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/function/OutputOnlyBindingSupplierTests.java new file mode 100644 index 0000000000..704af65cd9 --- /dev/null +++ b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/function/OutputOnlyBindingSupplierTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.function; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.endpoint.SourcePollingChannelAdapter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Regression test for gh-3203: an output-only binding that has no backing + * {@code Supplier}/{@code Function} bean must not be treated as a {@code Supplier}. + * Prior to the fix the standalone output binding (whose function definition is the + * binding name) reached {@code supplierInitializer}, which built a + * {@link SourcePollingChannelAdapter} on top of the plain output channel and threw a + * {@code ClassCastException} (the channel cannot be cast to {@code Supplier}) on the + * first poll. + * + * @author Spring Cloud Stream contributors + */ +class OutputOnlyBindingSupplierTests { + + @Test + void outputOnlyBindingIsNotTreatedAsSupplier() { + try (ConfigurableApplicationContext context = new SpringApplication(EmptyConfiguration.class).run( + "--spring.cloud.stream.output-bindings=testOutput", + "--spring.jmx.enabled=false", + "--spring.main.web-application-type=none", + "--spring.cloud.stream.default-binder=mock")) { + + Map pollingAdapters = + context.getBeansOfType(SourcePollingChannelAdapter.class); + + assertThat(pollingAdapters) + .as("No SourcePollingChannelAdapter must be created for an output-only " + + "binding without a backing Supplier") + .isEmpty(); + } + } + + @EnableAutoConfiguration + @Configuration + public static class EmptyConfiguration { + + } + +}