Add @HttpRequest and HttpServiceProxyFactory

See gh-28386
This commit is contained in:
rstoyanchev
2022-04-20 15:51:33 +00:00
parent 5378572b00
commit c418768f05
16 changed files with 1660 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2002-2022 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.web.service.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
*
* @author Rossen Stoyanchev
* @since 6.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@HttpRequest(method = "GET")
public @interface GetRequest {
/**
* Alias for {@link HttpRequest#value}.
*/
@AliasFor(annotation = HttpRequest.class)
String value() default "";
/**
* Alias for {@link HttpRequest#url()}.
*/
@AliasFor(annotation = HttpRequest.class)
String url() default "";
/**
* Alias for {@link HttpRequest#accept()}.
*/
@AliasFor(annotation = HttpRequest.class)
String[] accept() default {};
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2002-2022 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.web.service.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.Mapping;
/**
* Supported method parameters:
* <ul>
* <li>{@link java.net.URI} -- dynamic URL
* <li>{@link org.springframework.http.HttpMethod} - dynamic HTTP method
* <li>{@link org.springframework.http.HttpHeaders} - request headers
* <li>{@link org.springframework.http.HttpCookie} - request headers
* <li>...
* </ul>
*
* @author Rossen Stoyanchev
* @since 6.0
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface HttpRequest {
/**
* This is an alias for {@link #url}.
*/
@AliasFor("url")
String value() default "";
/**
* The URL for the request, either a full URL or a path only that is relative
* to a URL declared in a type-level {@code @HttpRequest}, and/or a globally
* configured base URL.
* <p>By default, this is empty.
*/
@AliasFor("value")
String url() default "";
/**
* The HTTP method to use.
* <p>Supported at the type level as well as at the method level.
* When used at the type level, all method-level mappings inherit this value.
* <p>By default, this is empty.
*/
String method() default "";
/**
* The media type for the {@code "Content-Type"} header.
* <p>Supported at the type level as well as at the method level, in which
* case the method-level values override type-level values.
* <p>By default, this is empty.
*/
String contentType() default "";
/**
* The media types for the {@code "Accept"} header.
* <p>Supported at the type level as well as at the method level, in which
* case the method-level values override type-level values.
* <p>By default, this is empty.
*/
String[] accept() default {};
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2002-2022 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.web.service.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
*
* @author Rossen Stoyanchev
* @since 6.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@HttpRequest(method = "POST")
public @interface PostRequest {
/**
* Alias for {@link HttpRequest#value}.
*/
@AliasFor(annotation = HttpRequest.class)
String value() default "";
/**
* Alias for {@link HttpRequest#url()}.
*/
@AliasFor(annotation = HttpRequest.class)
String url() default "";
/**
* Alias for {@link HttpRequest#contentType()}.
*/
@AliasFor(annotation = HttpRequest.class)
String contentType() default "";
/**
* Alias for {@link HttpRequest#accept()}.
*/
@AliasFor(annotation = HttpRequest.class)
String[] accept() default {};
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2002-2022 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.web.service.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
*
* @author Rossen Stoyanchev
* @since 6.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@HttpRequest(method = "PUT")
public @interface PutRequest {
/**
* Alias for {@link HttpRequest#value}.
*/
@AliasFor(annotation = HttpRequest.class)
String[] value() default {};
/**
* Alias for {@link HttpRequest#url()}.
*/
@AliasFor(annotation = HttpRequest.class)
String[] url() default {};
/**
* Alias for {@link HttpRequest#contentType()}.
*/
@AliasFor(annotation = HttpRequest.class)
String contentType() default "";
}

View File

@@ -0,0 +1,9 @@
/**
* Annotations to declare HTTP service, request methods.
*/
@NonNullApi
@NonNullFields
package org.springframework.web.service.annotation;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2002-2022 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.web.service.invoker;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
/**
* Decouple an {@link HttpServiceProxyFactory#createService(Class) HTTP Service proxy}
* from the underlying HTTP client.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public interface HttpClientAdapter {
Mono<Void> requestToVoid(HttpRequestDefinition requestDefinition);
Mono<HttpHeaders> requestToHeaders(HttpRequestDefinition requestDefinition);
<T> Mono<T> requestToBody(HttpRequestDefinition requestDefinition, ParameterizedTypeReference<T> bodyType);
<T> Flux<T> requestToBodyFlux(HttpRequestDefinition requestDefinition, ParameterizedTypeReference<T> bodyType);
Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestDefinition requestDefinition);
<T> Mono<ResponseEntity<T>> requestToEntity(HttpRequestDefinition requestDefinition, ParameterizedTypeReference<T> bodyType);
<T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(HttpRequestDefinition requestDefinition, ParameterizedTypeReference<T> bodyType);
}

View File

@@ -0,0 +1,192 @@
/*
* Copyright 2002-2022 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.web.service.invoker;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.reactivestreams.Publisher;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Container for HTTP request values accumulated from an
* {@link HttpRequest @HttpRequest}-annotated method and arguments passed to it.
* This allows an {@link HttpClientAdapter} adapt these inputs as it sees fit
* to the API of the underlying client.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public class HttpRequestDefinition {
private static final MultiValueMap<String, String> EMPTY_COOKIES_MAP =
CollectionUtils.toMultiValueMap(Collections.emptyMap());
@Nullable
private URI uri;
@Nullable
private String uriTemplate;
@Nullable
private Map<String, String> uriVariables;
@Nullable
private List<String> uriVariablesList;
@Nullable
private HttpMethod httpMethod;
@Nullable
private HttpHeaders headers;
@Nullable
private MultiValueMap<String, String> cookies;
@Nullable
private Object bodyValue;
@Nullable
private Publisher<?> bodyPublisher;
@Nullable
private ParameterizedTypeReference<?> bodyPublisherElementType;
private boolean complete;
public void setUri(URI uri) {
checkComplete();
this.uri = uri;
}
@Nullable
public URI getUri() {
return this.uri;
}
public void setUriTemplate(String uriTemplate) {
checkComplete();
this.uriTemplate = uriTemplate;
}
@Nullable
public String getUriTemplate() {
return this.uriTemplate;
}
public Map<String, String> getUriVariables() {
this.uriVariables = (this.uriVariables != null ? this.uriVariables : new LinkedHashMap<>());
return this.uriVariables;
}
public List<String> getUriVariableValues() {
this.uriVariablesList = (this.uriVariablesList != null ? this.uriVariablesList : new ArrayList<>());
return this.uriVariablesList;
}
public void setHttpMethod(HttpMethod httpMethod) {
checkComplete();
this.httpMethod = httpMethod;
}
@Nullable
public HttpMethod getHttpMethod() {
return this.httpMethod;
}
public HttpMethod getHttpMethodRequired() {
Assert.notNull(this.httpMethod, "No HttpMethod");
return this.httpMethod;
}
public HttpHeaders getHeaders() {
this.headers = (this.headers != null ? this.headers : new HttpHeaders());
return this.headers;
}
public MultiValueMap<String, String> getCookies() {
this.cookies = (this.cookies != null ? this.cookies : new LinkedMultiValueMap<>());
return this.cookies;
}
public void setBodyValue(Object bodyValue) {
checkComplete();
this.bodyValue = bodyValue;
}
@Nullable
public Object getBodyValue() {
return this.bodyValue;
}
public <T, P extends Publisher<T>> void setBodyPublisher(Publisher<P> bodyPublisher, MethodParameter parameter) {
checkComplete();
// Adapt to Mono/Flux and nest MethodParameter for element type
this.bodyPublisher = bodyPublisher;
this.bodyPublisherElementType = ParameterizedTypeReference.forType(parameter.nested().getGenericParameterType());
}
@Nullable
public Publisher<?> getBodyPublisher() {
return this.bodyPublisher;
}
public ParameterizedTypeReference<?> getBodyPublisherElementType() {
Assert.state(this.bodyPublisherElementType != null, "No body Publisher");
return this.bodyPublisherElementType;
}
private void checkComplete() {
Assert.isTrue(!this.complete, "setComplete already called");
}
void setComplete() {
this.uriVariables = (this.uriVariables != null ?
Collections.unmodifiableMap(this.uriVariables) : Collections.emptyMap());
this.uriVariablesList = (this.uriVariablesList != null ?
Collections.unmodifiableList(this.uriVariablesList) : Collections.emptyList());
this.headers = (this.headers != null ?
HttpHeaders.readOnlyHttpHeaders(this.headers) : HttpHeaders.EMPTY);
this.cookies = (this.cookies != null ?
CollectionUtils.unmodifiableMultiValueMap(this.cookies) : EMPTY_COOKIES_MAP);
}
}

View File

@@ -0,0 +1,361 @@
/*
* Copyright 2002-2022 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.web.service.invoker;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.service.annotation.HttpRequest;
/**
* Implements the invocation of an {@link HttpRequest @HttpRequest} annotated,
* {@link HttpServiceProxyFactory#createService(Class) HTTP Service proxy} method
* by delegating to an {@link HttpClientAdapter} to perform actual requests.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
final class HttpServiceMethod {
private final Method method;
private final MethodParameter[] parameters;
private final List<HttpServiceMethodArgumentResolver> argumentResolvers;
private final HttpRequestDefinitionFactory requestDefinitionFactory;
private final ResponseFunction responseFunction;
HttpServiceMethod(
Method method, Class<?> containingClass, List<HttpServiceMethodArgumentResolver> argumentResolvers,
HttpClientAdapter client, ReactiveAdapterRegistry reactiveRegistry,
Duration blockTimeout) {
this.method = method;
this.parameters = initMethodParameters(method);
this.argumentResolvers = argumentResolvers;
this.requestDefinitionFactory = HttpRequestDefinitionFactory.create(method, containingClass);
this.responseFunction = ResponseFunction.create(client, method, reactiveRegistry, blockTimeout);
}
private static MethodParameter[] initMethodParameters(Method method) {
int count = method.getParameterCount();
MethodParameter[] parameters = new MethodParameter[count];
for (int i = 0; i < count; i++) {
parameters[i] = new MethodParameter(method, i);
}
return parameters;
}
public Method getMethod() {
return this.method;
}
@Nullable
public Object invoke(Object[] arguments) {
HttpRequestDefinition requestDefinition = this.requestDefinitionFactory.initializeRequest();
applyArguments(requestDefinition, arguments);
requestDefinition.setComplete();
return this.responseFunction.execute(requestDefinition);
}
private void applyArguments(HttpRequestDefinition requestDefinition, Object[] arguments) {
Assert.isTrue(arguments.length == this.parameters.length, "Method argument mismatch");
for (int i = 0; i < this.parameters.length; i++) {
Object argumentValue = arguments[i];
for (HttpServiceMethodArgumentResolver resolver : this.argumentResolvers) {
resolver.resolve(argumentValue, this.parameters[i], requestDefinition);
}
}
}
/**
* Factory for an {@link HttpRequestDefinition} with values extracted from
* the type and method-level {@link HttpRequest @HttpRequest} annotations.
*/
private record HttpRequestDefinitionFactory(
@Nullable HttpMethod httpMethod, @Nullable String url,
@Nullable MediaType contentType, @Nullable List<MediaType> acceptMediaTypes) {
private HttpRequestDefinitionFactory(
@Nullable HttpMethod httpMethod, @Nullable String url,
@Nullable MediaType contentType, @Nullable List<MediaType> acceptMediaTypes) {
this.url = url;
this.httpMethod = httpMethod;
this.contentType = contentType;
this.acceptMediaTypes = acceptMediaTypes;
}
public HttpRequestDefinition initializeRequest() {
HttpRequestDefinition requestDefinition = new HttpRequestDefinition();
if (this.httpMethod != null) {
requestDefinition.setHttpMethod(this.httpMethod);
}
if (this.url != null) {
requestDefinition.setUriTemplate(this.url);
}
if (this.contentType != null) {
requestDefinition.getHeaders().setContentType(this.contentType);
}
if (this.acceptMediaTypes != null) {
requestDefinition.getHeaders().setAccept(this.acceptMediaTypes);
}
return requestDefinition;
}
/**
* Introspect the method and create the request factory for it.
*/
public static HttpRequestDefinitionFactory create(Method method, Class<?> containingClass) {
HttpRequest annot1 = AnnotatedElementUtils.findMergedAnnotation(containingClass, HttpRequest.class);
HttpRequest annot2 = AnnotatedElementUtils.findMergedAnnotation(method, HttpRequest.class);
Assert.notNull(annot2, "Expected HttpRequest annotation");
HttpMethod httpMethod = initHttpMethod(annot1, annot2);
String url = initUrl(annot1, annot2);
MediaType contentType = initContentType(annot1, annot2);
List<MediaType> acceptableMediaTypes = initAccept(annot1, annot2);
return new HttpRequestDefinitionFactory(httpMethod, url, contentType, acceptableMediaTypes);
}
@Nullable
private static HttpMethod initHttpMethod(@Nullable HttpRequest typeAnnot, HttpRequest annot) {
String value1 = (typeAnnot != null ? typeAnnot.method() : null);
String value2 = annot.method();
if (StringUtils.hasText(value2)) {
return HttpMethod.valueOf(value2);
}
if (StringUtils.hasText(value1)) {
return HttpMethod.valueOf(value1);
}
return null;
}
@Nullable
private static String initUrl(@Nullable HttpRequest typeAnnot, HttpRequest annot) {
String url1 = (typeAnnot != null ? typeAnnot.url() : null);
String url2 = annot.url();
boolean hasUrl1 = StringUtils.hasText(url1);
boolean hasUrl2 = StringUtils.hasText(url2);
if (hasUrl1 && hasUrl2) {
return (url1 + (!url1.endsWith("/") && !url2.startsWith("/") ? "/" : "") + url2);
}
if (!hasUrl1 && !hasUrl2) {
return null;
}
return (hasUrl2 ? url2 : url1);
}
@Nullable
private static MediaType initContentType(@Nullable HttpRequest typeAnnot, HttpRequest annot) {
String value1 = (typeAnnot != null ? typeAnnot.contentType() : null);
String value2 = annot.contentType();
if (StringUtils.hasText(value2)) {
return MediaType.parseMediaType(value2);
}
if (StringUtils.hasText(value1)) {
return MediaType.parseMediaType(value1);
}
return null;
}
@Nullable
private static List<MediaType> initAccept(@Nullable HttpRequest typeAnnot, HttpRequest annot) {
String[] value1 = (typeAnnot != null ? typeAnnot.accept() : null);
String[] value2 = annot.accept();
if (!ObjectUtils.isEmpty(value2)) {
return MediaType.parseMediaTypes(Arrays.asList(value2));
}
if (!ObjectUtils.isEmpty(value1)) {
return MediaType.parseMediaTypes(Arrays.asList(value1));
}
return null;
}
}
/**
* Function to execute a request, obtain a response, and adapt to the expected
* return type blocking if necessary.
*/
private record ResponseFunction(
Function<HttpRequestDefinition, Publisher<?>> responseFunction,
@Nullable ReactiveAdapter returnTypeAdapter,
boolean blockForOptional, Duration blockTimeout) {
private ResponseFunction(
Function<HttpRequestDefinition, Publisher<?>> responseFunction,
@Nullable ReactiveAdapter returnTypeAdapter,
boolean blockForOptional, Duration blockTimeout) {
this.responseFunction = responseFunction;
this.returnTypeAdapter = returnTypeAdapter;
this.blockForOptional = blockForOptional;
this.blockTimeout = blockTimeout;
}
@Nullable
public Object execute(HttpRequestDefinition requestDefinition) {
Publisher<?> responsePublisher = this.responseFunction.apply(requestDefinition);
if (this.returnTypeAdapter != null) {
return this.returnTypeAdapter.fromPublisher(responsePublisher);
}
return (this.blockForOptional ?
((Mono<?>) responsePublisher).blockOptional(this.blockTimeout) :
((Mono<?>) responsePublisher).block(this.blockTimeout));
}
/**
* Create the {@code ResponseFunction} that matches method return type.
*/
public static ResponseFunction create(
HttpClientAdapter client, Method method, ReactiveAdapterRegistry reactiveRegistry,
Duration blockTimeout) {
MethodParameter returnParam = new MethodParameter(method, -1);
Class<?> returnType = returnParam.getParameterType();
ReactiveAdapter reactiveAdapter = reactiveRegistry.getAdapter(returnType);
MethodParameter actualParam = (reactiveAdapter != null ? returnParam.nested() : returnParam.nestedIfOptional());
Class<?> actualType = actualParam.getNestedParameterType();
Function<HttpRequestDefinition, Publisher<?>> responseFunction;
if (actualType.equals(void.class) || actualType.equals(Void.class)) {
responseFunction = client::requestToVoid;
}
else if (reactiveAdapter != null && reactiveAdapter.isNoValue()) {
responseFunction = client::requestToVoid;
}
else if (actualType.equals(HttpHeaders.class)) {
responseFunction = client::requestToHeaders;
}
else if (actualType.equals(ResponseEntity.class)) {
MethodParameter bodyParam = actualParam.nested();
Class<?> bodyType = bodyParam.getNestedParameterType();
if (bodyType.equals(Void.class)) {
responseFunction = client::requestToBodilessEntity;
}
else {
ReactiveAdapter bodyAdapter = reactiveRegistry.getAdapter(bodyType);
responseFunction = initResponseEntityFunction(client, bodyParam, bodyAdapter);
}
}
else {
responseFunction = initBodyFunction(client, actualParam, reactiveAdapter);
}
boolean blockForOptional = actualType.equals(Optional.class);
return new ResponseFunction(responseFunction, reactiveAdapter, blockForOptional, blockTimeout);
}
@SuppressWarnings("ConstantConditions")
private static Function<HttpRequestDefinition, Publisher<?>> initResponseEntityFunction(
HttpClientAdapter client, MethodParameter methodParam, @Nullable ReactiveAdapter reactiveAdapter) {
if (reactiveAdapter == null) {
return request -> client.requestToEntity(
request, ParameterizedTypeReference.forType(methodParam.getNestedGenericParameterType()));
}
Assert.isTrue(reactiveAdapter.isMultiValue(),
"ResponseEntity body must be a concrete value or a multi-value Publisher");
ParameterizedTypeReference<?> bodyType =
ParameterizedTypeReference.forType(methodParam.nested().getNestedGenericParameterType());
// Shortcut for Flux
if (reactiveAdapter.getReactiveType().equals(Flux.class)) {
return request -> client.requestToEntityFlux(request, bodyType);
}
return request -> client.requestToEntityFlux(request, bodyType)
.map(entity -> {
Object body = reactiveAdapter.fromPublisher(entity.getBody());
return new ResponseEntity<>(body, entity.getHeaders(), entity.getStatusCode());
});
}
private static Function<HttpRequestDefinition, Publisher<?>> initBodyFunction(
HttpClientAdapter client, MethodParameter methodParam, @Nullable ReactiveAdapter reactiveAdapter) {
ParameterizedTypeReference<?> bodyType =
ParameterizedTypeReference.forType(methodParam.getNestedGenericParameterType());
return (reactiveAdapter != null && reactiveAdapter.isMultiValue() ?
request -> client.requestToBodyFlux(request, bodyType) :
request -> client.requestToBody(request, bodyType));
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2002-2022 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.web.service.invoker;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.web.service.annotation.HttpRequest;
/**
* Resolve an argument from an {@link HttpRequest @HttpRequest} annotated method
* to one or more HTTP request values.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public interface HttpServiceMethodArgumentResolver {
/**
* Resolve the argument value.
* @param argument the argument value
* @param parameter the method parameter for the argument
* @param requestDefinition container to add HTTP request values to
*/
void resolve(@Nullable Object argument, MethodParameter parameter, HttpRequestDefinition requestDefinition);
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2002-2022 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.web.service.invoker;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.web.service.annotation.HttpRequest;
/**
* Factory to create a proxy for an HTTP service with {@link HttpRequest} methods.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public class HttpServiceProxyFactory {
private final List<HttpServiceMethodArgumentResolver> argumentResolvers;
private final HttpClientAdapter clientAdapter;
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
private final Duration blockTimeout;
public HttpServiceProxyFactory(
List<HttpServiceMethodArgumentResolver> argumentResolvers, HttpClientAdapter clientAdapter,
ReactiveAdapterRegistry reactiveAdapterRegistry, Duration blockTimeout) {
this.argumentResolvers = argumentResolvers;
this.clientAdapter = clientAdapter;
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
this.blockTimeout = blockTimeout;
}
/**
* Create a proxy for executing requests to the given HTTP service.
* @param serviceType the HTTP service to create a proxy for
* @param <S> the service type
* @return the created proxy
*/
public <S> S createService(Class<S> serviceType) {
List<HttpServiceMethod> methods =
MethodIntrospector.selectMethods(serviceType, this::isHttpRequestMethod)
.stream()
.map(method -> initServiceMethod(method, serviceType))
.toList();
return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(methods));
}
private boolean isHttpRequestMethod(Method method) {
return AnnotatedElementUtils.hasAnnotation(method, HttpRequest.class);
}
private HttpServiceMethod initServiceMethod(Method method, Class<?> serviceType) {
return new HttpServiceMethod(
method, serviceType, this.argumentResolvers,
this.clientAdapter, this.reactiveAdapterRegistry, this.blockTimeout);
}
/**
*
*/
private static final class HttpServiceMethodInterceptor implements MethodInterceptor {
private final Map<Method, HttpServiceMethod> serviceMethodMap = new HashMap<>();
private HttpServiceMethodInterceptor(List<HttpServiceMethod> methods) {
methods.forEach(serviceMethod -> this.serviceMethodMap.put(serviceMethod.getMethod(), serviceMethod));
}
@Nullable
@Override
public Object invoke(@NotNull MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
HttpServiceMethod httpServiceMethod = this.serviceMethodMap.get(method);
return httpServiceMethod.invoke(invocation.getArguments());
}
}
}

View File

@@ -0,0 +1,9 @@
/**
*
*/
@NonNullApi
@NonNullFields
package org.springframework.web.service.invoker;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -0,0 +1,10 @@
/**
* Annotations to declare an HTTP service contract with request methods along
* with a proxy factory backed by client-driven implementation.
*/
@NonNullApi
@NonNullFields
package org.springframework.web.service;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;