From 452189edd86f1d1306c595e21aaa5b6e84de2ef3 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Fri, 6 Dec 2019 14:24:02 +0100 Subject: [PATCH] #1148 - Further performance improvements in link creation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The implementation details of WebHandler have been significantly refactored to rather work with structures that allow better cacheability by clearly separating abstractions over the statically available information from the per-invocation aspects. This results in a new HandlerMethodParameter(s) abstraction within WebHandler. BoundMethodParameter has been removed entirely. HandlerMethodParameters are create once then cached for every controller method being linked to. DummyInvocationUtils now creates a ThreadLocal cache of the proxies created for calls to methodOn(…) as they essentially only act as basis for subsequent calls to the methods on the proxy created which in turn are expected to be handed into a linkTo(…) call which obtains the invocation right away. This avoids overhead in cases methodOn(…) is called multiple times for the same controller from a single controller. The lookup of the LastInvocationAware was previously routed through the proxy, handled by InvocationRecordingMethodInterceptor. This resulted in a second, reflective call for every link creation. DummyInvocationUtils now provides a dedicated lookup method as it knows about the structure of the proxy it created and thus can unfold the recorded invocation more effectively. The LinkBuilder type hierarchy now works with UriComponents and only creates a UriComponentsBuilder if it needs to modify the backing link in the first place. This avoids superfluous back and forth between UriComponents and UriComponentsBuilders that involved quite a bit of String parsing and creation. EncodingUtils now starts from a StandardCharsets.UTF_8 to avoid repeated Charset creation. The changes result in a ~3x performance compared to 1.0.2.RELEASE: 1.0.2.RELEASE Benchmark Mode Cnt Score Error Units ControllerLinkBuilderBenchmark.noLinkCreation thrpt 10 39004583,189 ± 751668,181 ops/s ControllerLinkBuilderBenchmark.pureLinkCreation thrpt 10 43443,133 ± 783,120 ops/s ControllerLinkBuilderBenchmark.withLinkCreation thrpt 10 60201,629 ± 1292,179 ops/s 1.1 / 1.0.3 SNAPSHOT Benchmark Mode Cnt Score Error Units ControllerLinkBuilderBenchmark.noLinkCreation thrpt 10 39618560,950 ± 612794,310 ops/s ControllerLinkBuilderBenchmark.pureLinkCreation thrpt 10 121700,634 ± 1510,415 ops/s ControllerLinkBuilderBenchmark.withLinkCreation thrpt 10 121982,085 ± 3344,206 ops/s noLinkCreation - creates a single RepresentationModel instance but adds no links pureLinkCreation - creates a single link pointing to a controller method withLinkCreation - creates a single RepresentationModel instance adding a single link --- .../server/core/AnnotationAttribute.java | 33 +- .../server/core/DummyInvocationUtils.java | 46 +- .../hateoas/server/core/EncodingUtils.java | 5 +- .../server/core/LinkBuilderSupport.java | 59 +-- ...mplateVariableAwareLinkBuilderSupport.java | 17 +- .../hateoas/server/core/WebHandler.java | 458 ++++++++++-------- .../hateoas/server/mvc/BasicLinkBuilder.java | 17 +- .../server/mvc/ControllerLinkBuilder.java | 22 +- .../mvc/ControllerLinkBuilderFactory.java | 2 +- .../mvc/UriComponentsBuilderFactory.java | 5 + .../hateoas/server/mvc/WebMvcLinkBuilder.java | 20 +- .../server/mvc/WebMvcLinkBuilderFactory.java | 10 +- .../server/reactive/WebFluxLinkBuilder.java | 20 +- .../core/LinkBuilderSupportUnitTest.java | 18 +- 14 files changed, 382 insertions(+), 350 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/server/core/AnnotationAttribute.java b/src/main/java/org/springframework/hateoas/server/core/AnnotationAttribute.java index 72fd9526b..4c511ee52 100644 --- a/src/main/java/org/springframework/hateoas/server/core/AnnotationAttribute.java +++ b/src/main/java/org/springframework/hateoas/server/core/AnnotationAttribute.java @@ -16,7 +16,6 @@ package org.springframework.hateoas.server.core; import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; @@ -25,7 +24,7 @@ /** * Simply helper to reference a dedicated attribute of an {@link Annotation}. - * + * * @author Oliver Gierke */ public class AnnotationAttribute { @@ -35,7 +34,7 @@ public class AnnotationAttribute { /** * Creates a new {@link AnnotationAttribute} to the {@code value} attribute of the given {@link Annotation} type. - * + * * @param annotationType must not be {@literal null}. */ public AnnotationAttribute(Class annotationType) { @@ -44,7 +43,7 @@ public AnnotationAttribute(Class annotationType) { /** * Creates a new {@link AnnotationAttribute} for the given {@link Annotation} type and annotation attribute name. - * + * * @param annotationType must not be {@literal null}. * @param attributeName can be {@literal null}, defaults to {@code value}. */ @@ -58,7 +57,7 @@ public AnnotationAttribute(Class annotationType, @Nullable /** * Returns the annotation type. - * + * * @return the annotationType */ public Class getAnnotationType() { @@ -67,7 +66,7 @@ public Class getAnnotationType() { /** * Reads the {@link Annotation} attribute's value from the given {@link MethodParameter}. - * + * * @param parameter must not be {@literal null}. * @return */ @@ -75,27 +74,15 @@ public Class getAnnotationType() { public String getValueFrom(MethodParameter parameter) { Assert.notNull(parameter, "MethodParameter must not be null!"); - Annotation annotation = parameter.getParameterAnnotation(annotationType); - return annotation == null ? null : getValueFrom(annotation); - } - /** - * Reads the {@link Annotation} attribute's value from the given {@link AnnotatedElement}. - * - * @param annotatedElement must not be {@literal null}. - * @return - */ - @Nullable - public String getValueFrom(AnnotatedElement annotatedElement) { + Annotation annotation = parameter.getParameterAnnotation(annotationType); - Assert.notNull(annotatedElement, "Annotated element must not be null!"); - Annotation annotation = annotatedElement.getAnnotation(annotationType); return annotation == null ? null : getValueFrom(annotation); } /** * Returns the {@link Annotation} attribute's value from the given {@link Annotation}. - * + * * @param annotation must not be {@literal null}. * @return */ @@ -103,7 +90,9 @@ public String getValueFrom(AnnotatedElement annotatedElement) { public String getValueFrom(Annotation annotation) { Assert.notNull(annotation, "Annotation must not be null!"); - return (String) (attributeName == null ? AnnotationUtils.getValue(annotation) : AnnotationUtils.getValue( - annotation, attributeName)); + + return (String) (attributeName == null // + ? AnnotationUtils.getValue(annotation) // + : AnnotationUtils.getValue(annotation, attributeName)); } } diff --git a/src/main/java/org/springframework/hateoas/server/core/DummyInvocationUtils.java b/src/main/java/org/springframework/hateoas/server/core/DummyInvocationUtils.java index 3659ae777..eb8151552 100644 --- a/src/main/java/org/springframework/hateoas/server/core/DummyInvocationUtils.java +++ b/src/main/java/org/springframework/hateoas/server/core/DummyInvocationUtils.java @@ -20,9 +20,12 @@ import java.lang.reflect.Method; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; import org.aopalliance.intercept.MethodInterceptor; +import org.springframework.aop.framework.Advised; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.target.EmptyTargetSource; import org.springframework.lang.Nullable; @@ -36,6 +39,8 @@ */ public class DummyInvocationUtils { + private static final ThreadLocal, Object>> CACHE = ThreadLocal.withInitial(HashMap::new); + /** * Method interceptor that records the last method invocation and creates a proxy for the return value that exposes * the method invocation. @@ -44,18 +49,10 @@ public class DummyInvocationUtils { */ private static class InvocationRecordingMethodInterceptor implements MethodInterceptor, LastInvocationAware { - private static final Method GET_INVOCATIONS; - private static final Method GET_OBJECT_PARAMETERS; - private final Class targetType; private final Object[] objectParameters; private MethodInvocation invocation; - static { - GET_INVOCATIONS = ReflectionUtils.findMethod(LastInvocationAware.class, "getLastInvocation"); - GET_OBJECT_PARAMETERS = ReflectionUtils.findMethod(LastInvocationAware.class, "getObjectParameters"); - } - /** * Creates a new {@link InvocationRecordingMethodInterceptor} carrying the given parameters forward that might be * needed to populate the class level mapping. @@ -83,11 +80,7 @@ public Object invoke(org.aopalliance.intercept.MethodInvocation invocation) { Method method = invocation.getMethod(); - if (GET_INVOCATIONS.equals(method)) { - return getLastInvocation(); - } else if (GET_OBJECT_PARAMETERS.equals(method)) { - return getObjectParameters(); - } else if (ReflectionUtils.isObjectMethod(method)) { + if (ReflectionUtils.isObjectMethod(method)) { return ReflectionUtils.invokeMethod(method, invocation.getThis(), invocation.getArguments()); } @@ -131,12 +124,29 @@ public Iterator getObjectParameters() { * @param parameters parameters to extend template variables in the type level mapping. * @return */ + @SuppressWarnings("unchecked") public static T methodOn(Class type, Object... parameters) { Assert.notNull(type, "Given type must not be null!"); - InvocationRecordingMethodInterceptor interceptor = new InvocationRecordingMethodInterceptor(type, parameters); - return getProxyWithInterceptor(type, interceptor, type.getClassLoader()); + return (T) CACHE.get().computeIfAbsent(CacheKey.of(type, parameters), it -> { + + InvocationRecordingMethodInterceptor interceptor = new InvocationRecordingMethodInterceptor(it.type, + it.arguments); + return getProxyWithInterceptor(it.type, interceptor, type.getClassLoader()); + }); + } + + /** + * Returns the {@link LastInvocationAware} instance from the given source, that essentially has to be a proxy created + * via {@link #methodOn(Class, Object...)} and subsequent {@code linkTo(…)} calls. + * + * @param source must not be {@literal null}. + * @return + */ + @Nullable + public static LastInvocationAware getLastInvocationAware(Object source) { + return (LastInvocationAware) ((Advised) source).getAdvisors()[0].getAdvice(); } @SuppressWarnings("unchecked") @@ -157,6 +167,12 @@ private static T getProxyWithInterceptor(Class type, InvocationRecordingM return (T) factory.getProxy(classLoader); } + @Value(staticConstructor = "of") + private static class CacheKey { + Class type; + Object[] arguments; + } + @Value private static class SimpleMethodInvocation implements MethodInvocation { diff --git a/src/main/java/org/springframework/hateoas/server/core/EncodingUtils.java b/src/main/java/org/springframework/hateoas/server/core/EncodingUtils.java index 00bdd783b..b11e03a55 100644 --- a/src/main/java/org/springframework/hateoas/server/core/EncodingUtils.java +++ b/src/main/java/org/springframework/hateoas/server/core/EncodingUtils.java @@ -17,6 +17,9 @@ import lombok.experimental.UtilityClass; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + import org.springframework.util.Assert; import org.springframework.web.util.UriUtils; @@ -31,7 +34,7 @@ @UtilityClass class EncodingUtils { - private static final String ENCODING = "UTF-8"; + private static final Charset ENCODING = StandardCharsets.UTF_8; /** * Encodes the given path value. diff --git a/src/main/java/org/springframework/hateoas/server/core/LinkBuilderSupport.java b/src/main/java/org/springframework/hateoas/server/core/LinkBuilderSupport.java index 9b508a485..36f008285 100644 --- a/src/main/java/org/springframework/hateoas/server/core/LinkBuilderSupport.java +++ b/src/main/java/org/springframework/hateoas/server/core/LinkBuilderSupport.java @@ -25,7 +25,6 @@ import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.function.Function; import org.springframework.hateoas.Affordance; import org.springframework.hateoas.IanaLinkRelations; @@ -49,34 +48,28 @@ */ public abstract class LinkBuilderSupport implements LinkBuilder { - private final UriComponentsBuilder builder; private final @Getter List affordances; + private UriComponents components; + /** * Creates a new {@link LinkBuilderSupport} using the given {@link UriComponents}. * * @param builder must not be {@literal null}. */ - protected LinkBuilderSupport(UriComponentsBuilder builder) { + protected LinkBuilderSupport(UriComponents builder) { this(builder, Collections.emptyList()); } - protected LinkBuilderSupport(UriComponentsBuilder builder, List affordances) { - - Assert.notNull(builder, "UriComponentsBuilder must not be null!"); - Assert.notNull(affordances, "Affordances must not be null!"); - - this.builder = builder.cloneBuilder(); - this.affordances = affordances; - } - protected LinkBuilderSupport(UriComponents components, List affordances) { Assert.notNull(components, "UriComponents must not be null!"); Assert.notNull(affordances, "Affordances must not be null!"); - this.builder = UriComponentsBuilder.fromUriString(components.toUriString()); + // this.builder = UriComponentsBuilder.newInstance().uriComponents(components); this.affordances = affordances; + + this.components = components; } /* @@ -108,20 +101,19 @@ public T slash(@Nullable Object object) { protected T slash(UriComponents components, boolean encoded) { - return withFreshBuilder(builder -> { + UriComponentsBuilder builder = UriComponentsBuilder.newInstance().uriComponents(this.components); - for (String pathSegment : components.getPathSegments()) { - builder.pathSegment(encoded ? pathSegment : encodePath(pathSegment)); - } + for (String pathSegment : components.getPathSegments()) { + builder.pathSegment(encoded ? pathSegment : encodePath(pathSegment)); + } - String fragment = components.getFragment(); + String fragment = components.getFragment(); - if (fragment != null && !fragment.trim().isEmpty()) { - builder.fragment(encoded ? fragment : encodeFragment(fragment)); - } + if (fragment != null && !fragment.trim().isEmpty()) { + builder.fragment(encoded ? fragment : encodeFragment(fragment)); + } - return createNewInstance(builder.query(components.getQuery()), affordances); - }); + return createNewInstance(builder.query(components.getQuery()).build(), affordances); } /* @@ -129,7 +121,7 @@ protected T slash(UriComponents components, boolean encoded) { * @see org.springframework.hateoas.LinkBuilder#toUri() */ public URI toUri() { - return builder.build().toUri().normalize(); + return components.toUri().normalize(); } public T addAffordances(Collection affordances) { @@ -138,7 +130,7 @@ public T addAffordances(Collection affordances) { newAffordances.addAll(this.affordances); newAffordances.addAll(affordances); - return createNewInstance(builder, newAffordances); + return createNewInstance(components, newAffordances); } /* @@ -165,20 +157,7 @@ public Link withSelfRel() { */ @Override public String toString() { - return builder.build().toUriString(); - } - - /** - * Executes the given {@link Function} using a freshly cloned {@link UriComponentsBuilder}. - * - * @param function must not be {@literal null}. - * @return - */ - protected S withFreshBuilder(Function function) { - - Assert.notNull(function, "Function must not be null!"); - - return function.apply(builder.cloneBuilder()); + return components.toUriString(); } /** @@ -194,5 +173,5 @@ protected S withFreshBuilder(Function function) { * @param builder will never be {@literal null}. * @return */ - protected abstract T createNewInstance(UriComponentsBuilder builder, List affordances); + protected abstract T createNewInstance(UriComponents components, List affordances); } diff --git a/src/main/java/org/springframework/hateoas/server/core/TemplateVariableAwareLinkBuilderSupport.java b/src/main/java/org/springframework/hateoas/server/core/TemplateVariableAwareLinkBuilderSupport.java index a456fbc54..dc036ad23 100644 --- a/src/main/java/org/springframework/hateoas/server/core/TemplateVariableAwareLinkBuilderSupport.java +++ b/src/main/java/org/springframework/hateoas/server/core/TemplateVariableAwareLinkBuilderSupport.java @@ -20,7 +20,6 @@ import org.springframework.hateoas.Affordance; import org.springframework.hateoas.TemplateVariables; import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; /** * A {@link LinkBuilderSupport} extension that can keep a list of {@link TemplateVariables} around. @@ -32,14 +31,6 @@ public abstract class TemplateVariableAwareLinkBuilderSupport affordances) { - - super(builder, affordances); - - this.variables = variables; - } - protected TemplateVariableAwareLinkBuilderSupport(UriComponents components, TemplateVariables variables, List affordances) { @@ -50,14 +41,14 @@ protected TemplateVariableAwareLinkBuilderSupport(UriComponents components, Temp /* * (non-Javadoc) - * @see org.springframework.hateoas.core.LinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder, java.util.List) + * @see org.springframework.hateoas.server.core.LinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponents, java.util.List) */ @Override - protected final T createNewInstance(UriComponentsBuilder builder, List affordances) { - return createNewInstance(builder, affordances, variables); + protected final T createNewInstance(UriComponents components, List affordances) { + return createNewInstance(components, affordances, variables); } - protected abstract T createNewInstance(UriComponentsBuilder builder, List affordances, + protected abstract T createNewInstance(UriComponents components, List affordances, TemplateVariables variables); /* diff --git a/src/main/java/org/springframework/hateoas/server/core/WebHandler.java b/src/main/java/org/springframework/hateoas/server/core/WebHandler.java index 961cb0729..e62c02859 100644 --- a/src/main/java/org/springframework/hateoas/server/core/WebHandler.java +++ b/src/main/java/org/springframework/hateoas/server/core/WebHandler.java @@ -20,21 +20,23 @@ import static org.springframework.hateoas.server.core.EncodingUtils.*; import static org.springframework.web.util.UriComponents.UriTemplateVariables.*; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; import lombok.Value; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.stream.Collectors; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; @@ -47,6 +49,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -62,15 +65,12 @@ * Utility for taking a method invocation and extracting a {@link LinkBuilder}. * * @author Greg Turnquist + * @author Oliver Drotbohm */ public class WebHandler { private static final MappingDiscoverer DISCOVERER = CachingMappingDiscoverer .of(new AnnotationMappingDiscoverer(RequestMapping.class)); - private static final AnnotatedParametersParameterAccessor PATH_VARIABLE_ACCESSOR // - = new AnnotatedParametersParameterAccessor(new AnnotationAttribute(PathVariable.class)); - private static final AnnotatedParametersParameterAccessor REQUEST_PARAM_ACCESSOR // - = new RequestParamParameterAccessor(); private static final Map> AFFORDANCES_CACHE = new ConcurrentReferenceHashMap<>(); @@ -78,49 +78,68 @@ public interface LinkBuilderCreator { T createBuilder(UriComponents components, TemplateVariables variables, List affordances); } - public static Function, T> linkTo( - Object invocationValue, LinkBuilderCreator creator) { - return linkTo(invocationValue, creator, null); + public interface PreparedWebHandler { + T conclude(Function finisher); } - public static Function, T> linkTo( - Object invocationValue, LinkBuilderCreator creator, + public static PreparedWebHandler linkTo(Object invocationValue, + LinkBuilderCreator creator) { + return linkTo(invocationValue, creator, + (BiFunction) null); + } + + public static T linkTo(Object invocationValue, LinkBuilderCreator creator, + @Nullable BiFunction additionalUriHandler, + Function finisher) { + + return linkTo(invocationValue, creator, additionalUriHandler).conclude(finisher); + } + + private static PreparedWebHandler linkTo(Object invocationValue, + LinkBuilderCreator creator, @Nullable BiFunction additionalUriHandler) { Assert.isInstanceOf(LastInvocationAware.class, invocationValue); - LastInvocationAware invocations = (LastInvocationAware) invocationValue; + LastInvocationAware invocations = (LastInvocationAware) DummyInvocationUtils + .getLastInvocationAware(invocationValue); + + if (invocations == null) { + throw new IllegalStateException(String.format("Could not obtain previous invocation from %s!", invocationValue)); + } + MethodInvocation invocation = invocations.getLastInvocation(); - return mappingToUriComponentsBuilder -> { + String mapping = DISCOVERER.getMapping(invocation.getTargetType(), invocation.getMethod()); - String mapping = DISCOVERER.getMapping(invocation.getTargetType(), invocation.getMethod()); + return finisher -> { - UriComponentsBuilder builder = mappingToUriComponentsBuilder.apply(mapping); + UriComponentsBuilder builder = finisher.apply(mapping); UriTemplate template = UriTemplateFactory.templateFor(mapping == null ? "/" : mapping); Map values = new HashMap<>(); - Iterator names = template.getVariableNames().iterator(); + List variableNames = template.getVariableNames(); + Iterator names = variableNames.iterator(); Iterator classMappingParameters = invocations.getObjectParameters(); while (classMappingParameters.hasNext()) { values.put(names.next(), encodePath(classMappingParameters.next())); } - for (AnnotatedParametersParameterAccessor.BoundMethodParameter parameter : PATH_VARIABLE_ACCESSOR - .getBoundParameters(invocation)) { + HandlerMethodParameters parameters = HandlerMethodParameters.of(invocation.getMethod()); + Object[] arguments = invocation.getArguments(); - values.put(parameter.getVariableName(), encodePath(parameter.asString())); + for (HandlerMethodParameter parameter : parameters.getParameterAnnotatedWith(PathVariable.class, arguments)) { + values.put(parameter.getVariableName(), encodePath(parameter.getValueAsString(arguments))); } List optionalEmptyParameters = new ArrayList<>(); - for (AnnotatedParametersParameterAccessor.BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR - .getBoundParameters(invocation)) { + for (HandlerMethodParameter parameter : parameters.getParameterAnnotatedWith(RequestParam.class, arguments)) { - bindRequestParameters(builder, parameter); + bindRequestParameters(builder, parameter, arguments); - if (SKIP_VALUE.equals(parameter.getValue())) { + if (SKIP_VALUE.equals(parameter.getVerifiedValue(arguments))) { values.put(parameter.getVariableName(), SKIP_VALUE); @@ -130,14 +149,14 @@ public static Function Function affordances = AFFORDANCES_CACHE.computeIfAbsent( - AffordanceKey.of(invocation.getTargetType(), invocation.getMethod(), href), - key -> SpringAffordanceBuilder.create(key.type, key.method, key.href, DISCOVERER)); + AffordanceKey.of(invocation.getTargetType(), invocation.getMethod(), components), + key -> SpringAffordanceBuilder.create(key.type, key.method, key.href.toUriString(), DISCOVERER)); return creator.createBuilder(components, variables, affordances); }; @@ -168,10 +185,15 @@ public static Function type; + Method method; + UriComponents href; + } - /* - * (non-Javadoc) - * @see org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor#createParameter(org.springframework.core.MethodParameter, java.lang.Object, org.springframework.hateoas.core.AnnotationAttribute) - */ - @Override - protected BoundMethodParameter createParameter(final MethodParameter parameter, @Nullable Object value, - AnnotationAttribute attribute) { + private static class HandlerMethodParameters { - return new BoundMethodParameter(parameter, value, attribute) { + private static final List> ANNOTATIONS = Arrays.asList(RequestParam.class, + PathVariable.class); + private static final Map CACHE = new ConcurrentHashMap(); - /* - * (non-Javadoc) - * @see org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor.BoundMethodParameter#isRequired() - */ - @Override - public boolean isRequired() { + private final MultiValueMap, HandlerMethodParameter> byAnnotationCache; - RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class); + private HandlerMethodParameters(MethodParameters parameters) { - if (parameter.isOptional()) { - return false; - } + this.byAnnotationCache = new LinkedMultiValueMap<>(); - return annotation != null && annotation.required() // - && annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE); - } - }; + for (Class annotation : ANNOTATIONS) { + + this.byAnnotationCache.putAll(parameters.getParametersWith(annotation).stream() // + .map(it -> HandlerMethodParameter.of(it, annotation)) // + .collect(Collectors.groupingBy(HandlerMethodParameter::getAnnotationType, LinkedMultiValueMap::new, + Collectors.toList()))); + } } - /* - * (non-Javadoc) - * @see org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor#verifyParameterValue(org.springframework.core.MethodParameter, java.lang.Object) - */ - @Override - @Nullable - protected Object verifyParameterValue(MethodParameter parameter, @Nullable Object value) { + public static HandlerMethodParameters of(Method method) { - RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class); + return CACHE.computeIfAbsent(method, it -> { - value = ObjectUtils.unwrapOptional(value); + MethodParameters parameters = MethodParameters.of(it); + return new HandlerMethodParameters(parameters); + }); + } - if (value != null) { - return value; + public List getParameterAnnotatedWith(Class annotation, + Object[] arguments) { + + List parameters = byAnnotationCache.get(annotation); + + if (parameters == null) { + return Collections.emptyList(); } - if (!(annotation != null && annotation.required()) || parameter.isOptional()) { - return SKIP_VALUE; + List result = new ArrayList<>(); + + for (HandlerMethodParameter parameter : parameters) { + if (parameter.getVerifiedValue(arguments) != null) { + result.add(parameter); + } } - return annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE) ? SKIP_VALUE : null; + return result; } } - /** - * Value object to allow accessing {@link MethodInvocation} parameters with the configured - * {@link AnnotationAttribute}. - * - * @author Oliver Gierke - */ - @RequiredArgsConstructor - private static class AnnotatedParametersParameterAccessor { + private abstract static class HandlerMethodParameter { - private final @NonNull AnnotationAttribute attribute; + private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService(); + private static final TypeDescriptor STRING_DESCRIPTOR = TypeDescriptor.valueOf(String.class); + private static final Map, Function> FACTORY; + private static final String NO_PARAMETER_NAME = "Could not determine name of parameter %s! Make sure you compile with parameter information or explicitly define a parameter name in %s."; - /** - * Returns {@link BoundMethodParameter}s contained in the given {@link MethodInvocation}. - * - * @param invocation must not be {@literal null}. - * @return - */ - public List getBoundParameters(MethodInvocation invocation) { + static { + FACTORY = new HashMap<>(); + FACTORY.put(RequestParam.class, RequestParamParameter::new); + FACTORY.put(PathVariable.class, PathVariableParameter::new); + } - Assert.notNull(invocation, "MethodInvocation must not be null!"); + private final MethodParameter parameter; + private final AnnotationAttribute attribute; + private final TypeDescriptor typeDescriptor; - MethodParameters parameters = MethodParameters.of(invocation.getMethod()); - Object[] arguments = invocation.getArguments(); - List result = new ArrayList<>(); + private String variableName; - for (MethodParameter parameter : parameters.getParametersWith(attribute.getAnnotationType())) { + /** + * Creates a new {@link HandlerMethodParameter} for the given {@link MethodParameter} and + * {@link AnnotationAttribute}. + * + * @param parameter + * @param attribute + */ + private HandlerMethodParameter(MethodParameter parameter, AnnotationAttribute attribute) { - Object value = arguments[parameter.getParameterIndex()]; - Object verifiedValue = verifyParameterValue(parameter, value); + this.parameter = parameter; + this.attribute = attribute; - if (verifiedValue != null) { - result.add(createParameter(parameter, verifiedValue, attribute)); - } - } + int nestingIndex = Optional.class.isAssignableFrom(parameter.getParameterType()) ? 1 : 0; - return result; + this.typeDescriptor = TypeDescriptor.nested(parameter, nestingIndex); } /** - * Create the {@link BoundMethodParameter} for the given {@link MethodParameter}, parameter value and - * {@link AnnotationAttribute}. + * Creates a new {@link HandlerMethodParameter} for the given {@link MethodParameter} and annotation type. * * @param parameter must not be {@literal null}. - * @param value can be {@literal null}. - * @param attribute must not be {@literal null}. + * @param type must not be {@literal null}. * @return */ - protected BoundMethodParameter createParameter(MethodParameter parameter, @Nullable Object value, - AnnotationAttribute attribute) { - return new BoundMethodParameter(parameter, value, attribute); + public static HandlerMethodParameter of(MethodParameter parameter, Class type) { + + Function function = FACTORY.get(type); + + if (function == null) { + throw new IllegalArgumentException(String.format("Unsupported annotation type %s!", type.getName())); + } + + return function.apply(parameter); } - /** - * Callback to verify the parameter values given for a dummy invocation. Default implementation rejects - * {@literal null} values as they indicate an invalid dummy call. - * - * @param parameter will never be {@literal null}. - * @param value could be {@literal null}. - * @return the verified value. - */ - @Nullable - protected Object verifyParameterValue(MethodParameter parameter, @Nullable Object value) { - return value; + Class getAnnotationType() { + return attribute.getAnnotationType(); } - /** - * Represents a {@link MethodParameter} alongside the value it has been bound to. - * - * @author Oliver Gierke - */ - static class BoundMethodParameter { + public String getVariableName() { - private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService(); - private static final TypeDescriptor STRING_DESCRIPTOR = TypeDescriptor.valueOf(String.class); + if (variableName == null) { + this.variableName = determineVariableName(); + } - private final MethodParameter parameter; - private final Object value; - private final AnnotationAttribute attribute; - private final TypeDescriptor parameterTypeDescriptor; + return variableName; + } - /** - * Creates a new {@link BoundMethodParameter} - * - * @param parameter must not be {@literal null}. - * @param value can be {@literal null}. - * @param attribute can be {@literal null}. - */ - public BoundMethodParameter(MethodParameter parameter, @Nullable Object value, AnnotationAttribute attribute) { + public String getValueAsString(Object[] values) { - Assert.notNull(parameter, "MethodParameter must not be null!"); + Object value = values[parameter.getParameterIndex()]; - boolean isOptionalWrapper = Optional.class.isAssignableFrom(parameter.getParameterType()); + if (value == null) { + throw new IllegalArgumentException("Cannot turn null value into required String!"); + } - this.parameter = parameter; - this.value = value; - this.attribute = attribute; - this.parameterTypeDescriptor = TypeDescriptor.nested(parameter, isOptionalWrapper ? 1 : 0); + if (String.class.isInstance(value)) { + return (String) value; } - /** - * Returns the name of the {@link UriTemplate} variable to be bound. The name will be derived from the configured - * {@link AnnotationAttribute} or the {@link MethodParameter} name as fallback. - * - * @return - */ - @Nullable - public String getVariableName() { - - if (attribute == null) { - return parameter.getParameterName(); - } + value = ObjectUtils.unwrapOptional(value); - Annotation annotation = parameter.getParameterAnnotation(attribute.getAnnotationType()); - String annotationAttributeValue = annotation != null ? attribute.getValueFrom(annotation) : ""; + Object result = CONVERSION_SERVICE.convert(value, typeDescriptor, STRING_DESCRIPTOR); - return StringUtils.hasText(annotationAttributeValue) ? annotationAttributeValue : parameter.getParameterName(); + if (result == null) { + throw new IllegalArgumentException(String.format("Conversion of value %s resulted in null!", value)); } - /** - * Returns the raw value bound to the {@link MethodParameter}. - * - * @return - */ - @Nullable - public Object getValue() { - return value; + return (String) result; + } + + private String determineVariableName() { + + if (attribute == null) { + + this.variableName = parameter.getParameterName(); + + return variableName; } - /** - * Returns the bound value converted into a {@link String} based on default conversion service setup. - * - * @return - */ - public String asString() { + Annotation annotation = parameter.getParameterAnnotation(attribute.getAnnotationType()); + String parameterName = annotation != null ? attribute.getValueFrom(annotation) : ""; - Object value = this.value; + if (parameterName != null && StringUtils.hasText(parameterName)) { + return parameterName; + } - if (value == null) { - throw new IllegalStateException("Cannot turn null value into required String!"); - } + parameterName = parameter.getParameterName(); - Object result = CONVERSION_SERVICE.convert(value, parameterTypeDescriptor, STRING_DESCRIPTOR); + if (parameterName == null) { + throw new IllegalStateException(String.format(NO_PARAMETER_NAME, parameter, attribute.getAnnotationType())); + } - if (result == null) { - throw new IllegalStateException(String.format("Conversion of value %s resulted in null!", value)); - } + return parameterName; + } + + /** + * Returns the value for the underlying {@link MethodParameter} potentially applying validation. + * + * @param values must not be {@literal null}. + * @return + */ + @Nullable + public Object getVerifiedValue(Object[] values) { + return values[parameter.getParameterIndex()]; + } - return (String) result; + public abstract boolean isRequired(); + } + + /** + * {@link HandlerMethodParameter} implementation to work with {@link RequestParam}. + * + * @author Oliver Drotbohm + */ + private static class RequestParamParameter extends HandlerMethodParameter { + + private final MethodParameter parameter; + + public RequestParamParameter(MethodParameter parameter) { + + super(parameter, new AnnotationAttribute(RequestParam.class)); + + this.parameter = parameter; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.server.core.WebHandler.HandlerMethodParameter#isRequired() + */ + @Override + public boolean isRequired() { + + RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class); + + if (parameter.isOptional()) { + return false; } - /** - * Returns whether the given parameter is a required one. Defaults to {@literal true}. - * - * @return - */ - public boolean isRequired() { - return true; + return annotation != null && annotation.required() // + && annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.server.core.WebHandler.HandlerMethodParameter#verifyValue(java.lang.Object[]) + */ + @Override + @Nullable + public Object getVerifiedValue(Object[] values) { + + Object value = ObjectUtils.unwrapOptional(values[parameter.getParameterIndex()]); + + if (value != null) { + return value; } + + RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class); + + if (!(annotation != null && annotation.required()) || parameter.isOptional()) { + return SKIP_VALUE; + } + + return annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE) ? SKIP_VALUE : null; } } - @Value(staticConstructor = "of") - private static class AffordanceKey { + /** + * {@link HandlerMethodParameter} extension dealing with {@link PathVariable} parameters. + * + * @author Oliver Drotbohm + */ + private static class PathVariableParameter extends HandlerMethodParameter { - Class type; - Method method; - String href; + public PathVariableParameter(MethodParameter parameter) { + super(parameter, new AnnotationAttribute(PathVariable.class)); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.server.core.WebHandler.HandlerMethodParameter#isRequired() + */ + @Override + public boolean isRequired() { + return true; + } } } diff --git a/src/main/java/org/springframework/hateoas/server/mvc/BasicLinkBuilder.java b/src/main/java/org/springframework/hateoas/server/mvc/BasicLinkBuilder.java index ab6176c9b..8df572d9e 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/BasicLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/BasicLinkBuilder.java @@ -21,6 +21,7 @@ import org.springframework.hateoas.server.LinkBuilder; import org.springframework.hateoas.server.core.LinkBuilderSupport; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -35,12 +36,12 @@ public class BasicLinkBuilder extends LinkBuilderSupport { * * @param builder must not be {@literal null}. */ - private BasicLinkBuilder(UriComponentsBuilder builder) { - super(builder); + private BasicLinkBuilder(UriComponents components) { + super(components); } - private BasicLinkBuilder(UriComponentsBuilder builder, List affordances) { - super(builder, affordances); + private BasicLinkBuilder(UriComponents components, List affordances) { + super(components, affordances); } /** @@ -49,16 +50,16 @@ private BasicLinkBuilder(UriComponentsBuilder builder, List affordan * @return */ public static BasicLinkBuilder linkToCurrentMapping() { - return new BasicLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()); + return new BasicLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping().build()); } /* * (non-Javadoc) - * @see org.springframework.hateoas.server.core.LinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder, java.util.List) + * @see org.springframework.hateoas.server.core.LinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponents, java.util.List) */ @Override - protected BasicLinkBuilder createNewInstance(UriComponentsBuilder builder, List affordances) { - return new BasicLinkBuilder(builder, affordances); + protected BasicLinkBuilder createNewInstance(UriComponents components, List affordances) { + return new BasicLinkBuilder(components, affordances); } /* diff --git a/src/main/java/org/springframework/hateoas/server/mvc/ControllerLinkBuilder.java b/src/main/java/org/springframework/hateoas/server/mvc/ControllerLinkBuilder.java index c0e7ff47b..d566809b8 100755 --- a/src/main/java/org/springframework/hateoas/server/mvc/ControllerLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/ControllerLinkBuilder.java @@ -63,16 +63,12 @@ public class ControllerLinkBuilder extends TemplateVariableAwareLinkBuilderSuppo * * @param builder must not be {@literal null}. */ - ControllerLinkBuilder(UriComponentsBuilder builder) { - this(builder, TemplateVariables.NONE, Collections.emptyList()); + ControllerLinkBuilder(UriComponents components) { + this(components, TemplateVariables.NONE, Collections.emptyList()); } - ControllerLinkBuilder(UriComponentsBuilder builder, TemplateVariables variables, List affordances) { - super(builder, variables, affordances); - } - - ControllerLinkBuilder(UriComponents uriComponents, TemplateVariables variables, List affordances) { - super(uriComponents, variables, affordances); + ControllerLinkBuilder(UriComponents components, TemplateVariables variables, List affordances) { + super(components, variables, affordances); } /** @@ -104,7 +100,7 @@ public static ControllerLinkBuilder linkTo(Class controller, Object... parame UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(mapping == null ? "/" : mapping); UriComponents uriComponents = HANDLER.expandAndEncode(builder, parameters); - return new ControllerLinkBuilder(getBuilder()).slash(uriComponents, true); + return new ControllerLinkBuilder(UriComponentsBuilderFactory.getComponents()).slash(uriComponents, true); } /** @@ -126,7 +122,7 @@ public static ControllerLinkBuilder linkTo(Class controller, Map p UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(mapping == null ? "/" : mapping); UriComponents uriComponents = HANDLER.expandAndEncode(builder, parameters); - return new ControllerLinkBuilder(getBuilder()).slash(uriComponents, true); + return new ControllerLinkBuilder(UriComponentsBuilderFactory.getComponents()).slash(uriComponents, true); } /* @@ -147,7 +143,7 @@ public static ControllerLinkBuilder linkTo(Class controller, Method method, O UriTemplate template = UriTemplateFactory.templateFor(DISCOVERER.getMapping(controller, method)); URI uri = template.expand(parameters); - return new ControllerLinkBuilder(getBuilder()).slash(uri); + return new ControllerLinkBuilder(UriComponentsBuilderFactory.getComponents()).slash(uri); } /** @@ -225,9 +221,9 @@ protected ControllerLinkBuilder getThis() { * @see org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder, java.util.List, org.springframework.hateoas.TemplateVariables) */ @Override - protected ControllerLinkBuilder createNewInstance(UriComponentsBuilder builder, List affordances, + protected ControllerLinkBuilder createNewInstance(UriComponents components, List affordances, TemplateVariables variables) { - return new ControllerLinkBuilder(builder, variables, affordances); + return new ControllerLinkBuilder(components, variables, affordances); } /** diff --git a/src/main/java/org/springframework/hateoas/server/mvc/ControllerLinkBuilderFactory.java b/src/main/java/org/springframework/hateoas/server/mvc/ControllerLinkBuilderFactory.java index e3c309faa..9ef8736b1 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/ControllerLinkBuilderFactory.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/ControllerLinkBuilderFactory.java @@ -120,7 +120,7 @@ public ControllerLinkBuilder linkTo(Object invocationValue) { } return builder; - }).apply(mapping -> ControllerLinkBuilder.getBuilder().path(mapping)); + }, mapping -> ControllerLinkBuilder.getBuilder().path(mapping)); } /* diff --git a/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsBuilderFactory.java b/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsBuilderFactory.java index 20436d235..35e016423 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsBuilderFactory.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsBuilderFactory.java @@ -23,6 +23,7 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -56,6 +57,10 @@ public static UriComponentsBuilder getBuilder() { : cacheBaseUri(ServletUriComponentsBuilder.fromCurrentServletMapping()); } + public static UriComponents getComponents() { + return getBuilder().build(); + } + private static RequestAttributes getRequestAttributes() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); diff --git a/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilder.java b/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilder.java index a43b03a03..644d140ee 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilder.java @@ -61,12 +61,8 @@ public class WebMvcLinkBuilder extends TemplateVariableAwareLinkBuilderSupport affordances) { - super(builder, variables, affordances); + WebMvcLinkBuilder(UriComponents components) { + this(components, TemplateVariables.NONE, Collections.emptyList()); } WebMvcLinkBuilder(UriComponents uriComponents, TemplateVariables variables, List affordances) { @@ -102,7 +98,7 @@ public static WebMvcLinkBuilder linkTo(Class controller, Object... parameters UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(mapping == null ? "/" : mapping); UriComponents uriComponents = HANDLER.expandAndEncode(builder, parameters); - return new WebMvcLinkBuilder(UriComponentsBuilderFactory.getBuilder()).slash(uriComponents, true); + return new WebMvcLinkBuilder(UriComponentsBuilderFactory.getComponents()).slash(uriComponents, true); } /** @@ -124,7 +120,7 @@ public static WebMvcLinkBuilder linkTo(Class controller, Map param UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(mapping == null ? "/" : mapping); UriComponents uriComponents = HANDLER.expandAndEncode(builder, parameters); - return new WebMvcLinkBuilder(UriComponentsBuilderFactory.getBuilder()).slash(uriComponents, true); + return new WebMvcLinkBuilder(UriComponentsBuilderFactory.getComponents()).slash(uriComponents, true); } /* @@ -146,7 +142,7 @@ public static WebMvcLinkBuilder linkTo(Class controller, Method method, Objec UriTemplate template = UriTemplateFactory.templateFor(mapping); URI uri = template.expand(parameters); - return new WebMvcLinkBuilder(UriComponentsBuilderFactory.getBuilder()).slash(uri); + return new WebMvcLinkBuilder(UriComponentsBuilderFactory.getComponents()).slash(uri); } /** @@ -221,12 +217,12 @@ protected WebMvcLinkBuilder getThis() { /* * (non-Javadoc) - * @see org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder, java.util.List, org.springframework.hateoas.TemplateVariables) + * @see org.springframework.hateoas.server.core.TemplateVariableAwareLinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponents, java.util.List, org.springframework.hateoas.TemplateVariables) */ @Override - protected WebMvcLinkBuilder createNewInstance(UriComponentsBuilder builder, List affordances, + protected WebMvcLinkBuilder createNewInstance(UriComponents components, List affordances, TemplateVariables variables) { - return new WebMvcLinkBuilder(builder, variables, affordances); + return new WebMvcLinkBuilder(components, variables, affordances); } /** diff --git a/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactory.java b/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactory.java index 1f297ef1c..eabe7723a 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactory.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactory.java @@ -115,14 +115,16 @@ public WebMvcLinkBuilder linkTo(Object invocationValue) { Object parameterValue = parameterValues.next(); - uriComponentsContributors.stream() // - .filter(it -> it.supportsParameter(parameter)) // - .forEach(it -> it.enhance(builder, parameter, parameterValue)); + for (UriComponentsContributor contributor : uriComponentsContributors) { + if (contributor.supportsParameter(parameter)) { + contributor.enhance(builder, parameter, parameterValue); + } + } } return builder; - }).apply(builderFactory); + }, builderFactory); } /* diff --git a/src/main/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilder.java b/src/main/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilder.java index 62039a844..fcb0d901a 100644 --- a/src/main/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilder.java @@ -18,11 +18,11 @@ import static org.springframework.web.filter.reactive.ServerWebExchangeContextFilter.*; import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Mono; import java.util.List; import java.util.function.Function; -import reactor.core.publisher.Mono; import org.springframework.hateoas.Affordance; import org.springframework.hateoas.IanaLinkRelations; import org.springframework.hateoas.Link; @@ -31,6 +31,7 @@ import org.springframework.hateoas.server.core.DummyInvocationUtils; import org.springframework.hateoas.server.core.TemplateVariableAwareLinkBuilderSupport; import org.springframework.hateoas.server.core.WebHandler; +import org.springframework.hateoas.server.core.WebHandler.PreparedWebHandler; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.server.ServerWebExchange; @@ -46,10 +47,6 @@ */ public class WebFluxLinkBuilder extends TemplateVariableAwareLinkBuilderSupport { - private WebFluxLinkBuilder(UriComponentsBuilder builder, TemplateVariables variables, List affordances) { - super(builder, variables, affordances); - } - private WebFluxLinkBuilder(UriComponents components, TemplateVariables variables, List affordances) { super(components, variables, affordances); } @@ -89,17 +86,18 @@ public static WebFluxBuilder linkTo(Object invocation, ServerWebExchange exchang * @return */ public static T methodOn(Class controller, Object... parameters) { + return DummyInvocationUtils.methodOn(controller, parameters); } /* * (non-Javadoc) - * @see org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder, java.util.List, org.springframework.hateoas.TemplateVariables) + * @see org.springframework.hateoas.server.core.TemplateVariableAwareLinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponents, java.util.List, org.springframework.hateoas.TemplateVariables) */ @Override - protected WebFluxLinkBuilder createNewInstance(UriComponentsBuilder builder, List affordances, + protected WebFluxLinkBuilder createNewInstance(UriComponents components, List affordances, TemplateVariables variables) { - return new WebFluxLinkBuilder(builder, variables, affordances); + return new WebFluxLinkBuilder(components, variables, affordances); } /* @@ -246,11 +244,9 @@ private static Mono linkToInternal(Object invocation) { private static Mono linkToInternal(Object invocation, Mono exchange) { - Function, WebFluxLinkBuilder> linkTo = // - WebHandler.linkTo(invocation, WebFluxLinkBuilder::new); + PreparedWebHandler handler = WebHandler.linkTo(invocation, WebFluxLinkBuilder::new); - return exchange.map(WebFluxLinkBuilder::getBuilderCreator) // - .map(linkTo); + return exchange.map(WebFluxLinkBuilder::getBuilderCreator).map(handler::conclude); } private static Function getBuilderCreator(UriComponentsBuilder exchange) { diff --git a/src/test/java/org/springframework/hateoas/server/core/LinkBuilderSupportUnitTest.java b/src/test/java/org/springframework/hateoas/server/core/LinkBuilderSupportUnitTest.java index daa1d24e1..05a41b747 100755 --- a/src/test/java/org/springframework/hateoas/server/core/LinkBuilderSupportUnitTest.java +++ b/src/test/java/org/springframework/hateoas/server/core/LinkBuilderSupportUnitTest.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import org.springframework.hateoas.Affordance; import org.springframework.hateoas.TestUtils; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -36,14 +37,16 @@ class LinkBuilderSupportUnitTest extends TestUtils { @Test void callingSlashWithEmptyStringIsNoOp() { - SampleLinkBuilder builder = new SampleLinkBuilder(UriComponentsBuilder.newInstance(), Collections.emptyList()); + SampleLinkBuilder builder = new SampleLinkBuilder(UriComponentsBuilder.newInstance().build(), + Collections.emptyList()); assertThat(builder.slash("")).isEqualTo(builder); } @Test void appendsFragmentCorrectly() { - SampleLinkBuilder builder = new SampleLinkBuilder(UriComponentsBuilder.newInstance(), Collections.emptyList()); + SampleLinkBuilder builder = new SampleLinkBuilder(UriComponentsBuilder.newInstance().build(), + Collections.emptyList()); builder = builder.slash("foo#bar"); assertThat(builder.toString()).endsWith("foo#bar"); builder = builder.slash("bar"); @@ -62,7 +65,8 @@ void appendsFragmentCorrectly() { @Test void appendsPathContainingColonsCorrectly() { - SampleLinkBuilder builder = new SampleLinkBuilder(UriComponentsBuilder.newInstance(), Collections.emptyList()); + SampleLinkBuilder builder = new SampleLinkBuilder(UriComponentsBuilder.newInstance().build(), + Collections.emptyList()); builder = builder.slash("47:11"); @@ -71,8 +75,8 @@ void appendsPathContainingColonsCorrectly() { static class SampleLinkBuilder extends LinkBuilderSupport { - public SampleLinkBuilder(UriComponentsBuilder builder, List afforances) { - super(builder, afforances); + public SampleLinkBuilder(UriComponents components, List afforances) { + super(components, afforances); } @Override @@ -85,8 +89,8 @@ protected SampleLinkBuilder getThis() { * @see org.springframework.hateoas.core.LinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder, java.util.List) */ @Override - protected SampleLinkBuilder createNewInstance(UriComponentsBuilder builder, List affordances) { - return new SampleLinkBuilder(builder, affordances); + protected SampleLinkBuilder createNewInstance(UriComponents components, List affordances) { + return new SampleLinkBuilder(components, affordances); } } }