Skip to content

Commit 21d335c

Browse files
committed
Update the global cache from a synchronized map to a concurrent map
1 parent bc67074 commit 21d335c

File tree

1 file changed

+51
-206
lines changed

1 file changed

+51
-206
lines changed

src/main/java/org/apache/commons/beanutils2/MethodUtils.java

Lines changed: 51 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,15 @@
1717

1818
package org.apache.commons.beanutils2;
1919

20-
import java.lang.ref.Reference;
21-
import java.lang.ref.WeakReference;
2220
import java.lang.reflect.Method;
2321
import java.lang.reflect.Modifier;
2422
import java.util.Arrays;
25-
import java.util.Collections;
2623
import java.util.Map;
2724
import java.util.Objects;
28-
import java.util.WeakHashMap;
25+
import java.util.function.Function;
2926

30-
import org.apache.commons.lang3.ClassUtils;
27+
import org.apache.commons.collections4.map.ConcurrentReferenceHashMap;
28+
import org.apache.commons.collections4.map.ConcurrentReferenceHashMap.ReferenceType;
3129
import org.apache.commons.logging.Log;
3230
import org.apache.commons.logging.LogFactory;
3331

@@ -57,6 +55,7 @@ public final class MethodUtils {
5755
* Represents the key to looking up a Method by reflection.
5856
*/
5957
private static final class MethodKey {
58+
6059
private final Class<?> cls;
6160
private final String methodName;
6261
private final Class<?>[] paramTypes;
@@ -104,6 +103,11 @@ public boolean equals(final Object obj) {
104103
public int hashCode() {
105104
return hashCode;
106105
}
106+
107+
@Override
108+
public String toString() {
109+
return "MethodKey [cls=" + cls + ", methodName=" + methodName + ", paramTypes=" + Arrays.toString(paramTypes) + ", exact=" + exact + "]";
110+
}
107111
}
108112

109113
private static final Log LOG = LogFactory.getLog(MethodUtils.class);
@@ -115,35 +119,28 @@ public int hashCode() {
115119
* would mean having a map keyed by context classloader which may introduce memory-leak problems.
116120
* </p>
117121
*/
118-
private static boolean CACHE_METHODS = true;
122+
private static boolean CACHE_ENABLED = true;
119123

120124
/**
121-
* Stores a cache of MethodDescriptor -> Method in a WeakHashMap.
125+
* Stores a cache of MethodKey -> Method.
122126
* <p>
123127
* The keys into this map only ever exist as temporary variables within methods of this class, and are never exposed to users of this class. This means that
124-
* the WeakHashMap is used only as a mechanism for limiting the size of the cache, that is, a way to tell the garbage collector that the contents of the
125-
* cache can be completely garbage-collected whenever it needs the memory. Whether this is a good approach to this problem is doubtful; something like the
126-
* commons-collections LRUMap may be more appropriate (though of course selecting an appropriate size is an issue).
128+
* this map is used only as a mechanism for limiting the size of the cache, that is, a way to tell the garbage collector that the contents of the cache can
129+
* be completely garbage-collected whenever it needs the memory. Whether this is a good approach to this problem is doubtful; something like the Commons
130+
* Collections LRUMap may be more appropriate (though of course selecting an appropriate size is an issue).
127131
* </p>
128132
* <p>
129-
* This static variable is safe even when this code is deployed via a shared classloader because it is keyed via a MethodDescriptor object which has a Class
130-
* as one of its members and that member is used in the MethodDescriptor.equals method. So two components that load the same class via different class
131-
* loaders will generate non-equal MethodDescriptor objects and hence end up with different entries in the map.
133+
* This static variable is safe even when this code is deployed via a shared class loader because it is keyed via a MethodKey object which has a Class as
134+
* one of its members and that member is used in the MethodKey.equals method. So two components that load the same class via different class loaders will
135+
* generate non-equal MethodKey objects and hence end up with different entries in the map.
132136
* </p>
133137
*/
134-
private static final Map<MethodKey, Reference<Method>> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
135-
136-
/**
137-
* Add a method to the cache.
138-
*
139-
* @param key The method descriptor.
140-
* @param method The method to cache.
141-
*/
142-
private static void cacheMethod(final MethodKey key, final Method method) {
143-
if (CACHE_METHODS && method != null) {
144-
CACHE.put(key, new WeakReference<>(method));
145-
}
146-
}
138+
// @formatter:off
139+
private static final Map<MethodKey, Method> CACHE = new ConcurrentReferenceHashMap.Builder<MethodKey, Method>()
140+
.setKeyReferenceType(ReferenceType.WEAK)
141+
.setValueReferenceType(ReferenceType.WEAK)
142+
.get();
143+
// @formatter:on
147144

148145
/**
149146
* Clear the method cache.
@@ -157,9 +154,17 @@ public static synchronized int clearCache() {
157154
return size;
158155
}
159156

157+
private static Method computeIfAbsent(final MethodKey key, final Function<MethodKey, ? extends Method> mappingFunction) {
158+
final Method method = CACHE_ENABLED ? CACHE.computeIfAbsent(key, k -> mappingFunction.apply(k)) : mappingFunction.apply(key);
159+
if (LOG.isTraceEnabled()) {
160+
LOG.trace("Matched " + key + " with method: " + method + ", CACHE_ENABLED: " + CACHE_ENABLED);
161+
}
162+
return method;
163+
}
164+
160165
/**
161-
* Gets an accessible method (that is, one that can be invoked via reflection) that implements the specified Method. If no such method can be found,
162-
* return {@code null}.
166+
* Gets an accessible method (that is, one that can be invoked via reflection) that implements the specified Method. If no such method can be found, return
167+
* {@code null}.
163168
*
164169
* @param clazz The class of the object.
165170
* @param method The method that we wish to call.
@@ -171,12 +176,10 @@ public static Method getAccessibleMethod(Class<?> clazz, Method method) {
171176
if (method == null) {
172177
return null;
173178
}
174-
175179
// If the requested method is not public we cannot call it
176180
if (!Modifier.isPublic(method.getModifiers())) {
177181
return null;
178182
}
179-
180183
boolean sameClass = true;
181184
if (clazz == null) {
182185
clazz = method.getDeclaringClass();
@@ -186,26 +189,21 @@ public static Method getAccessibleMethod(Class<?> clazz, Method method) {
186189
}
187190
sameClass = clazz.equals(method.getDeclaringClass());
188191
}
189-
190192
// If the class is public, we are done
191193
if (Modifier.isPublic(clazz.getModifiers())) {
192194
if (!sameClass && !Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
193195
setMethodAccessible(method); // Default access superclass workaround
194196
}
195197
return method;
196198
}
197-
198199
final String methodName = method.getName();
199200
final Class<?>[] parameterTypes = method.getParameterTypes();
200-
201201
// Check the implemented interfaces and subinterfaces
202202
method = getAccessibleMethodFromInterfaceNest(clazz, methodName, parameterTypes);
203-
204203
// Check the superclass chain
205204
if (method == null) {
206205
method = getAccessibleMethodFromSuperclass(clazz, methodName, parameterTypes);
207206
}
208-
209207
return method;
210208
}
211209

@@ -219,24 +217,18 @@ public static Method getAccessibleMethod(Class<?> clazz, Method method) {
219217
* @return The accessible method.
220218
*/
221219
public static Method getAccessibleMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes) {
222-
try {
223-
final MethodKey key = new MethodKey(clazz, methodName, parameterTypes, true);
224-
// Check the cache first
225-
Method method = getCachedMethod(key);
226-
if (method != null) {
227-
return method;
220+
return computeIfAbsent(new MethodKey(clazz, methodName, parameterTypes, true), k -> {
221+
try {
222+
return getAccessibleMethod(clazz, clazz.getMethod(methodName, parameterTypes));
223+
} catch (final NoSuchMethodException e) {
224+
return null;
228225
}
229-
method = getAccessibleMethod(clazz, clazz.getMethod(methodName, parameterTypes));
230-
cacheMethod(key, method);
231-
return method;
232-
} catch (final NoSuchMethodException e) {
233-
return null;
234-
}
226+
});
235227
}
236228

237229
/**
238-
* Gets an accessible method (that is, one that can be invoked via reflection) that implements the specified Method. If no such method can be found,
239-
* return {@code null}.
230+
* Gets an accessible method (that is, one that can be invoked via reflection) that implements the specified Method. If no such method can be found, return
231+
* {@code null}.
240232
*
241233
* @param method The method that we wish to call.
242234
* @return The accessible method.
@@ -292,8 +284,8 @@ private static Method getAccessibleMethodFromInterfaceNest(Class<?> clazz, final
292284
}
293285

294286
/**
295-
* Gets an accessible method (that is, one that can be invoked via reflection) by scanning through the superclasses. If no such method can be found,
296-
* return {@code null}.
287+
* Gets an accessible method (that is, one that can be invoked via reflection) by scanning through the superclasses. If no such method can be found, return
288+
* {@code null}.
297289
*
298290
* @param clazz Class to be checked.
299291
* @param methodName Method name of the method we wish to call.
@@ -314,22 +306,6 @@ private static Method getAccessibleMethodFromSuperclass(final Class<?> clazz, fi
314306
return null;
315307
}
316308

317-
/**
318-
* Gets the method from the cache, if present.
319-
*
320-
* @param key The method descriptor.
321-
* @return The cached method.
322-
*/
323-
private static Method getCachedMethod(final MethodKey key) {
324-
if (CACHE_METHODS) {
325-
final Reference<Method> methodRef = CACHE.get(key);
326-
if (methodRef != null) {
327-
return methodRef.get();
328-
}
329-
}
330-
return null;
331-
}
332-
333309
/**
334310
* Gets an accessible method that matches the given name and has compatible parameters. Compatible parameters mean that every method parameter is assignable
335311
* from the given parameters. In other words, it finds a method with the given name that will take the parameters given.
@@ -341,152 +317,21 @@ private static Method getCachedMethod(final MethodKey key) {
341317
* <p>
342318
* This method can match primitive parameter by passing in wrapper classes. For example, a {@code Boolean</code> will match a primitive <code>boolean}
343319
* parameter.
320+
* </p>
344321
*
345322
* @param clazz find method in this class.
346323
* @param methodName find method with this name.
347324
* @param parameterTypes find method with compatible parameters.
348325
* @return The accessible method.
349326
*/
350327
public static Method getMatchingAccessibleMethod(final Class<?> clazz, final String methodName, final Class<?>[] parameterTypes) {
351-
// trace logging
352-
if (LOG.isTraceEnabled()) {
353-
LOG.trace("Matching name=" + methodName + " on " + clazz);
354-
}
355-
final MethodKey key = new MethodKey(clazz, methodName, parameterTypes, false);
356-
// see if we can find the method directly
357-
// most of the time this works and it's much faster
358-
try {
359-
// Check the cache first
360-
Method method = getCachedMethod(key);
328+
return computeIfAbsent(new MethodKey(clazz, methodName, parameterTypes, false), k -> {
329+
final Method method = org.apache.commons.lang3.reflect.MethodUtils.getMatchingAccessibleMethod(clazz, methodName, parameterTypes);
361330
if (method != null) {
362-
return method;
363-
}
364-
method = clazz.getMethod(methodName, parameterTypes);
365-
if (LOG.isTraceEnabled()) {
366-
LOG.trace("Found straight match: " + method);
367-
LOG.trace("isPublic:" + Modifier.isPublic(method.getModifiers()));
331+
setMethodAccessible(method); // Default access superclass workaround
368332
}
369-
setMethodAccessible(method); // Default access superclass workaround
370-
cacheMethod(key, method);
371333
return method;
372-
} catch (final NoSuchMethodException e) {
373-
/* SWALLOW */ }
374-
// search through all methods
375-
final int paramSize = parameterTypes.length;
376-
Method bestMatch = null;
377-
final Method[] methods = clazz.getMethods();
378-
float bestMatchCost = Float.MAX_VALUE;
379-
float myCost = Float.MAX_VALUE;
380-
for (final Method method2 : methods) {
381-
if (method2.getName().equals(methodName)) {
382-
// log some trace information
383-
if (LOG.isTraceEnabled()) {
384-
LOG.trace("Found matching name:");
385-
LOG.trace(method2);
386-
}
387-
// compare parameters
388-
final Class<?>[] methodsParams = method2.getParameterTypes();
389-
final int methodParamSize = methodsParams.length;
390-
if (methodParamSize == paramSize) {
391-
boolean match = true;
392-
for (int n = 0; n < methodParamSize; n++) {
393-
if (LOG.isTraceEnabled()) {
394-
LOG.trace("Param=" + parameterTypes[n].getName());
395-
LOG.trace("Method=" + methodsParams[n].getName());
396-
}
397-
if (!ClassUtils.isAssignable(parameterTypes[n], methodsParams[n])) {
398-
if (LOG.isTraceEnabled()) {
399-
LOG.trace(methodsParams[n] + " is not assignable from " + parameterTypes[n]);
400-
}
401-
match = false;
402-
break;
403-
}
404-
}
405-
if (match) {
406-
// get accessible version of method
407-
final Method method = getAccessibleMethod(clazz, method2);
408-
if (method != null) {
409-
if (LOG.isTraceEnabled()) {
410-
LOG.trace(method + " accessible version of " + method2);
411-
}
412-
setMethodAccessible(method); // Default access superclass workaround
413-
myCost = getTotalTransformationCost(parameterTypes, method.getParameterTypes());
414-
if (myCost < bestMatchCost) {
415-
bestMatch = method;
416-
bestMatchCost = myCost;
417-
}
418-
}
419-
LOG.trace("Couldn't find accessible method.");
420-
}
421-
}
422-
}
423-
}
424-
if (bestMatch != null) {
425-
cacheMethod(key, bestMatch);
426-
} else {
427-
// didn't find a match
428-
LOG.trace("No match found.");
429-
}
430-
return bestMatch;
431-
}
432-
433-
/**
434-
* Gets the number of steps required needed to turn the source class into the destination class. This represents the number of steps in the object hierarchy
435-
* graph.
436-
*
437-
* @param srcClass The source class.
438-
* @param destClass The destination class.
439-
* @return The cost of transforming an object.
440-
*/
441-
private static float getObjectTransformationCost(Class<?> srcClass, final Class<?> destClass) {
442-
float cost = 0.0f;
443-
while (srcClass != null && !destClass.equals(srcClass)) {
444-
if (destClass.isPrimitive()) {
445-
final Class<?> destClassWrapperClazz = ClassUtils.wrapperToPrimitive(destClass);
446-
if (destClassWrapperClazz != null && destClassWrapperClazz.equals(srcClass)) {
447-
cost += 0.25f;
448-
break;
449-
}
450-
}
451-
final Class<?> cls = srcClass;
452-
if (destClass.isInterface() && ClassUtils.isAssignable(cls, destClass)) {
453-
// slight penalty for interface match.
454-
// we still want an exact match to override an interface match, but
455-
// an interface match should override anything where we have to get a
456-
// superclass.
457-
cost += 0.25f;
458-
break;
459-
}
460-
cost++;
461-
srcClass = srcClass.getSuperclass();
462-
}
463-
464-
/*
465-
* If the destination class is null, we've traveled all the way up to an Object match. We'll penalize this by adding 1.5 to the cost.
466-
*/
467-
if (srcClass == null) {
468-
cost += 1.5f;
469-
}
470-
471-
return cost;
472-
}
473-
474-
/**
475-
* Gets the sum of the object transformation cost for each class in the source argument list.
476-
*
477-
* @param srcArgs The source arguments.
478-
* @param destArgs The destination arguments.
479-
* @return The total transformation cost.
480-
*/
481-
private static float getTotalTransformationCost(final Class<?>[] srcArgs, final Class<?>[] destArgs) {
482-
float totalCost = 0.0f;
483-
for (int i = 0; i < srcArgs.length; i++) {
484-
Class<?> srcClass, destClass;
485-
srcClass = srcArgs[i];
486-
destClass = destArgs[i];
487-
totalCost += getObjectTransformationCost(srcClass, destClass);
488-
}
489-
return totalCost;
334+
});
490335
}
491336

492337
/**
@@ -496,9 +341,9 @@ private static float getTotalTransformationCost(final Class<?>[] srcArgs, final
496341
* @since 1.8.0
497342
*/
498343
public static synchronized void setCacheMethods(final boolean cacheMethods) {
499-
CACHE_METHODS = cacheMethods;
500-
if (!CACHE_METHODS) {
501-
clearCache();
344+
CACHE_ENABLED = cacheMethods;
345+
if (!CACHE_ENABLED) {
346+
CACHE.clear();
502347
}
503348
}
504349

0 commit comments

Comments
 (0)