#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:
Oliver Drotbohm
2019-04-03 18:31:46 +02:00
parent fc3767b90b
commit 39404ea2d9
15 changed files with 152 additions and 137 deletions

View File

@@ -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

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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());
});
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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()) {

View File

@@ -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()) {