Skip to content

Commit 89ed04e

Browse files
committed
Handle null fields and prevent reflection cycles
Updated reflection helpers to be more null-safe and robust. `FieldUtils#getFieldValue`/`setFieldValue` now accept nullable `Field` inputs and return `null` when a field is missing instead of throwing from null assertions, with trace logging aligned to the new flow. `ReflectionUtils#readFieldsAsMap` now reads inherited non-static fields and tracks visited instances to avoid recursive cycles while mapping nested objects. Tests were adjusted to match the new missing-field behavior, and `Data#hashCode` was hardened with null-safe/object-type-specific hash calls.
1 parent bb7cd0d commit 89ed04e

4 files changed

Lines changed: 60 additions & 52 deletions

File tree

microsphere-java-core/src/main/java/io/microsphere/reflect/FieldUtils.java

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import static io.microsphere.reflect.AccessibleObjectUtils.trySetAccessible;
3838
import static io.microsphere.reflect.TypeUtils.isObjectClass;
3939
import static io.microsphere.text.FormatUtils.format;
40-
import static io.microsphere.util.Assert.assertNotNull;
4140
import static io.microsphere.util.ClassUtils.getAllInheritedTypes;
4241
import static io.microsphere.util.ObjectUtils.defaultIfNull;
4342
import static java.util.Collections.unmodifiableSet;
@@ -767,17 +766,16 @@ public static <V> V getFieldValue(boolean forceAccess, @Nonnull Object instance,
767766
*
768767
* @param <V> The type of the field value
769768
* @param instance The object instance from which to retrieve the field value
770-
* @param field The {@link Field} object representing the field to retrieve
769+
* @param field The {@link Field} object representing the field to retrieve (nullable)
771770
* @return The field value, or {@code null} if the field is {@code null}
772771
* @throws IllegalStateException if this {@code Field} object is enforcing Java language access control and the
773772
* underlying field is inaccessible
774773
* @throws IllegalArgumentException if the specified object is not an instance of the class or interface declaring
775-
* the underlying field (or a subclass or implementor thereof), or if the field
776-
* is <code>null</code>.
774+
* the underlying field (or a subclass or implementor thereof)
777775
* @throws NullPointerException if the specified object is null and the field is an instance field.
778776
*/
779777
@Nullable
780-
public static <V> V getFieldValue(@Nullable Object instance, @Nonnull Field field) throws IllegalStateException,
778+
public static <V> V getFieldValue(@Nullable Object instance, @Nullable Field field) throws IllegalStateException,
781779
IllegalArgumentException, NullPointerException {
782780
return getFieldValue(false, instance, field);
783781
}
@@ -804,19 +802,21 @@ public static <V> V getFieldValue(@Nullable Object instance, @Nonnull Field fiel
804802
* @param <V> The type of the field value
805803
* @param forceAccess Whether to force reflective accessibility when reading the field
806804
* @param instance The object instance from which to retrieve the field value
807-
* @param field The {@link Field} object representing the field to retrieve
805+
* @param field The {@link Field} object representing the field to retrieve (nullable)
808806
* @return The value of the field if found and accessible; otherwise, {@code null}
809807
* @throws IllegalStateException if this {@code Field} object is enforcing Java language access control and the
810808
* underlying field is inaccessible.
811809
* @throws IllegalArgumentException if the specified object is not an
812810
* instance of the class or interface declaring the underlying field (or a subclass
813-
* or implementor thereof). or if the field is <code>null</code>.
811+
* or implementor thereof).
814812
* @throws NullPointerException if the specified object is null and the field is an instance field.
815813
*/
816814
@Nullable
817-
public static <V> V getFieldValue(boolean forceAccess, @Nullable Object instance, @Nonnull Field field) throws
815+
public static <V> V getFieldValue(boolean forceAccess, @Nullable Object instance, @Nullable Field field) throws
818816
IllegalStateException, IllegalArgumentException, NullPointerException {
819-
assertNotNull(field, () -> "The 'field' must not be null");
817+
if (field == null) {
818+
return null;
819+
}
820820

821821
V fieldValue = null;
822822
RuntimeException failure = null;
@@ -830,7 +830,7 @@ public static <V> V getFieldValue(boolean forceAccess, @Nullable Object instance
830830
failure = new IllegalStateException(e);
831831
} finally {
832832
if (logger.isTraceEnabled()) {
833-
logger.trace("The value of field[signature : '{}' , forceAccess : {} , trySetAccessible : {} , instance : {}] : {}",
833+
logger.trace("Got the value of field[signature : '{}' , forceAccess : {} , trySetAccessible : {} , instance : {}] : {}",
834834
field, forceAccess, trySetAccessible, instance, fieldValue, failure);
835835
}
836836
}
@@ -971,7 +971,7 @@ public static <V> V setFieldValue(boolean forceAccess, @Nullable Object instance
971971
* @throws NullPointerException if the specified object is null and the field is an instance field.
972972
*/
973973
@Nullable
974-
public static <V> V setFieldValue(@Nullable Object instance, @Nonnull Field field, @Nullable V value) throws IllegalStateException, IllegalArgumentException {
974+
public static <V> V setFieldValue(@Nullable Object instance, @Nullable Field field, @Nullable V value) throws IllegalStateException, IllegalArgumentException {
975975
return setFieldValue(false, instance, field, value);
976976
}
977977

@@ -1009,22 +1009,34 @@ public static <V> V setFieldValue(@Nullable Object instance, @Nonnull Field fiel
10091009
* @throws NullPointerException if the specified object is null and the field is an instance field
10101010
*/
10111011
@Nullable
1012-
public static <V> V setFieldValue(boolean forceAccess, @Nullable Object instance, @Nonnull Field field, @Nullable V value)
1012+
public static <V> V setFieldValue(boolean forceAccess, @Nullable Object instance, @Nullable Field field, @Nullable V value)
10131013
throws IllegalStateException, IllegalArgumentException, NullPointerException {
1014-
assertNotNull(field, () -> "The 'field' must not be null");
1014+
if (field == null) {
1015+
return null;
1016+
}
10151017

10161018
V previousValue = null;
1019+
RuntimeException failure = null;
1020+
boolean trySetAccessible = false;
10171021
try {
10181022
if (forceAccess) {
1019-
trySetAccessible(field);
1023+
trySetAccessible = trySetAccessible(field);
10201024
}
10211025
previousValue = (V) field.get(instance);
10221026
if (!Objects.equals(previousValue, value)) {
10231027
field.set(instance, value);
10241028
}
10251029
} catch (IllegalAccessException | IllegalArgumentException e) {
1026-
handleFieldException(e, instance, field);
1027-
throw new IllegalArgumentException(e);
1030+
failure = new IllegalArgumentException(e);
1031+
} finally {
1032+
if (logger.isTraceEnabled()) {
1033+
logger.trace("Set the value of field[signature : '{}' , forceAccess : {} , trySetAccessible : {} , instance : {}] : {} -> {}",
1034+
field, forceAccess, trySetAccessible, instance, previousValue, value, failure);
1035+
}
1036+
}
1037+
1038+
if (failure != null) {
1039+
throw failure;
10281040
}
10291041

10301042
return previousValue;
@@ -1073,12 +1085,6 @@ public static void assertFieldMatchType(Object instance, String fieldName, Class
10731085
}
10741086
}
10751087

1076-
static void handleFieldException(Exception e, Object instance, Field field) {
1077-
if (logger.isTraceEnabled()) {
1078-
logger.trace("The instance[object : {}] can't match the field[{}]", instance, field, e);
1079-
}
1080-
}
1081-
10821088
private FieldUtils() {
10831089
}
10841090
}

microsphere-java-core/src/main/java/io/microsphere/reflect/ReflectionUtils.java

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@
1515
import java.util.LinkedHashMap;
1616
import java.util.List;
1717
import java.util.Map;
18+
import java.util.Set;
1819
import java.util.function.Function;
1920
import java.util.stream.Collectors;
2021
import java.util.stream.Stream;
2122

2223
import static io.microsphere.collection.ListUtils.newArrayList;
2324
import static io.microsphere.collection.MapUtils.newLinkedHashMap;
25+
import static io.microsphere.collection.SetUtils.newHashSet;
2426
import static io.microsphere.invoke.MethodHandlesLookupUtils.findPublicStatic;
2527
import static io.microsphere.logging.LoggerFactory.getLogger;
28+
import static io.microsphere.reflect.FieldUtils.findAllDeclaredFields;
2629
import static io.microsphere.reflect.FieldUtils.getFieldValue;
27-
import static io.microsphere.reflect.MemberUtils.isStatic;
2830
import static io.microsphere.reflect.MethodUtils.findMethod;
2931
import static io.microsphere.reflect.MethodUtils.invokeMethod;
3032
import static io.microsphere.reflect.MethodUtils.invokeStaticMethod;
@@ -445,25 +447,33 @@ public static Map<String, Object> readFieldsAsMap(Object object) {
445447
if (object == null) {
446448
return emptyMap();
447449
}
448-
Class<?> type = object.getClass();
449-
Field[] fields = type.getDeclaredFields();
450-
LinkedHashMap<String, Object> fieldsAsMap = newLinkedHashMap(fields.length);
451-
for (Field field : fields) {
452-
453-
if (isStatic(field)) { // To filter static fields
454-
continue;
455-
}
450+
Set<Object> visitedInstances = newHashSet();
451+
return readFieldsAsMap(object, visitedInstances);
452+
}
456453

454+
static Map<String, Object> readFieldsAsMap(Object object, Set<Object> visitedInstances) {
455+
if (object == null) {
456+
return emptyMap();
457+
}
458+
Class<?> type = object.getClass();
459+
Set<Field> allDeclaredFields = findAllDeclaredFields(type, MemberUtils::isNonStatic);
460+
visitedInstances.add(object);
461+
LinkedHashMap<String, Object> fieldsAsMap = newLinkedHashMap(allDeclaredFields.size());
462+
for (Field field : allDeclaredFields) {
457463
String fieldName = field.getName();
458-
Object fieldValue = getFieldValue(true, object, field);
459464
Class<?> fieldValueType = field.getType();
460-
if (fieldValue != object) {
461-
if (!isPrimitive(fieldValueType) && !isSimpleType(fieldValueType)
462-
&& !object.getClass().equals(fieldValueType)) {
463-
fieldValue = readFieldsAsMap(fieldValue);
465+
Object fieldValue = getFieldValue(true, object, field);
466+
if (fieldValue != null) {
467+
if (!visitedInstances.add(fieldValue)) {
468+
continue;
464469
}
465-
fieldsAsMap.put(fieldName, fieldValue);
466470
}
471+
472+
if (!isPrimitive(fieldValueType) && !isSimpleType(fieldValueType)) {
473+
fieldValue = readFieldsAsMap(fieldValue, visitedInstances);
474+
}
475+
476+
fieldsAsMap.put(fieldName, fieldValue);
467477
}
468478
return unmodifiableMap(fieldsAsMap);
469479
}

microsphere-java-core/src/test/java/io/microsphere/reflect/FieldUtilsTest.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,11 @@
3030
import static io.microsphere.reflect.FieldUtils.getDeclaredField;
3131
import static io.microsphere.reflect.FieldUtils.getFieldValue;
3232
import static io.microsphere.reflect.FieldUtils.getStaticFieldValue;
33-
import static io.microsphere.reflect.FieldUtils.handleFieldException;
3433
import static io.microsphere.reflect.FieldUtils.setFieldValue;
3534
import static io.microsphere.reflect.FieldUtils.setStaticFieldValue;
3635
import static io.microsphere.util.VersionUtils.CURRENT_JAVA_VERSION;
3736
import static io.microsphere.util.VersionUtils.JAVA_VERSION_8;
3837
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
39-
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
4038
import static org.junit.jupiter.api.Assertions.assertEquals;
4139
import static org.junit.jupiter.api.Assertions.assertNotNull;
4240
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -239,7 +237,7 @@ void testSetFieldValue() {
239237

240238
@Test
241239
void testSetFieldValueOnFieldNotFound() {
242-
assertThrows(IllegalArgumentException.class, () -> setFieldValue(test, "notFoundField", null));
240+
assertNull(setFieldValue(test, "notFoundField", null));
243241
}
244242

245243
@Test
@@ -271,12 +269,6 @@ void testAssertFieldMatchTypeOnIllegalArgumentException() {
271269
assertThrows(IllegalArgumentException.class, () -> assertFieldMatchType(test, "privateField", Integer.class));
272270
}
273271

274-
@Test
275-
void testHandleFieldException() {
276-
Field field = findField(ReflectionTest.class, "staticField");
277-
assertDoesNotThrow(() -> handleFieldException(new IllegalAccessException(), test, field));
278-
}
279-
280272
private void assertFindField(Object object, String fieldName) {
281273
assertNotNull(findField(object, fieldName));
282274
}

microsphere-java-core/src/test/java/io/microsphere/test/Data.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,16 +179,16 @@ && compare(weight, data.weight) == 0
179179

180180
@Override
181181
public int hashCode() {
182-
int result = name.hashCode();
183-
result = 31 * result + age;
182+
int result = Objects.hashCode(name);
183+
result = 31 * result + Integer.hashCode(age);
184184
result = 31 * result + Boolean.hashCode(male);
185185
result = 31 * result + Double.hashCode(height);
186186
result = 31 * result + Float.hashCode(weight);
187187
result = 31 * result + Long.hashCode(birth);
188-
result = 31 * result + index;
189-
result = 31 * result + grade;
190-
result = 31 * result + sex;
191-
result = 31 * result + object.hashCode();
188+
result = 31 * result + Short.hashCode(index);
189+
result = 31 * result + Byte.hashCode(grade);
190+
result = 31 * result + Character.hashCode(sex);
191+
result = 31 * result + Objects.hashCode(object);
192192
result = 31 * result + Arrays.hashCode(names);
193193
return result;
194194
}

0 commit comments

Comments
 (0)