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 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)
97 .findAny().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 } else if (candidate.getName().startsWith("is")) {
124 winner = candidate;
125 }
126 } else if (candidateType.isAssignableFrom(winnerType)) {
127
128 } else if (winnerType.isAssignableFrom(candidateType)) {
129 winner = candidate;
130 } else {
131 isAmbiguous = true;
132 break;
133 }
134 }
135 addGetMethod(propName, winner, isAmbiguous);
136 }
137 }
138
139 private void addGetMethod(String name, Method method, boolean isAmbiguous) {
140 MethodInvoker invoker = isAmbiguous
141 ? 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()))
144 : new MethodInvoker(method);
145 getMethods.put(name, invoker);
146 Type returnType = TypeParameterResolver.resolveReturnType(method, type);
147 getTypes.put(name, typeToClass(returnType));
148 }
149
150 private void addSetMethods(Method[] methods) {
151 Map<String, List<Method>> conflictingSetters = new HashMap<>();
152 Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 1 && PropertyNamer.isSetter(m.getName()))
153 .forEach(m -> addMethodConflict(conflictingSetters, PropertyNamer.methodToProperty(m.getName()), m));
154 resolveSetterConflicts(conflictingSetters);
155 }
156
157 private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
158 if (isValidPropertyName(name)) {
159 List<Method> list = MapUtil.computeIfAbsent(conflictingMethods, name, k -> new ArrayList<>());
160 list.add(method);
161 }
162 }
163
164 private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
165 for (Entry<String, List<Method>> entry : conflictingSetters.entrySet()) {
166 String propName = entry.getKey();
167 List<Method> setters = entry.getValue();
168 Class<?> getterType = getTypes.get(propName);
169 boolean isGetterAmbiguous = getMethods.get(propName) instanceof AmbiguousMethodInvoker;
170 boolean isSetterAmbiguous = false;
171 Method match = null;
172 for (Method setter : setters) {
173 if (!isGetterAmbiguous && setter.getParameterTypes()[0].equals(getterType)) {
174
175 match = setter;
176 break;
177 }
178 if (!isSetterAmbiguous) {
179 match = pickBetterSetter(match, setter, propName);
180 isSetterAmbiguous = match == null;
181 }
182 }
183 if (match != null) {
184 addSetMethod(propName, match);
185 }
186 }
187 }
188
189 private Method pickBetterSetter(Method setter1, Method setter2, String property) {
190 if (setter1 == null) {
191 return setter2;
192 }
193 Class<?> paramType1 = setter1.getParameterTypes()[0];
194 Class<?> paramType2 = setter2.getParameterTypes()[0];
195 if (paramType1.isAssignableFrom(paramType2)) {
196 return setter2;
197 } else 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}''.",
203 property, 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 if (returnType != null) {
327 sb.append(returnType.getName()).append('#');
328 }
329 sb.append(method.getName());
330 Class<?>[] parameters = method.getParameterTypes();
331 for (int i = 0; i < parameters.length; i++) {
332 sb.append(i == 0 ? ':' : ',').append(parameters[i].getName());
333 }
334 return sb.toString();
335 }
336
337
338
339
340
341
342
343 public static boolean canControlMemberAccessible() {
344 try {
345 SecurityManager securityManager = System.getSecurityManager();
346 if (null != securityManager) {
347 securityManager.checkPermission(new ReflectPermission("suppressAccessChecks"));
348 }
349 } catch (SecurityException e) {
350 return false;
351 }
352 return true;
353 }
354
355
356
357
358
359
360 public Class<?> getType() {
361 return type;
362 }
363
364 public Constructor<?> getDefaultConstructor() {
365 if (defaultConstructor != null) {
366 return defaultConstructor;
367 } else {
368 throw new ReflectionException("There is no default constructor for " + type);
369 }
370 }
371
372 public boolean hasDefaultConstructor() {
373 return defaultConstructor != null;
374 }
375
376 public Invoker getSetInvoker(String propertyName) {
377 Invoker method = setMethods.get(propertyName);
378 if (method == null) {
379 throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
380 }
381 return method;
382 }
383
384 public Invoker getGetInvoker(String propertyName) {
385 Invoker method = getMethods.get(propertyName);
386 if (method == null) {
387 throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
388 }
389 return method;
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 public Class<?> getGetterType(String propertyName) {
413 Class<?> clazz = getTypes.get(propertyName);
414 if (clazz == null) {
415 throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
416 }
417 return clazz;
418 }
419
420
421
422
423
424
425 public String[] getGetablePropertyNames() {
426 return readablePropertyNames;
427 }
428
429
430
431
432
433
434 public String[] getSetablePropertyNames() {
435 return writablePropertyNames;
436 }
437
438
439
440
441
442
443
444 public boolean hasSetter(String propertyName) {
445 return setMethods.containsKey(propertyName);
446 }
447
448
449
450
451
452
453
454 public boolean hasGetter(String propertyName) {
455 return getMethods.containsKey(propertyName);
456 }
457
458 public String findPropertyName(String name) {
459 return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH));
460 }
461
462
463
464
465 private static boolean isRecord(Class<?> clazz) {
466 try {
467 return isRecordMethodHandle != null && (boolean)isRecordMethodHandle.invokeExact(clazz);
468 } catch (Throwable e) {
469 throw new ReflectionException("Failed to invoke 'Class.isRecord()'.", e);
470 }
471 }
472
473 private static MethodHandle getIsRecordMethodHandle() {
474 MethodHandles.Lookup lookup = MethodHandles.lookup();
475 MethodType mt = MethodType.methodType(boolean.class);
476 try {
477 return lookup.findVirtual(Class.class, "isRecord", mt);
478 } catch (NoSuchMethodException | IllegalAccessException e) {
479 return null;
480 }
481 }
482 }