16 changed files with 1660 additions and 0 deletions
@ -0,0 +1,57 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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