From 295929cc16f9ee48118c9903112b46aefdeaff55 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Apr 2018 21:08:25 +0200 Subject: [PATCH] Cache-safety check for sibling loaders resolving the same classes Issue: SPR-16714 --- .../org/springframework/util/ClassUtils.java | 1415 +++++++++-------- .../springframework/util/ClassUtilsTests.java | 52 +- 2 files changed, 754 insertions(+), 713 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index 52853dd19ad..8cbd7024cba 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -303,7 +303,9 @@ public abstract class ClassUtils { * (that is, the class could not be found or the class file could not be loaded) * @see #forName(String, ClassLoader) */ - public static Class resolveClassName(String className, @Nullable ClassLoader classLoader) throws IllegalArgumentException { + public static Class resolveClassName(String className, @Nullable ClassLoader classLoader) + throws IllegalArgumentException { + try { return forName(className, classLoader); } @@ -315,35 +317,13 @@ public abstract class ClassUtils { } } - /** - * Resolve the given class name as primitive class, if appropriate, - * according to the JVM's naming rules for primitive classes. - *

Also supports the JVM's internal class names for primitive arrays. - * Does not support the "[]" suffix notation for primitive arrays; - * this is only supported by {@link #forName(String, ClassLoader)}. - * @param name the name of the potentially primitive class - * @return the primitive class, or {@code null} if the name does not denote - * a primitive class or primitive array class - */ - @Nullable - public static Class resolvePrimitiveClassName(@Nullable String name) { - Class result = null; - // Most class names will be quite long, considering that they - // SHOULD sit in a package, so a length check is worthwhile. - if (name != null && name.length() <= 8) { - // Could be a primitive - likely. - result = primitiveTypeNameMap.get(name); - } - return result; - } - /** * Determine whether the {@link Class} identified by the supplied name is present * and can be loaded. Will return {@code false} if either the class or * one of its dependencies is not present or cannot be loaded. * @param className the name of the class to check * @param classLoader the class loader to use - * (may be {@code null}, which indicates the default class loader) + * (may be {@code null} which indicates the default class loader) * @return whether the specified class is present */ public static boolean isPresent(String className, @Nullable ClassLoader classLoader) { @@ -358,42 +338,23 @@ public abstract class ClassUtils { } /** - * Return the user-defined class for the given instance: usually simply - * the class of the given instance, but the original class in case of a - * CGLIB-generated subclass. - * @param instance the instance to check - * @return the user-defined class - */ - public static Class getUserClass(Object instance) { - Assert.notNull(instance, "Instance must not be null"); - return getUserClass(instance.getClass()); - } - - /** - * Return the user-defined class for the given class: usually simply the given - * class, but the original class in case of a CGLIB-generated subclass. - * @param clazz the class to check - * @return the user-defined class + * Check whether the given class is visible in the given ClassLoader. + * @param clazz the class to check (typically an interface) + * @param classLoader the ClassLoader to check against + * (may be {@code null} in which case this method will always return {@code true}) */ - public static Class getUserClass(Class clazz) { - if (clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) { - Class superclass = clazz.getSuperclass(); - if (superclass != null && Object.class != superclass) { - return superclass; - } + public static boolean isVisible(Class clazz, @Nullable ClassLoader classLoader) { + if (classLoader == null) { + return true; + } + try { + return (clazz == classLoader.loadClass(clazz.getName())); + // Else: different class with same name found + } + catch (ClassNotFoundException ex) { + // No corresponding class found at all + return false; } - return clazz; - } - - /** - * Determine if the supplied class is an inner class, - * i.e. a non-static member of an enclosing class. - * @return {@code true} if the supplied class is an inner class - * @since 5.0.5 - * @see Class#isMemberClass() - */ - public static boolean isInnerClass(Class clazz) { - return (clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers())); } /** @@ -401,886 +362,934 @@ public abstract class ClassUtils { * i.e. whether it is loaded by the given ClassLoader or a parent of it. * @param clazz the class to analyze * @param classLoader the ClassLoader to potentially cache metadata in + * (may be {@code null} which indicates the system class loader) */ public static boolean isCacheSafe(Class clazz, @Nullable ClassLoader classLoader) { Assert.notNull(clazz, "Class must not be null"); try { ClassLoader target = clazz.getClassLoader(); - if (target == null) { + // Common cases + if (target == classLoader || target == null) { return true; } - ClassLoader cur = classLoader; - if (cur == target) { - return true; + if (classLoader == null) { + return false; } - while (cur != null) { - cur = cur.getParent(); - if (cur == target) { + // Check for match in ancestors -> positive + ClassLoader current = classLoader; + while (current != null) { + current = current.getParent(); + if (current == target) { return true; } } - return false; + // Check for match in children -> negative + while (target != null) { + target = target.getParent(); + if (target == classLoader) { + return false; + } + } } catch (SecurityException ex) { - // Probably from the system ClassLoader - let's consider it safe. - return true; + // Fall through to Class reference comparison below } - } + // Fallback for ClassLoaders without parent/child relationship: + // safe if same Class can be loaded from given ClassLoader + return (classLoader != null && isVisible(clazz, classLoader)); + } /** - * Get the class name without the qualified package name. - * @param className the className to get the short name for - * @return the class name of the class without the package name - * @throws IllegalArgumentException if the className is empty + * Resolve the given class name as primitive class, if appropriate, + * according to the JVM's naming rules for primitive classes. + *

Also supports the JVM's internal class names for primitive arrays. + * Does not support the "[]" suffix notation for primitive arrays; + * this is only supported by {@link #forName(String, ClassLoader)}. + * @param name the name of the potentially primitive class + * @return the primitive class, or {@code null} if the name does not denote + * a primitive class or primitive array class */ - public static String getShortName(String className) { - Assert.hasLength(className, "Class name must not be empty"); - int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); - int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR); - if (nameEndIndex == -1) { - nameEndIndex = className.length(); + @Nullable + public static Class resolvePrimitiveClassName(@Nullable String name) { + Class result = null; + // Most class names will be quite long, considering that they + // SHOULD sit in a package, so a length check is worthwhile. + if (name != null && name.length() <= 8) { + // Could be a primitive - likely. + result = primitiveTypeNameMap.get(name); } - String shortName = className.substring(lastDotIndex + 1, nameEndIndex); - shortName = shortName.replace(INNER_CLASS_SEPARATOR, PACKAGE_SEPARATOR); - return shortName; + return result; } /** - * Get the class name without the qualified package name. - * @param clazz the class to get the short name for - * @return the class name of the class without the package name + * Check if the given class represents a primitive wrapper, + * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double. + * @param clazz the class to check + * @return whether the given class is a primitive wrapper class */ - public static String getShortName(Class clazz) { - return getShortName(getQualifiedName(clazz)); + public static boolean isPrimitiveWrapper(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return primitiveWrapperTypeMap.containsKey(clazz); } /** - * Return the short string name of a Java class in uncapitalized JavaBeans - * property format. Strips the outer class name in case of an inner class. - * @param clazz the class - * @return the short name rendered in a standard JavaBeans property format - * @see java.beans.Introspector#decapitalize(String) + * Check if the given class represents a primitive (i.e. boolean, byte, + * char, short, int, long, float, or double) or a primitive wrapper + * (i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double). + * @param clazz the class to check + * @return whether the given class is a primitive or primitive wrapper class */ - public static String getShortNameAsProperty(Class clazz) { - String shortName = getShortName(clazz); - int dotIndex = shortName.lastIndexOf(PACKAGE_SEPARATOR); - shortName = (dotIndex != -1 ? shortName.substring(dotIndex + 1) : shortName); - return Introspector.decapitalize(shortName); + public static boolean isPrimitiveOrWrapper(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isPrimitive() || isPrimitiveWrapper(clazz)); } /** - * Determine the name of the class file, relative to the containing - * package: e.g. "String.class" - * @param clazz the class - * @return the file name of the ".class" file + * Check if the given class represents an array of primitives, + * i.e. boolean, byte, char, short, int, long, float, or double. + * @param clazz the class to check + * @return whether the given class is a primitive array class */ - public static String getClassFileName(Class clazz) { + public static boolean isPrimitiveArray(Class clazz) { Assert.notNull(clazz, "Class must not be null"); - String className = clazz.getName(); - int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); - return className.substring(lastDotIndex + 1) + CLASS_FILE_SUFFIX; + return (clazz.isArray() && clazz.getComponentType().isPrimitive()); } /** - * Determine the name of the package of the given class, - * e.g. "java.lang" for the {@code java.lang.String} class. - * @param clazz the class - * @return the package name, or the empty String if the class - * is defined in the default package + * Check if the given class represents an array of primitive wrappers, + * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double. + * @param clazz the class to check + * @return whether the given class is a primitive wrapper array class */ - public static String getPackageName(Class clazz) { + public static boolean isPrimitiveWrapperArray(Class clazz) { Assert.notNull(clazz, "Class must not be null"); - return getPackageName(clazz.getName()); + return (clazz.isArray() && isPrimitiveWrapper(clazz.getComponentType())); } /** - * Determine the name of the package of the given fully-qualified class name, - * e.g. "java.lang" for the {@code java.lang.String} class name. - * @param fqClassName the fully-qualified class name - * @return the package name, or the empty String if the class - * is defined in the default package + * Resolve the given class if it is a primitive class, + * returning the corresponding primitive wrapper type instead. + * @param clazz the class to check + * @return the original class, or a primitive wrapper for the original primitive type */ - public static String getPackageName(String fqClassName) { - Assert.notNull(fqClassName, "Class name must not be null"); - int lastDotIndex = fqClassName.lastIndexOf(PACKAGE_SEPARATOR); - return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : ""); + public static Class resolvePrimitiveIfNecessary(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isPrimitive() && clazz != void.class ? primitiveTypeToWrapperMap.get(clazz) : clazz); } /** - * Return the qualified name of the given class: usually simply - * the class name, but component type class name + "[]" for arrays. - * @param clazz the class - * @return the qualified name of the class + * Check if the right-hand side type may be assigned to the left-hand side + * type, assuming setting by reflection. Considers primitive wrapper + * classes as assignable to the corresponding primitive types. + * @param lhsType the target type + * @param rhsType the value type that should be assigned to the target type + * @return if the target type is assignable from the value type + * @see TypeUtils#isAssignable */ - public static String getQualifiedName(Class clazz) { - Assert.notNull(clazz, "Class must not be null"); - return clazz.getTypeName(); + public static boolean isAssignable(Class lhsType, Class rhsType) { + Assert.notNull(lhsType, "Left-hand side type must not be null"); + Assert.notNull(rhsType, "Right-hand side type must not be null"); + if (lhsType.isAssignableFrom(rhsType)) { + return true; + } + if (lhsType.isPrimitive()) { + Class resolvedPrimitive = primitiveWrapperTypeMap.get(rhsType); + if (lhsType == resolvedPrimitive) { + return true; + } + } + else { + Class resolvedWrapper = primitiveTypeToWrapperMap.get(rhsType); + if (resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper)) { + return true; + } + } + return false; } /** - * Return the qualified name of the given method, consisting of - * fully qualified interface/class name + "." + method name. - * @param method the method - * @return the qualified name of the method + * Determine if the given type is assignable from the given value, + * assuming setting by reflection. Considers primitive wrapper classes + * as assignable to the corresponding primitive types. + * @param type the target type + * @param value the value that should be assigned to the type + * @return if the type is assignable from the value */ - public static String getQualifiedMethodName(Method method) { - return getQualifiedMethodName(method, null); + public static boolean isAssignableValue(Class type, @Nullable Object value) { + Assert.notNull(type, "Type must not be null"); + return (value != null ? isAssignable(type, value.getClass()) : !type.isPrimitive()); } /** - * Return the qualified name of the given method, consisting of - * fully qualified interface/class name + "." + method name. - * @param method the method - * @param clazz the clazz that the method is being invoked on - * (may be {@code null} to indicate the method's declaring class) - * @return the qualified name of the method - * @since 4.3.4 + * Convert a "/"-based resource path to a "."-based fully qualified class name. + * @param resourcePath the resource path pointing to a class + * @return the corresponding fully qualified class name */ - public static String getQualifiedMethodName(Method method, @Nullable Class clazz) { - Assert.notNull(method, "Method must not be null"); - return (clazz != null ? clazz : method.getDeclaringClass()).getName() + '.' + method.getName(); + public static String convertResourcePathToClassName(String resourcePath) { + Assert.notNull(resourcePath, "Resource path must not be null"); + return resourcePath.replace(PATH_SEPARATOR, PACKAGE_SEPARATOR); } /** - * Return a descriptive name for the given object's type: usually simply - * the class name, but component type class name + "[]" for arrays, - * and an appended list of implemented interfaces for JDK proxies. - * @param value the value to introspect - * @return the qualified name of the class + * Convert a "."-based fully qualified class name to a "/"-based resource path. + * @param className the fully qualified class name + * @return the corresponding resource path, pointing to the class */ - @Nullable - public static String getDescriptiveType(@Nullable Object value) { - if (value == null) { - return null; - } - Class clazz = value.getClass(); - if (Proxy.isProxyClass(clazz)) { - StringBuilder result = new StringBuilder(clazz.getName()); - result.append(" implementing "); - Class[] ifcs = clazz.getInterfaces(); - for (int i = 0; i < ifcs.length; i++) { - result.append(ifcs[i].getName()); - if (i < ifcs.length - 1) { - result.append(','); - } - } - return result.toString(); - } - else { - return clazz.getTypeName(); - } + public static String convertClassNameToResourcePath(String className) { + Assert.notNull(className, "Class name must not be null"); + return className.replace(PACKAGE_SEPARATOR, PATH_SEPARATOR); } /** - * Check whether the given class matches the user-specified type name. - * @param clazz the class to check - * @param typeName the type name to match + * Return a path suitable for use with {@code ClassLoader.getResource} + * (also suitable for use with {@code Class.getResource} by prepending a + * slash ('/') to the return value). Built by taking the package of the specified + * class file, converting all dots ('.') to slashes ('/'), adding a trailing slash + * if necessary, and concatenating the specified resource name to this. + *
As such, this function may be used to build a path suitable for + * loading a resource file that is in the same package as a class file, + * although {@link org.springframework.core.io.ClassPathResource} is usually + * even more convenient. + * @param clazz the Class whose package will be used as the base + * @param resourceName the resource name to append. A leading slash is optional. + * @return the built-up resource path + * @see ClassLoader#getResource + * @see Class#getResource */ - public static boolean matchesTypeName(Class clazz, @Nullable String typeName) { - return (typeName != null && - (typeName.equals(clazz.getTypeName()) || typeName.equals(clazz.getSimpleName()))); + public static String addResourcePathToPackagePath(Class clazz, String resourceName) { + Assert.notNull(resourceName, "Resource name must not be null"); + if (!resourceName.startsWith("/")) { + return classPackageAsResourcePath(clazz) + '/' + resourceName; + } + return classPackageAsResourcePath(clazz) + resourceName; } + /** + * Given an input class object, return a string which consists of the + * class's package name as a pathname, i.e., all dots ('.') are replaced by + * slashes ('/'). Neither a leading nor trailing slash is added. The result + * could be concatenated with a slash and the name of a resource and fed + * directly to {@code ClassLoader.getResource()}. For it to be fed to + * {@code Class.getResource} instead, a leading slash would also have + * to be prepended to the returned value. + * @param clazz the input class. A {@code null} value or the default + * (empty) package will result in an empty string ("") being returned. + * @return a path which represents the package name + * @see ClassLoader#getResource + * @see Class#getResource + */ + public static String classPackageAsResourcePath(@Nullable Class clazz) { + if (clazz == null) { + return ""; + } + String className = clazz.getName(); + int packageEndIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + if (packageEndIndex == -1) { + return ""; + } + String packageName = className.substring(0, packageEndIndex); + return packageName.replace(PACKAGE_SEPARATOR, PATH_SEPARATOR); + } /** - * Determine whether the given class has a public constructor with the given signature. - *

Essentially translates {@code NoSuchMethodException} to "false". - * @param clazz the clazz to analyze - * @param paramTypes the parameter types of the method - * @return whether the class has a corresponding constructor - * @see Class#getMethod + * Build a String that consists of the names of the classes/interfaces + * in the given array. + *

Basically like {@code AbstractCollection.toString()}, but stripping + * the "class "/"interface " prefix before every class name. + * @param classes an array of Class objects + * @return a String of form "[com.foo.Bar, com.foo.Baz]" + * @see java.util.AbstractCollection#toString() */ - public static boolean hasConstructor(Class clazz, Class... paramTypes) { - return (getConstructorIfAvailable(clazz, paramTypes) != null); + public static String classNamesToString(Class... classes) { + return classNamesToString(Arrays.asList(classes)); } /** - * Determine whether the given class has a public constructor with the given signature, - * and return it if available (else return {@code null}). - *

Essentially translates {@code NoSuchMethodException} to {@code null}. - * @param clazz the clazz to analyze - * @param paramTypes the parameter types of the method - * @return the constructor, or {@code null} if not found - * @see Class#getConstructor + * Build a String that consists of the names of the classes/interfaces + * in the given collection. + *

Basically like {@code AbstractCollection.toString()}, but stripping + * the "class "/"interface " prefix before every class name. + * @param classes a Collection of Class objects (may be {@code null}) + * @return a String of form "[com.foo.Bar, com.foo.Baz]" + * @see java.util.AbstractCollection#toString() */ - @Nullable - public static Constructor getConstructorIfAvailable(Class clazz, Class... paramTypes) { - Assert.notNull(clazz, "Class must not be null"); - try { - return clazz.getConstructor(paramTypes); + public static String classNamesToString(@Nullable Collection> classes) { + if (CollectionUtils.isEmpty(classes)) { + return "[]"; } - catch (NoSuchMethodException ex) { - return null; + StringBuilder sb = new StringBuilder("["); + for (Iterator> it = classes.iterator(); it.hasNext(); ) { + Class clazz = it.next(); + sb.append(clazz.getName()); + if (it.hasNext()) { + sb.append(", "); + } } + sb.append("]"); + return sb.toString(); } /** - * Determine whether the given class has a public method with the given signature. - *

Essentially translates {@code NoSuchMethodException} to "false". - * @param clazz the clazz to analyze - * @param methodName the name of the method - * @param paramTypes the parameter types of the method - * @return whether the class has a corresponding method - * @see Class#getMethod + * Copy the given {@code Collection} into a {@code Class} array. + *

The {@code Collection} must contain {@code Class} elements only. + * @param collection the {@code Collection} to copy + * @return the {@code Class} array + * @since 3.1 + * @see StringUtils#toStringArray */ - public static boolean hasMethod(Class clazz, String methodName, Class... paramTypes) { - return (getMethodIfAvailable(clazz, methodName, paramTypes) != null); + public static Class[] toClassArray(Collection> collection) { + return collection.toArray(new Class[0]); } /** - * Determine whether the given class has a public method with the given signature, - * and return it if available (else throws an {@code IllegalStateException}). - *

In case of any signature specified, only returns the method if there is a - * unique candidate, i.e. a single public method with the specified name. - *

Essentially translates {@code NoSuchMethodException} to {@code IllegalStateException}. - * @param clazz the clazz to analyze - * @param methodName the name of the method - * @param paramTypes the parameter types of the method - * (may be {@code null} to indicate any signature) - * @return the method (never {@code null}) - * @throws IllegalStateException if the method has not been found - * @see Class#getMethod + * Return all interfaces that the given instance implements as an array, + * including ones implemented by superclasses. + * @param instance the instance to analyze for interfaces + * @return all interfaces that the given instance implements as an array */ - public static Method getMethod(Class clazz, String methodName, @Nullable Class... paramTypes) { - Assert.notNull(clazz, "Class must not be null"); - Assert.notNull(methodName, "Method name must not be null"); - if (paramTypes != null) { - try { - return clazz.getMethod(methodName, paramTypes); - } - catch (NoSuchMethodException ex) { - throw new IllegalStateException("Expected method not found: " + ex); - } - } - else { - Set candidates = new HashSet<>(1); - Method[] methods = clazz.getMethods(); - for (Method method : methods) { - if (methodName.equals(method.getName())) { - candidates.add(method); - } - } - if (candidates.size() == 1) { - return candidates.iterator().next(); - } - else if (candidates.isEmpty()) { - throw new IllegalStateException("Expected method not found: " + clazz.getName() + '.' + methodName); - } - else { - throw new IllegalStateException("No unique method found: " + clazz.getName() + '.' + methodName); - } - } + public static Class[] getAllInterfaces(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getAllInterfacesForClass(instance.getClass()); } /** - * Determine whether the given class has a public method with the given signature, - * and return it if available (else return {@code null}). - *

In case of any signature specified, only returns the method if there is a - * unique candidate, i.e. a single public method with the specified name. - *

Essentially translates {@code NoSuchMethodException} to {@code null}. - * @param clazz the clazz to analyze - * @param methodName the name of the method - * @param paramTypes the parameter types of the method - * (may be {@code null} to indicate any signature) - * @return the method, or {@code null} if not found - * @see Class#getMethod + * Return all interfaces that the given class implements as an array, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @return all interfaces that the given object implements as an array */ - @Nullable - public static Method getMethodIfAvailable(Class clazz, String methodName, @Nullable Class... paramTypes) { - Assert.notNull(clazz, "Class must not be null"); - Assert.notNull(methodName, "Method name must not be null"); - if (paramTypes != null) { - try { - return clazz.getMethod(methodName, paramTypes); - } - catch (NoSuchMethodException ex) { - return null; - } - } - else { - Set candidates = new HashSet<>(1); - Method[] methods = clazz.getMethods(); - for (Method method : methods) { - if (methodName.equals(method.getName())) { - candidates.add(method); - } - } - if (candidates.size() == 1) { - return candidates.iterator().next(); - } - return null; - } + public static Class[] getAllInterfacesForClass(Class clazz) { + return getAllInterfacesForClass(clazz, null); } /** - * Return the number of methods with a given name (with any argument types), - * for the given class and/or its superclasses. Includes non-public methods. - * @param clazz the clazz to check - * @param methodName the name of the method - * @return the number of methods with the given name + * Return all interfaces that the given class implements as an array, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @param classLoader the ClassLoader that the interfaces need to be visible in + * (may be {@code null} when accepting all declared interfaces) + * @return all interfaces that the given object implements as an array */ - public static int getMethodCountForName(Class clazz, String methodName) { - Assert.notNull(clazz, "Class must not be null"); - Assert.notNull(methodName, "Method name must not be null"); - int count = 0; - Method[] declaredMethods = clazz.getDeclaredMethods(); - for (Method method : declaredMethods) { - if (methodName.equals(method.getName())) { - count++; - } - } - Class[] ifcs = clazz.getInterfaces(); - for (Class ifc : ifcs) { - count += getMethodCountForName(ifc, methodName); - } - if (clazz.getSuperclass() != null) { - count += getMethodCountForName(clazz.getSuperclass(), methodName); - } - return count; + public static Class[] getAllInterfacesForClass(Class clazz, @Nullable ClassLoader classLoader) { + return toClassArray(getAllInterfacesForClassAsSet(clazz, classLoader)); } /** - * Does the given class or one of its superclasses at least have one or more - * methods with the supplied name (with any argument types)? - * Includes non-public methods. - * @param clazz the clazz to check - * @param methodName the name of the method - * @return whether there is at least one method with the given name + * Return all interfaces that the given instance implements as a Set, + * including ones implemented by superclasses. + * @param instance the instance to analyze for interfaces + * @return all interfaces that the given instance implements as a Set */ - public static boolean hasAtLeastOneMethodWithName(Class clazz, String methodName) { - Assert.notNull(clazz, "Class must not be null"); - Assert.notNull(methodName, "Method name must not be null"); - Method[] declaredMethods = clazz.getDeclaredMethods(); - for (Method method : declaredMethods) { - if (method.getName().equals(methodName)) { - return true; - } - } - Class[] ifcs = clazz.getInterfaces(); - for (Class ifc : ifcs) { - if (hasAtLeastOneMethodWithName(ifc, methodName)) { - return true; - } - } - return (clazz.getSuperclass() != null && hasAtLeastOneMethodWithName(clazz.getSuperclass(), methodName)); + public static Set> getAllInterfacesAsSet(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getAllInterfacesForClassAsSet(instance.getClass()); } /** - * Given a method, which may come from an interface, and a target class used - * in the current reflective invocation, find the corresponding target method - * if there is one. E.g. the method may be {@code IFoo.bar()} and the - * target class may be {@code DefaultFoo}. In this case, the method may be - * {@code DefaultFoo.bar()}. This enables attributes on that method to be found. - *

NOTE: In contrast to {@link org.springframework.aop.support.AopUtils#getMostSpecificMethod}, - * this method does not resolve Java 5 bridge methods automatically. - * Call {@link org.springframework.core.BridgeMethodResolver#findBridgedMethod} - * if bridge method resolution is desirable (e.g. for obtaining metadata from - * the original method definition). - *

NOTE: Since Spring 3.1.1, if Java security settings disallow reflective - * access (e.g. calls to {@code Class#getDeclaredMethods} etc, this implementation - * will fall back to returning the originally provided method. - * @param method the method to be invoked, which may come from an interface - * @param targetClass the target class for the current invocation. - * May be {@code null} or may not even implement the method. - * @return the specific target method, or the original method if the - * {@code targetClass} doesn't implement it or is {@code null} + * Return all interfaces that the given class implements as a Set, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @return all interfaces that the given object implements as a Set */ - public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { - if (isOverridable(method, targetClass) && - targetClass != null && targetClass != method.getDeclaringClass()) { - try { - if (Modifier.isPublic(method.getModifiers())) { - try { - return targetClass.getMethod(method.getName(), method.getParameterTypes()); - } - catch (NoSuchMethodException ex) { - return method; - } - } - else { - Method specificMethod = - ReflectionUtils.findMethod(targetClass, method.getName(), method.getParameterTypes()); - return (specificMethod != null ? specificMethod : method); - } - } - catch (SecurityException ex) { - // Security settings are disallowing reflective access; fall back to 'method' below. + public static Set> getAllInterfacesForClassAsSet(Class clazz) { + return getAllInterfacesForClassAsSet(clazz, null); + } + + /** + * Return all interfaces that the given class implements as a Set, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @param classLoader the ClassLoader that the interfaces need to be visible in + * (may be {@code null} when accepting all declared interfaces) + * @return all interfaces that the given object implements as a Set + */ + public static Set> getAllInterfacesForClassAsSet(Class clazz, @Nullable ClassLoader classLoader) { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isInterface() && isVisible(clazz, classLoader)) { + return Collections.>singleton(clazz); + } + Set> interfaces = new LinkedHashSet<>(); + Class current = clazz; + while (current != null) { + Class[] ifcs = current.getInterfaces(); + for (Class ifc : ifcs) { + interfaces.addAll(getAllInterfacesForClassAsSet(ifc, classLoader)); } + current = current.getSuperclass(); } - return method; + return interfaces; } /** - * Determine whether the given method is declared by the user or at least pointing to - * a user-declared method. - *

Checks {@link Method#isSynthetic()} (for implementation methods) as well as the - * {@code GroovyObject} interface (for interface methods; on an implementation class, - * implementations of the {@code GroovyObject} methods will be marked as synthetic anyway). - * Note that, despite being synthetic, bridge methods ({@link Method#isBridge()}) are considered - * as user-level methods since they are eventually pointing to a user-declared generic method. - * @param method the method to check - * @return {@code true} if the method can be considered as user-declared; [@code false} otherwise + * Create a composite interface Class for the given interfaces, + * implementing the given interfaces in one single Class. + *

This implementation builds a JDK proxy class for the given interfaces. + * @param interfaces the interfaces to merge + * @param classLoader the ClassLoader to create the composite Class in + * @return the merged interface as Class + * @see java.lang.reflect.Proxy#getProxyClass */ - public static boolean isUserLevelMethod(Method method) { - Assert.notNull(method, "Method must not be null"); - return (method.isBridge() || (!method.isSynthetic() && !isGroovyObjectMethod(method))); - } - - private static boolean isGroovyObjectMethod(Method method) { - return method.getDeclaringClass().getName().equals("groovy.lang.GroovyObject"); + @SuppressWarnings("deprecation") + public static Class createCompositeInterface(Class[] interfaces, @Nullable ClassLoader classLoader) { + Assert.notEmpty(interfaces, "Interfaces must not be empty"); + return Proxy.getProxyClass(classLoader, interfaces); } /** - * Determine whether the given method is overridable in the given target class. - * @param method the method to check - * @param targetClass the target class to check against + * Determine the common ancestor of the given classes, if any. + * @param clazz1 the class to introspect + * @param clazz2 the other class to introspect + * @return the common ancestor (i.e. common superclass, one interface + * extending the other), or {@code null} if none found. If any of the + * given classes is {@code null}, the other class will be returned. + * @since 3.2.6 */ - private static boolean isOverridable(Method method, @Nullable Class targetClass) { - if (Modifier.isPrivate(method.getModifiers())) { - return false; + @Nullable + public static Class determineCommonAncestor(@Nullable Class clazz1, @Nullable Class clazz2) { + if (clazz1 == null) { + return clazz2; } - if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) { - return true; + if (clazz2 == null) { + return clazz1; } - return (targetClass == null || - getPackageName(method.getDeclaringClass()).equals(getPackageName(targetClass))); + if (clazz1.isAssignableFrom(clazz2)) { + return clazz1; + } + if (clazz2.isAssignableFrom(clazz1)) { + return clazz2; + } + Class ancestor = clazz1; + do { + ancestor = ancestor.getSuperclass(); + if (ancestor == null || Object.class == ancestor) { + return null; + } + } + while (!ancestor.isAssignableFrom(clazz2)); + return ancestor; } /** - * Return a public static method of a class. - * @param clazz the class which defines the method - * @param methodName the static method name - * @param args the parameter types to the method - * @return the static method, or {@code null} if no static method was found - * @throws IllegalArgumentException if the method name is blank or the clazz is null + * Determine whether the given interface is a common Java language interface: + * {@link Serializable}, {@link Externalizable}, {@link Closeable}, {@link AutoCloseable}, + * {@link Cloneable}, {@link Comparable} - all of which can be ignored when looking + * for 'primary' user-level interfaces. Common characteristics: no service-level + * operations, no bean property methods, no default methods. + * @param ifc the interface to check + * @since 5.0.3 */ - @Nullable - public static Method getStaticMethod(Class clazz, String methodName, Class... args) { - Assert.notNull(clazz, "Class must not be null"); - Assert.notNull(methodName, "Method name must not be null"); - try { - Method method = clazz.getMethod(methodName, args); - return Modifier.isStatic(method.getModifiers()) ? method : null; - } - catch (NoSuchMethodException ex) { - return null; - } + public static boolean isJavaLanguageInterface(Class ifc) { + return javaLanguageInterfaces.contains(ifc); } + /** + * Determine if the supplied class is an inner class, + * i.e. a non-static member of an enclosing class. + * @return {@code true} if the supplied class is an inner class + * @since 5.0.5 + * @see Class#isMemberClass() + */ + public static boolean isInnerClass(Class clazz) { + return (clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers())); + } /** - * Check if the given class represents a primitive wrapper, - * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double. - * @param clazz the class to check - * @return whether the given class is a primitive wrapper class + * Check whether the given object is a CGLIB proxy. + * @param object the object to check + * @see #isCglibProxyClass(Class) + * @see org.springframework.aop.support.AopUtils#isCglibProxy(Object) */ - public static boolean isPrimitiveWrapper(Class clazz) { - Assert.notNull(clazz, "Class must not be null"); - return primitiveWrapperTypeMap.containsKey(clazz); + public static boolean isCglibProxy(Object object) { + return isCglibProxyClass(object.getClass()); } /** - * Check if the given class represents a primitive (i.e. boolean, byte, - * char, short, int, long, float, or double) or a primitive wrapper - * (i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double). + * Check whether the specified class is a CGLIB-generated class. * @param clazz the class to check - * @return whether the given class is a primitive or primitive wrapper class + * @see #isCglibProxyClassName(String) */ - public static boolean isPrimitiveOrWrapper(Class clazz) { - Assert.notNull(clazz, "Class must not be null"); - return (clazz.isPrimitive() || isPrimitiveWrapper(clazz)); + public static boolean isCglibProxyClass(@Nullable Class clazz) { + return (clazz != null && isCglibProxyClassName(clazz.getName())); } /** - * Check if the given class represents an array of primitives, - * i.e. boolean, byte, char, short, int, long, float, or double. - * @param clazz the class to check - * @return whether the given class is a primitive array class + * Check whether the specified class name is a CGLIB-generated class. + * @param className the class name to check */ - public static boolean isPrimitiveArray(Class clazz) { - Assert.notNull(clazz, "Class must not be null"); - return (clazz.isArray() && clazz.getComponentType().isPrimitive()); + public static boolean isCglibProxyClassName(@Nullable String className) { + return (className != null && className.contains(CGLIB_CLASS_SEPARATOR)); } /** - * Check if the given class represents an array of primitive wrappers, - * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double. - * @param clazz the class to check - * @return whether the given class is a primitive wrapper array class + * Return the user-defined class for the given instance: usually simply + * the class of the given instance, but the original class in case of a + * CGLIB-generated subclass. + * @param instance the instance to check + * @return the user-defined class */ - public static boolean isPrimitiveWrapperArray(Class clazz) { - Assert.notNull(clazz, "Class must not be null"); - return (clazz.isArray() && isPrimitiveWrapper(clazz.getComponentType())); + public static Class getUserClass(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getUserClass(instance.getClass()); } /** - * Resolve the given class if it is a primitive class, - * returning the corresponding primitive wrapper type instead. + * Return the user-defined class for the given class: usually simply the given + * class, but the original class in case of a CGLIB-generated subclass. * @param clazz the class to check - * @return the original class, or a primitive wrapper for the original primitive type + * @return the user-defined class */ - public static Class resolvePrimitiveIfNecessary(Class clazz) { - Assert.notNull(clazz, "Class must not be null"); - return (clazz.isPrimitive() && clazz != void.class ? primitiveTypeToWrapperMap.get(clazz) : clazz); + public static Class getUserClass(Class clazz) { + if (clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) { + Class superclass = clazz.getSuperclass(); + if (superclass != null && Object.class != superclass) { + return superclass; + } + } + return clazz; } /** - * Check if the right-hand side type may be assigned to the left-hand side - * type, assuming setting by reflection. Considers primitive wrapper - * classes as assignable to the corresponding primitive types. - * @param lhsType the target type - * @param rhsType the value type that should be assigned to the target type - * @return if the target type is assignable from the value type - * @see TypeUtils#isAssignable + * Return a descriptive name for the given object's type: usually simply + * the class name, but component type class name + "[]" for arrays, + * and an appended list of implemented interfaces for JDK proxies. + * @param value the value to introspect + * @return the qualified name of the class */ - public static boolean isAssignable(Class lhsType, Class rhsType) { - Assert.notNull(lhsType, "Left-hand side type must not be null"); - Assert.notNull(rhsType, "Right-hand side type must not be null"); - if (lhsType.isAssignableFrom(rhsType)) { - return true; + @Nullable + public static String getDescriptiveType(@Nullable Object value) { + if (value == null) { + return null; } - if (lhsType.isPrimitive()) { - Class resolvedPrimitive = primitiveWrapperTypeMap.get(rhsType); - if (lhsType == resolvedPrimitive) { - return true; + Class clazz = value.getClass(); + if (Proxy.isProxyClass(clazz)) { + StringBuilder result = new StringBuilder(clazz.getName()); + result.append(" implementing "); + Class[] ifcs = clazz.getInterfaces(); + for (int i = 0; i < ifcs.length; i++) { + result.append(ifcs[i].getName()); + if (i < ifcs.length - 1) { + result.append(','); + } } + return result.toString(); } else { - Class resolvedWrapper = primitiveTypeToWrapperMap.get(rhsType); - if (resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper)) { - return true; - } + return clazz.getTypeName(); } - return false; } /** - * Determine if the given type is assignable from the given value, - * assuming setting by reflection. Considers primitive wrapper classes - * as assignable to the corresponding primitive types. - * @param type the target type - * @param value the value that should be assigned to the type - * @return if the type is assignable from the value + * Check whether the given class matches the user-specified type name. + * @param clazz the class to check + * @param typeName the type name to match */ - public static boolean isAssignableValue(Class type, @Nullable Object value) { - Assert.notNull(type, "Type must not be null"); - return (value != null ? isAssignable(type, value.getClass()) : !type.isPrimitive()); + public static boolean matchesTypeName(Class clazz, @Nullable String typeName) { + return (typeName != null && + (typeName.equals(clazz.getTypeName()) || typeName.equals(clazz.getSimpleName()))); } + /** + * Get the class name without the qualified package name. + * @param className the className to get the short name for + * @return the class name of the class without the package name + * @throws IllegalArgumentException if the className is empty + */ + public static String getShortName(String className) { + Assert.hasLength(className, "Class name must not be empty"); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR); + if (nameEndIndex == -1) { + nameEndIndex = className.length(); + } + String shortName = className.substring(lastDotIndex + 1, nameEndIndex); + shortName = shortName.replace(INNER_CLASS_SEPARATOR, PACKAGE_SEPARATOR); + return shortName; + } /** - * Convert a "/"-based resource path to a "."-based fully qualified class name. - * @param resourcePath the resource path pointing to a class - * @return the corresponding fully qualified class name + * Get the class name without the qualified package name. + * @param clazz the class to get the short name for + * @return the class name of the class without the package name */ - public static String convertResourcePathToClassName(String resourcePath) { - Assert.notNull(resourcePath, "Resource path must not be null"); - return resourcePath.replace(PATH_SEPARATOR, PACKAGE_SEPARATOR); + public static String getShortName(Class clazz) { + return getShortName(getQualifiedName(clazz)); } /** - * Convert a "."-based fully qualified class name to a "/"-based resource path. - * @param className the fully qualified class name - * @return the corresponding resource path, pointing to the class + * Return the short string name of a Java class in uncapitalized JavaBeans + * property format. Strips the outer class name in case of an inner class. + * @param clazz the class + * @return the short name rendered in a standard JavaBeans property format + * @see java.beans.Introspector#decapitalize(String) */ - public static String convertClassNameToResourcePath(String className) { - Assert.notNull(className, "Class name must not be null"); - return className.replace(PACKAGE_SEPARATOR, PATH_SEPARATOR); + public static String getShortNameAsProperty(Class clazz) { + String shortName = getShortName(clazz); + int dotIndex = shortName.lastIndexOf(PACKAGE_SEPARATOR); + shortName = (dotIndex != -1 ? shortName.substring(dotIndex + 1) : shortName); + return Introspector.decapitalize(shortName); } /** - * Return a path suitable for use with {@code ClassLoader.getResource} - * (also suitable for use with {@code Class.getResource} by prepending a - * slash ('/') to the return value). Built by taking the package of the specified - * class file, converting all dots ('.') to slashes ('/'), adding a trailing slash - * if necessary, and concatenating the specified resource name to this. - *
As such, this function may be used to build a path suitable for - * loading a resource file that is in the same package as a class file, - * although {@link org.springframework.core.io.ClassPathResource} is usually - * even more convenient. - * @param clazz the Class whose package will be used as the base - * @param resourceName the resource name to append. A leading slash is optional. - * @return the built-up resource path - * @see ClassLoader#getResource - * @see Class#getResource + * Determine the name of the class file, relative to the containing + * package: e.g. "String.class" + * @param clazz the class + * @return the file name of the ".class" file */ - public static String addResourcePathToPackagePath(Class clazz, String resourceName) { - Assert.notNull(resourceName, "Resource name must not be null"); - if (!resourceName.startsWith("/")) { - return classPackageAsResourcePath(clazz) + '/' + resourceName; - } - return classPackageAsResourcePath(clazz) + resourceName; + public static String getClassFileName(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + String className = clazz.getName(); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + return className.substring(lastDotIndex + 1) + CLASS_FILE_SUFFIX; } /** - * Given an input class object, return a string which consists of the - * class's package name as a pathname, i.e., all dots ('.') are replaced by - * slashes ('/'). Neither a leading nor trailing slash is added. The result - * could be concatenated with a slash and the name of a resource and fed - * directly to {@code ClassLoader.getResource()}. For it to be fed to - * {@code Class.getResource} instead, a leading slash would also have - * to be prepended to the returned value. - * @param clazz the input class. A {@code null} value or the default - * (empty) package will result in an empty string ("") being returned. - * @return a path which represents the package name - * @see ClassLoader#getResource - * @see Class#getResource + * Determine the name of the package of the given class, + * e.g. "java.lang" for the {@code java.lang.String} class. + * @param clazz the class + * @return the package name, or the empty String if the class + * is defined in the default package */ - public static String classPackageAsResourcePath(@Nullable Class clazz) { - if (clazz == null) { - return ""; - } - String className = clazz.getName(); - int packageEndIndex = className.lastIndexOf(PACKAGE_SEPARATOR); - if (packageEndIndex == -1) { - return ""; - } - String packageName = className.substring(0, packageEndIndex); - return packageName.replace(PACKAGE_SEPARATOR, PATH_SEPARATOR); + public static String getPackageName(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return getPackageName(clazz.getName()); } /** - * Build a String that consists of the names of the classes/interfaces - * in the given array. - *

Basically like {@code AbstractCollection.toString()}, but stripping - * the "class "/"interface " prefix before every class name. - * @param classes an array of Class objects - * @return a String of form "[com.foo.Bar, com.foo.Baz]" - * @see java.util.AbstractCollection#toString() + * Determine the name of the package of the given fully-qualified class name, + * e.g. "java.lang" for the {@code java.lang.String} class name. + * @param fqClassName the fully-qualified class name + * @return the package name, or the empty String if the class + * is defined in the default package */ - public static String classNamesToString(Class... classes) { - return classNamesToString(Arrays.asList(classes)); + public static String getPackageName(String fqClassName) { + Assert.notNull(fqClassName, "Class name must not be null"); + int lastDotIndex = fqClassName.lastIndexOf(PACKAGE_SEPARATOR); + return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : ""); } /** - * Build a String that consists of the names of the classes/interfaces - * in the given collection. - *

Basically like {@code AbstractCollection.toString()}, but stripping - * the "class "/"interface " prefix before every class name. - * @param classes a Collection of Class objects (may be {@code null}) - * @return a String of form "[com.foo.Bar, com.foo.Baz]" - * @see java.util.AbstractCollection#toString() + * Return the qualified name of the given class: usually simply + * the class name, but component type class name + "[]" for arrays. + * @param clazz the class + * @return the qualified name of the class */ - public static String classNamesToString(@Nullable Collection> classes) { - if (CollectionUtils.isEmpty(classes)) { - return "[]"; - } - StringBuilder sb = new StringBuilder("["); - for (Iterator> it = classes.iterator(); it.hasNext(); ) { - Class clazz = it.next(); - sb.append(clazz.getName()); - if (it.hasNext()) { - sb.append(", "); - } - } - sb.append("]"); - return sb.toString(); + public static String getQualifiedName(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return clazz.getTypeName(); } /** - * Copy the given {@code Collection} into a {@code Class} array. - *

The {@code Collection} must contain {@code Class} elements only. - * @param collection the {@code Collection} to copy - * @return the {@code Class} array - * @since 3.1 - * @see StringUtils#toStringArray + * Return the qualified name of the given method, consisting of + * fully qualified interface/class name + "." + method name. + * @param method the method + * @return the qualified name of the method */ - public static Class[] toClassArray(Collection> collection) { - return collection.toArray(new Class[0]); + public static String getQualifiedMethodName(Method method) { + return getQualifiedMethodName(method, null); } /** - * Return all interfaces that the given instance implements as an array, - * including ones implemented by superclasses. - * @param instance the instance to analyze for interfaces - * @return all interfaces that the given instance implements as an array + * Return the qualified name of the given method, consisting of + * fully qualified interface/class name + "." + method name. + * @param method the method + * @param clazz the clazz that the method is being invoked on + * (may be {@code null} to indicate the method's declaring class) + * @return the qualified name of the method + * @since 4.3.4 */ - public static Class[] getAllInterfaces(Object instance) { - Assert.notNull(instance, "Instance must not be null"); - return getAllInterfacesForClass(instance.getClass()); + public static String getQualifiedMethodName(Method method, @Nullable Class clazz) { + Assert.notNull(method, "Method must not be null"); + return (clazz != null ? clazz : method.getDeclaringClass()).getName() + '.' + method.getName(); } /** - * Return all interfaces that the given class implements as an array, - * including ones implemented by superclasses. - *

If the class itself is an interface, it gets returned as sole interface. - * @param clazz the class to analyze for interfaces - * @return all interfaces that the given object implements as an array + * Determine whether the given class has a public constructor with the given signature. + *

Essentially translates {@code NoSuchMethodException} to "false". + * @param clazz the clazz to analyze + * @param paramTypes the parameter types of the method + * @return whether the class has a corresponding constructor + * @see Class#getMethod */ - public static Class[] getAllInterfacesForClass(Class clazz) { - return getAllInterfacesForClass(clazz, null); + public static boolean hasConstructor(Class clazz, Class... paramTypes) { + return (getConstructorIfAvailable(clazz, paramTypes) != null); } /** - * Return all interfaces that the given class implements as an array, - * including ones implemented by superclasses. - *

If the class itself is an interface, it gets returned as sole interface. - * @param clazz the class to analyze for interfaces - * @param classLoader the ClassLoader that the interfaces need to be visible in - * (may be {@code null} when accepting all declared interfaces) - * @return all interfaces that the given object implements as an array + * Determine whether the given class has a public constructor with the given signature, + * and return it if available (else return {@code null}). + *

Essentially translates {@code NoSuchMethodException} to {@code null}. + * @param clazz the clazz to analyze + * @param paramTypes the parameter types of the method + * @return the constructor, or {@code null} if not found + * @see Class#getConstructor */ - public static Class[] getAllInterfacesForClass(Class clazz, @Nullable ClassLoader classLoader) { - return toClassArray(getAllInterfacesForClassAsSet(clazz, classLoader)); + @Nullable + public static Constructor getConstructorIfAvailable(Class clazz, Class... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + try { + return clazz.getConstructor(paramTypes); + } + catch (NoSuchMethodException ex) { + return null; + } } /** - * Return all interfaces that the given instance implements as a Set, - * including ones implemented by superclasses. - * @param instance the instance to analyze for interfaces - * @return all interfaces that the given instance implements as a Set + * Determine whether the given class has a public method with the given signature. + *

Essentially translates {@code NoSuchMethodException} to "false". + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * @return whether the class has a corresponding method + * @see Class#getMethod */ - public static Set> getAllInterfacesAsSet(Object instance) { - Assert.notNull(instance, "Instance must not be null"); - return getAllInterfacesForClassAsSet(instance.getClass()); + public static boolean hasMethod(Class clazz, String methodName, Class... paramTypes) { + return (getMethodIfAvailable(clazz, methodName, paramTypes) != null); } /** - * Return all interfaces that the given class implements as a Set, - * including ones implemented by superclasses. - *

If the class itself is an interface, it gets returned as sole interface. - * @param clazz the class to analyze for interfaces - * @return all interfaces that the given object implements as a Set + * Determine whether the given class has a public method with the given signature, + * and return it if available (else throws an {@code IllegalStateException}). + *

In case of any signature specified, only returns the method if there is a + * unique candidate, i.e. a single public method with the specified name. + *

Essentially translates {@code NoSuchMethodException} to {@code IllegalStateException}. + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * (may be {@code null} to indicate any signature) + * @return the method (never {@code null}) + * @throws IllegalStateException if the method has not been found + * @see Class#getMethod */ - public static Set> getAllInterfacesForClassAsSet(Class clazz) { - return getAllInterfacesForClassAsSet(clazz, null); + public static Method getMethod(Class clazz, String methodName, @Nullable Class... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + if (paramTypes != null) { + try { + return clazz.getMethod(methodName, paramTypes); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Expected method not found: " + ex); + } + } + else { + Set candidates = new HashSet<>(1); + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (methodName.equals(method.getName())) { + candidates.add(method); + } + } + if (candidates.size() == 1) { + return candidates.iterator().next(); + } + else if (candidates.isEmpty()) { + throw new IllegalStateException("Expected method not found: " + clazz.getName() + '.' + methodName); + } + else { + throw new IllegalStateException("No unique method found: " + clazz.getName() + '.' + methodName); + } + } } /** - * Return all interfaces that the given class implements as a Set, - * including ones implemented by superclasses. - *

If the class itself is an interface, it gets returned as sole interface. - * @param clazz the class to analyze for interfaces - * @param classLoader the ClassLoader that the interfaces need to be visible in - * (may be {@code null} when accepting all declared interfaces) - * @return all interfaces that the given object implements as a Set + * Determine whether the given class has a public method with the given signature, + * and return it if available (else return {@code null}). + *

In case of any signature specified, only returns the method if there is a + * unique candidate, i.e. a single public method with the specified name. + *

Essentially translates {@code NoSuchMethodException} to {@code null}. + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * (may be {@code null} to indicate any signature) + * @return the method, or {@code null} if not found + * @see Class#getMethod */ - public static Set> getAllInterfacesForClassAsSet(Class clazz, @Nullable ClassLoader classLoader) { + @Nullable + public static Method getMethodIfAvailable(Class clazz, String methodName, @Nullable Class... paramTypes) { Assert.notNull(clazz, "Class must not be null"); - if (clazz.isInterface() && isVisible(clazz, classLoader)) { - return Collections.>singleton(clazz); + Assert.notNull(methodName, "Method name must not be null"); + if (paramTypes != null) { + try { + return clazz.getMethod(methodName, paramTypes); + } + catch (NoSuchMethodException ex) { + return null; + } } - Set> interfaces = new LinkedHashSet<>(); - Class current = clazz; - while (current != null) { - Class[] ifcs = current.getInterfaces(); - for (Class ifc : ifcs) { - interfaces.addAll(getAllInterfacesForClassAsSet(ifc, classLoader)); + else { + Set candidates = new HashSet<>(1); + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (methodName.equals(method.getName())) { + candidates.add(method); + } } - current = current.getSuperclass(); + if (candidates.size() == 1) { + return candidates.iterator().next(); + } + return null; } - return interfaces; } /** - * Create a composite interface Class for the given interfaces, - * implementing the given interfaces in one single Class. - *

This implementation builds a JDK proxy class for the given interfaces. - * @param interfaces the interfaces to merge - * @param classLoader the ClassLoader to create the composite Class in - * @return the merged interface as Class - * @see java.lang.reflect.Proxy#getProxyClass + * Return the number of methods with a given name (with any argument types), + * for the given class and/or its superclasses. Includes non-public methods. + * @param clazz the clazz to check + * @param methodName the name of the method + * @return the number of methods with the given name */ - @SuppressWarnings("deprecation") - public static Class createCompositeInterface(Class[] interfaces, @Nullable ClassLoader classLoader) { - Assert.notEmpty(interfaces, "Interfaces must not be empty"); - return Proxy.getProxyClass(classLoader, interfaces); + public static int getMethodCountForName(Class clazz, String methodName) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + int count = 0; + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method method : declaredMethods) { + if (methodName.equals(method.getName())) { + count++; + } + } + Class[] ifcs = clazz.getInterfaces(); + for (Class ifc : ifcs) { + count += getMethodCountForName(ifc, methodName); + } + if (clazz.getSuperclass() != null) { + count += getMethodCountForName(clazz.getSuperclass(), methodName); + } + return count; } /** - * Determine the common ancestor of the given classes, if any. - * @param clazz1 the class to introspect - * @param clazz2 the other class to introspect - * @return the common ancestor (i.e. common superclass, one interface - * extending the other), or {@code null} if none found. If any of the - * given classes is {@code null}, the other class will be returned. - * @since 3.2.6 + * Does the given class or one of its superclasses at least have one or more + * methods with the supplied name (with any argument types)? + * Includes non-public methods. + * @param clazz the clazz to check + * @param methodName the name of the method + * @return whether there is at least one method with the given name */ - @Nullable - public static Class determineCommonAncestor(@Nullable Class clazz1, @Nullable Class clazz2) { - if (clazz1 == null) { - return clazz2; - } - if (clazz2 == null) { - return clazz1; - } - if (clazz1.isAssignableFrom(clazz2)) { - return clazz1; - } - if (clazz2.isAssignableFrom(clazz1)) { - return clazz2; + public static boolean hasAtLeastOneMethodWithName(Class clazz, String methodName) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method method : declaredMethods) { + if (method.getName().equals(methodName)) { + return true; + } } - Class ancestor = clazz1; - do { - ancestor = ancestor.getSuperclass(); - if (ancestor == null || Object.class == ancestor) { - return null; + Class[] ifcs = clazz.getInterfaces(); + for (Class ifc : ifcs) { + if (hasAtLeastOneMethodWithName(ifc, methodName)) { + return true; } } - while (!ancestor.isAssignableFrom(clazz2)); - return ancestor; + return (clazz.getSuperclass() != null && hasAtLeastOneMethodWithName(clazz.getSuperclass(), methodName)); } /** - * Check whether the given class is visible in the given ClassLoader. - * @param clazz the class to check (typically an interface) - * @param classLoader the ClassLoader to check against (may be {@code null}, - * in which case this method will always return {@code true}) + * Given a method, which may come from an interface, and a target class used + * in the current reflective invocation, find the corresponding target method + * if there is one. E.g. the method may be {@code IFoo.bar()} and the + * target class may be {@code DefaultFoo}. In this case, the method may be + * {@code DefaultFoo.bar()}. This enables attributes on that method to be found. + *

NOTE: In contrast to {@link org.springframework.aop.support.AopUtils#getMostSpecificMethod}, + * this method does not resolve Java 5 bridge methods automatically. + * Call {@link org.springframework.core.BridgeMethodResolver#findBridgedMethod} + * if bridge method resolution is desirable (e.g. for obtaining metadata from + * the original method definition). + *

NOTE: Since Spring 3.1.1, if Java security settings disallow reflective + * access (e.g. calls to {@code Class#getDeclaredMethods} etc, this implementation + * will fall back to returning the originally provided method. + * @param method the method to be invoked, which may come from an interface + * @param targetClass the target class for the current invocation. + * May be {@code null} or may not even implement the method. + * @return the specific target method, or the original method if the + * {@code targetClass} doesn't implement it or is {@code null} */ - public static boolean isVisible(Class clazz, @Nullable ClassLoader classLoader) { - if (classLoader == null) { - return true; - } - try { - Class actualClass = classLoader.loadClass(clazz.getName()); - return (clazz == actualClass); - // Else: different interface class found... - } - catch (ClassNotFoundException ex) { - // No interface class found... - return false; + public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { + if (isOverridable(method, targetClass) && + targetClass != null && targetClass != method.getDeclaringClass()) { + try { + if (Modifier.isPublic(method.getModifiers())) { + try { + return targetClass.getMethod(method.getName(), method.getParameterTypes()); + } + catch (NoSuchMethodException ex) { + return method; + } + } + else { + Method specificMethod = + ReflectionUtils.findMethod(targetClass, method.getName(), method.getParameterTypes()); + return (specificMethod != null ? specificMethod : method); + } + } + catch (SecurityException ex) { + // Security settings are disallowing reflective access; fall back to 'method' below. + } } + return method; } /** - * Determine whether the given interface is a common Java language interface: - * {@link Serializable}, {@link Externalizable}, {@link Closeable}, {@link AutoCloseable}, - * {@link Cloneable}, {@link Comparable} - all of which can be ignored when looking - * for 'primary' user-level interfaces. Common characteristics: no service-level - * operations, no bean property methods, no default methods. - * @param ifc the interface to check - * @since 5.0.3 + * Determine whether the given method is declared by the user or at least pointing to + * a user-declared method. + *

Checks {@link Method#isSynthetic()} (for implementation methods) as well as the + * {@code GroovyObject} interface (for interface methods; on an implementation class, + * implementations of the {@code GroovyObject} methods will be marked as synthetic anyway). + * Note that, despite being synthetic, bridge methods ({@link Method#isBridge()}) are considered + * as user-level methods since they are eventually pointing to a user-declared generic method. + * @param method the method to check + * @return {@code true} if the method can be considered as user-declared; [@code false} otherwise */ - public static boolean isJavaLanguageInterface(Class ifc) { - return javaLanguageInterfaces.contains(ifc); + public static boolean isUserLevelMethod(Method method) { + Assert.notNull(method, "Method must not be null"); + return (method.isBridge() || (!method.isSynthetic() && !isGroovyObjectMethod(method))); } - /** - * Check whether the given object is a CGLIB proxy. - * @param object the object to check - * @see #isCglibProxyClass(Class) - * @see org.springframework.aop.support.AopUtils#isCglibProxy(Object) - */ - public static boolean isCglibProxy(Object object) { - return isCglibProxyClass(object.getClass()); + private static boolean isGroovyObjectMethod(Method method) { + return method.getDeclaringClass().getName().equals("groovy.lang.GroovyObject"); } /** - * Check whether the specified class is a CGLIB-generated class. - * @param clazz the class to check - * @see #isCglibProxyClassName(String) + * Determine whether the given method is overridable in the given target class. + * @param method the method to check + * @param targetClass the target class to check against */ - public static boolean isCglibProxyClass(@Nullable Class clazz) { - return (clazz != null && isCglibProxyClassName(clazz.getName())); + private static boolean isOverridable(Method method, @Nullable Class targetClass) { + if (Modifier.isPrivate(method.getModifiers())) { + return false; + } + if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) { + return true; + } + return (targetClass == null || + getPackageName(method.getDeclaringClass()).equals(getPackageName(targetClass))); } /** - * Check whether the specified class name is a CGLIB-generated class. - * @param className the class name to check + * Return a public static method of a class. + * @param clazz the class which defines the method + * @param methodName the static method name + * @param args the parameter types to the method + * @return the static method, or {@code null} if no static method was found + * @throws IllegalArgumentException if the method name is blank or the clazz is null */ - public static boolean isCglibProxyClassName(@Nullable String className) { - return (className != null && className.contains(CGLIB_CLASS_SEPARATOR)); + @Nullable + public static Method getStaticMethod(Class clazz, String methodName, Class... args) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + try { + Method method = clazz.getMethod(methodName, args); + return Modifier.isStatic(method.getModifiers()) ? method : null; + } + catch (NoSuchMethodException ex) { + return null; + } } } diff --git a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java index 8786363c681..5d783df70d9 100644 --- a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java @@ -16,6 +16,7 @@ package org.springframework.util; +import java.io.Externalizable; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -44,20 +45,21 @@ import static org.junit.Assert.*; * @author Rob Harrop * @author Rick Evans */ -@SuppressWarnings({ "rawtypes", "unchecked" }) public class ClassUtilsTests { private ClassLoader classLoader = getClass().getClassLoader(); + @Before - public void setUp() { + public void clearStatics() { InnerClass.noArgCalled = false; InnerClass.argCalled = false; InnerClass.overloadedCalled = false; } + @Test - public void testIsPresent() throws Exception { + public void testIsPresent() { assertTrue(ClassUtils.isPresent("java.lang.String", classLoader)); assertFalse(ClassUtils.isPresent("java.lang.MySpecialString", classLoader)); } @@ -114,6 +116,36 @@ public class ClassUtilsTests { assertEquals(double[].class, ClassUtils.forName(double[].class.getName(), classLoader)); } + @Test + public void testIsCacheSafe() { + ClassLoader childLoader1 = new ClassLoader(classLoader) {}; + ClassLoader childLoader2 = new ClassLoader(classLoader) {}; + ClassLoader childLoader3 = new ClassLoader(classLoader) { + @Override + public Class loadClass(String name) throws ClassNotFoundException { + return childLoader1.loadClass(name); + } + }; + Class composite = ClassUtils.createCompositeInterface( + new Class[] {Serializable.class, Externalizable.class}, childLoader1); + + assertTrue(ClassUtils.isCacheSafe(String.class, null)); + assertTrue(ClassUtils.isCacheSafe(String.class, classLoader)); + assertTrue(ClassUtils.isCacheSafe(String.class, childLoader1)); + assertTrue(ClassUtils.isCacheSafe(String.class, childLoader2)); + assertTrue(ClassUtils.isCacheSafe(String.class, childLoader3)); + assertFalse(ClassUtils.isCacheSafe(InnerClass.class, null)); + assertTrue(ClassUtils.isCacheSafe(InnerClass.class, classLoader)); + assertTrue(ClassUtils.isCacheSafe(InnerClass.class, childLoader1)); + assertTrue(ClassUtils.isCacheSafe(InnerClass.class, childLoader2)); + assertTrue(ClassUtils.isCacheSafe(InnerClass.class, childLoader3)); + assertFalse(ClassUtils.isCacheSafe(composite, null)); + assertFalse(ClassUtils.isCacheSafe(composite, classLoader)); + assertTrue(ClassUtils.isCacheSafe(composite, childLoader1)); + assertFalse(ClassUtils.isCacheSafe(composite, childLoader2)); + assertTrue(ClassUtils.isCacheSafe(composite, childLoader3)); + } + @Test public void testGetShortName() { String className = ClassUtils.getShortName(getClass()); @@ -199,7 +231,7 @@ public class ClassUtilsTests { } @Test - public void testHasMethod() throws Exception { + public void testHasMethod() { assertTrue(ClassUtils.hasMethod(Collection.class, "size")); assertTrue(ClassUtils.hasMethod(Collection.class, "remove", Object.class)); assertFalse(ClassUtils.hasMethod(Collection.class, "remove")); @@ -207,7 +239,7 @@ public class ClassUtilsTests { } @Test - public void testGetMethodIfAvailable() throws Exception { + public void testGetMethodIfAvailable() { Method method = ClassUtils.getMethodIfAvailable(Collection.class, "size"); assertNotNull(method); assertEquals("size", method.getName()); @@ -278,7 +310,7 @@ public class ClassUtilsTests { @Test public void testClassPackageAsResourcePath() { String result = ClassUtils.classPackageAsResourcePath(Proxy.class); - assertTrue(result.equals("java/lang/reflect")); + assertEquals("java/lang/reflect", result); } @Test @@ -294,7 +326,7 @@ public class ClassUtilsTests { @Test public void testGetAllInterfaces() { DerivedTestObject testBean = new DerivedTestObject(); - List ifcs = Arrays.asList(ClassUtils.getAllInterfaces(testBean)); + List> ifcs = Arrays.asList(ClassUtils.getAllInterfaces(testBean)); assertEquals("Correct number of interfaces", 4, ifcs.size()); assertTrue("Contains Serializable", ifcs.contains(Serializable.class)); assertTrue("Contains ITestBean", ifcs.contains(ITestObject.class)); @@ -303,13 +335,13 @@ public class ClassUtilsTests { @Test public void testClassNamesToString() { - List ifcs = new LinkedList(); + List> ifcs = new LinkedList<>(); ifcs.add(Serializable.class); ifcs.add(Runnable.class); assertEquals("[interface java.io.Serializable, interface java.lang.Runnable]", ifcs.toString()); assertEquals("[java.io.Serializable, java.lang.Runnable]", ClassUtils.classNamesToString(ifcs)); - List classes = new LinkedList(); + List> classes = new LinkedList<>(); classes.add(LinkedList.class); classes.add(Integer.class); assertEquals("[class java.util.LinkedList, class java.lang.Integer]", classes.toString()); @@ -319,7 +351,7 @@ public class ClassUtilsTests { assertEquals("[java.util.List]", ClassUtils.classNamesToString(List.class)); assertEquals("[]", Collections.EMPTY_LIST.toString()); - assertEquals("[]", ClassUtils.classNamesToString(Collections.EMPTY_LIST)); + assertEquals("[]", ClassUtils.classNamesToString(Collections.emptyList())); } @Test