Add support for RSocket interface client

See gh-24456
This commit is contained in:
rstoyanchev
2022-09-05 16:54:30 +01:00
parent ae861a2b3e
commit 8423b2cab7
22 changed files with 1956 additions and 93 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -51,7 +51,11 @@ public @interface Payload {
* <p>This attribute may or may not be supported depending on whether the message being
* handled contains a non-primitive Object as its payload or is in serialized form and
* requires message conversion.
* <p>When processing STOMP over WebSocket messages this attribute is not supported.
* <p>This attribute is not supported for:
* <ul>
* <li>STOMP over WebSocket messages</li>
* <li>RSocket interface client</li>
* </ul>
* @since 4.2
*/
@AliasFor("value")

View File

@@ -102,6 +102,12 @@ final class DefaultRSocketRequester implements RSocketRequester {
return this.metadataMimeType;
}
@Override
public RSocketStrategies strategies() {
return this.strategies;
}
@Override
public RequestSpec route(String route, Object... vars) {
return new DefaultRequestSpec(route, vars);

View File

@@ -83,6 +83,11 @@ public interface RSocketRequester extends Disposable {
*/
MimeType metadataMimeType();
/**
* Return the configured {@link RSocketStrategies}.
*/
RSocketStrategies strategies();
/**
* Begin to specify a new request with the given route to a remote handler.
* <p>The route can be a template with placeholders, e.g.

View File

@@ -0,0 +1,65 @@
/*
* 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.messaging.rsocket.service;
import java.util.Collection;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.handler.annotation.DestinationVariable;
/**
* {@link RSocketServiceArgumentResolver} for a
* {@link DestinationVariable @DestinationVariable} annotated argument.
*
* <p>The argument is treated as a single route variable, or in case of a
* Collection or an array, as multiple route variables.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public class DestinationVariableArgumentResolver implements RSocketServiceArgumentResolver {
@Override
public boolean resolve(
@Nullable Object argument, MethodParameter parameter, RSocketRequestValues.Builder requestValues) {
DestinationVariable annot = parameter.getParameterAnnotation(DestinationVariable.class);
if (annot == null) {
return false;
}
if (argument != null) {
if (argument instanceof Collection) {
((Collection<?>) argument).forEach(requestValues::addRouteVariable);
return true;
}
else if (argument.getClass().isArray()) {
for (Object variable : (Object[]) argument) {
requestValues.addRouteVariable(variable);
}
return true;
}
else {
requestValues.addRouteVariable(argument);
}
}
return true;
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.messaging.rsocket.service;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* {@link RSocketServiceArgumentResolver} for metadata entries.
*
* <p>Supports a sequence of an {@link Object} parameter for the metadata value,
* followed by a {@link MimeType} parameter for the metadata mime type.
*
* <p>This should be ordered last to give other, more specific resolvers a
* chance to resolve the argument.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public class MetadataArgumentResolver implements RSocketServiceArgumentResolver {
@Override
public boolean resolve(
@Nullable Object argument, MethodParameter parameter, RSocketRequestValues.Builder requestValues) {
int index = parameter.getParameterIndex();
Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes();
if (parameter.getParameterType().equals(MimeType.class)) {
Assert.notNull(argument, "MimeType parameter is required");
Assert.state(index > 0, "MimeType parameter should have preceding metadata object parameter");
requestValues.addMimeType((MimeType) argument);
return true;
}
if (paramTypes.length > (index + 1) && MimeType.class.equals(paramTypes[index + 1])) {
Assert.notNull(argument, "MimeType parameter is required");
requestValues.addMetadata(argument);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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.messaging.rsocket.service;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.lang.Nullable;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.util.Assert;
/**
* {@link RSocketServiceArgumentResolver} for {@link Payload @Payload}
* annotated arguments.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public class PayloadArgumentResolver implements RSocketServiceArgumentResolver {
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
private final boolean useDefaultResolution;
public PayloadArgumentResolver(ReactiveAdapterRegistry reactiveAdapterRegistry, boolean useDefaultResolution) {
this.useDefaultResolution = useDefaultResolution;
Assert.notNull(reactiveAdapterRegistry, "ReactiveAdapterRegistry is required");
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
}
@Override
public boolean resolve(
@Nullable Object argument, MethodParameter parameter, RSocketRequestValues.Builder requestValues) {
Payload annot = parameter.getParameterAnnotation(Payload.class);
if (annot == null && !this.useDefaultResolution) {
return false;
}
if (argument != null) {
ReactiveAdapter reactiveAdapter = this.reactiveAdapterRegistry.getAdapter(parameter.getParameterType());
if (reactiveAdapter == null) {
requestValues.setPayloadValue(argument);
}
else {
MethodParameter nestedParameter = parameter.nested();
String message = "Async type for @Payload should produce value(s)";
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
Assert.isTrue(!reactiveAdapter.isNoValue(), message);
requestValues.setPayload(
reactiveAdapter.toPublisher(argument),
ParameterizedTypeReference.forType(nestedParameter.getNestedGenericParameterType()));
}
}
return true;
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.messaging.rsocket.service;
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;
/**
* Annotation to declare a method on an RSocket service interface as an RSocket
* endpoint. The endpoint route is defined statically through the annotation
* attributes, and through the input method argument types.
*
* <p>Supported at the type level to express common attributes, to be inherited
* by all methods, such as a base route.
*
* <p>Supported method arguments:
* <table border="1">
* <tr>
* <th>Method Argument</th>
* <th>Description</th>
* <th>Resolver</th>
* </tr>
* <tr>
* <td>{@link org.springframework.messaging.handler.annotation.DestinationVariable @DestinationVariable}</td>
* <td>Add a route variable to expand into the route</td>
* <td>{@link DestinationVariableArgumentResolver}</td>
* </tr>
* <tr>
* <td>{@link org.springframework.messaging.handler.annotation.Payload @Payload}</td>
* <td>Set the input payload(s) for the request</td>
* <td>{@link PayloadArgumentResolver}</td>
* </tr>
* <tr>
* <td>{@link Object} argument followed by {@link org.springframework.util.MimeType} argument</td>
* <td>Add a metadata value</td>
* <td>{@link MetadataArgumentResolver}</td>
* </tr>
* <tr>
* <td>{@link org.springframework.util.MimeType} argument preceded by {@link Object} argument</td>
* <td>Specify the mime type for the preceding metadata value</td>
* <td>{@link MetadataArgumentResolver}</td>
* </tr>
* </table>
*
* @author Rossen Stoyanchev
* @since 6.0
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RSocketExchange {
/**
* Destination-based mapping expressed by this annotation. This is either
* {@link org.springframework.util.AntPathMatcher AntPathMatcher} or
* {@link org.springframework.web.util.pattern.PathPattern PathPattern}
* based pattern, depending on which is configured, matched to the route of
* the stream request.
*/
String value() default "";
}

View File

@@ -0,0 +1,268 @@
/*
* 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.messaging.rsocket.service;
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.ParameterizedTypeReference;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.util.StringUtils;
/**
* Container for RSocket request values extracted from an
* {@link RSocketExchange @RSocketExchange}-annotated
* method and argument values passed to it. This is then used to define a request
* via {@link org.springframework.messaging.rsocket.RSocketRequester}.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public final class RSocketRequestValues {
@Nullable
private final String route;
private final Object[] routeVariables;
private final Map<Object, MimeType> metadata;
@Nullable
private final Object payloadValue;
@Nullable
private final Publisher<?> payload;
@Nullable
private final ParameterizedTypeReference<?> payloadElementType;
public RSocketRequestValues(
@Nullable String route, @Nullable List<Object> routeVariables, @Nullable MetadataHelper metadataHelper,
@Nullable Object payloadValue, @Nullable Publisher<?> payload,
@Nullable ParameterizedTypeReference<?> payloadElementType) {
this.route = route;
this.routeVariables = (routeVariables != null ? routeVariables.toArray() : new Object[0]);
this.metadata = (metadataHelper != null ? metadataHelper.toMap() : Collections.emptyMap());
this.payloadValue = payloadValue;
this.payload = payload;
this.payloadElementType = payloadElementType;
}
/**
* Return the route value for
* {@link org.springframework.messaging.rsocket.RSocketRequester#route(String, Object...) route}.
*/
@Nullable
public String getRoute() {
return this.route;
}
/**
* Return the route variables for
* {@link org.springframework.messaging.rsocket.RSocketRequester#route(String, Object...) route}.
*/
public Object[] getRouteVariables() {
return this.routeVariables;
}
/**
* Return the metadata entries for
* {@link org.springframework.messaging.rsocket.RSocketRequester.RequestSpec#metadata(Object, MimeType)}.
*/
public Map<Object, MimeType> getMetadata() {
return this.metadata;
}
/**
* Return the request payload as a value to be serialized, if set.
* <p>This is mutually exclusive with {@link #getPayload()}.
* Only one of the two or neither is set.
*/
@Nullable
public Object getPayloadValue() {
return this.payloadValue;
}
/**
* Return the request payload as a Publisher.
* <p>This is mutually exclusive with {@link #getPayloadValue()}.
* Only one of the two or neither is set.
*/
@Nullable
public Publisher<?> getPayload() {
return this.payload;
}
/**
* Return the element type for a {@linkplain #getPayload() Publisher payload}.
*/
@Nullable
public ParameterizedTypeReference<?> getPayloadElementType() {
return this.payloadElementType;
}
public static Builder builder(@Nullable String route) {
return new Builder(route);
}
/**
* Builder for {@link RSocketRequestValues}.
*/
public final static class Builder {
@Nullable
private String route;
@Nullable
private List<Object> routeVariables;
@Nullable
private MetadataHelper metadataHelper;
@Nullable
private Object payloadValue;
@Nullable
private Publisher<?> payload;
@Nullable
private ParameterizedTypeReference<?> payloadElementType;
Builder(@Nullable String route) {
this.route = (StringUtils.hasText(route) ? route : null);
}
/**
* Set the route for the request.
*/
public Builder setRoute(String route) {
this.route = route;
this.routeVariables = null;
return this;
}
/**
* Add a route variable.
*/
public Builder addRouteVariable(Object variable) {
this.routeVariables = (this.routeVariables != null ? this.routeVariables : new ArrayList<>());
this.routeVariables.add(variable);
return this;
}
/**
* Add a metadata entry.
* This must be followed by a corresponding call to {@link #addMimeType(MimeType)}.
*/
public Builder addMetadata(Object metadata) {
this.metadataHelper = (this.metadataHelper != null ? this.metadataHelper : new MetadataHelper());
this.metadataHelper.addMetadata(metadata);
return this;
}
/**
* Set the mime type for a metadata entry.
* This must be preceded by a call to {@link #addMetadata(Object)}.
*/
public Builder addMimeType(MimeType mimeType) {
this.metadataHelper = (this.metadataHelper != null ? this.metadataHelper : new MetadataHelper());
this.metadataHelper.addMimeType(mimeType);
return this;
}
/**
* Set the request payload as a concrete value to be serialized.
* <p>This is mutually exclusive with, and resets any previously set
* {@linkplain #setPayload(Publisher, ParameterizedTypeReference) payload Publisher}.
*/
public Builder setPayloadValue(Object payloadValue) {
this.payloadValue = payloadValue;
this.payload = null;
this.payloadElementType = null;
return this;
}
/**
* Set the request payload value to be serialized.
*/
public <T, P extends Publisher<T>> Builder setPayload(P payload, ParameterizedTypeReference<T> elementTye) {
this.payload = payload;
this.payloadElementType = elementTye;
this.payloadValue = null;
return this;
}
/**
* Build the {@link RSocketRequestValues} instance.
*/
public RSocketRequestValues build() {
return new RSocketRequestValues(
this.route, this.routeVariables, this.metadataHelper,
this.payloadValue, this.payload, this.payloadElementType);
}
}
/**
* Class that helps to collect a map of metadata entries as a series of calls
* to provide each metadata and mime type pair.
*/
private static class MetadataHelper {
private final List<Object> metadata = new ArrayList<>();
private final List<MimeType> mimeTypes = new ArrayList<>();
public void addMetadata(Object metadata) {
Assert.isTrue(this.metadata.size() == this.mimeTypes.size(), "Invalid state: " + this);
this.metadata.add(metadata);
}
public void addMimeType(MimeType mimeType) {
Assert.isTrue(this.metadata.size() == (this.mimeTypes.size() + 1), "Invalid state: " + this);
this.mimeTypes.add(mimeType);
}
public Map<Object, MimeType> toMap() {
Map<Object, MimeType> map = new LinkedHashMap<>(this.metadata.size());
for (int i = 0; i < this.metadata.size(); i++) {
map.put(this.metadata.get(i), this.mimeTypes.get(i));
}
return map;
}
@Override
public String toString() {
return "metadata=" + this.metadata + ", mimeTypes=" + this.mimeTypes;
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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.messaging.rsocket.service;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
/**
* Resolve an argument from an {@link RSocketExchange @RSocketExchange}-annotated
* method to one or more RSocket request values.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public interface RSocketServiceArgumentResolver {
/**
* Resolve the argument value.
* @param argument the argument value
* @param parameter the method parameter for the argument
* @param requestValues builder to add RSocket request values to
* @return {@code true} if the argument was resolved, {@code false} otherwise
*/
boolean resolve(@Nullable Object argument, MethodParameter parameter, RSocketRequestValues.Builder requestValues);
}

View File

@@ -0,0 +1,242 @@
/*
* 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.messaging.rsocket.service;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.DefaultParameterNameDiscoverer;
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.core.annotation.SynthesizingMethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
/**
* Implements the invocation of an {@link RSocketExchange @RSocketExchange}-annotated,
* {@link RSocketServiceProxyFactory#createClient(Class) RSocket service proxy} method
* by delegating to an {@link RSocketRequester} to perform actual requests.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
final class RSocketServiceMethod {
private final Method method;
private final MethodParameter[] parameters;
private final List<RSocketServiceArgumentResolver> argumentResolvers;
@Nullable
private final String route;
private final Function<RSocketRequestValues, Object> responseFunction;
RSocketServiceMethod(
Method method, Class<?> containingClass, List<RSocketServiceArgumentResolver> argumentResolvers,
RSocketRequester rsocketRequester, @Nullable StringValueResolver embeddedValueResolver,
ReactiveAdapterRegistry reactiveRegistry, Duration blockTimeout) {
this.method = method;
this.parameters = initMethodParameters(method);
this.argumentResolvers = argumentResolvers;
this.route = initRoute(method, containingClass, rsocketRequester.strategies(), embeddedValueResolver);
this.responseFunction = initResponseFunction(
rsocketRequester, method, reactiveRegistry, blockTimeout);
}
private static MethodParameter[] initMethodParameters(Method method) {
int count = method.getParameterCount();
if (count == 0) {
return new MethodParameter[0];
}
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;
}
@Nullable
private static String initRoute(
Method method, Class<?> containingClass, RSocketStrategies strategies,
@Nullable StringValueResolver embeddedValueResolver) {
RSocketExchange annot1 = AnnotatedElementUtils.findMergedAnnotation(containingClass, RSocketExchange.class);
RSocketExchange annot2 = AnnotatedElementUtils.findMergedAnnotation(method, RSocketExchange.class);
Assert.notNull(annot2, "Expected RSocketExchange annotation");
String route1 = (annot1 != null ? annot1.value() : null);
String route2 = annot2.value();
if (embeddedValueResolver != null) {
route1 = (route1 != null ? embeddedValueResolver.resolveStringValue(route1) : null);
route2 = embeddedValueResolver.resolveStringValue(route2);
}
boolean hasRoute1 = StringUtils.hasText(route1);
boolean hasRoute2 = StringUtils.hasText(route2);
if (hasRoute1 && hasRoute2) {
return strategies.routeMatcher().combine(route1, route2);
}
if (!hasRoute1 && !hasRoute2) {
return null;
}
return (hasRoute2 ? route2 : route1);
}
private static Function<RSocketRequestValues, Object> initResponseFunction(
RSocketRequester requester, 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<RSocketRequestValues, Publisher<?>> responseFunction;
if (actualType.equals(void.class) || actualType.equals(Void.class) ||
(reactiveAdapter != null && reactiveAdapter.isNoValue())) {
responseFunction = values -> {
RSocketRequester.RetrieveSpec retrieveSpec = initRequest(requester, values);
return (values.getPayload() == null && values.getPayloadValue() == null ?
((RSocketRequester.RequestSpec) retrieveSpec).sendMetadata() : retrieveSpec.send());
};
}
else if (reactiveAdapter == null) {
responseFunction = values -> initRequest(requester, values).retrieveMono(actualType);
}
else {
ParameterizedTypeReference<?> payloadType =
ParameterizedTypeReference.forType(actualParam.getNestedGenericParameterType());
responseFunction = values -> (
reactiveAdapter.isMultiValue() ?
initRequest(requester, values).retrieveFlux(payloadType) :
initRequest(requester, values).retrieveMono(payloadType));
}
boolean blockForOptional = returnType.equals(Optional.class);
return responseFunction.andThen(responsePublisher -> {
if (reactiveAdapter != null) {
return reactiveAdapter.fromPublisher(responsePublisher);
}
return (blockForOptional ?
((Mono<?>) responsePublisher).blockOptional(blockTimeout) :
((Mono<?>) responsePublisher).block(blockTimeout));
});
}
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
private static RSocketRequester.RetrieveSpec initRequest(
RSocketRequester requester, RSocketRequestValues requestValues) {
RSocketRequester.RequestSpec spec;
String route = requestValues.getRoute();
Map<Object, MimeType> metadata = requestValues.getMetadata();
if (StringUtils.hasText(route)) {
spec = requester.route(route, requestValues.getRouteVariables());
for (Map.Entry<Object, MimeType> entry : metadata.entrySet()) {
spec.metadata(entry.getKey(), entry.getValue());
}
}
else {
Iterator<Map.Entry<Object, MimeType>> iterator = metadata.entrySet().iterator();
Assert.isTrue(iterator.hasNext(), "Neither route nor metadata provided");
Map.Entry<Object, MimeType> entry = iterator.next();
spec = requester.metadata(entry.getKey(), entry.getValue());
while (iterator.hasNext()) {
spec.metadata(entry.getKey(), entry.getValue());
}
}
if (requestValues.getPayloadValue() != null) {
spec.data(requestValues.getPayloadValue());
}
else if (requestValues.getPayload() != null) {
Assert.notNull(requestValues.getPayloadElementType(), "Publisher body element type is required");
spec.data(requestValues.getPayload(), requestValues.getPayloadElementType());
}
return spec;
}
public Method getMethod() {
return this.method;
}
@Nullable
public Object invoke(Object[] arguments) {
RSocketRequestValues.Builder requestValues = RSocketRequestValues.builder(this.route);
applyArguments(requestValues, arguments);
return this.responseFunction.apply(requestValues.build());
}
private void applyArguments(RSocketRequestValues.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 (RSocketServiceArgumentResolver resolver : this.argumentResolvers) {
if (resolver.resolve(value, this.parameters[i], requestValues)) {
resolved = true;
break;
}
}
Assert.state(resolved, formatArgumentError(this.parameters[i], "No suitable resolver"));
}
}
@SuppressWarnings("SameParameterValue")
private static String formatArgumentError(MethodParameter param, String message) {
return "Could not resolve parameter [" + param.getParameterIndex() + "] in " +
param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : "");
}
}

View File

@@ -0,0 +1,217 @@
/*
* 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.messaging.rsocket.service;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.framework.ReflectiveMethodInvocation;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.util.Assert;
import org.springframework.util.StringValueResolver;
/**
* Factory for creating a client proxy given an RSocket service interface with
* {@link RSocketExchange @RSocketExchange} methods.
*
* <p>This class is intended to be declared as a bean in Spring configuration.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public final class RSocketServiceProxyFactory implements InitializingBean, EmbeddedValueResolverAware {
private final RSocketRequester rsocketRequester;
@Nullable
private List<RSocketServiceArgumentResolver> customArgumentResolvers;
@Nullable
private List<RSocketServiceArgumentResolver> argumentResolvers;
@Nullable
private StringValueResolver embeddedValueResolver;
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
private Duration blockTimeout = Duration.ofSeconds(5);
/**
* Create an instance with the underlying RSocketRequester to perform requests with.
* @param rsocketRequester the requester to use
*/
public RSocketServiceProxyFactory(RSocketRequester rsocketRequester) {
Assert.notNull(rsocketRequester, "RSocketRequester is required");
this.rsocketRequester = rsocketRequester;
}
/**
* Register a custom argument resolver, invoked ahead of default resolvers.
* @param resolver the resolver to add
*/
public void addCustomArgumentResolver(RSocketServiceArgumentResolver resolver) {
if (this.customArgumentResolvers == null) {
this.customArgumentResolvers = new ArrayList<>();
}
this.customArgumentResolvers.add(resolver);
}
/**
* Set the custom argument resolvers to use, ahead of default resolvers.
* @param resolvers the resolvers to use
*/
public void setCustomArgumentResolvers(List<RSocketServiceArgumentResolver> resolvers) {
this.customArgumentResolvers = new ArrayList<>(resolvers);
}
/**
* Set the StringValueResolver to use for resolving placeholders and
* expressions in {@link RSocketExchange#value()}.
* @param resolver the resolver to use
*/
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
/**
* Set the {@link ReactiveAdapterRegistry} to use to support different
* asynchronous types for RSocket service method return values.
* <p>By default this is {@link ReactiveAdapterRegistry#getSharedInstance()}.
*/
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
this.reactiveAdapterRegistry = registry;
}
/**
* Configure how long to wait for a response for an RSocket service method
* with a synchronous (blocking) method signature.
* <p>By default this is 5 seconds.
* @param blockTimeout the timeout value
*/
public void setBlockTimeout(Duration blockTimeout) {
this.blockTimeout = blockTimeout;
}
@Override
public void afterPropertiesSet() throws Exception {
this.argumentResolvers = initArgumentResolvers();
}
private List<RSocketServiceArgumentResolver> initArgumentResolvers() {
List<RSocketServiceArgumentResolver> resolvers = new ArrayList<>();
// Custom
if (this.customArgumentResolvers != null) {
resolvers.addAll(this.customArgumentResolvers);
}
// Annotation-based
resolvers.add(new PayloadArgumentResolver(this.reactiveAdapterRegistry, false));
resolvers.add(new DestinationVariableArgumentResolver());
// Type-based
resolvers.add(new MetadataArgumentResolver());
// Fallback
resolvers.add(new PayloadArgumentResolver(this.reactiveAdapterRegistry, true));
return resolvers;
}
/**
* Return a proxy that implements the given RSocket service interface to
* perform RSocket requests and retrieve responses through the configured
* {@link RSocketRequester}.
* @param serviceType the RSocket service to create a proxy for
* @param <S> the RSocket service type
* @return the created proxy
*/
public <S> S createClient(Class<S> serviceType) {
List<RSocketServiceMethod> serviceMethods =
MethodIntrospector.selectMethods(serviceType, this::isExchangeMethod).stream()
.map(method -> createRSocketServiceMethod(serviceType, method))
.toList();
return ProxyFactory.getProxy(serviceType, new ServiceMethodInterceptor(serviceMethods));
}
private boolean isExchangeMethod(Method method) {
return AnnotatedElementUtils.hasAnnotation(method, RSocketExchange.class);
}
private <S> RSocketServiceMethod createRSocketServiceMethod(Class<S> serviceType, Method method) {
Assert.notNull(this.argumentResolvers,
"No argument resolvers: afterPropertiesSet was not called");
return new RSocketServiceMethod(
method, serviceType, this.argumentResolvers, this.rsocketRequester,
this.embeddedValueResolver, this.reactiveAdapterRegistry, this.blockTimeout);
}
/**
* {@link MethodInterceptor} that invokes an {@link RSocketServiceMethod}.
*/
private static final class ServiceMethodInterceptor implements MethodInterceptor {
private final Map<Method, RSocketServiceMethod> serviceMethods;
private ServiceMethodInterceptor(List<RSocketServiceMethod> methods) {
this.serviceMethods = methods.stream()
.collect(Collectors.toMap(RSocketServiceMethod::getMethod, Function.identity()));
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
RSocketServiceMethod serviceMethod = this.serviceMethods.get(method);
if (serviceMethod != null) {
return serviceMethod.invoke(invocation.getArguments());
}
if (method.isDefault()) {
if (invocation instanceof ReflectiveMethodInvocation reflectiveMethodInvocation) {
Object proxy = reflectiveMethodInvocation.getProxy();
return InvocationHandler.invokeDefault(proxy, method, invocation.getArguments());
}
}
throw new IllegalStateException("Unexpected method invocation: " + method);
}
}
}

View File

@@ -0,0 +1,11 @@
/**
* Annotations to declare an RSocket service contract with request methods along
* with a proxy factory backed by an
* {@link org.springframework.messaging.rsocket.RSocketRequester}.
*/
@NonNullApi
@NonNullFields
package org.springframework.messaging.rsocket.service;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;