Skip to content

Commit d80cb5c

Browse files
committed
Speed up reflective comparison
- cache results of Method lookup, both positive and negative - use conditions instead of catching exceptions when possible
1 parent 796862e commit d80cb5c

File tree

1 file changed

+52
-16
lines changed

1 file changed

+52
-16
lines changed

src/main/java/org/assertj/core/util/introspection/Introspection.java

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121

2222
import java.lang.reflect.Method;
2323
import java.lang.reflect.Modifier;
24+
import java.util.Map;
25+
import java.util.Objects;
26+
import java.util.Optional;
27+
import java.util.concurrent.ConcurrentHashMap;
2428

2529
import org.assertj.core.configuration.ConfigurationProvider;
2630
import org.assertj.core.util.VisibleForTesting;
@@ -33,6 +37,10 @@
3337
*/
3438
public final class Introspection {
3539

40+
// We want to cache negative results (i.e. absence of methods) to avoid same overhead on subsequent lookups
41+
// However ConcurrentHashMap does not permit nulls - Optional allows caching of 'missing' values
42+
private static final Map<MethodKey, Optional<Method>> METHOD_CACHE = new ConcurrentHashMap<>();
43+
3644
private static boolean bareNamePropertyMethods = true;
3745

3846
/**
@@ -49,16 +57,19 @@ public final class Introspection {
4957
public static Method getPropertyGetter(String propertyName, Object target) {
5058
checkNotNullOrEmpty(propertyName);
5159
requireNonNull(target);
52-
Method getter;
60+
Method getter = findGetter(propertyName, target);
61+
if (getter == null) {
62+
throw new IntrospectionError(propertyNotFoundErrorMessage("No getter for property %s in %s", propertyName, target));
63+
}
64+
if (!isPublic(getter.getModifiers())) {
65+
throw new IntrospectionError(propertyNotFoundErrorMessage("No public getter for property %s in %s", propertyName, target));
66+
}
5367
try {
54-
getter = findGetter(propertyName, target);
55-
if (isPublic(getter.getModifiers())) {
56-
// force access for static class with public getter
57-
getter.setAccessible(true);
58-
}
68+
// force access for static class with public getter
69+
getter.setAccessible(true);
5970
getter.invoke(target);
6071
} catch (Exception t) {
61-
throw new IntrospectionError(propertyNotFoundErrorMessage(propertyName, target), t);
72+
throw new IntrospectionError(propertyNotFoundErrorMessage("Unable to find property %s in %s", propertyName, target), t);
6273
}
6374
return getter;
6475
}
@@ -73,13 +84,10 @@ public static boolean canIntrospectExtractBareNamePropertyMethods() {
7384
return bareNamePropertyMethods;
7485
}
7586

76-
private static String propertyNotFoundErrorMessage(String propertyName, Object target) {
87+
private static String propertyNotFoundErrorMessage(String message, String propertyName, Object target) {
7788
String targetTypeName = target.getClass().getName();
7889
String property = quote(propertyName);
79-
Method getter = findGetter(propertyName, target);
80-
if (getter == null) return format("No getter for property %s in %s", property, targetTypeName);
81-
if (!isPublic(getter.getModifiers())) return format("No public getter for property %s in %s", property, targetTypeName);
82-
return format("Unable to find property %s in %s", property, targetTypeName);
90+
return format(message, property, targetTypeName);
8391
}
8492

8593
private static Method findGetter(String propertyName, Object target) {
@@ -102,19 +110,47 @@ private static boolean isValidGetter(Method method) {
102110
}
103111

104112
private static Method findMethod(String name, Object target) {
105-
Class<?> clazz = target.getClass();
113+
final MethodKey methodKey = new MethodKey(name, target.getClass());
114+
return METHOD_CACHE.computeIfAbsent(methodKey, Introspection::findMethodByKey).orElse(null);
115+
}
116+
117+
private static Optional<Method> findMethodByKey(MethodKey key) {
106118
// try public methods only
119+
Class<?> clazz = key.clazz;
107120
try {
108-
return clazz.getMethod(name);
121+
return Optional.of(clazz.getMethod(key.name));
109122
} catch (NoSuchMethodException | SecurityException ignored) {}
110123
// search all methods
111124
while (clazz != null) {
112125
try {
113-
return clazz.getDeclaredMethod(name);
126+
return Optional.of(clazz.getDeclaredMethod(key.name));
114127
} catch (NoSuchMethodException | SecurityException ignored) {}
115128
clazz = clazz.getSuperclass();
116129
}
117-
return null;
130+
return Optional.empty();
131+
}
132+
133+
private static final class MethodKey {
134+
private final String name;
135+
private final Class<?> clazz;
136+
137+
private MethodKey(final String name, final Class<?> clazz) {
138+
this.name = name;
139+
this.clazz = clazz;
140+
}
141+
142+
@Override
143+
public boolean equals(final Object o) {
144+
if (this == o) return true;
145+
if (o == null || getClass() != o.getClass()) return false;
146+
final MethodKey methodKey = (MethodKey)o;
147+
return Objects.equals(name, methodKey.name) && Objects.equals(clazz, methodKey.clazz);
148+
}
149+
150+
@Override
151+
public int hashCode() {
152+
return Objects.hash(name, clazz);
153+
}
118154
}
119155

120156
private Introspection() {}

0 commit comments

Comments
 (0)