强大的反射功能详解与应用源码解析

标签: Java

保留所有版权,请引用而不是转载本文(原文地址 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());
}

图 6-2 程序运行结果

基于反射的diffObj方法完成了User和Book两个完全不同类的对象属性对比工作。因此,反射极大地提升了java的灵活性,降低了diffObj方法和输入参数的耦合,使我们的功能更为通用。(以上均参考自《通用源码阅读指导书——MyBatis源码详解》)

以上例子的来源于《通用源码阅读指导书——MyBatis源码详解》中的扩展阅读部分提及的开源项目ObjectLogger( https://github.com/yeecode/ObjectLogger )。我们这里只是ObjectLogger的一个非常简化的实现。

ObjectLogger是一个强大且易用的Java对象日志记录系统,能够自动分析和记录任何对象的一个或者多个属性变化。

图 6-3 ObjectLogger日志展示效果图

ObjectLogger可应用在用户操作记录、系统状态跟踪等多种应用场合。

MyBatis作为一个出色的ORM框架,需要完成参数对象的解析、目标对象的生成等工作,都涉及了大量的反射。因为这些对象都是在运行时才获取的,而不是编程时已知的。因此,阅读MyBatis的源码能让我们对反射的使用有深入的了解。

MyBatis源码中与反射相关的代码在reflection包中,其中最为核心的类就是Reflector类。下图给出了与Reflector最为密切的几个类的类图。

图 6-8 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),欢迎关注。

作者书籍推荐