Add @HttpRequest and HttpServiceProxyFactory
See gh-28386
This commit is contained in:
parent
5378572b00
commit
c418768f05
|
@ -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 {};
|
||||
|
||||
}
|
|
@ -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 {};
|
||||
|
||||
|
||||
}
|
|
@ -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 {};
|
||||
|
||||
}
|
|
@ -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 "";
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.web.service.invoker;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
|
@ -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;
|
|
@ -0,0 +1,378 @@
|
|||
/*
|
||||
* 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.time.Duration;
|
||||
import java.util.Collections;
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.http.HttpEntity;
|
||||
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.web.service.annotation.GetRequest;
|
||||
import org.springframework.web.service.annotation.HttpRequest;
|
||||
import org.springframework.web.service.annotation.PostRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.http.MediaType.APPLICATION_CBOR_VALUE;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpServiceMethod} with a test {@link TestHttpClientAdapter}
|
||||
* that stubs the client invocations.
|
||||
*
|
||||
* <p>The tests do not create nor invoke {@code HttpServiceMethod} directly but
|
||||
* rather use {@link HttpServiceProxyFactory} to create a service proxy in order to
|
||||
* use a strongly typed interface without the need for class casts.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class HttpServiceMethodTests {
|
||||
|
||||
private static final ParameterizedTypeReference<String> BODY_TYPE = new ParameterizedTypeReference<>() {};
|
||||
|
||||
|
||||
private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter();
|
||||
|
||||
|
||||
@Test
|
||||
void reactorService() {
|
||||
|
||||
ReactorService service = createService(ReactorService.class);
|
||||
|
||||
Mono<Void> voidMono = service.execute();
|
||||
StepVerifier.create(voidMono).verifyComplete();
|
||||
verifyClientInvocation("requestToVoid", null);
|
||||
|
||||
Mono<HttpHeaders> headersMono = service.getHeaders();
|
||||
StepVerifier.create(headersMono).expectNextCount(1).verifyComplete();
|
||||
verifyClientInvocation("requestToHeaders", null);
|
||||
|
||||
Mono<String> body = service.getBody();
|
||||
StepVerifier.create(body).expectNext("requestToBody").verifyComplete();
|
||||
verifyClientInvocation("requestToBody", BODY_TYPE);
|
||||
|
||||
Flux<String> fluxBody = service.getFluxBody();
|
||||
StepVerifier.create(fluxBody).expectNext("request", "To", "Body", "Flux").verifyComplete();
|
||||
verifyClientInvocation("requestToBodyFlux", BODY_TYPE);
|
||||
|
||||
Mono<ResponseEntity<Void>> voidEntity = service.getVoidEntity();
|
||||
StepVerifier.create(voidEntity).expectNext(ResponseEntity.ok().build()).verifyComplete();
|
||||
verifyClientInvocation("requestToBodilessEntity", null);
|
||||
|
||||
Mono<ResponseEntity<String>> entity = service.getEntity();
|
||||
StepVerifier.create(entity).expectNext(ResponseEntity.ok("requestToEntity"));
|
||||
verifyClientInvocation("requestToEntity", BODY_TYPE);
|
||||
|
||||
Mono<ResponseEntity<Flux<String>>> fluxEntity= service.getFluxEntity();
|
||||
StepVerifier.create(fluxEntity.flatMapMany(HttpEntity::getBody)).expectNext("request", "To", "Entity", "Flux").verifyComplete();
|
||||
verifyClientInvocation("requestToEntityFlux", BODY_TYPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rxJavaService() {
|
||||
|
||||
RxJavaService service = createService(RxJavaService.class);
|
||||
|
||||
Completable completable = service.execute();
|
||||
assertThat(completable).isNotNull();
|
||||
|
||||
Single<HttpHeaders> headersSingle = service.getHeaders();
|
||||
assertThat(headersSingle.blockingGet()).isNotNull();
|
||||
|
||||
Single<String> bodySingle = service.getBody();
|
||||
assertThat(bodySingle.blockingGet()).isEqualTo("requestToBody");
|
||||
|
||||
Flowable<String> bodyFlow = service.getFlowableBody();
|
||||
assertThat(bodyFlow.toList().blockingGet()).asList().containsExactly("request", "To", "Body", "Flux");
|
||||
|
||||
Single<ResponseEntity<Void>> voidEntity = service.getVoidEntity();
|
||||
assertThat(voidEntity.blockingGet().getBody()).isNull();
|
||||
|
||||
Single<ResponseEntity<String>> entitySingle = service.getEntity();
|
||||
assertThat(entitySingle.blockingGet().getBody()).isEqualTo("requestToEntity");
|
||||
|
||||
Single<ResponseEntity<Flowable<String>>> entityFlow = service.getFlowableEntity();
|
||||
Flowable<String> body = (entityFlow.blockingGet()).getBody();
|
||||
assertThat(body.toList().blockingGet()).containsExactly("request", "To", "Entity", "Flux");
|
||||
}
|
||||
|
||||
@Test
|
||||
void blockingService() {
|
||||
|
||||
BlockingService service = createService(BlockingService.class);
|
||||
|
||||
service.execute();
|
||||
|
||||
HttpHeaders headers = service.getHeaders();
|
||||
assertThat(headers).isNotNull();
|
||||
|
||||
String body = service.getBody();
|
||||
assertThat(body).isEqualTo("requestToBody");
|
||||
|
||||
ResponseEntity<String> entity = service.getEntity();
|
||||
assertThat(entity.getBody()).isEqualTo("requestToEntity");
|
||||
|
||||
ResponseEntity<Void> voidEntity = service.getVoidEntity();
|
||||
assertThat(voidEntity.getBody()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void methodAnnotatedService() {
|
||||
|
||||
MethodAnnotatedService service = createService(MethodAnnotatedService.class);
|
||||
|
||||
service.performGet();
|
||||
|
||||
HttpRequestDefinition request = this.clientAdapter.getRequestDefinition();
|
||||
assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.GET);
|
||||
assertThat(request.getUriTemplate()).isNull();
|
||||
assertThat(request.getHeaders().getContentType()).isNull();
|
||||
assertThat(request.getHeaders().getAccept()).isEmpty();
|
||||
|
||||
service.performPost();
|
||||
|
||||
request = this.clientAdapter.getRequestDefinition();
|
||||
assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(request.getUriTemplate()).isEqualTo("/url");
|
||||
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
assertThat(request.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeAndMethodAnnotatedService() {
|
||||
|
||||
MethodAnnotatedService service = createService(TypeAndMethodAnnotatedService.class);
|
||||
|
||||
service.performGet();
|
||||
|
||||
HttpRequestDefinition request = this.clientAdapter.getRequestDefinition();
|
||||
assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.GET);
|
||||
assertThat(request.getUriTemplate()).isEqualTo("/base");
|
||||
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_CBOR);
|
||||
assertThat(request.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_CBOR);
|
||||
|
||||
service.performPost();
|
||||
|
||||
request = this.clientAdapter.getRequestDefinition();
|
||||
assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(request.getUriTemplate()).isEqualTo("/base/url");
|
||||
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
assertThat(request.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
private <S> S createService(Class<S> serviceType) {
|
||||
|
||||
HttpServiceProxyFactory factory = new HttpServiceProxyFactory(
|
||||
Collections.emptyList(), this.clientAdapter, ReactiveAdapterRegistry.getSharedInstance(),
|
||||
Duration.ofSeconds(5));
|
||||
|
||||
return factory.createService(serviceType);
|
||||
}
|
||||
|
||||
private void verifyClientInvocation(String methodName, @Nullable ParameterizedTypeReference<?> expectedBodyType) {
|
||||
assertThat((this.clientAdapter.getMethodName())).isEqualTo(methodName);
|
||||
assertThat(this.clientAdapter.getBodyType()).isEqualTo(expectedBodyType);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private interface ReactorService {
|
||||
|
||||
@HttpRequest
|
||||
Mono<Void> execute();
|
||||
|
||||
@GetRequest
|
||||
Mono<HttpHeaders> getHeaders();
|
||||
|
||||
@GetRequest
|
||||
Mono<String> getBody();
|
||||
|
||||
@GetRequest
|
||||
Flux<String> getFluxBody();
|
||||
|
||||
@GetRequest
|
||||
Mono<ResponseEntity<Void>> getVoidEntity();
|
||||
|
||||
@GetRequest
|
||||
Mono<ResponseEntity<String>> getEntity();
|
||||
|
||||
@GetRequest
|
||||
Mono<ResponseEntity<Flux<String>>> getFluxEntity();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private interface RxJavaService {
|
||||
|
||||
@HttpRequest
|
||||
Completable execute();
|
||||
|
||||
@GetRequest
|
||||
Single<HttpHeaders> getHeaders();
|
||||
|
||||
@GetRequest
|
||||
Single<String> getBody();
|
||||
|
||||
@GetRequest
|
||||
Flowable<String> getFlowableBody();
|
||||
|
||||
@GetRequest
|
||||
Single<ResponseEntity<Void>> getVoidEntity();
|
||||
|
||||
@GetRequest
|
||||
Single<ResponseEntity<String>> getEntity();
|
||||
|
||||
@GetRequest
|
||||
Single<ResponseEntity<Flowable<String>>> getFlowableEntity();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private interface BlockingService {
|
||||
|
||||
@HttpRequest
|
||||
void execute();
|
||||
|
||||
@GetRequest
|
||||
HttpHeaders getHeaders();
|
||||
|
||||
@GetRequest
|
||||
String getBody();
|
||||
|
||||
@GetRequest
|
||||
ResponseEntity<Void> getVoidEntity();
|
||||
|
||||
@GetRequest
|
||||
ResponseEntity<String> getEntity();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private interface MethodAnnotatedService {
|
||||
|
||||
@GetRequest
|
||||
void performGet();
|
||||
|
||||
@PostRequest(url = "/url", contentType = APPLICATION_JSON_VALUE, accept = APPLICATION_JSON_VALUE)
|
||||
void performPost();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@HttpRequest(url = "/base", contentType = APPLICATION_CBOR_VALUE, accept = APPLICATION_CBOR_VALUE)
|
||||
private interface TypeAndMethodAnnotatedService extends MethodAnnotatedService {
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static class TestHttpClientAdapter implements HttpClientAdapter {
|
||||
|
||||
@Nullable
|
||||
private String methodName;
|
||||
|
||||
@Nullable
|
||||
private HttpRequestDefinition requestDefinition;
|
||||
|
||||
@Nullable
|
||||
private ParameterizedTypeReference<?> bodyType;
|
||||
|
||||
|
||||
public String getMethodName() {
|
||||
assertThat(this.methodName).isNotNull();
|
||||
return this.methodName;
|
||||
}
|
||||
|
||||
public HttpRequestDefinition getRequestDefinition() {
|
||||
assertThat(this.requestDefinition).isNotNull();
|
||||
return this.requestDefinition;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ParameterizedTypeReference<?> getBodyType() {
|
||||
return this.bodyType;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Void> requestToVoid(HttpRequestDefinition def) {
|
||||
saveInput("requestToVoid", def, null);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<HttpHeaders> requestToHeaders(HttpRequestDefinition def) {
|
||||
saveInput("requestToHeaders", def, null);
|
||||
return Mono.just(new HttpHeaders());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> requestToBody(HttpRequestDefinition def, ParameterizedTypeReference<T> bodyType) {
|
||||
saveInput("requestToBody", def, bodyType);
|
||||
return (Mono<T>) Mono.just(getMethodName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> requestToBodyFlux(HttpRequestDefinition def, ParameterizedTypeReference<T> bodyType) {
|
||||
saveInput("requestToBodyFlux", def, bodyType);
|
||||
return (Flux<T>) Flux.just("request", "To", "Body", "Flux");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestDefinition def) {
|
||||
saveInput("requestToBodilessEntity", def, null);
|
||||
return Mono.just(ResponseEntity.ok().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ResponseEntity<T>> requestToEntity(HttpRequestDefinition def, ParameterizedTypeReference<T> bodyType) {
|
||||
saveInput("requestToEntity", def, bodyType);
|
||||
return Mono.just((ResponseEntity<T>) ResponseEntity.ok("requestToEntity"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(HttpRequestDefinition def, ParameterizedTypeReference<T> bodyType) {
|
||||
saveInput("requestToEntityFlux", def, bodyType);
|
||||
return Mono.just(ResponseEntity.ok((Flux<T>) Flux.just("request", "To", "Entity", "Flux")));
|
||||
}
|
||||
|
||||
private <T> void saveInput(
|
||||
String methodName, HttpRequestDefinition definition, @Nullable ParameterizedTypeReference<T> bodyType) {
|
||||
|
||||
this.methodName = methodName;
|
||||
this.requestDefinition = definition;
|
||||
this.bodyType = bodyType;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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.reactive.function.client.support;
|
||||
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.service.invoker.HttpClientAdapter;
|
||||
import org.springframework.web.service.invoker.HttpRequestDefinition;
|
||||
|
||||
|
||||
/**
|
||||
* {@link HttpClientAdapter} implementation for {@link WebClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 6.0
|
||||
*/
|
||||
public class WebClientAdapter implements HttpClientAdapter {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
|
||||
public WebClientAdapter(WebClient webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Void> requestToVoid(HttpRequestDefinition request) {
|
||||
return toBodySpec(request).exchangeToMono(ClientResponse::releaseBody);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<HttpHeaders> requestToHeaders(HttpRequestDefinition request) {
|
||||
return toBodySpec(request).retrieve().toBodilessEntity().map(ResponseEntity::getHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> requestToBody(HttpRequestDefinition request, ParameterizedTypeReference<T> bodyType) {
|
||||
return toBodySpec(request).retrieve().bodyToMono(bodyType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> requestToBodyFlux(HttpRequestDefinition request, ParameterizedTypeReference<T> bodyType) {
|
||||
return toBodySpec(request).retrieve().bodyToFlux(bodyType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestDefinition request) {
|
||||
return toBodySpec(request).retrieve().toBodilessEntity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ResponseEntity<T>> requestToEntity(HttpRequestDefinition request, ParameterizedTypeReference<T> bodyType) {
|
||||
return toBodySpec(request).retrieve().toEntity(bodyType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(HttpRequestDefinition request, ParameterizedTypeReference<T> bodyType) {
|
||||
return toBodySpec(request).retrieve().toEntityFlux(bodyType);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
|
||||
private WebClient.RequestBodySpec toBodySpec(HttpRequestDefinition request) {
|
||||
|
||||
HttpMethod httpMethod = request.getHttpMethodRequired();
|
||||
WebClient.RequestBodyUriSpec uriSpec = this.webClient.method(httpMethod);
|
||||
|
||||
WebClient.RequestBodySpec bodySpec;
|
||||
if (request.getUri() != null) {
|
||||
bodySpec = uriSpec.uri(request.getUri());
|
||||
}
|
||||
else if (request.getUriTemplate() != null) {
|
||||
bodySpec = (!request.getUriVariables().isEmpty() ?
|
||||
uriSpec.uri(request.getUriTemplate(), request.getUriVariables()) :
|
||||
uriSpec.uri(request.getUriTemplate(), request.getUriVariableValues()));
|
||||
}
|
||||
else {
|
||||
bodySpec = uriSpec.uri("");
|
||||
}
|
||||
|
||||
bodySpec.headers(headers -> headers.putAll(request.getHeaders()));
|
||||
bodySpec.cookies(cookies -> cookies.putAll(request.getCookies()));
|
||||
|
||||
if (request.getBodyValue() != null) {
|
||||
bodySpec.bodyValue(request.getBodyValue());
|
||||
}
|
||||
else if (request.getBodyPublisher() != null) {
|
||||
bodySpec.body(request.getBodyPublisher(), request.getBodyPublisherElementType());
|
||||
}
|
||||
|
||||
return bodySpec;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Support for an HTTP service proxy created from an interface declaration.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.web.reactive.service;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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.reactive.function.client.support;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.service.annotation.GetRequest;
|
||||
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Integration tests for {@link HttpServiceProxyFactory HTTP Service proxy}
|
||||
* using {@link WebClient} and {@link MockWebServer}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class WebClientHttpServiceProxyTests {
|
||||
|
||||
private MockWebServer server;
|
||||
|
||||
private TestHttpService httpService;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.server = new MockWebServer();
|
||||
WebClient webClient = WebClient
|
||||
.builder()
|
||||
.clientConnector(new ReactorClientHttpConnector())
|
||||
.baseUrl(this.server.url("/").toString())
|
||||
.build();
|
||||
|
||||
WebClientAdapter webClientAdapter = new WebClientAdapter(webClient);
|
||||
|
||||
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(
|
||||
Collections.emptyList(), webClientAdapter, ReactiveAdapterRegistry.getSharedInstance(),
|
||||
Duration.ofSeconds(5));
|
||||
|
||||
this.httpService = proxyFactory.createService(TestHttpService.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@AfterEach
|
||||
void shutdown() throws IOException {
|
||||
if (this.server != null) {
|
||||
this.server.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void greeting() {
|
||||
|
||||
prepareResponse(response ->
|
||||
response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
|
||||
|
||||
StepVerifier.create(this.httpService.getGreeting())
|
||||
.expectNext("Hello Spring!")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
private void prepareResponse(Consumer<MockResponse> consumer) {
|
||||
MockResponse response = new MockResponse();
|
||||
consumer.accept(response);
|
||||
this.server.enqueue(response);
|
||||
}
|
||||
|
||||
|
||||
private interface TestHttpService {
|
||||
|
||||
@GetRequest("/greeting")
|
||||
Mono<String> getGreeting();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue