From 4b0e0fa27dad32aade70e8a120580e1ef266e9ca Mon Sep 17 00:00:00 2001 From: Chengang Guan Date: Thu, 9 Jul 2026 15:20:28 +0800 Subject: [PATCH] Support recursive resolution for nested class names in Java source style Prior to this commit, ClassUtils#forName only performed a single-level conversion when resolving nested class names in Java source style (e.g. "java.lang.Thread.State"). For multi-level nested classes such as "com.example.Outer.Middle.Inner", the method would fail to resolve the actual class "com.example.Outer$Middle$Inner" because only the last dot was converted to a dollar sign. This commit improves the resolution by introducing a loop that iteratively converts dots to dollar signs from right to left, until the class is found or no further conversion is possible. Additionally, if the given name already contains a dollar sign, the method now fails fast without attempting unnecessary conversions. Signed-off-by: Chengang Guan --- .../org/springframework/util/ClassUtils.java | 30 +++++++++++++------ .../springframework/util/ClassUtilsTests.java | 22 ++++++++++++++ 2 files changed, 43 insertions(+), 9 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 e53db635fdc2..d87344818c07 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -67,6 +67,7 @@ * @author Sam Brannen * @author Sebastien Deleuze * @author Sungbin Yang + * @author Chengang Guan * @since 1.1 * @see TypeUtils * @see ReflectionUtils @@ -308,16 +309,27 @@ public static Class forName(String name, @Nullable ClassLoader classLoader) return Class.forName(name, false, clToUse); } catch (ClassNotFoundException ex) { - int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR); - int previousDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR, lastDotIndex - 1); - if (lastDotIndex != -1 && previousDotIndex != -1 && Character.isUpperCase(name.charAt(previousDotIndex + 1))) { - String nestedClassName = - name.substring(0, lastDotIndex) + NESTED_CLASS_SEPARATOR + name.substring(lastDotIndex + 1); - try { - return Class.forName(nestedClassName, false, clToUse); + if (name.lastIndexOf(NESTED_CLASS_SEPARATOR) > 0) { + // not java source style + throw ex; + } + String curName = name; + for (;;) { + int lastDotIndex = curName.lastIndexOf(PACKAGE_SEPARATOR); + int previousDotIndex = curName.lastIndexOf(PACKAGE_SEPARATOR, lastDotIndex - 1); + if (lastDotIndex != -1 && previousDotIndex != -1 && + Character.isUpperCase(curName.charAt(previousDotIndex + 1))) { + curName = curName.substring(0, lastDotIndex) + NESTED_CLASS_SEPARATOR + + curName.substring(lastDotIndex + 1); + try { + return Class.forName(curName, false, clToUse); + } + catch (ClassNotFoundException ex2) { + // Swallow - let original exception get through + } } - catch (ClassNotFoundException ex2) { - // Swallow - let original exception get through + else { + break; } } throw ex; 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 1baeb027d6c7..2a36bb5f5d52 100644 --- a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java @@ -54,6 +54,7 @@ import org.springframework.tests.sample.objects.TestObject; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link ClassUtils}. @@ -63,6 +64,7 @@ * @author Rob Harrop * @author Rick Evans * @author Sam Brannen + * @author Chengang Guan */ class ClassUtilsTests { @@ -101,6 +103,20 @@ void forNameWithNestedType() throws ClassNotFoundException { assertThat(ClassUtils.forName("a.ClassHavingNestedClass.NestedClass", classLoader)).isEqualTo(ClassHavingNestedClass.NestedClass.class); } + @Test + void forNamesWithDeepNestingTypes() throws ClassNotFoundException { + assertThat(ClassUtils.forName("org.springframework.util.ClassUtilsTests.NestedClass.NestedClassLevel1", classLoader)) + .isEqualTo(NestedClass.NestedClassLevel1.class); + assertThat(ClassUtils.forName("org.springframework.util.ClassUtilsTests.NestedClass.NestedClassLevel1.NestedClassLevel2", classLoader)) + .isEqualTo(NestedClass.NestedClassLevel1.NestedClassLevel2.class); + assertThat(ClassUtils.forName("org.springframework.util.ClassUtilsTests$NestedClass$NestedClassLevel1$NestedClassLevel2", classLoader)) + .isEqualTo(NestedClass.NestedClassLevel1.NestedClassLevel2.class); + assertThatThrownBy(() -> ClassUtils.forName("org.springframework.util.ClassUtilsTests.NestedClass$NestedClassLevel1", classLoader)) + .isInstanceOf(ClassNotFoundException.class); + assertThatThrownBy(() -> ClassUtils.forName("org.springframework.util.ClassUtilsTests$NestedClass.NestedClassLevel1", classLoader)) + .isInstanceOf(ClassNotFoundException.class); + } + @Test void forNameWithPrimitiveClasses() throws ClassNotFoundException { assertThat(ClassUtils.forName("boolean", classLoader)).isEqualTo(boolean.class); @@ -980,6 +996,12 @@ private static void assertStatic(Member member) { public static class NestedClass { + public static class NestedClassLevel1 { + + public static class NestedClassLevel2 { + } + } + static boolean noArgCalled; static boolean argCalled; static boolean overloadedCalled;