1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.apache.ibatis.reflection;
17
18 import java.lang.invoke.MethodHandle;
19 import java.lang.invoke.MethodHandles;
20 import java.lang.invoke.MethodType;
21 import java.lang.reflect.Array;
22 import java.lang.reflect.Constructor;
23 import java.lang.reflect.Field;
24 import java.lang.reflect.GenericArrayType;
25 import java.lang.reflect.Method;
26 import java.lang.reflect.Modifier;
27 import java.lang.reflect.ParameterizedType;
28 import java.lang.reflect.ReflectPermission;
29 import java.lang.reflect.Type;
30 import java.text.MessageFormat;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Collection;
34 import java.util.HashMap;
35 import java.util.List;
36 import java.util.Locale;
37 import java.util.Map;
38 import java.util.Map.Entry;
39
40 import org.apache.ibatis.reflection.invoker.AmbiguousMethodInvoker;
41 import org.apache.ibatis.reflection.invoker.GetFieldInvoker;
42 import org.apache.ibatis.reflection.invoker.Invoker;
43 import org.apache.ibatis.reflection.invoker.MethodInvoker;
44 import org.apache.ibatis.reflection.invoker.SetFieldInvoker;
45 import org.apache.ibatis.reflection.property.PropertyNamer;
46 import org.apache.ibatis.util.MapUtil;
47
48
49
50
51
52
53
54 public class Reflector {
55
56 private static final MethodHandle isRecordMethodHandle = getIsRecordMethodHandle();
57 private final Class<?> type;
58 private final String[] readablePropertyNames;
59 private final String[] writablePropertyNames;
60 private final Map<String, Invoker> setMethods = new HashMap<>();
61 private final Map<String, Invoker> getMethods = new HashMap<>();
62 private final Map<String, Class<?>> setTypes = new HashMap<>();
63 private final Map<String, Class<?>> getTypes = new HashMap<>();
64 private Constructor<?> defaultConstructor;
65
66 private final Map<String, String> caseInsensitivePropertyMap = new HashMap<>();
67
68 public Reflector(Class<?> clazz) {
69 type = clazz;
70 addDefaultConstructor(clazz);
71 Method[] classMethods = getClassMethods(clazz);
72 if (isRecord(type)) {
73 addRecordGetMethods(classMethods);
74 } else {
75 addGetMethods(classMethods);
76 addSetMethods(classMethods);
77 addFields(clazz);
78 }
79 readablePropertyNames = getMethods.keySet().toArray(new String[0]);
80 writablePropertyNames = setMethods.keySet().toArray(new String[0]);
81 for (String propName : readablePropertyNames) {
82 caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
83 }
84 for (String propName : writablePropertyNames) {
85 caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
86 }
87 }
88
89 private void addRecordGetMethods(Method[] methods) {
90 Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0)
91 .forEach(m -> addGetMethod(m.getName(), m, false));
92 }
93
94 private void addDefaultConstructor(Class<?> clazz) {
95 Constructor<?>[] constructors = clazz.getDeclaredConstructors();
96 Arrays.stream(constructors).filter(constructor -> constructor.getParameterTypes().length == 0).findAny()
97 .ifPresent(constructor -> this.defaultConstructor = constructor);
98 }
99
100 private void addGetMethods(Method[] methods) {
101 Map<String, List<Method>> conflictingGetters = new HashMap<>();
102 Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
103 .forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
104 resolveGetterConflicts(conflictingGetters);
105 }
106
107 private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
108 for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
109 Method winner = null;
110 String propName = entry.getKey();
111 boolean isAmbiguous = false;
112 for (Method candidate : entry.getValue()) {
113 if (winner == null) {
114 winner = candidate;
115 continue;
116 }
117 Class<?> winnerType = winner.getReturnType();
118 Class<?> candidateType = candidate.getReturnType();
119 if (candidateType.equals(winnerType)) {
120 if (!boolean.class.equals(candidateType)) {
121 isAmbiguous = true;
122 break;
123 }
124 if (candidate.getName().startsWith("is")) {
125 winner = candidate;
126 }
127 } else if (candidateType.isAssignableFrom(winnerType)) {
128
129 } else if (winnerType.isAssignableFrom(candidateType)) {
130 winner = candidate;
131 } else {
132 isAmbiguous = true;
133 break;
134 }
135 }
136 addGetMethod(propName, winner, isAmbiguous);
137 }
138 }
139
140 private void addGetMethod(String name, Method method, boolean isAmbiguous) {
141 MethodInvoker invoker = isAmbiguous ? new AmbiguousMethodInvoker(method, MessageFormat.format(
142 "Illegal overloaded getter method with ambiguous type for property ''{0}'' in class ''{1}''. This breaks the JavaBeans specification and can cause unpredictable results.",
143 name, method.getDeclaringClass().getName())) : new MethodInvoker(method);
144 getMethods.put(name, invoker);
145 Type returnType = TypeParameterResolver.resolveReturnType(method, type);
146 getTypes.put(name, typeToClass(returnType));
147 }
148
149 private void addSetMethods(Method[] methods) {
150 Map<String, List<Method>> conflictingSetters = new HashMap<>();
151 Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 1 && PropertyNamer.isSetter(m.getName()))
152 .forEach(m -> addMethodConflict(conflictingSetters, PropertyNamer.methodToProperty(m.getName()), m));
153 resolveSetterConflicts(conflictingSetters);
154 }
155
156 private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
157 if (isValidPropertyName(name)) {
158 List<Method> list = MapUtil.computeIfAbsent(conflictingMethods, name, k -> new ArrayList<>());
159 list.add(method);
160 }
161 }
162
163 private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
164 for (Entry<String, List<Method>> entry : conflictingSetters.entrySet()) {
165 String propName = entry.getKey();
166 List<Method> setters = entry.getValue();
167 Class<?> getterType = getTypes.get(propName);
168 boolean isGetterAmbiguous = getMethods.get(propName) instanceof AmbiguousMethodInvoker;
169 boolean isSetterAmbiguous = false;
170 Method match = null;
171 for (Method setter : setters) {
172 if (!isGetterAmbiguous && setter.getParameterTypes()[0].equals(getterType)) {
173
174 match = setter;
175 break;
176 }
177 if (!isSetterAmbiguous) {
178 match = pickBetterSetter(match, setter, propName);
179 isSetterAmbiguous = match == null;
180 }
181 }
182 if (match != null) {
183 addSetMethod(propName, match);
184 }
185 }
186 }
187
188 private Method pickBetterSetter(Method setter1, Method setter2, String property) {
189 if (setter1 == null) {
190 return setter2;
191 }
192 Class<?> paramType1 = setter1.getParameterTypes()[0];
193 Class<?> paramType2 = setter2.getParameterTypes()[0];
194 if (paramType1.isAssignableFrom(paramType2)) {
195 return setter2;
196 }
197 if (paramType2.isAssignableFrom(paramType1)) {
198 return setter1;
199 }
200 MethodInvoker invoker = new AmbiguousMethodInvoker(setter1,
201 MessageFormat.format(
202 "Ambiguous setters defined for property ''{0}'' in class ''{1}'' with types ''{2}'' and ''{3}''.", property,
203 setter2.getDeclaringClass().getName(), paramType1.getName(), paramType2.getName()));
204 setMethods.put(property, invoker);
205 Type[] paramTypes = TypeParameterResolver.resolveParamTypes(setter1, type);
206 setTypes.put(property, typeToClass(paramTypes[0]));
207 return null;
208 }
209
210 private void addSetMethod(String name, Method method) {
211 MethodInvoker invoker = new MethodInvoker(method);
212 setMethods.put(name, invoker);
213 Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);
214 setTypes.put(name, typeToClass(paramTypes[0]));
215 }
216
217 private Class<?> typeToClass(Type src) {
218 Class<?> result = null;
219 if (src instanceof Class) {
220 result = (Class<?>) src;
221 } else if (src instanceof ParameterizedType) {
222 result = (Class<?>) ((ParameterizedType) src).getRawType();
223 } else if (src instanceof GenericArrayType) {
224 Type componentType = ((GenericArrayType) src).getGenericComponentType();
225 if (componentType instanceof Class) {
226 result = Array.newInstance((Class<?>) componentType, 0).getClass();
227 } else {
228 Class<?> componentClass = typeToClass(componentType);
229 result = Array.newInstance(componentClass, 0).getClass();
230 }
231 }
232 if (result == null) {
233 result = Object.class;
234 }
235 return result;
236 }
237
238 private void addFields(Class<?> clazz) {
239 Field[] fields = clazz.getDeclaredFields();
240 for (Field field : fields) {
241 if (!setMethods.containsKey(field.getName())) {
242
243
244
245 int modifiers = field.getModifiers();
246 if ((!Modifier.isFinal(modifiers) || !Modifier.isStatic(modifiers))) {
247 addSetField(field);
248 }
249 }
250 if (!getMethods.containsKey(field.getName())) {
251 addGetField(field);
252 }
253 }
254 if (clazz.getSuperclass() != null) {
255 addFields(clazz.getSuperclass());
256 }
257 }
258
259 private void addSetField(Field field) {
260 if (isValidPropertyName(field.getName())) {
261 setMethods.put(field.getName(), new SetFieldInvoker(field));
262 Type fieldType = TypeParameterResolver.resolveFieldType(field, type);
263 setTypes.put(field.getName(), typeToClass(fieldType));
264 }
265 }
266
267 private void addGetField(Field field) {
268 if (isValidPropertyName(field.getName())) {
269 getMethods.put(field.getName(), new GetFieldInvoker(field));
270 Type fieldType = TypeParameterResolver.resolveFieldType(field, type);
271 getTypes.put(field.getName(), typeToClass(fieldType));
272 }
273 }
274
275 private boolean isValidPropertyName(String name) {
276 return (!name.startsWith("$") && !"serialVersionUID".equals(name) && !"class".equals(name));
277 }
278
279
280
281
282
283
284
285
286
287
288 private Method[] getClassMethods(Class<?> clazz) {
289 Map<String, Method> uniqueMethods = new HashMap<>();
290 Class<?> currentClass = clazz;
291 while (currentClass != null && currentClass != Object.class) {
292 addUniqueMethods(uniqueMethods, currentClass.getDeclaredMethods());
293
294
295
296 Class<?>[] interfaces = currentClass.getInterfaces();
297 for (Class<?> anInterface : interfaces) {
298 addUniqueMethods(uniqueMethods, anInterface.getMethods());
299 }
300
301 currentClass = currentClass.getSuperclass();
302 }
303
304 Collection<Method> methods = uniqueMethods.values();
305
306 return methods.toArray(new Method[0]);
307 }
308
309 private void addUniqueMethods(Map<String, Method> uniqueMethods, Method[] methods) {
310 for (Method currentMethod : methods) {
311 if (!currentMethod.isBridge()) {
312 String signature = getSignature(currentMethod);
313
314
315
316 if (!uniqueMethods.containsKey(signature)) {
317 uniqueMethods.put(signature, currentMethod);
318 }
319 }
320 }
321 }
322
323 private String getSignature(Method method) {
324 StringBuilder sb = new StringBuilder();
325 Class<?> returnType = method.getReturnType();
326 sb.append(returnType.getName()).append('#');
327 sb.append(method.getName());
328 Class<?>[] parameters = method.getParameterTypes();
329 for (int i = 0; i < parameters.length; i++) {
330 sb.append(i == 0 ? ':' : ',').append(parameters[i].getName());
331 }
332 return sb.toString();
333 }
334
335
336
337
338
339
340
341
342 public static boolean canControlMemberAccessible() {
343 try {
344 SecurityManager securityManager = System.getSecurityManager();
345 if (null != securityManager) {
346 securityManager.checkPermission(new ReflectPermission("suppressAccessChecks"));
347 }
348 } catch (SecurityException e) {
349 return false;
350 }
351 return true;
352 }
353
354
355
356
357
358
359 public Class<?> getType() {
360 return type;
361 }
362
363 public Constructor<?> getDefaultConstructor() {
364 if (defaultConstructor != null) {
365 return defaultConstructor;
366 }
367 throw new ReflectionException("There is no default constructor for " + type);
368 }
369
370 public boolean hasDefaultConstructor() {
371 return defaultConstructor != null;
372 }
373
374 public Invoker getSetInvoker(String propertyName) {
375 Invoker method = setMethods.get(propertyName);
376 if (method == null) {
377 throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
378 }
379 return method;
380 }
381
382 public Invoker getGetInvoker(String propertyName) {
383 Invoker method = getMethods.get(propertyName);
384 if (method == null) {
385 throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
386 }
387 return method;
388 }
389
390
391
392
393
394
395
396
397
398 public Class<?> getSetterType(String propertyName) {
399 Class<?> clazz = setTypes.get(propertyName);
400 if (clazz == null) {
401 throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
402 }
403 return clazz;
404 }
405
406
407
408
409
410
411
412
413
414 public Class<?> getGetterType(String propertyName) {
415 Class<?> clazz = getTypes.get(propertyName);
416 if (clazz == null) {
417 throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
418 }
419 return clazz;
420 }
421
422
423
424
425
426
427 public String[] getGetablePropertyNames() {
428 return readablePropertyNames;
429 }
430
431
432
433
434
435
436 public String[] getSetablePropertyNames() {
437 return writablePropertyNames;
438 }
439
440
441
442
443
444
445
446
447
448 public boolean hasSetter(String propertyName) {
449 return setMethods.containsKey(propertyName);
450 }
451
452
453
454
455
456
457
458
459
460 public boolean hasGetter(String propertyName) {
461 return getMethods.containsKey(propertyName);
462 }
463
464 public String findPropertyName(String name) {
465 return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH));
466 }
467
468
469
470
471 private static boolean isRecord(Class<?> clazz) {
472 try {
473 return isRecordMethodHandle != null && (boolean) isRecordMethodHandle.invokeExact(clazz);
474 } catch (Throwable e) {
475 throw new ReflectionException("Failed to invoke 'Class.isRecord()'.", e);
476 }
477 }
478
479 private static MethodHandle getIsRecordMethodHandle() {
480 MethodHandles.Lookup lookup = MethodHandles.lookup();
481 MethodType mt = MethodType.methodType(boolean.class);
482 try {
483 return lookup.findVirtual(Class.class, "isRecord", mt);
484 } catch (NoSuchMethodException | IllegalAccessException e) {
485 return null;
486 }
487 }
488 }