Skip to content

Commit 8e78cf2

Browse files
committed
#1484 - Fix performance regression in WebHandler.
Moved the affordance metadata caching into SpringAffordanceBuilder and separate the caching of the metadata lookup from the assembly of the affordance. The latter is based on the URI resulting from expanding the request mapping with the given value and thus is likely to produce a lot of different values so that they're rather unsuitable as cache key. That means that we now return fresh Affordance instances for every request which probably causes a tiny hit on performance but still does not need additional metadata lookups. Also, it opens up the door to enrich the affordances with property value providers that would allow us to render affordance fields that refer to a instances property, e.g. to render edit forms.
1 parent a665388 commit 8e78cf2

File tree

3 files changed

+113
-84
lines changed

3 files changed

+113
-84
lines changed

src/main/java/org/springframework/hateoas/server/core/SpringAffordanceBuilder.java

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,24 @@
1616
package org.springframework.hateoas.server.core;
1717

1818
import java.lang.reflect.Method;
19+
import java.util.Collection;
1920
import java.util.List;
21+
import java.util.Objects;
22+
import java.util.function.Function;
2023
import java.util.stream.Collectors;
2124

2225
import org.springframework.core.ResolvableType;
2326
import org.springframework.hateoas.Affordance;
2427
import org.springframework.hateoas.Link;
2528
import org.springframework.hateoas.LinkRelation;
2629
import org.springframework.hateoas.QueryParameter;
27-
import org.springframework.hateoas.mediatype.AffordanceModelFactory;
2830
import org.springframework.hateoas.mediatype.Affordances;
31+
import org.springframework.http.HttpMethod;
32+
import org.springframework.http.MediaType;
33+
import org.springframework.lang.Nullable;
34+
import org.springframework.util.ConcurrentLruCache;
2935
import org.springframework.web.bind.annotation.RequestBody;
36+
import org.springframework.web.bind.annotation.RequestMapping;
3037
import org.springframework.web.bind.annotation.RequestParam;
3138

