-
Notifications
You must be signed in to change notification settings - Fork 40.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support declarative HTTP clients #31337
Comments
public class HttpServiceFactory implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private final HttpServiceProxyFactory proxyFactory;
private BeanFactory beanFactory;
private ResourceLoader resourceLoader;
public HttpServiceFactory() {
WebClient client = WebClient.builder().build();
this.proxyFactory = HttpServiceProxyFactory.builder(new WebClientAdapter(client)).build();
}
@Override
public void registerBeanDefinitions(@NonNull AnnotationMetadata importingClassMetadata,
@NonNull BeanDefinitionRegistry registry) {
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
Set<Class<?>> typesAnnotatedClass = findByAnnotationType(HttpExchange.class, resourceLoader,
packages.toArray(String[]::new));
for (Class<?> exchangeClass : typesAnnotatedClass) {
BeanName name = AnnotationUtils.getAnnotation(exchangeClass, BeanName.class);
String beanName = name != null ? name.value()
: CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, exchangeClass.getSimpleName());
registry.registerBeanDefinition(beanName, getBeanDefinition(exchangeClass));
}
}
private <T> BeanDefinition getBeanDefinition(Class<T> exchangeClass) {
return new RootBeanDefinition(exchangeClass, () -> proxyFactory.createClient(exchangeClass));
}
@Override
public void setResourceLoader(@NonNull ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
public Set<Class<?>> findByAnnotationType(Class<? extends Annotation> annotationClass,
ResourceLoader resourceLoader, String... packages) {
Assert.notNull(annotationClass, "annotation not null");
Set<Class<?>> classSet = new HashSet<>();
if (packages == null || packages.length == 0) {
return classSet;
}
ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
CachingMetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
try {
for (String packageStr : packages) {
packageStr = packageStr.replace(".", "/");
Resource[] resources = resolver.getResources("classpath*:" + packageStr + "/**/*.class");
for (Resource resource : resources) {
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
String className = metadataReader.getClassMetadata().getClassName();
Class<?> clazz = Class.forName(className);
if (AnnotationUtils.findAnnotation(clazz, annotationClass) != null) {
classSet.add(clazz);
}
}
}
}
catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
return classSet;
}
}
/**
* Just used to set the BeanName
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BeanName {
String value();
}
|
What would an additional annotation bring beyond |
The underlying client may need to be configured with different base URL, codecs, etc. That means detecting Given an Currently |
I played around with it here. The auto-configuration supplies a bean of type |
Thanks, this helps me to move my thought process forward. I see now it is necessary to separate more formally the I've an experiment locally where The Boot auto-config could then declare a single |
After a few different experiments, I think trying to have one @Bean
HttpServiceProxyFactory httpServiceProxyFactory1(WebClient.Builder clientBuilder) {
WebClient client = clientBuilder.baseUrl("http://host1.com").build();
return new HttpServiceProxyFactory(new WebClientAdapter(client));
}
@Bean
HttpServiceProxyFactory httpServiceProxyFactory2(WebClient.Builder clientBuilder) {
WebClient client = clientBuilder.baseUrl("http://host2.com").build();
return new HttpServiceProxyFactory(new WebClientAdapter(client));
} A couple of extra shortcuts on @Bean
HttpServiceProxyFactory httpServiceProxyFactory1(WebClient.Builder clientBuilder) {
return WebClientAdapter.createProxyFactory(clientBuilder.baseUrl("http://host1.com"));
}
@Bean
HttpServiceProxyFactory httpServiceProxyFactory2(WebClient.Builder clientBuilder) {
return WebClientAdapter.createProxyFactory(clientBuilder.baseUrl("http://host2.com"));
} If we settle on the above as the expected configuration, then I think it's not essential to have any Boot auto-config to start, although some ideas may still come along. Perhaps, specifying baseUrl's in properties, which would allow Boot to create the above beans? |
Thanks, Rossen.
We don't yet support specifying multiple property values to auto-configure multiple beans anywhere in Boot. It's something that we'd like to do, but it's a complex and wide-ranging topic. #15732 is tracking auto-configured multiple DataSources, for example. Having discussed this today, we don't think there's anything to do in Boot at this time. We can revisit this if the picture changes for auto-configuring multiple beans. |
Note that there is now spring-projects/spring-framework#29296, which will likely give us a better model for dealing with multiple |
Reopening because of changes made in spring-projects/spring-framework#29296 |
With regard to this nice tutorial on HTTP Interface https://softice.dev/posts/introduction_to_spring_framework_6_http_interfaces/, I don't quite understand. Why do developer need to manually write a Also why would one use Http Interface over |
I think we need an annotation like We can already know whether an interface is a http client through Here‘s my workaround. |
I think people have gotten used to using the Feign client approach. Here you can find very similar aproach: Exchange client It is really simple to use. |
I have prototyped following approach, that reduces the boilerplate to minimum:
|
Another proposed implementation: https://github.com/joshlong/declarative-client-registration by @joshlong . |
Let me introduce once again to httpexchange-spring-boot-starter, which is probably the most comprehensive implementation I could find. This project is entirely configuration-driven and can achieve the same functionalities as Spring Cloud OpenFeign without introducing any external annotations. Including setting different baseUrl/timeout for each client, integration with Spring Cloud LoadBalancer, dynamic refresh, and more. http-exchange:
base-packages: [com.example]
connect-timeout: 1000
read-timeout: 3000
client-type: rest_client
channels:
- base-url: http://order
clients:
- com.example.order.api.*
- base-url: http://user
read-timeout: 5000
clients:
- com.example.user.api.* The main goals of this project:
It's definitely worth a try! |
Great Effort!! Wish the spring boot will include your starter as an official one! |
So is there any plan or any work in progress to include this feature natively? |
We're discussing it within the team now. We will update here once decision's been taken. |
@OlgaMaciaszek are there any updates on this topic? |
Internal POC planned for this month to discussed with the team. Will post any updates here. |
@OlgaMaciaszek Since spring-cloud-openfeign is going to enter maintenance mode, will this become the alternative options to openfeign, any update for now? |
Hey Folks, any updates here. when the declrative approach will be available natively? |
Working on POC at this point. Once that's done we'll able to hold a discussion on adding it to a backlog of a specific release. Will post any relevant updates here. |
@OlgaMaciaszek Thank you for introduce me to this post. httpexchange-spring-boot-starter is really helpful, but may be it still have a small difference from what I want. That's we always configure the base url in database, not in property files, and we need to obtain the base url during every call to the HttpExchange interface to ensure that we use the proper value for each customer (Yes, different customer may use different base url, and the remote site is not constucted by ourself) |
Maybe you can do it with an |
Always get your configuration from the spring environment and use different configuration sources to load from a property or DB or whatever |
|
Thank you for your advice, what you mean is to configurer Proxy instance per customer for specified HttpExchange at startup, like Bean instance A for customer A, Bean instance B for customer B, and so on. Is this understanding correct? |
You can replace the request, just like the example with kotlin: val urlPrefixedInterceptor = ClientHttpRequestInterceptor { request, body, execution ->
execution.execute(object : HttpRequest by request {
override fun getURI(): URI {
return URI.create("https://example.org${request.uri}")
}
}, body)
} |
Good idea; example of Java code public class DemoUrlInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpRequest newRequest = new HttpRequestWrapper(request) {
@Override
public URI getURI() {
return URI.create("https://example.org" + request.getURI());
}
};
return execution.execute(newRequest, body);
}
} |
I ran some experiments around I took a pretty different direction than httpexchange-spring-boot-starter for two reasons:
I believe that it's a good thing to de-couple the client bean definition from the generated
I end with YAML for each client and generate no more than pre-configured com:
c4-soft:
springaddons:
rest:
client:
# Exposes a RestClient bean named machinClient (or WebClient in a WebFlux app)
machin-client:
base-url: ${machin-api}
authorization:
oauth2:
# Authorize outgoing requests with the Bearer token in the security context (possible only in a resource server app)
forward-bearer: true
# Exposes a RestClient.Builder bean named biduleClientBuilder (mind the "expose-builder: true")
bidule-client:
base-url: ${bidule-api}
# Expose the builder instead of an already built client (to fine tune its conf)
expose-builder: true
authorization:
oauth2:
# Authorize outgoing requests with the Bearer token obtained using an OAuth2 client registration
oauth2-registration-id: bidule-registration Once the REST clients are configured, producing the @Configuration
public class RestConfiguration {
/**
* @param machinClient pre-configured by spring-addons-starter-rest using application properties
* @return a generated implementation of the {@link MachinApi} {@link HttpExchange @HttpExchange}, exposed as a bean named "machinApi".
*/
@Bean
MachinApi machinApi(RestClient machinClient) throws Exception {
return new RestClientHttpExchangeProxyFactoryBean<>(MachinApi.class, machinClient).getObject();
}
/**
* @param biduleClientBuilder pre-configured using application properties
* @return a {@link RestClient} bean named "biduleClient"
*/
@Bean
RestClient biduleClient(RestClient.Builder biduleClientBuilder) throws Exception {
// Fine-tune biduleClientBuilder configuration here
return biduleClientBuilder.build();
}
/**
* @param biduleClient the bean exposed just above
* @return a generated implementation of the {@link BiduleApi} {@link HttpExchange @HttpExchange}, exposed as a bean named "biduleApi".
*/
@Bean
BiduleApi biduleApi(RestClient biduleClient) throws Exception {
return new RestClientHttpExchangeProxyFactoryBean<>(BiduleApi.class, biduleClient).getObject();
}
} This exposes two generated The next step could be replacing most of the Java code above ( keep just the service:
machin-service:
client: machinClient
exchange-class: pf.c4soft.MachinApi
bidule-service:
client: biduleClient
exchange-class: pf.c4soft.BiduleApi But I couldn't find a way to post-process the bean definition registry to do so - and the gains would be marginal. |
Spring Framework 6.0 introduces declarative HTTP clients. We should add some auto-configuration in Boot which supplies for example the
HttpServiceProxyFactory
.We could even think about an annotation with which the HTTP interface can be annotated and then directly injected into a consumer (like
@FeignClient
).The text was updated successfully, but these errors were encountered: