I have a few use cases in my project, that require me to call the same endpoint on several different base URLs and I need to be able to pass the base URL as a parameter into the method of the client.
Using Feign, this is possible to achieve by just adding a URI as a parameter. In a spring http interface, there is also the possibility to add a URI as a parameter, but the functionality is not the same because here it overwrites the whole path that is set in the HttpExchange annotation. What I want is to provide only the base URL as a parameter but that the path in the annotation gets added to the base URL.
For example,
If i do
@GetExchange(value = "/animals/cats/", accept = APPLICATION_JSON_VALUE)
List<Cat> getCats(URI baseUrl)
and provide https://some.cats.url.com as value for the baseUrl parameter
it will send a request to the URL https://some.url.com but actually what I want to do is to call https://some.url.com/animals/cats.
Thanks!
Comment From: OlgaMaciaszek
Hi @mprevisic this, in fact, works in Feign, however it would be a breaking change for us and it might not be a feature that is very commonly used. Seeing that you can already override the entire URI, most scenarios should be handled. For your specific-use case, if there are not too many different base urls you use, you might want to create various clients using the same interface, but with WebClient
s, each with a specific baseUrl
set. Alternatively, you might also handle this use-case by passing the url as a request attribute and creating a client filter/interceptor to further handle it.
Comment From: floriandreher
Hi @OlgaMaciaszek thanks for your reply. I understand, that this would be a breaking change and maybe not that any people would need a feature like that, but maybe a few people will need it :) and it would be maybe encourage people to migrate from feign to http interfaces, if clients behave the same. Maybe i'm missing something, but i can't see a usage for the current implementation. Why would someone define a path and do not want to use it?
If you don't like the idea of a breaking change, would it be possible, that a path variable like @PathVariable URI baseUrl
would be encoded?
So that a user could do something like:
@GetExchange(value = "{baseUrl}/animals/cats/", accept = APPLICATION_JSON_VALUE)
List<Cat> getCats(@PathVariable URI baseUrl)
At the moment this would call something like http%3A%2F%2F/animals/cats/
Comment From: rstoyanchev
Maybe i'm missing something, but i can't see a usage for the current implementation.
That's not a good enough reason to break compatibility. It would be a regression to someone else whose use case is different from yours.
This is not to say that we don't want to make your use case easy. But given where we are today, we'll need to imagine a way in the programming model to make the intent clear.
I'm afraid the suggestion for @PathVariable
won't work as it's meant to apply to the path.
Note also that in our clients (HTTP and WebSocket), you can usually provide a URI template that we expand that into a URI
or you provide an already prepared URI
and we use it as is. That's worth keeping in mind as something many are accustomed to from using Spring HTTP clients.
I'm now wondering about supporting this through a UriComponentsBuilder
argument. While URI
implies a fully prepared and encoded URL, a UriComponentsBuilder
implies a partially prepared URL to which we can add the path from the annotation and then build it.
Comment From: rstoyanchev
Better yet, we can support UriBuilderFactory
as an argument, similar to what we support on RestTemplate
, and now also on the new RestClient
. You would create a DefaultUriBuilderFactory
with the base URL (scheme, host, port, or even base path if you want). Pass that into the HTTP service interface method as UriBuilderFactory
, and internally we call UriBuilderFactory#expand
with the URI template from the annotation to build the complete URL.
Comment From: floriandreher
Thanks for your reply @rstoyanchev
That's not a good enough reason to break compatibility. It would be a regression to someone else whose use case is different from yours.
Such a break is not nice. However, i think, that most people will migrate from feign to http-interfaces and so they are used to feign behavior.
But your suggestion with the UriBuilderFactory
would be fine for me.
Can we set the path then on the interface like:
@GetExchange(value = "/animals/{type}", accept = APPLICATION_JSON_VALUE)
List<Animal> getAnimals(UriBuilderFactory factory, @PathVariable(value = "type") String type)
var factory = new DefaultUriBuilderFactory(UriComponentsBuilder.fromUri(URI.create("http://example.com")));
getAnimals(factory, "cat");
or do we have to do something like:
@GetExchange(accept = APPLICATION_JSON_VALUE)
List<Animal> getAnimals(UriBuilderFactory factory, @PathVariable(value = "type") String type)
var factory = new DefaultUriBuilderFactory(UriComponentsBuilder.fromUriString("http://example.com/animals/{type}")));
getAnimals(factory, "cat");
Comment From: rstoyanchev
You would be able to do:
@GetExchange(value = "/animals/{type}", accept = APPLICATION_JSON_VALUE)
List<Animal> getAnimals(UriBuilderFactory factory, @PathVariable(value = "type") String type)
var uriBuilderfactory = new DefaultUriBuilderFactory("http://example.com");
getAnimals(uriBuilderfactory, "cat");
Internally we would call:
var uriTemplate = annotation.url();
var uri = uriBuilderFactory.expand(uriTemplate);
which would expand the URI template from the annotation with the base URL in the UriBuilderFactory
. In effect, replacing the internal UriBuilderFactory
with the base URL that each WebClient
and RestTemplate
can be configured with.
Comment From: rstoyanchev
Superseded by #31413.
Comment From: yuexueyang
Althougth use UriBuilderFactory as a parameter can solve the issue to determine the base url at runtime, but it will need some extra unnessesary code in business logic, in feign, this work can be done on a request interceptor, by setting the target. Dose this product can support this feature?
Besides, as I see from document, every declared interface need to be created at a configuration class one by one, but in OpenFeign there's no need to do so. Dose this product can do the same thing as OpenFeign
Comment From: OlgaMaciaszek
@yuexueyang, we provide this method of doing it here. It is rather straightforward. You could also opt for getting your base url for the underlying client from properties.
Regarding auto-configuration, I'm working on it now. Track progress under https://github.com/spring-projects/spring-boot/issues/31337.
Comment From: rezamirali-leanix
@rstoyanchev or @OlgaMaciaszek could you please provide a more detailed example? In the sample code you have writing calling a method inside the interface which has the GetExchange which is not possible theoretically.
Comment From: bclozel
@rezamirali-leanix here is a complete example:
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.util.UriBuilderFactory;
public interface ApiServiceClient {
@GetExchange("/{method}")
String makeApiRequest(UriBuilderFactory uriBuilderFactory, @PathVariable String method);
}
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriBuilderFactory;
@SpringBootApplication
public class IntclientApplication {
public static void main(String[] args) {
SpringApplication.run(IntclientApplication.class, args);
}
@Bean
public ApplicationRunner runCommand(RestClient.Builder builder) {
return new ApplicationRunner() {
@Override
public void run(ApplicationArguments args) throws Exception {
RestClient restClient = builder.build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
ApiServiceClient service = factory.createClient(ApiServiceClient.class);
var uriBuilderfactory = new DefaultUriBuilderFactory("http://httpbin.org/");
String result = service.makeApiRequest(uriBuilderfactory, "get");
System.out.println(result);
}
};
}
}