强大的反射功能详解与应用源码解析
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/48/ )。
反射是Java提供的一个灵活又强大的功能。通过Java反射,能够在类的运行过程中知道这个类有哪些属性和方法,还可以修改属性、调用方法、建立类的实例。
说起来可能有些不好理解。假设我们有一个代码所示的类。
public class User {
private Integer id;
private String name;
// 省略构造方法与get、set方法
}
那我们在编程时可以使用下面的代码为对象赋值。
User user = new User();
user.setName("xiaoming");
我们能调用User实例的setName方法,是因为编译器通过分析源码知道User中确实存在一个setName方法。那如果我们要实现一个对象比较功能,比较两个User对象的属性有什么不同。则可以通过下面的代码实现。
/**
* 比较两个User对象的属性不同
* @param oldUser 第一个User对象
* @param newUser 第二个User对象
* @return 两个User对象的属性变化
*/
public static Map<String,String> diffUser(User oldUser, User newUser) {
Map<String,String> diffMap = new HashMap<>();
if ((oldUser.getId() == null && newUser.getId() != null) ||
(oldUser.getId()!= null && !oldUser.getId().equals(newUser.getId())))
{
diffMap.put("id","from " + oldUser.getId() + " to " + newUser.getId());
}
if ((oldUser.getName() == null && newUser.getName() != null) ||
(oldUser.getName()!= null && !oldUser.getName().equals(newUser.getName())))
{
diffMap.put("name","from " + oldUser.getName() + " to " + newUser.getName());
}
return diffMap;
}
在上面代码所示的diffUser方法中,我们在编码时就知道User对象有哪些属性、方法,然后轮番比较即可。因此,该diffUser方法只能比较两个User对象的不同,而无法比较其他类的对象。
那我们如何修改才能使得该比较方法适用于任何类的对象呢?我们面临两个问题:
- 不知道所要传入的对象的具体类型
- 因为传入对象的类型位置,那对象的属性也是未知的
要解决上述两个问题,我们需要在系统运行阶段,准确说是在参数传入后,直接判断传入对象的类型以及其包含的属性和方法。这时,反射就派上用场了。
JAVA反射机制主要提供了以下功能:
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时修改任意一个对象的成员变量
- 在运行时调用任意一个对象的方法
于是,我们可以先通过反射获取对象的类,从而判断两个对象是否属于同一个类;然后获取对象的成员变量,轮番比较两个对象的成员变量是否一致。最终,我们将功能改写为如下所示的形式。
/**
* 比较两个任意对象的属性不同
* @param oldObj 第一个对象
* @param newObj 第二个对象
* @return 两个对象的属性不同
*/
public static Map<String, String> diffObj(Object oldObj, Object newObj) {
Map<String, String> diffMap = new HashMap<>();
try {
// 获取对象的类
Class oldObjClazz = oldObj.getClass();
Class newObjClazz = newObj.getClass();
// 判断两个对象是否属于同一个类
if (oldObjClazz.equals(newObjClazz)) {
// 获取对象的所有属性
Field[] fields = oldObjClazz.getDeclaredFields();
// 对每个属性逐一判断
for (Field field : fields) {
// 使得属性可以被反射访问
field.setAccessible(true);
// 拿到当前属性的值
Object oldValue = field.get(oldObj);
Object newValue = field.get(newObj);
// 如果某个属性的值在两个对象中不同,则进行记录
if ((oldValue == null && newValue != null) || oldValue != null && !oldValue.equals(newValue)) {
diffMap.put(field.getName(), "from " + oldValue + " to " + newValue);
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return diffMap;
}
这样,diffObj方法可以比较任意类型对象的属性不同。例如我们给出上面代码的测试代码,使用diffObj方法分别比较User对象和Book对象的不同。便可以得到下面的结果。
User oldUser = new User(1,"yee");
User newUser = new User(1,"yeecode");
System.out.println("不使用反射,只能比较单一类型的对象:");
Map<String,String> diffUserMap = diffUser(oldUser,newUser);
for (Map.Entry entry : diffUserMap.entrySet()) {
System.out.println("属性" + entry.getKey() + "; 变化为:" + entry.getValue());
}
System.out.println("使用反射,可以比较属性不同的各类对象:");
Map<String,String> diffObjMap = diffObj(oldUser,newUser);
for (Map.Entry entry : diffObjMap.entrySet()) {
System.out.println("属性" + entry.getKey() + "; 变化为:" + entry.getValue());
}
Book oldBook = new Book("语文",15.7);
Book newBook = new Book("语文",18.7);
diffObjMap = diffObj(oldBook,newBook);
for (Map.Entry entry : diffObjMap.entrySet()) {
System.out.println("属性" + entry.getKey() + "; 变化为:" + entry.getValue());
}
基于反射的diffObj方法完成了User和Book两个完全不同类的对象属性对比工作。因此,反射极大地提升了java的灵活性,降低了diffObj方法和输入参数的耦合,使我们的功能更为通用。(以上均参考自《通用源码阅读指导书——MyBatis源码详解》)
以上例子的来源于《通用源码阅读指导书——MyBatis源码详解》中的扩展阅读部分提及的开源项目ObjectLogger( https://github.com/yeecode/ObjectLogger )。我们这里只是ObjectLogger的一个非常简化的实现。
ObjectLogger是一个强大且易用的Java对象日志记录系统,能够自动分析和记录任何对象的一个或者多个属性变化。
ObjectLogger可应用在用户操作记录、系统状态跟踪等多种应用场合。
MyBatis作为一个出色的ORM框架,需要完成参数对象的解析、目标对象的生成等工作,都涉及了大量的反射。因为这些对象都是在运行时才获取的,而不是编程时已知的。因此,阅读MyBatis的源码能让我们对反射的使用有深入的了解。
MyBatis源码中与反射相关的代码在reflection
包中,其中最为核心的类就是Reflector类。下图给出了与Reflector最为密切的几个类的类图。
Reflector类负责对一个类进行反射解析,并将解析后的结果在属性中存储起来。该类包含的属性如下所示,各个属性的含义我已经通过注释进行了标注。
// 要被反射解析的类
private final Class<?> type;
// 能够读的属性列表,即有get方法的属性列表
private final String[] readablePropertyNames;
// 能够写的属性列表,即有set方法的属性列表
private final String[] writablePropertyNames;
// set方法映射表。键为属性名,值为对应的set方法
private final Map<String, Invoker> setMethods = new HashMap<>();
// get方法映射表。键为属性名,值为对应的get方法
private final Map<String, Invoker> getMethods = new HashMap<>();
// set方法输入类型。键为属性名,值为对应的该属性的set方法的类型(实际为set方法的第一个参数的类型)
private final Map<String, Class<?>> setTypes = new HashMap<>();
// get方法输出类型。键为属性名,值为对应的该属性的set方法的类型(实际为set方法的返回值类型)
private final Map<String, Class<?>> getTypes = new HashMap<>();
// 默认构造函数
private Constructor<?> defaultConstructor;
// 大小写无关的属性映射表。键为属性名全大写值,值为属性名
private Map<String, String> caseInsensitivePropertyMap = new HashMap<>();
Reflector类将一个类反射解析后,会将该类的属性、方法等一一归类放到以上的各个属性中。因此Reflector类完成了主要的反射解析工作,这也是我们将其称为反射核心类的原因。reflection
包中的其他类则多是在其反射的结果的基础上进一步包装,使得整个反射功能更易用。
Reflector类反射解析一个类的过程是由构造函数触发的,逻辑非常清晰。Reflector类的构造函数如代码下所示。
/**
* Reflector的构造方法
* @param clazz 需要被反射处理的目标类
*/
public Reflector(Class<?> clazz) {
// 要被反射解析的类
type = clazz;
// 设置默认构造器属性
addDefaultConstructor(clazz);
// 解析所有的getter
addGetMethods(clazz);
// 解析所有有setter
addSetMethods(clazz);
// 解析所有属性
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);
}
}
具体到每个子方法,其逻辑比较简单。我们以其中的addGetMethods
方法为例进行介绍。addGetMethods
子方法的功能是分析参数中传入的类,将类中的get方法添加到getMethods
中。其带注释的源码如下所示。(均参考自书籍《通用源码阅读指导书——MyBatis源码详解》)
/**
* 找出类中的get方法
* @param clazz 需要被反射处理的目标类
*/
private void addGetMethods(Class<?> clazz) {
// 存储属性的get方法。Map的键为属性名,值为get方法列表。某个属性的get方法用列表存储是因为前期可能会为某一个属性找到多个可能get方法。
Map<String, List<Method>> conflictingGetters = new HashMap<>();
// 找出该类中所有的方法
Method[] methods = getClassMethods(clazz);
// 过滤出get方法,过滤条件有:无入参、符合Java Bean的命名规则;然后取出方法对应的属性名、方法,放入conflictingGetters
Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
.forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
// 如果一个属性有多个疑似get方法,resolveGetterConflicts用来找出合适的那个
resolveGetterConflicts(conflictingGetters);
}
其中的conflictingGetters
变量是一个Map,它的key是属性名称,value是该属性的可能的get方法的列表。但是,最终每个属性真正的get方法应该只有一个。resolveGetterConflicts
方法负责尝试找出该属性真正的get方法,该方法源码并不复杂,大家可以自行阅读。
ReflectorFactory是Reflector的工厂接口,而DefaultReflectorFactory是该工厂接口的默认实现。我们直接以DefaultReflectorFactory为例,介绍Reflector工厂。
DefaultReflectorFactory中最核心的方法即用来生成一个类的Reflector对象的findForClass
方法,如下所示。
/**
* 生产Reflector对象
* @param type 目标类型
* @return 目标类型的Reflector对象
*/
@Override
public Reflector findForClass(Class<?> type) {
if (classCacheEnabled) { // 允许缓存
// 生产入参type的反射器对象,并放入缓存
return reflectorMap.computeIfAbsent(type, Reflector::new);
} else {
return new Reflector(type);
}
}
进一步的,MyBatis还对Reflector类进行了进一步的封装获得了更多的辅助类,例如MetaClass类和MetaObject类等。它们使得MyBatis可以更为便捷地使用反射。关于这些,大家可以参阅《通用源码阅读指导书——MyBatis源码详解》。
这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注解。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用,非常推荐。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。