View Javadoc
1   /*
2    *    Copyright 2009-2023 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    *       https://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 allows for easy mapping between property
50   * 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 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           // OK getter type is descendant
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           // should be the best match
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         // 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 declared in this class and any superclass. We use this method,
281    * instead of the simpler <code>Class.getMethods()</code>, because we want to look for private methods as well.
282    *
283    * @param clazz
284    *          The class
285    *
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     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    * Checks whether can control member accessible.
337    *
338    * @return If can control member accessible, it return {@literal true}
339    *
340    * @since 3.5.0
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    * Gets the name of the class the instance provides information for.
356    *
357    * @return The class name
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    * Gets the type for a property setter.
392    *
393    * @param propertyName
394    *          - the name of the property
395    *
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
410    *          - the name of the property
411    *
412    * @return The Class of the property getter
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    * Gets an array of the readable properties for an object.
424    *
425    * @return The array
426    */
427   public String[] getGetablePropertyNames() {
428     return readablePropertyNames;
429   }
430 
431   /**
432    * Gets an array of the writable properties for an object.
433    *
434    * @return The array
435    */
436   public String[] getSetablePropertyNames() {
437     return writablePropertyNames;
438   }
439 
440   /**
441    * Check to see if a class has a writable property by name.
442    *
443    * @param propertyName
444    *          - the name of the property to check
445    *
446    * @return True if the object has a writable property by the name
447    */
448   public boolean hasSetter(String propertyName) {
449     return setMethods.containsKey(propertyName);
450   }
451 
452   /**
453    * Check to see if a class has a readable property by name.
454    *
455    * @param propertyName
456    *          - the name of the property to check
457    *
458    * @return True if the object has a readable property by the name
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    * Class.isRecord() alternative for Java 15 and older.
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 }