org.apache.ibatis.reflection.Reflector Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mybatis Show documentation
Show all versions of mybatis Show documentation
The MyBatis SQL mapper framework makes it easier to use a relational database with object-oriented
applications. MyBatis couples objects with stored procedures or SQL statements using a XML descriptor or
annotations. Simplicity is the biggest advantage of the MyBatis data mapper over object relational mapping
tools.
/*
* Copyright 2009-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.reflection;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.ReflectPermission;
import java.lang.reflect.Type;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.ibatis.reflection.invoker.AmbiguousMethodInvoker;
import org.apache.ibatis.reflection.invoker.GetFieldInvoker;
import org.apache.ibatis.reflection.invoker.Invoker;
import org.apache.ibatis.reflection.invoker.MethodInvoker;
import org.apache.ibatis.reflection.invoker.SetFieldInvoker;
import org.apache.ibatis.reflection.property.PropertyNamer;
import org.apache.ibatis.util.MapUtil;
/**
* This class represents a cached set of class definition information that allows for easy mapping between property
* names and getter/setter methods.
*
* @author Clinton Begin
*/
public class Reflector {
private static final MethodHandle isRecordMethodHandle = getIsRecordMethodHandle();
private final Class> type;
private final String[] readablePropertyNames;
private final String[] writablePropertyNames;
private final Map setMethods = new HashMap<>();
private final Map getMethods = new HashMap<>();
private final Map> setTypes = new HashMap<>();
private final Map> getTypes = new HashMap<>();
private Constructor> defaultConstructor;
private final Map caseInsensitivePropertyMap = new HashMap<>();
public Reflector(Class> clazz) {
type = clazz;
addDefaultConstructor(clazz);
Method[] classMethods = getClassMethods(clazz);
if (isRecord(type)) {
addRecordGetMethods(classMethods);
} else {
addGetMethods(classMethods);
addSetMethods(classMethods);
addFields(clazz);
}
readablePropertyNames = getMethods.keySet().toArray(new String[0]);
writablePropertyNames = setMethods.keySet().toArray(new String[0]);
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
}
private void addRecordGetMethods(Method[] methods) {
Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0)
.forEach(m -> addGetMethod(m.getName(), m, false));
}
private void addDefaultConstructor(Class> clazz) {
Constructor>[] constructors = clazz.getDeclaredConstructors();
Arrays.stream(constructors).filter(constructor -> constructor.getParameterTypes().length == 0).findAny()
.ifPresent(constructor -> this.defaultConstructor = constructor);
}
private void addGetMethods(Method[] methods) {
Map> conflictingGetters = new HashMap<>();
Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
.forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
resolveGetterConflicts(conflictingGetters);
}
private void resolveGetterConflicts(Map> conflictingGetters) {
for (Entry> entry : conflictingGetters.entrySet()) {
Method winner = null;
String propName = entry.getKey();
boolean isAmbiguous = false;
for (Method candidate : entry.getValue()) {
if (winner == null) {
winner = candidate;
continue;
}
Class> winnerType = winner.getReturnType();
Class> candidateType = candidate.getReturnType();
if (candidateType.equals(winnerType)) {
if (!boolean.class.equals(candidateType)) {
isAmbiguous = true;
break;
}
if (candidate.getName().startsWith("is")) {
winner = candidate;
}
} else if (candidateType.isAssignableFrom(winnerType)) {
// OK getter type is descendant
} else if (winnerType.isAssignableFrom(candidateType)) {
winner = candidate;
} else {
isAmbiguous = true;
break;
}
}
addGetMethod(propName, winner, isAmbiguous);
}
}
private void addGetMethod(String name, Method method, boolean isAmbiguous) {
MethodInvoker invoker = isAmbiguous ? new AmbiguousMethodInvoker(method, MessageFormat.format(
"Illegal overloaded getter method with ambiguous type for property ''{0}'' in class ''{1}''. This breaks the JavaBeans specification and can cause unpredictable results.",
name, method.getDeclaringClass().getName())) : new MethodInvoker(method);
getMethods.put(name, invoker);
Type returnType = TypeParameterResolver.resolveReturnType(method, type);
getTypes.put(name, typeToClass(returnType));
}
private void addSetMethods(Method[] methods) {
Map> conflictingSetters = new HashMap<>();
Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 1 && PropertyNamer.isSetter(m.getName()))
.forEach(m -> addMethodConflict(conflictingSetters, PropertyNamer.methodToProperty(m.getName()), m));
resolveSetterConflicts(conflictingSetters);
}
private void addMethodConflict(Map> conflictingMethods, String name, Method method) {
if (isValidPropertyName(name)) {
List list = MapUtil.computeIfAbsent(conflictingMethods, name, k -> new ArrayList<>());
list.add(method);
}
}
private void resolveSetterConflicts(Map> conflictingSetters) {
for (Entry> entry : conflictingSetters.entrySet()) {
String propName = entry.getKey();
List setters = entry.getValue();
Class> getterType = getTypes.get(propName);
boolean isGetterAmbiguous = getMethods.get(propName) instanceof AmbiguousMethodInvoker;
boolean isSetterAmbiguous = false;
Method match = null;
for (Method setter : setters) {
if (!isGetterAmbiguous && setter.getParameterTypes()[0].equals(getterType)) {
// should be the best match
match = setter;
break;
}
if (!isSetterAmbiguous) {
match = pickBetterSetter(match, setter, propName);
isSetterAmbiguous = match == null;
}
}
if (match != null) {
addSetMethod(propName, match);
}
}
}
private Method pickBetterSetter(Method setter1, Method setter2, String property) {
if (setter1 == null) {
return setter2;
}
Class> paramType1 = setter1.getParameterTypes()[0];
Class> paramType2 = setter2.getParameterTypes()[0];
if (paramType1.isAssignableFrom(paramType2)) {
return setter2;
}
if (paramType2.isAssignableFrom(paramType1)) {
return setter1;
}
MethodInvoker invoker = new AmbiguousMethodInvoker(setter1,
MessageFormat.format(
"Ambiguous setters defined for property ''{0}'' in class ''{1}'' with types ''{2}'' and ''{3}''.", property,
setter2.getDeclaringClass().getName(), paramType1.getName(), paramType2.getName()));
setMethods.put(property, invoker);
Type[] paramTypes = TypeParameterResolver.resolveParamTypes(setter1, type);
setTypes.put(property, typeToClass(paramTypes[0]));
return null;
}
private void addSetMethod(String name, Method method) {
MethodInvoker invoker = new MethodInvoker(method);
setMethods.put(name, invoker);
Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);
setTypes.put(name, typeToClass(paramTypes[0]));
}
private Class> typeToClass(Type src) {
Class> result = null;
if (src instanceof Class) {
result = (Class>) src;
} else if (src instanceof ParameterizedType) {
result = (Class>) ((ParameterizedType) src).getRawType();
} else if (src instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) src).getGenericComponentType();
if (componentType instanceof Class) {
result = Array.newInstance((Class>) componentType, 0).getClass();
} else {
Class> componentClass = typeToClass(componentType);
result = Array.newInstance(componentClass, 0).getClass();
}
}
if (result == null) {
result = Object.class;
}
return result;
}
private void addFields(Class> clazz) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (!setMethods.containsKey(field.getName())) {
// issue #379 - removed the check for final because JDK 1.5 allows
// modification of final fields through reflection (JSR-133). (JGB)
// pr #16 - final static can only be set by the classloader
int modifiers = field.getModifiers();
if ((!Modifier.isFinal(modifiers) || !Modifier.isStatic(modifiers))) {
addSetField(field);
}
}
if (!getMethods.containsKey(field.getName())) {
addGetField(field);
}
}
if (clazz.getSuperclass() != null) {
addFields(clazz.getSuperclass());
}
}
private void addSetField(Field field) {
if (isValidPropertyName(field.getName())) {
setMethods.put(field.getName(), new SetFieldInvoker(field));
Type fieldType = TypeParameterResolver.resolveFieldType(field, type);
setTypes.put(field.getName(), typeToClass(fieldType));
}
}
private void addGetField(Field field) {
if (isValidPropertyName(field.getName())) {
getMethods.put(field.getName(), new GetFieldInvoker(field));
Type fieldType = TypeParameterResolver.resolveFieldType(field, type);
getTypes.put(field.getName(), typeToClass(fieldType));
}
}
private boolean isValidPropertyName(String name) {
return (!name.startsWith("$") && !"serialVersionUID".equals(name) && !"class".equals(name));
}
/**
* This method returns an array containing all methods declared in this class and any superclass. We use this method,
* instead of the simpler Class.getMethods()
, because we want to look for private methods as well.
*
* @param clazz
* The class
*
* @return An array containing all methods in this class
*/
private Method[] getClassMethods(Class> clazz) {
Map uniqueMethods = new HashMap<>();
Class> currentClass = clazz;
while (currentClass != null && currentClass != Object.class) {
addUniqueMethods(uniqueMethods, currentClass.getDeclaredMethods());
// we also need to look for interface methods -
// because the class may be abstract
Class>[] interfaces = currentClass.getInterfaces();
for (Class> anInterface : interfaces) {
addUniqueMethods(uniqueMethods, anInterface.getMethods());
}
currentClass = currentClass.getSuperclass();
}
Collection methods = uniqueMethods.values();
return methods.toArray(new Method[0]);
}
private void addUniqueMethods(Map uniqueMethods, Method[] methods) {
for (Method currentMethod : methods) {
if (!currentMethod.isBridge()) {
String signature = getSignature(currentMethod);
// check to see if the method is already known
// if it is known, then an extended class must have
// overridden a method
if (!uniqueMethods.containsKey(signature)) {
uniqueMethods.put(signature, currentMethod);
}
}
}
}
private String getSignature(Method method) {
StringBuilder sb = new StringBuilder();
Class> returnType = method.getReturnType();
if (returnType != null) {
sb.append(returnType.getName()).append('#');
}
sb.append(method.getName());
Class>[] parameters = method.getParameterTypes();
for (int i = 0; i < parameters.length; i++) {
sb.append(i == 0 ? ':' : ',').append(parameters[i].getName());
}
return sb.toString();
}
/**
* Checks whether can control member accessible.
*
* @return If can control member accessible, it return {@literal true}
*
* @since 3.5.0
*/
public static boolean canControlMemberAccessible() {
try {
SecurityManager securityManager = System.getSecurityManager();
if (null != securityManager) {
securityManager.checkPermission(new ReflectPermission("suppressAccessChecks"));
}
} catch (SecurityException e) {
return false;
}
return true;
}
/**
* Gets the name of the class the instance provides information for.
*
* @return The class name
*/
public Class> getType() {
return type;
}
public Constructor> getDefaultConstructor() {
if (defaultConstructor != null) {
return defaultConstructor;
}
throw new ReflectionException("There is no default constructor for " + type);
}
public boolean hasDefaultConstructor() {
return defaultConstructor != null;
}
public Invoker getSetInvoker(String propertyName) {
Invoker method = setMethods.get(propertyName);
if (method == null) {
throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
}
return method;
}
public Invoker getGetInvoker(String propertyName) {
Invoker method = getMethods.get(propertyName);
if (method == null) {
throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
}
return method;
}
/**
* Gets the type for a property setter.
*
* @param propertyName
* - the name of the property
*
* @return The Class of the property setter
*/
public Class> getSetterType(String propertyName) {
Class> clazz = setTypes.get(propertyName);
if (clazz == null) {
throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
}
return clazz;
}
/**
* Gets the type for a property getter.
*
* @param propertyName
* - the name of the property
*
* @return The Class of the property getter
*/
public Class> getGetterType(String propertyName) {
Class> clazz = getTypes.get(propertyName);
if (clazz == null) {
throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
}
return clazz;
}
/**
* Gets an array of the readable properties for an object.
*
* @return The array
*/
public String[] getGetablePropertyNames() {
return readablePropertyNames;
}
/**
* Gets an array of the writable properties for an object.
*
* @return The array
*/
public String[] getSetablePropertyNames() {
return writablePropertyNames;
}
/**
* Check to see if a class has a writable property by name.
*
* @param propertyName
* - the name of the property to check
*
* @return True if the object has a writable property by the name
*/
public boolean hasSetter(String propertyName) {
return setMethods.containsKey(propertyName);
}
/**
* Check to see if a class has a readable property by name.
*
* @param propertyName
* - the name of the property to check
*
* @return True if the object has a readable property by the name
*/
public boolean hasGetter(String propertyName) {
return getMethods.containsKey(propertyName);
}
public String findPropertyName(String name) {
return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH));
}
/**
* Class.isRecord() alternative for Java 15 and older.
*/
private static boolean isRecord(Class> clazz) {
try {
return isRecordMethodHandle != null && (boolean) isRecordMethodHandle.invokeExact(clazz);
} catch (Throwable e) {
throw new ReflectionException("Failed to invoke 'Class.isRecord()'.", e);
}
}
private static MethodHandle getIsRecordMethodHandle() {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(boolean.class);
try {
return lookup.findVirtual(Class.class, "isRecord", mt);
} catch (NoSuchMethodException | IllegalAccessException e) {
return null;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy