View Javadoc
1   /**
2    *    Copyright 2009-2019 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.reflect.Array;
19  import java.lang.reflect.Constructor;
20  import java.lang.reflect.Field;
21  import java.lang.reflect.GenericArrayType;
22  import java.lang.reflect.Method;
23  import java.lang.reflect.Modifier;
24  import java.lang.reflect.ParameterizedType;
25  import java.lang.reflect.ReflectPermission;
26  import java.lang.reflect.Type;
27  import java.text.MessageFormat;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Collection;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Map;
35  import java.util.Map.Entry;
36  
37  import org.apache.ibatis.reflection.invoker.AmbiguousMethodInvoker;
38  import org.apache.ibatis.reflection.invoker.GetFieldInvoker;
39  import org.apache.ibatis.reflection.invoker.Invoker;
40  import org.apache.ibatis.reflection.invoker.MethodInvoker;
41  import org.apache.ibatis.reflection.invoker.SetFieldInvoker;
42  import org.apache.ibatis.reflection.property.PropertyNamer;
43  
44  /**
45   * This class represents a cached set of class definition information that
46   * allows for easy mapping between property names and getter/setter methods.
47   *
48   * @author Clinton Begin
49   */
50  public class Reflector {
51  
52    private final Class<?> type;
53    private final String[] readablePropertyNames;
54    private final String[] writablePropertyNames;
55    private final Map<String, Invoker> setMethods = new HashMap<>();
56    private final Map<String, Invoker> getMethods = new HashMap<>();
57    private final Map<String, Class<?>> setTypes = new HashMap<>();
58    private final Map<String, Class<?>> getTypes = new HashMap<>();
59    private Constructor<?> defaultConstructor;
60  
61    private Map<String, String> caseInsensitivePropertyMap = new HashMap<>();
62  
63    public Reflector(Class<?> clazz) {
64      type = clazz;
65      addDefaultConstructor(clazz);
66      addGetMethods(clazz);
67      addSetMethods(clazz);
68      addFields(clazz);
69      readablePropertyNames = getMethods.keySet().toArray(new String[0]);
70      writablePropertyNames = setMethods.keySet().toArray(new String[0]);
71      for (String propName : readablePropertyNames) {
72        caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
73      }
74      for (String propName : writablePropertyNames) {
75        caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
76      }
77    }
78  
79    private void addDefaultConstructor(Class<?> clazz) {
80      Constructor<?>[] constructors = clazz.getDeclaredConstructors();
81      Arrays.stream(constructors).filter(constructor -> constructor.getParameterTypes().length == 0)
82        .findAny().ifPresent(constructor -> this.defaultConstructor = constructor);
83    }
84  
85    private void addGetMethods(Class<?> clazz) {
86      Map<String, List<Method>> conflictingGetters = new HashMap<>();
87      Method[] methods = getClassMethods(clazz);
88      Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
89        .forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
90      resolveGetterConflicts(conflictingGetters);
91    }
92  
93    private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
94      for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
95        Method winner = null;
96        String propName = entry.getKey();
97        boolean isAmbiguous = false;
98        for (Method candidate : entry.getValue()) {
99          if (winner == null) {
100           winner = candidate;
101           continue;
102         }
103         Class<?> winnerType = winner.getReturnType();
104         Class<?> candidateType = candidate.getReturnType();
105         if (candidateType.equals(winnerType)) {
106           if (!boolean.class.equals(candidateType)) {
107             isAmbiguous = true;
108             break;
109           } else if (candidate.getName().startsWith("is")) {
110             winner = candidate;
111           }
112         } else if (candidateType.isAssignableFrom(winnerType)) {
113           // OK getter type is descendant
114         } else if (winnerType.isAssignableFrom(candidateType)) {
115           winner = candidate;
116         } else {
117           isAmbiguous = true;
118           break;
119         }
120       }
121       addGetMethod(propName, winner, isAmbiguous);
122     }
123   }
124 
125   private void addGetMethod(String name, Method method, boolean isAmbiguous) {
126     MethodInvoker invoker = isAmbiguous
127         ? new AmbiguousMethodInvoker(method, MessageFormat.format(
128             "Illegal overloaded getter method with ambiguous type for property ''{0}'' in class ''{1}''. This breaks the JavaBeans specification and can cause unpredictable results.",
129             name, method.getDeclaringClass().getName()))
130         : new MethodInvoker(method);
131     getMethods.put(name, invoker);
132     Type returnType = TypeParameterResolver.resolveReturnType(method, type);
133     getTypes.put(name, typeToClass(returnType));
134   }
135 
136   private void addSetMethods(Class<?> clazz) {
137     Map<String, List<Method>> conflictingSetters = new HashMap<>();
138     Method[] methods = getClassMethods(clazz);
139     Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 1 && PropertyNamer.isSetter(m.getName()))
140       .forEach(m -> addMethodConflict(conflictingSetters, PropertyNamer.methodToProperty(m.getName()), m));
141     resolveSetterConflicts(conflictingSetters);
142   }
143 
144   private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
145     if (isValidPropertyName(name)) {
146       List<Method> list = conflictingMethods.computeIfAbsent(name, k -> new ArrayList<>());
147       list.add(method);
148     }
149   }
150 
151   private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
152     for (String propName : conflictingSetters.keySet()) {
153       List<Method> setters = conflictingSetters.get(propName);
154       Class<?> getterType = getTypes.get(propName);
155       boolean isGetterAmbiguous = getMethods.get(propName) instanceof AmbiguousMethodInvoker;
156       boolean isSetterAmbiguous = false;
157       Method match = null;
158       for (Method setter : setters) {
159         if (!isGetterAmbiguous && setter.getParameterTypes()[0].equals(getterType)) {
160           // should be the best match
161           match = setter;
162           break;
163         }
164         if (!isSetterAmbiguous) {
165           match = pickBetterSetter(match, setter, propName);
166           isSetterAmbiguous = match == null;
167         }
168       }
169       if (match != null) {
170         addSetMethod(propName, match);
171       }
172     }
173   }
174 
175   private Method pickBetterSetter(Method setter1, Method setter2, String property) {
176     if (setter1 == null) {
177       return setter2;
178     }
179     Class<?> paramType1 = setter1.getParameterTypes()[0];
180     Class<?> paramType2 = setter2.getParameterTypes()[0];
181     if (paramType1.isAssignableFrom(paramType2)) {
182       return setter2;
183     } else if (paramType2.isAssignableFrom(paramType1)) {
184       return setter1;
185     }
186     MethodInvoker invoker = new AmbiguousMethodInvoker(setter1,
187         MessageFormat.format(
188             "Ambiguous setters defined for property ''{0}'' in class ''{1}'' with types ''{2}'' and ''{3}''.",
189             property, setter2.getDeclaringClass().getName(), paramType1.getName(), paramType2.getName()));
190     setMethods.put(property, invoker);
191     Type[] paramTypes = TypeParameterResolver.resolveParamTypes(setter1, type);
192     setTypes.put(property, typeToClass(paramTypes[0]));
193     return null;
194   }
195 
196   private void addSetMethod(String name, Method method) {
197     MethodInvoker/MethodInvoker.html#MethodInvoker">MethodInvoker invoker = new MethodInvoker(method);
198     setMethods.put(name, invoker);
199     Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);
200     setTypes.put(name, typeToClass(paramTypes[0]));
201   }
202 
203   private Class<?> typeToClass(Type src) {
204     Class<?> result = null;
205     if (src instanceof Class) {
206       result = (Class<?>) src;
207     } else if (src instanceof ParameterizedType) {
208       result = (Class<?>) ((ParameterizedType) src).getRawType();
209     } else if (src instanceof GenericArrayType) {
210       Type componentType = ((GenericArrayType) src).getGenericComponentType();
211       if (componentType instanceof Class) {
212         result = Array.newInstance((Class<?>) componentType, 0).getClass();
213       } else {
214         Class<?> componentClass = typeToClass(componentType);
215         result = Array.newInstance(componentClass, 0).getClass();
216       }
217     }
218     if (result == null) {
219       result = Object.class;
220     }
221     return result;
222   }
223 
224   private void addFields(Class<?> clazz) {
225     Field[] fields = clazz.getDeclaredFields();
226     for (Field field : fields) {
227       if (!setMethods.containsKey(field.getName())) {
228         // issue #379 - removed the check for final because JDK 1.5 allows
229         // modification of final fields through reflection (JSR-133). (JGB)
230         // pr #16 - final static can only be set by the classloader
231         int modifiers = field.getModifiers();
232         if (!(Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers))) {
233           addSetField(field);
234         }
235       }
236       if (!getMethods.containsKey(field.getName())) {
237         addGetField(field);
238       }
239     }
240     if (clazz.getSuperclass() != null) {
241       addFields(clazz.getSuperclass());
242     }
243   }
244 
245   private void addSetField(Field field) {
246     if (isValidPropertyName(field.getName())) {
247       setMethods.put(field.getName(), new SetFieldInvoker(field));
248       Type fieldType = TypeParameterResolver.resolveFieldType(field, type);
249       setTypes.put(field.getName(), typeToClass(fieldType));
250     }
251   }
252 
253   private void addGetField(Field field) {
254     if (isValidPropertyName(field.getName())) {
255       getMethods.put(field.getName(), new GetFieldInvoker(field));
256       Type fieldType = TypeParameterResolver.resolveFieldType(field, type);
257       getTypes.put(field.getName(), typeToClass(fieldType));
258     }
259   }
260 
261   private boolean isValidPropertyName(String name) {
262     return !(name.startsWith("$") || "serialVersionUID".equals(name) || "class".equals(name));
263   }
264 
265   /**
266    * This method returns an array containing all methods
267    * declared in this class and any superclass.
268    * We use this method, instead of the simpler <code>Class.getMethods()</code>,
269    * because we want to look for private methods as well.
270    *
271    * @param clazz The class
272    * @return An array containing all methods in this class
273    */
274   private Method[] getClassMethods(Class<?> clazz) {
275     Map<String, Method> uniqueMethods = new HashMap<>();
276     Class<?> currentClass = clazz;
277     while (currentClass != null && currentClass != Object.class) {
278       addUniqueMethods(uniqueMethods, currentClass.getDeclaredMethods());
279 
280       // we also need to look for interface methods -
281       // because the class may be abstract
282       Class<?>[] interfaces = currentClass.getInterfaces();
283       for (Class<?> anInterface : interfaces) {
284         addUniqueMethods(uniqueMethods, anInterface.getMethods());
285       }
286 
287       currentClass = currentClass.getSuperclass();
288     }
289 
290     Collection<Method> methods = uniqueMethods.values();
291 
292     return methods.toArray(new Method[0]);
293   }
294 
295   private void addUniqueMethods(Map<String, Method> uniqueMethods, Method[] methods) {
296     for (Method currentMethod : methods) {
297       if (!currentMethod.isBridge()) {
298         String signature = getSignature(currentMethod);
299         // check to see if the method is already known
300         // if it is known, then an extended class must have
301         // overridden a method
302         if (!uniqueMethods.containsKey(signature)) {
303           uniqueMethods.put(signature, currentMethod);
304         }
305       }
306     }
307   }
308 
309   private String getSignature(Method method) {
310     StringBuilder sb = new StringBuilder();
311     Class<?> returnType = method.getReturnType();
312     if (returnType != null) {
313       sb.append(returnType.getName()).append('#');
314     }
315     sb.append(method.getName());
316     Class<?>[] parameters = method.getParameterTypes();
317     for (int i = 0; i < parameters.length; i++) {
318       sb.append(i == 0 ? ':' : ',').append(parameters[i].getName());
319     }
320     return sb.toString();
321   }
322 
323   /**
324    * Checks whether can control member accessible.
325    *
326    * @return If can control member accessible, it return {@literal true}
327    * @since 3.5.0
328    */
329   public static boolean canControlMemberAccessible() {
330     try {
331       SecurityManager securityManager = System.getSecurityManager();
332       if (null != securityManager) {
333         securityManager.checkPermission(new ReflectPermission("suppressAccessChecks"));
334       }
335     } catch (SecurityException e) {
336       return false;
337     }
338     return true;
339   }
340 
341   /**
342    * Gets the name of the class the instance provides information for.
343    *
344    * @return The class name
345    */
346   public Class<?> getType() {
347     return type;
348   }
349 
350   public Constructor<?> getDefaultConstructor() {
351     if (defaultConstructor != null) {
352       return defaultConstructor;
353     } else {
354       throw new ReflectionException("There is no default constructor for " + type);
355     }
356   }
357 
358   public boolean hasDefaultConstructor() {
359     return defaultConstructor != null;
360   }
361 
362   public Invoker getSetInvoker(String propertyName) {
363     Invoker method = setMethods.get(propertyName);
364     if (method == null) {
365       throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
366     }
367     return method;
368   }
369 
370   public Invoker getGetInvoker(String propertyName) {
371     Invoker method = getMethods.get(propertyName);
372     if (method == null) {
373       throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
374     }
375     return method;
376   }
377 
378   /**
379    * Gets the type for a property setter.
380    *
381    * @param propertyName - the name of the property
382    * @return The Class of the property setter
383    */
384   public Class<?> getSetterType(String propertyName) {
385     Class<?> clazz = setTypes.get(propertyName);
386     if (clazz == null) {
387       throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
388     }
389     return clazz;
390   }
391 
392   /**
393    * Gets the type for a property getter.
394    *
395    * @param propertyName - the name of the property
396    * @return The Class of the property getter
397    */
398   public Class<?> getGetterType(String propertyName) {
399     Class<?> clazz = getTypes.get(propertyName);
400     if (clazz == null) {
401       throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
402     }
403     return clazz;
404   }
405 
406   /**
407    * Gets an array of the readable properties for an object.
408    *
409    * @return The array
410    */
411   public String[] getGetablePropertyNames() {
412     return readablePropertyNames;
413   }
414 
415   /**
416    * Gets an array of the writable properties for an object.
417    *
418    * @return The array
419    */
420   public String[] getSetablePropertyNames() {
421     return writablePropertyNames;
422   }
423 
424   /**
425    * Check to see if a class has a writable property by name.
426    *
427    * @param propertyName - the name of the property to check
428    * @return True if the object has a writable property by the name
429    */
430   public boolean hasSetter(String propertyName) {
431     return setMethods.keySet().contains(propertyName);
432   }
433 
434   /**
435    * Check to see if a class has a readable property by name.
436    *
437    * @param propertyName - the name of the property to check
438    * @return True if the object has a readable property by the name
439    */
440   public boolean hasGetter(String propertyName) {
441     return getMethods.keySet().contains(propertyName);
442   }
443 
444   public String findPropertyName(String name) {
445     return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH));
446   }
447 }