spring-cloud / spring-cloud/spring-cloud-stream

Protocol resolver from parent context is not used in binder context

Open
#3,112 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
1.1k
Forks
646
Avg merge
2d 3h
Merged PRs (30d)
8

Description

Describe the issue
When a binder context is created in DefaultBinderFactory, the protocol resolvers from parent context are not copied to the binder context. In our case it breaks our application because we have a protocol resolver that makes sure that the kafka truststore is a local file (by downloading it), and org.springframework.boot.autoconfigure.kafka.KafkaProperties.Ssl#resourceToPath fails if it is not on file system. I.e. it fails because the protocol resolver is not applied in the binding context, and the KafkaProperties bean is created in that context.

This is caused by the fix for https://github.com/spring-projects/spring-boot/issues/41487 where configuration properties beans are re-created in child contexts rather than inherited from the parent context. I'll leave it for you to evaluate if a fix should go into spring-boot or spring-cloud-stream. E.g. should the protocol resolvers be explicitly copied when creating the child context here? Or should protocol resolvers be automatically inherited from parent context?

To Reproduce
I create this test to demonstrate the issue:

package org.springframework.cloud.stream.binder;

import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.stream.config.BinderFactoryAutoConfiguration;
import org.springframework.cloud.stream.config.BindingServiceConfiguration;
import org.springframework.cloud.stream.function.FunctionConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.FileUrlResource;
import org.springframework.core.io.ProtocolResolver;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

import java.net.URI;
import java.util.Collections;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;

class CustomProtocolResolverBindingTest {

	@Test
	void bindingContextUsesParentProtocolResolvers() {

		new ApplicationContextRunner()
			.withInitializer(ctx -> ctx.addProtocolResolver(new CustomProtocolResolver()))
			.withPropertyValues("test.resource=backwards://foo-bar")
			.withConfiguration(AutoConfigurations.of(BinderFactoryAutoConfiguration.class,
				BindingServiceConfiguration.class, FunctionConfiguration.class))
			.withUserConfiguration(AppConfig.class)
			.run(ctx -> {

				DefaultBinderFactory defaultBinderFactory = ctx.getBean(DefaultBinderFactory.class);
				ConfigurableApplicationContext bindingContext = defaultBinderFactory.initializeBinderContextSimple("mock",
					Collections.emptyMap(),
					new BinderType("custom-resolvers", new Class[]{AppConfig.class}),
					new BinderConfiguration("custom-resolvers", Collections.emptyMap(), true, true),
					true);

				TestProperties props = bindingContext.getBean(TestProperties.class);
				assertNotNull(props);
				assertNotNull(props.getResource());
				assertInstanceOf(FileUrlResource.class, props.getResource());
				assertEquals("file://rab-oof", props.getResource().getURI().toString());
			});
	}

	@SpringBootApplication
	@EnableConfigurationProperties(TestProperties.class)
	static class AppConfig {
	}

	@ConfigurationProperties(prefix = "test")
	static class TestProperties {
		private Resource resource;

		public Resource getResource() {
			return resource;
		}

		public void setResource(Resource resource) {
			this.resource = resource;
		}
	}

	/**
	 * Custom protocol resolver that handles the "backwards" scheme by replacing it with the
	 * "file" scheme and reversing the host part.
	 */
	static class CustomProtocolResolver implements ProtocolResolver {

		@Override
		public Resource resolve(String location, ResourceLoader resourceLoader) {
			URI uri = URI.create(location);
			if (uri.getScheme().equals("backwards")) {
				String reversed = new StringBuilder(uri.getHost()).reverse().toString();
				return resourceLoader.getResource("file://" + reversed);
			}
			return null;
		}
	}
}

Version of the framework
4.2.1 (with spring-boot 3.4.3)

Expected behavior
Protocol resolvers are applied also in the child context.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with DefaultBinderFactory.initializeBinderContextSimple and the CustomProtocolResolverBindingTest reproduction. Run the test and trace how the child binding context is created; done means the context resolves the backwards:// resource through the parent protocol resolver and the assertions pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring, spring-boot
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.