From 35881d4c8a12d4ae159ca564b3180c232a6ba593 Mon Sep 17 00:00:00 2001 From: Iwauo Tajima Date: Thu, 23 Oct 2025 00:56:19 +0900 Subject: [PATCH] [LANG-1685] reflectionToString falls back to toString if reflective access is not permitted. --- .../builder/ReflectionToStringBuilder.java | 8 ++ .../lang3/builder/ReflectiveAccessUtil.java | 96 +++++++++++++++++++ .../ReflectionToStringBuilderTest.java | 45 +++++++++ 3 files changed, 149 insertions(+) create mode 100644 src/main/java/org/apache/commons/lang3/builder/ReflectiveAccessUtil.java diff --git a/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java b/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java index e3080bdc2d5..9acaf27396c 100644 --- a/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java +++ b/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java @@ -849,6 +849,12 @@ public String toString() { validate(); Class clazz = getObject().getClass(); + + if (!reflectiveAccess.isAllowed(clazz)) { + appendToString(getStyle().getContentStart() + getObject().toString() + getStyle().getContentEnd()); + return super.toString(); + } + appendFieldsIn(clazz); while (clazz.getSuperclass() != null && clazz != getUpToClass()) { clazz = clazz.getSuperclass(); @@ -857,6 +863,8 @@ public String toString() { return super.toString(); } + ReflectiveAccessUtil reflectiveAccess = new ReflectiveAccessUtil(); + /** * Validates that include and exclude names do not intersect. */ diff --git a/src/main/java/org/apache/commons/lang3/builder/ReflectiveAccessUtil.java b/src/main/java/org/apache/commons/lang3/builder/ReflectiveAccessUtil.java new file mode 100644 index 00000000000..d78bbc756bc --- /dev/null +++ b/src/main/java/org/apache/commons/lang3/builder/ReflectiveAccessUtil.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3.builder; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Utility class to determine whether reflective access to a target module is permitted from the current module. + * This class assumes all reflective accesses are allowed in JVM where JPMS is not supported (i.e. JDK8 or earlier). + */ +class ReflectiveAccessUtil { + + /** + * Determines whether {@code targetClass} is accessible via reflection from the module that defines this class. + * On JVMs that do not support the Java Platform Module System, the method always returns {@code true}. + * Otherwise, it checks whether the module that declares {@code targetClass} is open to the module containing + * this class at runtime. + * + * @param targetClass the {@link Class} object to inspect. + * @return {@code true} if {@code targetClass} can be reflected on from this module, + * {@code false} otherwise. + */ + boolean isAllowed(Class targetClass) { + if (!isJpmsSupported()) { + return true; + } + if (targetClass.isArray()) { + return true; + } + try { + final Object targetModule = getModule.invoke(targetClass); + return (Boolean) isOpen.invoke(targetModule, targetClass.getPackage().getName(), selfModule); + } catch (IllegalAccessException | InvocationTargetException e) { + return false; + } + } + + /** + * Indicates whether the current JVM supports the Java Platform Module System (JPMS). + * @return {@code true} if JPMS is available. + */ + boolean isJpmsSupported() { + return getModule != null; + } + + // Cache the following methods and object in static fields to minimize performance impact. + private static final Method getModule = initializeGetModuleMethod(); + private static final Object selfModule = initializeSelfModule(); + private static final Method isOpen = initializeIsOpenMethod(); + + private static Method initializeGetModuleMethod() { + try { + return Class.class.getMethod("getModule"); + } catch (NoSuchMethodException e) { + return null; + } + } + + private static Object initializeSelfModule() { + if (getModule == null) { + return null; + } + try { + return getModule.invoke(ReflectiveAccessUtil.class); + } catch (InvocationTargetException | IllegalAccessException e) { + return null; + } + } + + private static Method initializeIsOpenMethod() { + if (selfModule == null) { + return null; + } + try { + final Class moduleClass = selfModule.getClass(); + return moduleClass.getMethod("isOpen", String.class, moduleClass); + } catch (NoSuchMethodException e) { + return null; + } + } +} diff --git a/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderTest.java b/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderTest.java index bcbbc690105..8913861ee62 100644 --- a/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderTest.java +++ b/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderTest.java @@ -17,6 +17,7 @@ package org.apache.commons.lang3.builder; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.apache.commons.lang3.AbstractLangTest; import org.junit.jupiter.api.Test; @@ -31,4 +32,48 @@ void testConstructorWithNullObject() { assertEquals("", new ReflectionToStringBuilder(null, ToStringStyle.DEFAULT_STYLE, new StringBuffer()).toString()); } + @Test + void testFallsBackToCallingToStringMethodIfReflectiveAccessNotAllowed() { + final String data = "string-value"; + final ReflectionToStringBuilder builder = new ReflectionToStringBuilder(data, ToStringStyle.DEFAULT_STYLE, new StringBuffer()); + builder.reflectiveAccess = new ReflectiveAccessUtil() { + @Override + boolean isAllowed(Class targetClass) { + return false; + } + }; + assertEquals(String.format("java.lang.String@%s[%s]", Integer.toHexString(System.identityHashCode(data)), data), builder.toString()); + } + + @Test + void testUsesReflectionToGetFieldsIfReflectiveAccessAllowed() { + final String data = "string-value"; + final ReflectionToStringBuilder builder = new ReflectionToStringBuilder(data, ToStringStyle.DEFAULT_STYLE, new StringBuffer()); + builder.reflectiveAccess = new ReflectiveAccessUtil() { + @Override + boolean isAllowed(Class targetClass) { + return true; + } + }; + final String result = builder.toString(); + assertTrue(result.startsWith(String.format("java.lang.String@%s[", Integer.toHexString(System.identityHashCode(data))))); + assertTrue(result.contains(String.format("hash=%s", data.hashCode()))); + assertTrue(result.endsWith("]")); + } + + @Test + void testUsesReflectionToGetFieldsIfJpmsNotSupported() { + final String data = "string-value"; + final ReflectionToStringBuilder builder = new ReflectionToStringBuilder(data, ToStringStyle.DEFAULT_STYLE, new StringBuffer()); + builder.reflectiveAccess = new ReflectiveAccessUtil() { + @Override + boolean isJpmsSupported() { + return false; + } + }; + final String result = builder.toString(); + assertTrue(result.startsWith(String.format("java.lang.String@%s[", Integer.toHexString(System.identityHashCode(data))))); + assertTrue(result.contains(String.format("hash=%s", data.hashCode()))); + assertTrue(result.endsWith("]")); + } }