#977 - Performance improvements in link generation.
Introduced more caches for reusable value objects: UriTemplate, MethodParameters. Cache UriTemplate.toString() to avoid re-computation. Avoid recomputing the affordances if possible in WebHandler. Removed cache in AnnotatedParametersParameterAccessor as that one now transitively benefits from the one introduced in MethodParameters. Related ticket: spring-projects/spring-framework#22730.
This commit is contained in:
@@ -118,7 +118,7 @@ public class Link implements Serializable {
|
||||
* @param rel must not be {@literal null} or empty.
|
||||
*/
|
||||
public Link(String href, String rel) {
|
||||
this(new UriTemplate(href), LinkRelation.of(rel));
|
||||
this(UriTemplate.of(href), LinkRelation.of(rel));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +128,7 @@ public class Link implements Serializable {
|
||||
* @param rel must not be {@literal null} or empty.
|
||||
*/
|
||||
public Link(String href, LinkRelation rel) {
|
||||
this(new UriTemplate(href), rel);
|
||||
this(UriTemplate.of(href), rel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,7 +299,7 @@ public class Link implements Serializable {
|
||||
*/
|
||||
@JsonIgnore
|
||||
public List<String> getVariableNames() {
|
||||
return getUriTemplate().getVariableNames();
|
||||
return template.getVariableNames();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -309,7 +309,7 @@ public class Link implements Serializable {
|
||||
*/
|
||||
@JsonIgnore
|
||||
public List<TemplateVariable> getVariables() {
|
||||
return getUriTemplate().getVariables();
|
||||
return template.getVariables();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,7 +318,7 @@ public class Link implements Serializable {
|
||||
* @return
|
||||
*/
|
||||
public boolean isTemplated() {
|
||||
return !getUriTemplate().getVariables().isEmpty();
|
||||
return !template.getVariables().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,7 +328,7 @@ public class Link implements Serializable {
|
||||
* @return
|
||||
*/
|
||||
public Link expand(Object... arguments) {
|
||||
return new Link(getUriTemplate().expand(arguments).toString(), getRel());
|
||||
return new Link(template.expand(arguments).toString(), getRel());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -338,7 +338,7 @@ public class Link implements Serializable {
|
||||
* @return
|
||||
*/
|
||||
public Link expand(Map<String, ? extends Object> arguments) {
|
||||
return new Link(getUriTemplate().expand(arguments).toString(), getRel());
|
||||
return new Link(template.expand(arguments).toString(), getRel());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,15 +390,6 @@ public class Link implements Serializable {
|
||||
return this.rel.isSameAs(rel);
|
||||
}
|
||||
|
||||
private UriTemplate getUriTemplate() {
|
||||
|
||||
if (this.template == null) {
|
||||
this.template = new UriTemplate(href);
|
||||
}
|
||||
|
||||
return this.template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current href as URI after expanding the links without any arguments, i.e. all optional URI
|
||||
* {@link TemplateVariable}s will be dropped. If the href contains mandatory {@link TemplateVariable}s, the URI
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -46,7 +47,10 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
|
||||
private static final Pattern VARIABLE_REGEX = Pattern.compile("\\{([\\?\\&#/]?)([\\w\\,*]+)\\}");
|
||||
private static final long serialVersionUID = -1007874653930162262L;
|
||||
|
||||
private static final Map<String, UriTemplate> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private final TemplateVariables variables;
|
||||
private String toString;
|
||||
private String baseUri;
|
||||
|
||||
/**
|
||||
@@ -54,7 +58,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
|
||||
*
|
||||
* @param template must not be {@literal null} or empty.
|
||||
*/
|
||||
public UriTemplate(String template) {
|
||||
private UriTemplate(String template) {
|
||||
|
||||
Assert.hasText(template, "Template must not be null or empty!");
|
||||
|
||||
@@ -97,7 +101,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
|
||||
* @param baseUri must not be {@literal null} or empty.
|
||||
* @param variables must not be {@literal null}.
|
||||
*/
|
||||
public UriTemplate(String baseUri, TemplateVariables variables) {
|
||||
UriTemplate(String baseUri, TemplateVariables variables) {
|
||||
|
||||
Assert.hasText(baseUri, "Base URI must not be null or empty!");
|
||||
Assert.notNull(variables, "Template variables must not be null!");
|
||||
@@ -106,6 +110,19 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
|
||||
this.variables = variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link UriTemplate} for the given {@link String} template.
|
||||
*
|
||||
* @param template must not be {@literal null} or empty.
|
||||
* @return
|
||||
*/
|
||||
public static UriTemplate of(String template) {
|
||||
|
||||
Assert.hasText(template, "Template must not be null or empty!");
|
||||
|
||||
return CACHE.computeIfAbsent(template, UriTemplate::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link UriTemplate} with the current {@link TemplateVariable}s augmented with the given ones.
|
||||
*
|
||||
@@ -267,10 +284,15 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
UriComponents components = UriComponentsBuilder.fromUriString(baseUri).build();
|
||||
boolean hasQueryParameters = !components.getQueryParams().isEmpty();
|
||||
if (toString == null) {
|
||||
|
||||
return baseUri + getOptionalVariables().toString(hasQueryParameters);
|
||||
UriComponents components = UriComponentsBuilder.fromUriString(baseUri).build();
|
||||
boolean hasQueryParameters = !components.getQueryParams().isEmpty();
|
||||
|
||||
this.toString = baseUri + getOptionalVariables().toString(hasQueryParameters);
|
||||
}
|
||||
|
||||
return toString;
|
||||
}
|
||||
|
||||
private TemplateVariables getOptionalVariables() {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.hateoas.client;
|
||||
|
||||
import static org.springframework.http.HttpMethod.GET;
|
||||
import static org.springframework.http.HttpMethod.*;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
@@ -385,15 +385,14 @@ public class Traverson {
|
||||
|
||||
UriStringAndHeaders uriAndHeaders = getAndFindLinkWithRel(baseUri.toString(), rels.iterator(), HttpHeaders.EMPTY);
|
||||
|
||||
return new UriStringAndHeaders(new UriTemplate(uriAndHeaders.getUri()).toString(),
|
||||
uriAndHeaders.getHttpHeaders());
|
||||
return new UriStringAndHeaders(UriTemplate.of(uriAndHeaders.getUri()).toString(), uriAndHeaders.getHttpHeaders());
|
||||
}
|
||||
|
||||
private URIAndHeaders traverseToExpandedFinalUrl() {
|
||||
|
||||
UriStringAndHeaders uriAndHeaders = getAndFindLinkWithRel(baseUri.toString(), rels.iterator(), HttpHeaders.EMPTY);
|
||||
|
||||
return new URIAndHeaders(new UriTemplate(uriAndHeaders.getUri()).expand(templateParameters),
|
||||
return new URIAndHeaders(UriTemplate.of(uriAndHeaders.getUri()).expand(templateParameters),
|
||||
uriAndHeaders.getHttpHeaders());
|
||||
}
|
||||
|
||||
@@ -404,7 +403,7 @@ public class Traverson {
|
||||
}
|
||||
|
||||
HttpEntity<?> request = prepareRequest(mergeHeaders(this.headers, extraHeaders));
|
||||
URI target = new UriTemplate(uri).expand();
|
||||
URI target = UriTemplate.of(uri).expand();
|
||||
|
||||
ResponseEntity<String> responseEntity = operations.exchange(target, GET, request, String.class);
|
||||
MediaType contentType = responseEntity.getHeaders().getContentType();
|
||||
|
||||
@@ -38,7 +38,8 @@ import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
*/
|
||||
public class MethodParameters {
|
||||
|
||||
private static ParameterNameDiscoverer DISCOVERER = new DefaultParameterNameDiscoverer();
|
||||
private static final ParameterNameDiscoverer DISCOVERER = new DefaultParameterNameDiscoverer();
|
||||
private static final Map<Method, MethodParameters> CACHE = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private final List<MethodParameter> parameters;
|
||||
private final Map<Class<?>, List<MethodParameter>> parametersWithAnnotationCache = new ConcurrentReferenceHashMap<>();
|
||||
@@ -48,10 +49,23 @@ public class MethodParameters {
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
*/
|
||||
public MethodParameters(Method method) {
|
||||
private MethodParameters(Method method) {
|
||||
this(method, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link MethodParameters} for the given {@link Method}.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static MethodParameters of(Method method) {
|
||||
|
||||
Assert.notNull(method, "Method must not be null!");
|
||||
|
||||
return CACHE.computeIfAbsent(method, MethodParameters::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link MethodParameters} for the given {@link Method} and {@link AnnotationAttribute}. If the latter
|
||||
* is given, method parameter names will be looked up from the annotation attribute if present.
|
||||
@@ -115,14 +129,15 @@ public class MethodParameters {
|
||||
* @param annotation must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<MethodParameter> getParametersWith(Class<? extends Annotation> annotation) {
|
||||
|
||||
Assert.notNull(annotation, "Annotation must not be null!");
|
||||
|
||||
return parametersWithAnnotationCache.computeIfAbsent(annotation, key -> {
|
||||
|
||||
Assert.notNull(annotation, "Annotation must not be null!");
|
||||
|
||||
return getParameters().stream()//
|
||||
.filter(it -> it.hasParameterAnnotation(annotation))//
|
||||
.filter(it -> it.hasParameterAnnotation((Class<? extends Annotation>) key))//
|
||||
.collect(Collectors.toList());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.hateoas.server.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -26,10 +26,8 @@ import org.springframework.hateoas.AffordanceModelFactory;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.LinkRelation;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* Extract information needed to assemble an {@link Affordance} from a Spring MVC web method.
|
||||
@@ -43,44 +41,34 @@ public class SpringAffordanceBuilder {
|
||||
* Use the attributes of the current method call along with a collection of {@link AffordanceModelFactory}'s to create
|
||||
* a set of {@link Affordance}s.
|
||||
*
|
||||
* @param invocation
|
||||
* @param discoverer
|
||||
* @param components
|
||||
* @param type must not be {@literal null}.
|
||||
* @param method must not be {@literal null}.
|
||||
* @param href must not be {@literal null}.
|
||||
* @param discoverer must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static List<Affordance> create(MethodInvocation invocation, MappingDiscoverer discoverer,
|
||||
UriComponents components) {
|
||||
public static List<Affordance> create(Class<?> type, Method method, String href, MappingDiscoverer discoverer) {
|
||||
|
||||
List<Affordance> affordances = new ArrayList<>();
|
||||
String methodName = method.getName();
|
||||
Link affordanceLink = new Link(href, LinkRelation.of(methodName));
|
||||
|
||||
for (HttpMethod requestMethod : discoverer.getRequestMethod(invocation.getTargetType(), invocation.getMethod())) {
|
||||
MethodParameters parameters = MethodParameters.of(method);
|
||||
|
||||
String methodName = invocation.getMethod().getName();
|
||||
ResolvableType inputType = parameters.getParametersWith(RequestBody.class).stream() //
|
||||
.findFirst() //
|
||||
.map(ResolvableType::forMethodParameter) //
|
||||
.orElse(ResolvableType.NONE);
|
||||
|
||||
String href = components.toUriString().equals("") ? "/" : components.toUriString();
|
||||
Link affordanceLink = new Link(href).withRel(LinkRelation.of(methodName));
|
||||
List<QueryParameter> queryMethodParameters = parameters.getParametersWith(RequestParam.class).stream() //
|
||||
.map(it -> it.getParameterAnnotation(RequestParam.class)) //
|
||||
.filter(Objects::nonNull) //
|
||||
.map(it -> new QueryParameter(it.name(), it.value(), it.required())) //
|
||||
.collect(Collectors.toList());
|
||||
|
||||
MethodParameters invocationMethodParameters = new MethodParameters(invocation.getMethod());
|
||||
ResolvableType outputType = ResolvableType.forMethodReturnType(method);
|
||||
|
||||
ResolvableType inputType = invocationMethodParameters.getParametersWith(RequestBody.class).stream() //
|
||||
.findFirst() //
|
||||
.map(ResolvableType::forMethodParameter) //
|
||||
.orElse(ResolvableType.NONE);
|
||||
|
||||
List<QueryParameter> queryMethodParameters = invocationMethodParameters.getParametersWith(RequestParam.class)
|
||||
.stream() //
|
||||
.map(methodParameter -> methodParameter.getParameterAnnotation(RequestParam.class)) //
|
||||
.filter(Objects::nonNull) //
|
||||
.map(requestParam -> new QueryParameter(requestParam.name(), requestParam.value(), requestParam.required())) //
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ResolvableType outputType = ResolvableType.forMethodReturnType(invocation.getMethod());
|
||||
|
||||
affordances
|
||||
.add(new Affordance(methodName, affordanceLink, requestMethod, inputType, queryMethodParameters, outputType));
|
||||
|
||||
}
|
||||
|
||||
return affordances;
|
||||
return discoverer.getRequestMethod(type, method).stream() //
|
||||
.map(it -> new Affordance(methodName, affordanceLink, it, inputType, queryMethodParameters, outputType)) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ 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;
|
||||
@@ -70,6 +71,8 @@ public class WebHandler {
|
||||
private static final AnnotatedParametersParameterAccessor REQUEST_PARAM_ACCESSOR //
|
||||
= new RequestParamParameterAccessor();
|
||||
|
||||
private static final Map<AffordanceKey, List<Affordance>> AFFORDANCES_CACHE = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
public interface LinkBuilderCreator<T extends LinkBuilder> {
|
||||
T createBuilder(UriComponents components, TemplateVariables variables, List<Affordance> affordances);
|
||||
}
|
||||
@@ -89,6 +92,7 @@ public class WebHandler {
|
||||
MethodInvocation invocation = invocations.getLastInvocation();
|
||||
|
||||
return mappingToUriComponentsBuilder -> {
|
||||
|
||||
String mapping = DISCOVERER.getMapping(invocation.getTargetType(), invocation.getMethod());
|
||||
|
||||
UriComponentsBuilder builder = mappingToUriComponentsBuilder.apply(mapping);
|
||||
@@ -145,7 +149,11 @@ public class WebHandler {
|
||||
variables = variables.concat(variable);
|
||||
}
|
||||
|
||||
List<Affordance> affordances = SpringAffordanceBuilder.create(invocation, DISCOVERER, components);
|
||||
String href = components.toUriString().equals("") ? "/" : components.toUriString();
|
||||
|
||||
List<Affordance> affordances = AFFORDANCES_CACHE.computeIfAbsent(
|
||||
AffordanceKey.of(invocation.getTargetType(), invocation.getMethod(), href),
|
||||
key -> SpringAffordanceBuilder.create(key.type, key.method, key.href, DISCOVERER));
|
||||
|
||||
return creator.createBuilder(components, variables, affordances);
|
||||
};
|
||||
@@ -280,9 +288,6 @@ public class WebHandler {
|
||||
@RequiredArgsConstructor
|
||||
private static class AnnotatedParametersParameterAccessor {
|
||||
|
||||
private static final Map<Method, MethodParameters> METHOD_PARAMETERS_CACHE = new ConcurrentReferenceHashMap<>(16,
|
||||
ConcurrentReferenceHashMap.ReferenceType.WEAK);
|
||||
|
||||
private final @NonNull AnnotationAttribute attribute;
|
||||
|
||||
/**
|
||||
@@ -295,7 +300,7 @@ public class WebHandler {
|
||||
|
||||
Assert.notNull(invocation, "MethodInvocation must not be null!");
|
||||
|
||||
MethodParameters parameters = getOrCreateMethodParametersFor(invocation.getMethod());
|
||||
MethodParameters parameters = MethodParameters.of(invocation.getMethod());
|
||||
Object[] arguments = invocation.getArguments();
|
||||
List<BoundMethodParameter> result = new ArrayList<>();
|
||||
|
||||
@@ -339,16 +344,6 @@ public class WebHandler {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link MethodParameters} for the given {@link Method}.
|
||||
*
|
||||
* @param method
|
||||
* @return
|
||||
*/
|
||||
private static MethodParameters getOrCreateMethodParametersFor(Method method) {
|
||||
return METHOD_PARAMETERS_CACHE.computeIfAbsent(method, MethodParameters::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a {@link MethodParameter} alongside the value it has been bound to.
|
||||
*
|
||||
@@ -442,4 +437,12 @@ public class WebHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
private static class AffordanceKey {
|
||||
|
||||
Class<?> type;
|
||||
Method method;
|
||||
String href;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<Co
|
||||
|
||||
return WebHandler.linkTo(invocationValue, ControllerLinkBuilder::new, (builder, invocation) -> {
|
||||
|
||||
MethodParameters parameters = new MethodParameters(invocation.getMethod());
|
||||
MethodParameters parameters = MethodParameters.of(invocation.getMethod());
|
||||
Iterator<Object> parameterValues = Arrays.asList(invocation.getArguments()).iterator();
|
||||
|
||||
for (MethodParameter parameter : parameters.getParameters()) {
|
||||
|
||||
@@ -108,7 +108,7 @@ public class WebMvcLinkBuilderFactory implements MethodLinkBuilderFactory<WebMvc
|
||||
|
||||
return WebHandler.linkTo(invocationValue, WebMvcLinkBuilder::new, (builder, invocation) -> {
|
||||
|
||||
MethodParameters parameters = new MethodParameters(invocation.getMethod());
|
||||
MethodParameters parameters = MethodParameters.of(invocation.getMethod());
|
||||
Iterator<Object> parameterValues = Arrays.asList(invocation.getArguments()).iterator();
|
||||
|
||||
for (MethodParameter parameter : parameters.getParameters()) {
|
||||
|
||||
Reference in New Issue
Block a user