3239
/**
@@ -37,21 +44,50 @@
3744
*/
3845
public class SpringAffordanceBuilder {
3946

47+
@SuppressWarnings("deprecation") //
48+
public static final MappingDiscoverer DISCOVERER = CachingMappingDiscoverer
49+
.of(new PropertyResolvingMappingDiscoverer(new AnnotationMappingDiscoverer(RequestMapping.class)));
50+
51+
private static final ConcurrentLruCache<AffordanceKey, Function<Affordances, List<Affordance>>> AFFORDANCES_CACHE = new ConcurrentLruCache<>(
52+
256, key -> SpringAffordanceBuilder.create(key.type, key.method));
53+
4054
/**
41-
* Use the attributes of the current method call along with a collection of {@link AffordanceModelFactory}'s to create
42-
* a set of {@link Affordance}s.
55+
* Returns all {@link Affordance}s for the given type's method and base URI.
4356
*
4457
* @param type must not be {@literal null}.
4558
* @param method must not be {@literal null}.
46-
* @param href must not be {@literal null}.
47-
* @param discoverer must not be {@literal null}.
59+
* @param href must not be {@literal null} or empty.
4860
* @return
4961
*/
50-
public static List<Affordance> create(Class<?> type, Method method, String href, MappingDiscoverer discoverer) {
62+
public static List<Affordance> getAffordances(Class<?> type, Method method, String href) {
5163

5264
String methodName = method.getName();
5365
Link affordanceLink = Link.of(href, LinkRelation.of(methodName));
5466

67+
return AFFORDANCES_CACHE
68+
.get(new AffordanceKey(type, method))
69+
.apply(Affordances.of(affordanceLink));
70+
}
71+
72+
/**
73+
* Returns the mapping for the given type's method.
74+
*
75+
* @param type must not be {@literal null}.
76+
* @param method must not be {@literal null}.
77+
* @return
78+
*/
79+
@Nullable
80+
public static String getMapping(Class<?> type, Method method) {
81+
return DISCOVERER.getMapping(type, method);
82+
}
83+
84+
private static Function<Affordances, List<Affordance>> create(Class<?> type, Method method) {
85+
86+
String methodName = method.getName();
87+
ResolvableType outputType = ResolvableType.forMethodReturnType(method);
88+
Collection<HttpMethod> requestMethods = DISCOVERER.getRequestMethod(type, method);
89+
List<MediaType> inputMediaTypes = DISCOVERER.getConsumes(method);
90+
5591
MethodParameters parameters = MethodParameters.of(method);
5692

5793
ResolvableType inputType = parameters.getParametersWith(RequestBody.class).stream() //
@@ -63,18 +99,66 @@ public static List<Affordance> create(Class<?> type, Method method, String href,
6399
.map(QueryParameter::of) //
64100
.collect(Collectors.toList());
65101

66-
ResolvableType outputType = ResolvableType.forMethodReturnType(method);
67-
Affordances affordances = Affordances.of(affordanceLink);
68-
69-
return discoverer.getRequestMethod(type, method).stream() //
102+
return affordances -> requestMethods.stream() //
70103
.flatMap(it -> affordances.afford(it) //
71104
.withInput(inputType) //
72105
.withOutput(outputType) //
73106
.withParameters(queryMethodParameters) //
74107
.withName(methodName) //
75-
.withInputMediaTypes(discoverer.getConsumes(method)) //
108+
.withInputMediaTypes(inputMediaTypes) //
76109
.build() //
77110
.stream()) //
78111
.collect(Collectors.toList());
79112
}
113+
114+
private static final class AffordanceKey {
115+
116+
private final Class<?> type;
117+
private final Method method;
118+
119+
AffordanceKey(Class<?> type, Method method) {
120+
121+
this.type = type;
122+
this.method = method;
123+
}
124+
125+
/*
126+
* (non-Javadoc)
127+
* @see java.lang.Object#equals(java.lang.Object)
128+
*/
129+
@Override
130+
public boolean equals(@Nullable Object o) {
131+
132+
if (this == o) {
133+
return true;
134+
}
135+
136+
if (!(o instanceof AffordanceKey)) {
137+
return false;
138+
}
139+
140+
AffordanceKey that = (AffordanceKey) o;
141+
142+
return Objects.equals(this.type, that.type) //
143+
&& Objects.equals(this.method, that.method);
144+
}
145+
146+
/*
147+
* (non-Javadoc)
148+
* @see java.lang.Object#hashCode()
149+
*/
150+
@Override
151+
public int hashCode() {
152+
return Objects.hash(this.type, this.method);
153+
}
154+
155+
/*
156+
* (non-Javadoc)
157+
* @see java.lang.Object#toString()
158+
*/
159+
@Override
160+
public String toString() {
161+
return "WebHandler.AffordanceKey(type=" + this.type + ", method=" + this.method + ")";
162+
}
163+
}
80164
}

src/main/java/org/springframework/hateoas/server/core/WebHandler.java

Lines changed: 14 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,15 @@
2222

2323
import java.lang.annotation.Annotation;
2424
import java.lang.reflect.Method;
25-
import java.util.*;
25+
import java.util.ArrayList;
26+
import java.util.Arrays;
27+
import java.util.Collection;
28+
import java.util.Collections;
29+
import java.util.HashMap;
30+
import java.util.Iterator;
31+
import java.util.List;
32+
import java.util.Map;
33+
import java.util.Optional;
2634
import java.util.concurrent.ConcurrentHashMap;
2735
import java.util.function.BiFunction;
2836
import java.util.function.Function;
@@ -38,13 +46,11 @@
3846
import org.springframework.hateoas.server.LinkBuilder;
3947
import org.springframework.lang.Nullable;
4048
import org.springframework.util.Assert;
41-
import org.springframework.util.ConcurrentLruCache;
4249
import org.springframework.util.LinkedMultiValueMap;
4350
import org.springframework.util.MultiValueMap;
4451
import org.springframework.util.ObjectUtils;
4552
import org.springframework.util.StringUtils;
4653
import org.springframework.web.bind.annotation.PathVariable;
47-
import org.springframework.web.bind.annotation.RequestMapping;
4854
import org.springframework.web.bind.annotation.RequestParam;
4955
import org.springframework.web.bind.annotation.ValueConstants;
5056
import org.springframework.web.util.UriComponents;
@@ -59,13 +65,6 @@
5965
*/
6066
public class WebHandler {
6167

62-
@SuppressWarnings("deprecation") //
63-
public static final MappingDiscoverer DISCOVERER = CachingMappingDiscoverer
64-
.of(new PropertyResolvingMappingDiscoverer(new AnnotationMappingDiscoverer(RequestMapping.class)));
65-
66-
private static final ConcurrentLruCache<AffordanceKey, List<Affordance>> AFFORDANCES_CACHE = new ConcurrentLruCache<>(
67-
256, key -> SpringAffordanceBuilder.create(key.type, key.method, key.href.toUriString(), DISCOVERER));
68-
6968
public interface LinkBuilderCreator<T extends LinkBuilder> {
7069
T createBuilder(UriComponents components, TemplateVariables variables, List<Affordance> affordances);
7170
}
@@ -101,8 +100,7 @@ private static <T extends LinkBuilder> PreparedWebHandler<T> linkTo(Object invoc
101100
}
102101

103102
MethodInvocation invocation = invocations.getLastInvocation();
104-
105-
String mapping = DISCOVERER.getMapping(invocation.getTargetType(), invocation.getMethod());
103+
String mapping = SpringAffordanceBuilder.getMapping(invocation.getTargetType(), invocation.getMethod());
106104

107105
return (finisher, conversionService) -> {
108106

@@ -118,7 +116,8 @@ private static <T extends LinkBuilder> PreparedWebHandler<T> linkTo(Object invoc
118116
values.put(names.next(), encodePath(classMappingParameters.next()));
119117
}
120118

121-
HandlerMethodParameters parameters = HandlerMethodParameters.of(invocation.getMethod());
119+
Method method = invocation.getMethod();
120+
HandlerMethodParameters parameters = HandlerMethodParameters.of(method);
122121
Object[] arguments = invocation.getArguments();
123122
ConversionService resolved = conversionService;
124123

@@ -163,8 +162,8 @@ private static <T extends LinkBuilder> PreparedWebHandler<T> linkTo(Object invoc
163162
variables = variables.concat(variable);
164163
}
165164

166-
List<Affordance> affordances = AFFORDANCES_CACHE
167-
.get(new AffordanceKey(invocation.getTargetType(), invocation.getMethod(), components));
165+
List<Affordance> affordances = SpringAffordanceBuilder.getAffordances(invocation.getTargetType(), method,
166+
components.toUriString());
168167

169168
return creator.createBuilder(components, variables, affordances);
170169
};
@@ -230,60 +229,6 @@ private static void bindRequestParameters(UriComponentsBuilder builder, HandlerM
230229
}
231230
}
232231

233-
private static final class AffordanceKey {
234-
235-
private final Class<?> type;
236-
private final Method method;
237-
private final UriComponents href;
238-
239-
AffordanceKey(Class<?> type, Method method, UriComponents href) {
240-
241-
this.type = type;
242-
this.method = method;
243-
this.href = href;
244-
}
245-
246-
/*
247-
* (non-Javadoc)
248-
* @see java.lang.Object#equals(java.lang.Object)
249-
*/
250-
@Override
251-
public boolean equals(@Nullable Object o) {
252-
253-
if (this == o) {
254-
return true;
255-
}
256-
257-
if (!(o instanceof AffordanceKey)) {
258-
return false;
259-
}
260-
261-
AffordanceKey that = (AffordanceKey) o;
262-
263-
return Objects.equals(this.type, that.type) //
264-
&& Objects.equals(this.method, that.method) //
265-
&& Objects.equals(this.href, that.href);
266-
}
267-
268-
/*
269-
* (non-Javadoc)
270-
* @see java.lang.Object#hashCode()
271-
*/
272-
@Override
273-
public int hashCode() {
274-
return Objects.hash(this.type, this.method, this.href);
275-
}
276-
277-
/*
278-
* (non-Javadoc)
279-
* @see java.lang.Object#toString()
280-
*/
281-
@Override
282-
public String toString() {
283-
return "WebHandler.AffordanceKey(type=" + this.type + ", method=" + this.method + ", href=" + this.href + ")";
284-
}
285-
}
286-
287232
private static class HandlerMethodParameters {
288233

289234
private static final List<Class<? extends Annotation>> ANNOTATIONS = Arrays.asList(RequestParam.class,

src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
import org.springframework.hateoas.Link;
2626
import org.springframework.hateoas.TemplateVariables;
2727
import org.springframework.hateoas.server.core.DummyInvocationUtils;
28+
import org.springframework.hateoas.server.core.SpringAffordanceBuilder;
2829
import org.springframework.hateoas.server.core.TemplateVariableAwareLinkBuilderSupport;
2930
import org.springframework.hateoas.server.core.UriTemplateFactory;
30-
import org.springframework.hateoas.server.core.WebHandler;
3131
import org.springframework.util.Assert;
3232
import org.springframework.web.util.DefaultUriTemplateHandler;
3333
import org.springframework.web.util.UriComponents;
@@ -89,7 +89,7 @@ public static WebMvcLinkBuilder linkTo(Class<?> controller, Object... parameters
8989
Assert.notNull(controller, "Controller must not be null!");
9090
Assert.notNull(parameters, "Parameters must not be null!");
9191

92-
String mapping = WebHandler.DISCOVERER.getMapping(controller);
92+
String mapping = SpringAffordanceBuilder.DISCOVERER.getMapping(controller);
9393

9494
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(mapping == null ? "/" : mapping);
9595
UriComponents uriComponents = HANDLER.expandAndEncode(builder, parameters);
@@ -111,7 +111,7 @@ public static WebMvcLinkBuilder linkTo(Class<?> controller, Map<String, ?> param
111111
Assert.notNull(controller, "Controller must not be null!");
112112
Assert.notNull(parameters, "Parameters must not be null!");
113113

114-
String mapping = WebHandler.DISCOVERER.getMapping(controller);
114+
String mapping = SpringAffordanceBuilder.DISCOVERER.getMapping(controller);
115115

116116
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(mapping == null ? "/" : mapping);
117117
UriComponents uriComponents = HANDLER.expandAndEncode(builder, parameters);
@@ -134,7 +134,7 @@ public static WebMvcLinkBuilder linkTo(Class<?> controller, Method method, Objec
134134
Assert.notNull(controller, "Controller type must not be null!");
135135
Assert.notNull(method, "Method must not be null!");
136136

137-
String mapping = WebHandler.DISCOVERER.getMapping(controller, method);
137+
String mapping = SpringAffordanceBuilder.DISCOVERER.getMapping(controller, method);
138138
UriTemplate template = UriTemplateFactory.templateFor(mapping);
139139
URI uri = template.expand(parameters);
140140

0 commit comments

Comments
 (0)