When an API requires multiple values, the number of arguments for HTTP service method can be a bit painful. For example:
interface ExampleService {
@PostMapping(path = "/example", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
void example(@RequestBody Object example, @RequestHeader("X-Custom-Requirement") String foo,
@RequestHeader("X-Another-Custom-Requirement") String bar,
@RequestParam String one,
@RequestParam String two,
@RequestParam String three);
}
The service method can be simplified to:
@PostMapping(path = "/example", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
void example(@RequestBody Object example, @RequestHeader HttpHeaders httpHeaders, @RequestParam MultiValueMap<String, Object> params);
Which is better, but I think it would be easier or nicer even to allow a single argument that encapsulates all of the request details.
That argument could also override the predefined ones such as the consumes
or path
from the exchange annotation. This is more or less what UriBuilderFactory
does today for the URL.
If I'm not mistaken, RestTemplate
provided something similar through RequestEntity
.
Note I know that you can drop down to the 'lower level' RestClient
to fully customize the request as indicated in this table in the documentation. But providing that control at the HTTP interface level I think will be a welcome addition.
Looking at Framework's code, HttpServiceArgumentResolver
is responsible for consuming the various annotations and applying those values to HttpRequestValues
. So a HttpServiceArgumentResolver
that accepts a HttpRequestValues
or similar may work I think.
My company has variety of modern and legacy APIs and Web services. While we try to conform to some standard when modernizing legacy services or APIs, there still exists APIs that require a lot of information in their request.
To work around this, I started wrapping the request details into an intermediary object and then delegate to the service method. This leads to cleaner code (IMO). For example:
record RequestSpecification(Object body, String param, String header) {}
interface ExampleService {
@PostMapping(path = "/example", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
void example(@RequestBody Object example, @RequestHeader HttpHeaders httpHeaders, @RequestParam MultiValueMap<String, Object> params);
default void example(RequestSpecification specification) {
HttpHeaders headers = new HttpHeaders();
headers.add("X-Custom-Requirement", specification.header());
MultiValueMap<String, Object> params = MultiValueMap.fromSingleValue(Map.of("one", specification.param()));
example(specification.body(), headers, params);
}
}
Comment From: rstoyanchev
HttpRequestValues
is mainly for use by arguments resolvers to have a place to add values to. It's not intended for direct use by applications. RequestEntity
is much more along the lines of a public API for generalized request input, but then there is not much value in passing that to an HTTP interface. You could just as well pass it directly to RestClient
.
If you want something in between like a dedicated RequestSpecification
type that both indicates the necessary input specific to the service and is more concise, then could create a custom argument resolver for it, and use it to add the necessary values to HttpRequestValues
. You can register such custom resolvers on the HttpServiceProxyFactory
builder.
Comment From: ciscoo
HttpRequestValues
is mainly for use by arguments resolvers to have a place to add values to. It's not intended for direct use by applications.
Yes, I did not find anything else used for HTTP interfaces.
RequestEntity
is much more along the lines of a public API for generalized request input, but then there is not much value in passing that to an HTTP interface. You could just as well pass it directly toRestClient
.
But it does not work that way though, at least from what I can see. There does not exist any methods on RestClient
where you can just provide a RequestEntity
and have it extract all values from it internally. Instead, you must use the DSL provided by RestClient
.
If you want something in between like a dedicated
RequestSpecification
type that both indicates the necessary input specific to the service and is more concise, then could create a custom argument resolver for it, and use it to add the necessary values to HttpRequestValues. You can register such custom resolvers on the HttpServiceProxyFactory builder.
This is what this issue is requesting from Spring Framework. To provide the ability to provide an object (RequestEntity
for example), and extract all values from it.
Comment From: rstoyanchev
Sorry, my bad. It doesn't make sense to pass RequestEntity into RestClient as the two provide a similar API for building the request. RequestEntity is useful to provide a similar experience with the RestTemplate.
We could support RequestEntity as an argument, but the main value of an HTTP interface is to indicate the inputs required for the endpoint. If you generalize that how would does user has to know what to pass? If you generalize it all the way into RequestEntity then what value do you get for an HTTP interface?
Comment From: ciscoo
but the main value of an HTTP interface is to indicate the inputs required for the endpoint
I understand and agree with that.
However, I think this falls apart by allowing/accepting a generic Map
:
https://github.com/spring-projects/spring-framework/blob/d5da602bc29c3a61bb0b3203414a21447ed0cbe5/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractNamedValueArgumentResolver.java#L86
That does not really indicate the required input since a map is just that, a map with no meaning, but I digress.
What I'm trying to get at here, how does one express the required inputs without having excessively long method arguments?
Here's a concrete example: https://developer.usps.com/addressesv3
interface AddressesService {
@GetMapping(path = "/addresses/v3/address", consumes = MediaType.APPLICATION_JSON_VALUE)
Address standardizeAddress(@RequestParam String firm,
@RequestParam String streetAddress,
@RequestParam String secondaryAddress,
@RequestParam String city,
@RequestParam String state,
@RequestParam String urbanization,
@RequestParam String ZIPCode,
@RequestParam String ZIPPlus4);
}
That's just not nice to look at which led me to creating wrapper objects that I mentioned originally. This then led me to thinking that maybe Spring Framework could provide support for HttpRequestValues
without introducing another type or concept in Framework which then could lead to:
interface AddressesService {
@GetMapping(path = "/addresses/v3/address", consumes = MediaType.APPLICATION_JSON_VALUE)
Address standardizeAddress(HttpRequestValues values);
}
record AddressRequest(...) {
public HttpRequestValues toHttpRequestValues() {
return HttpRequestValues.builder()
.addRequestParameter(...)
.build();
}
}
AddressesService addressService = ....
addressService.standardizeAddress(new AddressRequest(...).toHttpRequestValues())
This just avoids what I originally had above where a default
method just delegated to the main exchange method.
Given all that though, I think the best approach is what was said initially to create a custom argument resolver to add values to HttpRequestValues
; feel free to close this issue.
However, consider repurpsing this issue or a new one to at minimum document that HttpServiceArgumentResolver
can be used for this use case or others that don't fit well with the current setup. The current documentation does not mention HttpServiceArgumentResolver
as far as I can tell.
Comment From: bclozel
However, I think this falls apart by allowing/accepting a generic Map
We're supporting map as a way to easily multiple values for @RequestParam
, @RequestHeader
or @PathVariable
. In this case the API semantics are still present for the developer and they can't just ship any HTTP request as a result.
What I'm trying to get at here, how does one express the required inputs without having excessively long method arguments?
I agree, this AddressService
API is not very elegant, but it's a direct consequence of the design of the REST API. I'm not in favor of supporting HttpRequestValues
for the reasons listed above.
However, consider repurposing this issue or a new one to at minimum document that
HttpServiceArgumentResolver
can be used for this use case or others that don't fit well with the current setup.
I think this is a sane approach, especially if you can provide to API users a sensible type (with a builder?). You can then model a complex HTTP request and still design an HTTP interface that has clear semantics. I'm repurposing this issue.