#728 - Make sure invocation mappings are resolved eagerly.
Tweaked WebFluxLinkBuilder to make sure the controller invocations are resolved eagerly as otherwise, the controller proxy created would override the invocations due to the lazy evaluation of the links and for multiple method references, only the last one would have been used.
This commit is contained in:
@@ -38,19 +38,18 @@ import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Utility methods to capture dummy method invocations.
|
||||
*
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class DummyInvocationUtils {
|
||||
|
||||
private static final ObjenesisStd OBJENESIS = new ObjenesisStd();
|
||||
private static final Map<Class<?>, Class<?>> CLASS_CACHE = new ConcurrentReferenceHashMap<>(16,
|
||||
ReferenceType.WEAK);
|
||||
private static final Map<Class<?>, Class<?>> CLASS_CACHE = new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK);
|
||||
|
||||
/**
|
||||
* Method interceptor that records the last method invocation and creates a proxy for the return value that exposes
|
||||
* the method invocation.
|
||||
*
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
private static class InvocationRecordingMethodInterceptor
|
||||
@@ -71,7 +70,7 @@ public class DummyInvocationUtils {
|
||||
/**
|
||||
* Creates a new {@link InvocationRecordingMethodInterceptor} carrying the given parameters forward that might be
|
||||
* needed to populate the class level mapping.
|
||||
*
|
||||
*
|
||||
* @param targetType must not be {@literal null}.
|
||||
* @param parameters must not be {@literal null}.
|
||||
*/
|
||||
@@ -95,7 +94,7 @@ public class DummyInvocationUtils {
|
||||
return getLastInvocation();
|
||||
} else if (GET_OBJECT_PARAMETERS.equals(method)) {
|
||||
return getObjectParameters();
|
||||
} else if (Object.class.equals(method.getDeclaringClass())) {
|
||||
} else if (ReflectionUtils.isObjectMethod(method)) {
|
||||
return ReflectionUtils.invokeMethod(method, obj, args);
|
||||
}
|
||||
|
||||
@@ -105,7 +104,7 @@ public class DummyInvocationUtils {
|
||||
return returnType.cast(getProxyWithInterceptor(returnType, this, obj.getClass().getClassLoader()));
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
|
||||
*/
|
||||
@@ -123,7 +122,7 @@ public class DummyInvocationUtils {
|
||||
return invocation;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware#getObjectParameters()
|
||||
*/
|
||||
@@ -138,9 +137,10 @@ public class DummyInvocationUtils {
|
||||
* equips it with an {@link InvocationRecordingMethodInterceptor}. The interceptor records the last invocation and
|
||||
* returns a proxy of the return type that also implements {@link LastInvocationAware} so that the last method
|
||||
* invocation can be inspected. Parameters passed to the subsequent method invocation are generally neglected except
|
||||
* the ones that might be mapped into the URI translation eventually, e.g. {@link org.springframework.web.bind.annotation.PathVariable} in the case of Spring
|
||||
* MVC. Note, that the return types of the methods have to be capable to be proxied.
|
||||
*
|
||||
* the ones that might be mapped into the URI translation eventually, e.g.
|
||||
* {@link org.springframework.web.bind.annotation.PathVariable} in the case of Spring MVC. Note, that the return types
|
||||
* of the methods have to be capable to be proxied.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @param parameters parameters to extend template variables in the type level mapping.
|
||||
* @return
|
||||
@@ -174,7 +174,7 @@ public class DummyInvocationUtils {
|
||||
|
||||
/**
|
||||
* Returns the already created proxy class for the given source type or creates a new one.
|
||||
*
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @param classLoader must not be {@literal null}.
|
||||
* @return
|
||||
|
||||
@@ -73,13 +73,13 @@ public class WebHandler {
|
||||
T createBuilder(UriComponents components, TemplateVariables variables, List<Affordance> affordances);
|
||||
}
|
||||
|
||||
public static <T extends LinkBuilder> T linkTo(Object invocationValue,
|
||||
Function<String, UriComponentsBuilder> mappingToUriComponentsBuilder, LinkBuilderCreator<T> creator) {
|
||||
return linkTo(invocationValue, mappingToUriComponentsBuilder, creator, null);
|
||||
public static <T extends LinkBuilder> Function<Function<String, UriComponentsBuilder>, T> linkTo(
|
||||
Object invocationValue, LinkBuilderCreator<T> creator) {
|
||||
return linkTo(invocationValue, creator, null);
|
||||
}
|
||||
|
||||
public static <T extends LinkBuilder> T linkTo(Object invocationValue,
|
||||
Function<String, UriComponentsBuilder> mappingToUriComponentsBuilder, LinkBuilderCreator<T> creator,
|
||||
public static <T extends LinkBuilder> Function<Function<String, UriComponentsBuilder>, T> linkTo(
|
||||
Object invocationValue, LinkBuilderCreator<T> creator,
|
||||
BiFunction<UriComponentsBuilder, MethodInvocation, UriComponentsBuilder> additionalUriHandler) {
|
||||
|
||||
Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
|
||||
@@ -87,64 +87,66 @@ public class WebHandler {
|
||||
LastInvocationAware invocations = (LastInvocationAware) invocationValue;
|
||||
MethodInvocation invocation = invocations.getLastInvocation();
|
||||
|
||||
String mapping = DISCOVERER.getMapping(invocation.getTargetType(), invocation.getMethod());
|
||||
return mappingToUriComponentsBuilder -> {
|
||||
String mapping = DISCOVERER.getMapping(invocation.getTargetType(), invocation.getMethod());
|
||||
|
||||
UriComponentsBuilder builder = mappingToUriComponentsBuilder.apply(mapping);
|
||||
UriTemplate template = UriTemplateFactory.templateFor(mapping == null ? "/" : mapping);
|
||||
Map<String, Object> values = new HashMap<>();
|
||||
UriComponentsBuilder builder = mappingToUriComponentsBuilder.apply(mapping);
|
||||
UriTemplate template = UriTemplateFactory.templateFor(mapping == null ? "/" : mapping);
|
||||
Map<String, Object> values = new HashMap<>();
|
||||
|
||||
Iterator<String> names = template.getVariableNames().iterator();
|
||||
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
|
||||
Iterator<String> names = template.getVariableNames().iterator();
|
||||
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
|
||||
|
||||
while (classMappingParameters.hasNext()) {
|
||||
values.put(names.next(), encodePath(classMappingParameters.next()));
|
||||
}
|
||||
while (classMappingParameters.hasNext()) {
|
||||
values.put(names.next(), encodePath(classMappingParameters.next()));
|
||||
}
|
||||
|
||||
for (AnnotatedParametersParameterAccessor.BoundMethodParameter parameter : PATH_VARIABLE_ACCESSOR
|
||||
.getBoundParameters(invocation)) {
|
||||
values.put(parameter.getVariableName(), encodePath(parameter.asString()));
|
||||
}
|
||||
for (AnnotatedParametersParameterAccessor.BoundMethodParameter parameter : PATH_VARIABLE_ACCESSOR
|
||||
.getBoundParameters(invocation)) {
|
||||
values.put(parameter.getVariableName(), encodePath(parameter.asString()));
|
||||
}
|
||||
|
||||
List<String> optionalEmptyParameters = new ArrayList<>();
|
||||
List<String> optionalEmptyParameters = new ArrayList<>();
|
||||
|
||||
for (AnnotatedParametersParameterAccessor.BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR
|
||||
.getBoundParameters(invocation)) {
|
||||
for (AnnotatedParametersParameterAccessor.BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR
|
||||
.getBoundParameters(invocation)) {
|
||||
|
||||
bindRequestParameters(builder, parameter);
|
||||
bindRequestParameters(builder, parameter);
|
||||
|
||||
if (SKIP_VALUE.equals(parameter.getValue())) {
|
||||
if (SKIP_VALUE.equals(parameter.getValue())) {
|
||||
|
||||
values.put(parameter.getVariableName(), SKIP_VALUE);
|
||||
values.put(parameter.getVariableName(), SKIP_VALUE);
|
||||
|
||||
if (!parameter.isRequired()) {
|
||||
optionalEmptyParameters.add(parameter.getVariableName());
|
||||
if (!parameter.isRequired()) {
|
||||
optionalEmptyParameters.add(parameter.getVariableName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (String variable : template.getVariableNames()) {
|
||||
if (!values.containsKey(variable)) {
|
||||
values.put(variable, SKIP_VALUE);
|
||||
for (String variable : template.getVariableNames()) {
|
||||
if (!values.containsKey(variable)) {
|
||||
values.put(variable, SKIP_VALUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UriComponents components = additionalUriHandler == null //
|
||||
? builder.buildAndExpand(values)
|
||||
: additionalUriHandler.apply(builder, invocation).buildAndExpand(values);
|
||||
UriComponents components = additionalUriHandler == null //
|
||||
? builder.buildAndExpand(values)
|
||||
: additionalUriHandler.apply(builder, invocation).buildAndExpand(values);
|
||||
|
||||
TemplateVariables variables = NONE;
|
||||
TemplateVariables variables = NONE;
|
||||
|
||||
for (String parameter : optionalEmptyParameters) {
|
||||
for (String parameter : optionalEmptyParameters) {
|
||||
|
||||
boolean previousRequestParameter = components.getQueryParams().isEmpty() && variables.equals(NONE);
|
||||
TemplateVariable variable = new TemplateVariable(parameter,
|
||||
previousRequestParameter ? REQUEST_PARAM : REQUEST_PARAM_CONTINUED);
|
||||
variables = variables.concat(variable);
|
||||
}
|
||||
boolean previousRequestParameter = components.getQueryParams().isEmpty() && variables.equals(NONE);
|
||||
TemplateVariable variable = new TemplateVariable(parameter,
|
||||
previousRequestParameter ? REQUEST_PARAM : REQUEST_PARAM_CONTINUED);
|
||||
variables = variables.concat(variable);
|
||||
}
|
||||
|
||||
List<Affordance> affordances = SpringAffordanceBuilder.create(invocation, DISCOVERER, components);
|
||||
List<Affordance> affordances = SpringAffordanceBuilder.create(invocation, DISCOVERER, components);
|
||||
|
||||
return creator.createBuilder(components, variables, affordances);
|
||||
return creator.createBuilder(components, variables, affordances);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -103,25 +103,24 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<Co
|
||||
@Override
|
||||
public ControllerLinkBuilder linkTo(Object invocationValue) {
|
||||
|
||||
return WebHandler.linkTo(invocationValue, mapping -> ControllerLinkBuilder.getBuilder().path(mapping),
|
||||
ControllerLinkBuilder::new, (builder, invocation) -> {
|
||||
return WebHandler.linkTo(invocationValue, ControllerLinkBuilder::new, (builder, invocation) -> {
|
||||
|
||||
MethodParameters parameters = new MethodParameters(invocation.getMethod());
|
||||
Iterator<Object> parameterValues = Arrays.asList(invocation.getArguments()).iterator();
|
||||
MethodParameters parameters = new MethodParameters(invocation.getMethod());
|
||||
Iterator<Object> parameterValues = Arrays.asList(invocation.getArguments()).iterator();
|
||||
|
||||
for (MethodParameter parameter : parameters.getParameters()) {
|
||||
Object parameterValue = parameterValues.next();
|
||||
for (MethodParameter parameter : parameters.getParameters()) {
|
||||
Object parameterValue = parameterValues.next();
|
||||
|
||||
for (UriComponentsContributor contributor : this.uriComponentsContributors) {
|
||||
for (UriComponentsContributor contributor : this.uriComponentsContributors) {
|
||||
|
||||
if (contributor.supportsParameter(parameter)) {
|
||||
contributor.enhance(builder, parameter, parameterValue);
|
||||
}
|
||||
}
|
||||
if (contributor.supportsParameter(parameter)) {
|
||||
contributor.enhance(builder, parameter, parameterValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
});
|
||||
return builder;
|
||||
}).apply(mapping -> ControllerLinkBuilder.getBuilder().path(mapping));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -106,7 +106,7 @@ public class WebMvcLinkBuilderFactory implements MethodLinkBuilderFactory<WebMvc
|
||||
Function<String, UriComponentsBuilder> builderFactory = mapping -> UriComponentsBuilderFactory.getBuilder()
|
||||
.path(mapping);
|
||||
|
||||
return WebHandler.linkTo(invocationValue, builderFactory, WebMvcLinkBuilder::new, (builder, invocation) -> {
|
||||
return WebHandler.linkTo(invocationValue, WebMvcLinkBuilder::new, (builder, invocation) -> {
|
||||
|
||||
MethodParameters parameters = new MethodParameters(invocation.getMethod());
|
||||
Iterator<Object> parameterValues = Arrays.asList(invocation.getArguments()).iterator();
|
||||
@@ -121,7 +121,8 @@ public class WebMvcLinkBuilderFactory implements MethodLinkBuilderFactory<WebMvc
|
||||
}
|
||||
|
||||
return builder;
|
||||
});
|
||||
|
||||
}).apply(builderFactory);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -76,7 +76,7 @@ public class WebFluxLinkBuilder extends TemplateVariableAwareLinkBuilderSupport<
|
||||
* @param exchange must not be {@literal null}.
|
||||
*/
|
||||
public static WebFluxBuilder linkTo(Object invocation, ServerWebExchange exchange) {
|
||||
return new WebFluxBuilder(linkToInternal(invocation, exchange));
|
||||
return new WebFluxBuilder(linkToInternal(invocation, Mono.just(getBuilder(exchange))));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,7 +180,9 @@ public class WebFluxLinkBuilder extends TemplateVariableAwareLinkBuilderSupport<
|
||||
|
||||
Assert.notNull(invocation, "Invocation must not be null!");
|
||||
|
||||
return new WebFluxLink(link.flatMap(it -> linkToInternal(invocation) //
|
||||
Mono<WebFluxLinkBuilder> builder = linkToInternal(invocation);
|
||||
|
||||
return new WebFluxLink(link.flatMap(it -> builder //
|
||||
.flatMapIterable(WebFluxLinkBuilder::getAffordances) //
|
||||
.singleOrEmpty() //
|
||||
.map(it::andAffordance)));
|
||||
@@ -237,14 +239,20 @@ public class WebFluxLinkBuilder extends TemplateVariableAwareLinkBuilderSupport<
|
||||
|
||||
private static Mono<WebFluxLinkBuilder> linkToInternal(Object invocation) {
|
||||
|
||||
return Mono.subscriberContext() //
|
||||
.flatMap(context -> linkToInternal(invocation, context.getOrDefault(SERVER_WEB_EXCHANGE, null)));
|
||||
return linkToInternal(invocation,
|
||||
Mono.subscriberContext().map(context -> getBuilder(context.getOrDefault(SERVER_WEB_EXCHANGE, null))));
|
||||
}
|
||||
|
||||
private static Mono<WebFluxLinkBuilder> linkToInternal(Object invocation, ServerWebExchange exchange) {
|
||||
private static Mono<WebFluxLinkBuilder> linkToInternal(Object invocation, Mono<UriComponentsBuilder> exchange) {
|
||||
|
||||
return Mono.just(WebHandler.linkTo(invocation, //
|
||||
path -> getBuilder(exchange).replacePath(path == null ? "/" : path), //
|
||||
WebFluxLinkBuilder::new));
|
||||
Function<Function<String, UriComponentsBuilder>, WebFluxLinkBuilder> linkTo = //
|
||||
WebHandler.linkTo(invocation, WebFluxLinkBuilder::new);
|
||||
|
||||
return exchange.map(WebFluxLinkBuilder::getBuilderCreator) //
|
||||
.map(linkTo::apply);
|
||||
}
|
||||
|
||||
private static Function<String, UriComponentsBuilder> getBuilderCreator(UriComponentsBuilder exchange) {
|
||||
return path -> exchange.replacePath(path == null ? "/" : path);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user