Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.springframework.web.service.invoker.HttpServiceMethod Maven / Gradle / Ivy
/*
* Copyright 2002-2024 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.AnnotatedElement;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.core.annotation.SynthesizingMethodParameter;
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.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
import org.springframework.web.service.annotation.HttpExchange;
/**
* Implements the invocation of an {@link HttpExchange @HttpExchange}-annotated,
* {@link HttpServiceProxyFactory#createClient(Class) HTTP service proxy} method
* by delegating to an {@link HttpExchangeAdapter} to perform actual requests.
*
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @author Olga Maciaszek-Sharma
* @author Sam Brannen
* @since 6.0
*/
final class HttpServiceMethod {
private static final boolean REACTOR_PRESENT =
ClassUtils.isPresent("reactor.core.publisher.Mono", HttpServiceMethod.class.getClassLoader());
private final Method method;
private final MethodParameter[] parameters;
private final List argumentResolvers;
private final HttpRequestValuesInitializer requestValuesInitializer;
private final ResponseFunction responseFunction;
HttpServiceMethod(
Method method, Class containingClass, List argumentResolvers,
HttpExchangeAdapter adapter, @Nullable StringValueResolver embeddedValueResolver) {
this.method = method;
this.parameters = initMethodParameters(method);
this.argumentResolvers = argumentResolvers;
boolean isReactorAdapter = (REACTOR_PRESENT && adapter instanceof ReactorHttpExchangeAdapter);
this.requestValuesInitializer =
HttpRequestValuesInitializer.create(
method, containingClass, embeddedValueResolver,
(isReactorAdapter ? ReactiveHttpRequestValues::builder : HttpRequestValues::builder));
this.responseFunction = (isReactorAdapter ?
ReactorExchangeResponseFunction.create((ReactorHttpExchangeAdapter) adapter, method) :
ExchangeResponseFunction.create(adapter, method));
}
private static MethodParameter[] initMethodParameters(Method method) {
int count = method.getParameterCount();
if (count == 0) {
return new MethodParameter[0];
}
if (KotlinDetector.isSuspendingFunction(method)) {
count -= 1;
}
DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
MethodParameter[] parameters = new MethodParameter[count];
for (int i = 0; i < count; i++) {
parameters[i] = new SynthesizingMethodParameter(method, i);
parameters[i].initParameterNameDiscovery(nameDiscoverer);
}
return parameters;
}
public Method getMethod() {
return this.method;
}
@Nullable
public Object invoke(Object[] arguments) {
HttpRequestValues.Builder requestValues = this.requestValuesInitializer.initializeRequestValuesBuilder();
applyArguments(requestValues, arguments);
return this.responseFunction.execute(requestValues.build());
}
private void applyArguments(HttpRequestValues.Builder requestValues, Object[] arguments) {
Assert.isTrue(arguments.length == this.parameters.length, "Method argument mismatch");
for (int i = 0; i < arguments.length; i++) {
Object value = arguments[i];
boolean resolved = false;
for (HttpServiceArgumentResolver resolver : this.argumentResolvers) {
if (resolver.resolve(value, this.parameters[i], requestValues)) {
resolved = true;
break;
}
}
int index = i;
Assert.state(resolved, () ->
"Could not resolve parameter [" + this.parameters[index].getParameterIndex() + "] in " +
this.parameters[index].getExecutable().toGenericString() + ": No suitable resolver");
}
}
/**
* Factory for {@link HttpRequestValues} with values extracted from the type
* and method-level {@link HttpExchange @HttpExchange} annotations.
*/
private record HttpRequestValuesInitializer(
@Nullable HttpMethod httpMethod, @Nullable String url,
@Nullable MediaType contentType, @Nullable List acceptMediaTypes,
Supplier requestValuesSupplier) {
public HttpRequestValues.Builder initializeRequestValuesBuilder() {
HttpRequestValues.Builder requestValues = this.requestValuesSupplier.get();
if (this.httpMethod != null) {
requestValues.setHttpMethod(this.httpMethod);
}
if (this.url != null) {
requestValues.setUriTemplate(this.url);
}
if (this.contentType != null) {
requestValues.setContentType(this.contentType);
}
if (this.acceptMediaTypes != null) {
requestValues.setAccept(this.acceptMediaTypes);
}
return requestValues;
}
/**
* Introspect the method and create the request factory for it.
*/
public static HttpRequestValuesInitializer create(
Method method, Class containingClass, @Nullable StringValueResolver embeddedValueResolver,
Supplier requestValuesSupplier) {
List methodHttpExchanges = getAnnotationDescriptors(method);
Assert.state(!methodHttpExchanges.isEmpty(),
() -> "Expected @HttpExchange annotation on method " + method);
Assert.state(methodHttpExchanges.size() == 1,
() -> "Multiple @HttpExchange annotations found on method %s, but only one is allowed: %s"
.formatted(method, methodHttpExchanges));
List typeHttpExchanges = getAnnotationDescriptors(containingClass);
Assert.state(typeHttpExchanges.size() <= 1,
() -> "Multiple @HttpExchange annotations found on %s, but only one is allowed: %s"
.formatted(containingClass, typeHttpExchanges));
HttpExchange methodAnnotation = methodHttpExchanges.get(0).httpExchange;
HttpExchange typeAnnotation = (!typeHttpExchanges.isEmpty() ? typeHttpExchanges.get(0).httpExchange : null);
HttpMethod httpMethod = initHttpMethod(typeAnnotation, methodAnnotation);
String url = initUrl(typeAnnotation, methodAnnotation, embeddedValueResolver);
MediaType contentType = initContentType(typeAnnotation, methodAnnotation);
List acceptableMediaTypes = initAccept(typeAnnotation, methodAnnotation);
return new HttpRequestValuesInitializer(
httpMethod, url, contentType, acceptableMediaTypes, requestValuesSupplier);
}
@Nullable
private static HttpMethod initHttpMethod(@Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation) {
String methodLevelMethod = methodAnnotation.method();
if (StringUtils.hasText(methodLevelMethod)) {
return HttpMethod.valueOf(methodLevelMethod);
}
String typeLevelMethod = (typeAnnotation != null ? typeAnnotation.method() : null);
if (StringUtils.hasText(typeLevelMethod)) {
return HttpMethod.valueOf(typeLevelMethod);
}
return null;
}
@Nullable
private static String initUrl(
@Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation,
@Nullable StringValueResolver embeddedValueResolver) {
String typeLevelUrl = (typeAnnotation != null ? typeAnnotation.url() : null);
String methodLevelUrl = methodAnnotation.url();
if (embeddedValueResolver != null) {
typeLevelUrl = (typeLevelUrl != null ? embeddedValueResolver.resolveStringValue(typeLevelUrl) : null);
methodLevelUrl = embeddedValueResolver.resolveStringValue(methodLevelUrl);
}
boolean hasTypeLevelUrl = StringUtils.hasText(typeLevelUrl);
boolean hasMethodLevelUrl = StringUtils.hasText(methodLevelUrl);
if (hasTypeLevelUrl && hasMethodLevelUrl) {
return (typeLevelUrl + (!typeLevelUrl.endsWith("/") && !methodLevelUrl.startsWith("/") ? "/" : "") + methodLevelUrl);
}
if (!hasTypeLevelUrl && !hasMethodLevelUrl) {
return null;
}
return (hasMethodLevelUrl ? methodLevelUrl : typeLevelUrl);
}
@Nullable
private static MediaType initContentType(@Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation) {
String methodLevelContentType = methodAnnotation.contentType();
if (StringUtils.hasText(methodLevelContentType)) {
return MediaType.parseMediaType(methodLevelContentType);
}
String typeLevelContentType = (typeAnnotation != null ? typeAnnotation.contentType() : null);
if (StringUtils.hasText(typeLevelContentType)) {
return MediaType.parseMediaType(typeLevelContentType);
}
return null;
}
@Nullable
private static List initAccept(@Nullable HttpExchange typeAnnotation, HttpExchange methodAnnotation) {
String[] methodLevelAccept = methodAnnotation.accept();
if (!ObjectUtils.isEmpty(methodLevelAccept)) {
return MediaType.parseMediaTypes(List.of(methodLevelAccept));
}
String[] typeLevelAccept = (typeAnnotation != null ? typeAnnotation.accept() : null);
if (!ObjectUtils.isEmpty(typeLevelAccept)) {
return MediaType.parseMediaTypes(List.of(typeLevelAccept));
}
return null;
}
private static List getAnnotationDescriptors(AnnotatedElement element) {
return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none())
.stream(HttpExchange.class)
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
.map(AnnotationDescriptor::new)
.distinct()
.toList();
}
private static class AnnotationDescriptor {
private final HttpExchange httpExchange;
private final MergedAnnotation root;
AnnotationDescriptor(MergedAnnotation mergedAnnotation) {
this.httpExchange = mergedAnnotation.synthesize();
this.root = mergedAnnotation.getRoot();
}
@Override
public boolean equals(Object obj) {
return (obj instanceof AnnotationDescriptor that && this.httpExchange.equals(that.httpExchange));
}
@Override
public int hashCode() {
return this.httpExchange.hashCode();
}
@Override
public String toString() {
return this.root.synthesize().toString();
}
}
}
/**
* Execute a request, obtain a response, and adapt to the expected return type.
*/
private interface ResponseFunction {
@Nullable
Object execute(HttpRequestValues requestValues);
}
private record ExchangeResponseFunction(
Function responseFunction) implements ResponseFunction {
@Override
public Object execute(HttpRequestValues requestValues) {
return this.responseFunction.apply(requestValues);
}
/**
* Create the {@code ResponseFunction} that matches the method return type.
*/
public static ResponseFunction create(HttpExchangeAdapter client, Method method) {
if (KotlinDetector.isSuspendingFunction(method)) {
throw new IllegalStateException(
"Kotlin Coroutines are only supported with reactive implementations");
}
MethodParameter param = new MethodParameter(method, -1).nestedIfOptional();
Class paramType = param.getNestedParameterType();
Function responseFunction;
if (ClassUtils.isVoidType(paramType)) {
responseFunction = requestValues -> {
client.exchange(requestValues);
return null;
};
}
else if (paramType.equals(HttpHeaders.class)) {
responseFunction = request -> asOptionalIfNecessary(client.exchangeForHeaders(request), param);
}
else if (paramType.equals(ResponseEntity.class)) {
MethodParameter bodyParam = param.nested();
if (bodyParam.getNestedParameterType().equals(Void.class)) {
responseFunction = request ->
asOptionalIfNecessary(client.exchangeForBodilessEntity(request), param);
}
else {
ParameterizedTypeReference bodyTypeRef =
ParameterizedTypeReference.forType(bodyParam.getNestedGenericParameterType());
responseFunction = request ->
asOptionalIfNecessary(client.exchangeForEntity(request, bodyTypeRef), param);
}
}
else {
ParameterizedTypeReference bodyTypeRef =
ParameterizedTypeReference.forType(param.getNestedGenericParameterType());
responseFunction = request ->
asOptionalIfNecessary(client.exchangeForBody(request, bodyTypeRef), param);
}
return new ExchangeResponseFunction(responseFunction);
}
private static @Nullable Object asOptionalIfNecessary(@Nullable Object response, MethodParameter param) {
return param.getParameterType().equals(Optional.class) ? Optional.ofNullable(response) : response;
}
}
/**
* {@link ResponseFunction} for {@link ReactorHttpExchangeAdapter}.
*/
private record ReactorExchangeResponseFunction(
Function> responseFunction,
@Nullable ReactiveAdapter returnTypeAdapter,
boolean blockForOptional, @Nullable Duration blockTimeout) implements ResponseFunction {
@Override
@Nullable
public Object execute(HttpRequestValues requestValues) {
Publisher responsePublisher = this.responseFunction.apply(requestValues);
if (this.returnTypeAdapter != null) {
return this.returnTypeAdapter.fromPublisher(responsePublisher);
}
if (this.blockForOptional) {
return (this.blockTimeout != null ?
((Mono) responsePublisher).blockOptional(this.blockTimeout) :
((Mono) responsePublisher).blockOptional());
}
else {
return (this.blockTimeout != null ?
((Mono) responsePublisher).block(this.blockTimeout) :
((Mono) responsePublisher).block());
}
}
/**
* Create the {@code ResponseFunction} that matches the method return type.
*/
public static ResponseFunction create(ReactorHttpExchangeAdapter client, Method method) {
MethodParameter returnParam = new MethodParameter(method, -1);
Class returnType = returnParam.getParameterType();
boolean isSuspending = KotlinDetector.isSuspendingFunction(method);
if (isSuspending) {
returnType = Mono.class;
}
ReactiveAdapter reactiveAdapter = client.getReactiveAdapterRegistry().getAdapter(returnType);
MethodParameter actualParam = (reactiveAdapter != null ? returnParam.nested() : returnParam.nestedIfOptional());
Class actualType = isSuspending ? actualParam.getParameterType() : actualParam.getNestedParameterType();
Function> responseFunction;
if (ClassUtils.isVoidType(actualType)) {
responseFunction = client::exchangeForMono;
}
else if (reactiveAdapter != null && reactiveAdapter.isNoValue()) {
responseFunction = client::exchangeForMono;
}
else if (actualType.equals(HttpHeaders.class)) {
responseFunction = client::exchangeForHeadersMono;
}
else if (actualType.equals(ResponseEntity.class)) {
MethodParameter bodyParam = isSuspending ? actualParam : actualParam.nested();
Class bodyType = bodyParam.getNestedParameterType();
if (bodyType.equals(Void.class)) {
responseFunction = client::exchangeForBodilessEntityMono;
}
else {
ReactiveAdapter bodyAdapter = client.getReactiveAdapterRegistry().getAdapter(bodyType);
responseFunction = initResponseEntityFunction(client, bodyParam, bodyAdapter, isSuspending);
}
}
else {
responseFunction = initBodyFunction(client, actualParam, reactiveAdapter, isSuspending);
}
return new ReactorExchangeResponseFunction(
responseFunction, reactiveAdapter, returnType.equals(Optional.class), client.getBlockTimeout());
}
@SuppressWarnings("ConstantConditions")
private static Function> initResponseEntityFunction(
ReactorHttpExchangeAdapter client, MethodParameter methodParam,
@Nullable ReactiveAdapter reactiveAdapter, boolean isSuspending) {
if (reactiveAdapter == null) {
return request -> client.exchangeForEntityMono(
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(isSuspending ? methodParam.nested().getGenericParameterType() :
methodParam.nested().getNestedGenericParameterType());
// Shortcut for Flux
if (reactiveAdapter.getReactiveType().equals(Flux.class)) {
return request -> client.exchangeForEntityFlux(request, bodyType);
}
return request -> client.exchangeForEntityFlux(request, bodyType)
.map(entity -> {
Object body = reactiveAdapter.fromPublisher(entity.getBody());
return new ResponseEntity<>(body, entity.getHeaders(), entity.getStatusCode());
});
}
private static Function> initBodyFunction(
ReactorHttpExchangeAdapter client, MethodParameter methodParam,
@Nullable ReactiveAdapter reactiveAdapter, boolean isSuspending) {
ParameterizedTypeReference bodyType =
ParameterizedTypeReference.forType(isSuspending ? methodParam.getGenericParameterType() :
methodParam.getNestedGenericParameterType());
return (reactiveAdapter != null && reactiveAdapter.isMultiValue() ?
request -> client.exchangeForBodyFlux(request, bodyType) :
request -> client.exchangeForBodyMono(request, bodyType));
}
}
}