View Javadoc
1   /*
2    *    Copyright 2009-2022 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
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   * This class represents a cached set of class definition information that
50   * allows for easy mapping between property names and getter/setter methods.
51   *
52   * @author Clinton Begin
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           // OK getter type is descendant
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           // should be the best match
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         // issue #379 - removed the check for final because JDK 1.5 allows
243         // modification of final fields through reflection (JSR-133). (JGB)
244         // pr #16 - final static can only be set by the classloader
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    * This method returns an array containing all methods
281    * declared in this class and any superclass.
282    * We use this method, instead of the simpler <code>Class.getMethods()</code>,
283    * because we want to look for private methods as well.
284    *
285    * @param clazz The class
286    * @return An array containing all methods in this class
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       // we also need to look for interface methods -
295       // because the class may be abstract
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         // check to see if the method is already known
314         // if it is known, then an extended class must have
315         // overridden a method
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    * Checks whether can control member accessible.
339    *
340    * @return If can control member accessible, it return {@literal true}
341    * @since 3.5.0
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    * Gets the name of the class the instance provides information for.
357    *
358    * @return The class name
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    * Gets the type for a property setter.
394    *
395    * @param propertyName - the name of the property
396    * @return The Class of the property setter
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    * Gets the type for a property getter.
408    *
409    * @param propertyName - the name of the property
410    * @return The Class of the property getter
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    * Gets an array of the readable properties for an object.
422    *
423    * @return The array
424    */
425   public String[] getGetablePropertyNames() {
426     return readablePropertyNames;
427   }
428 
429   /**
430    * Gets an array of the writable properties for an object.
431    *
432    * @return The array
433    */
434   public String[] getSetablePropertyNames() {
435     return writablePropertyNames;
436   }
437 
438   /**
439    * Check to see if a class has a writable property by name.
440    *
441    * @param propertyName - the name of the property to check
442    * @return True if the object has a writable property by the name
443    */
444   public boolean hasSetter(String propertyName) {
445     return setMethods.containsKey(propertyName);
446   }
447 
448   /**
449    * Check to see if a class has a readable property by name.
450    *
451    * @param propertyName - the name of the property to check
452    * @return True if the object has a readable property by the name
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    * Class.isRecord() alternative for Java 15 and older.
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 }