@

反射就是在运行的状态中, 对于任意的一个实体类, 都能知道这个类的所有属性和方法。 并将其封装成一个个对象, 对通过这些对象来实现对应实体类的创建, 以及访问该类的方法和属性。

在我们创建了一个Java类之后, 编译出的.class文件在虚拟机中加载, 都会在JVM中创建一个Class对象,通过该对象来创建这个类的所有对象。

在 Mybatis 中, 有对应的反射模块, 本文就是探究 mybatis 是如何进行反射的。 mybatis 中的反射主要与JavaBean相关。

1 JavaBean 规范

JavaBean 具有如下特征:

  1. 所有的属性都是私有的(通过 getter和setter 访问)
  2. 拥有公有的无参数构造函数
  3. 提供 setter/getter
  4. 实现 Serializable 接口

2 Reflector和ReflectorFactory

mybatis 这种框架, 出于性能等方面的考虑, 必然不是等到使用的时候再去解析XML/再去解析反射类。

mybatis 为每一个类提供了反射器类(Reflector), 该类中存储了反射需要使用的类的元信息。

2.1 Reflector 属性

2.1.1 属性

从类的属性中, 我们可以看出:

  1. 一个反射器(Reflector)对应着一个 Class对象。
  2. 记录了默认构造函数
  3. 其余的是属性及其setter|getter相关

对于一个属性(没错, 属性, 只有有 setter|getter 才能被称之为属性)

  1. 如果是可读的(有getter方法)则Reflector会将其及其方法处理后放入对应的集合中;
  2. 如果是可写的(有setter方法), 则Reflector会将其及其方法处理后放入对应的可写相关的集合中。
  3. 最后使用 Map<String, String> caseInsensitivePropertyMap 来记录所有的属性。

2.1.2 Invoker 接口

在存储方法的时候, Reflector 使用的是 Map<String, Invoker>。 而不是 Map<String, Method>

该接口的定义也很简单

/**
* Invoker: 与方法的 invoke 相关
* @author Clinton Begin
*/
public interface Invoker {
// 调用方法
Object invoke(Object target, Object[] args) throws IllegalAccessException, InvocationTargetException; // 获取类型
Class<?> getType();
}

定义了方法的调用和获取类型。

  • MethodInvoker: 方法的Invoker
  • GetFieldInvoker: 如果没有setter, 则使用该方法, 通过Filed类直接设置成员变量的值
  • SetFieldInvoker: 如果没有getter, 则使用该方法, 通过Field类直接读取成员变量的值

通过该封装之后, 本来需要声明 Map<String, Method>Map<String, Field> 表示的, 只需要使用 Map<String, Invoker> 即可表示。

2.2 Reflector 对外提供的方法

Reflector 对外提供的方法主要与构造函数和属性相关。

构造函数:根据 Class 对象,设置 Reflector 相应的成员变量。

  public Reflector(Class<?> clazz)

检查是否拥有了访问的权限:除了访问公有的变量, 还能访问 default , protected 和p rivate 变量

   public static boolean canControlMemberAccessible()

查找是否有相应的属性

  public String   findPropertyName(String name)

获取默认的构造函数:说实话, 不清楚为啥要有这个方法, 不是可以通过 Class.newInstance() 进行创建吗?

  public Constructor<?> getDefaultConstructor()

getter相关的方法

  public String[] getGetablePropertyNames() // 获取所有的可读属性
public Invoker getGetInvoker(String propertyName)// 获取所有可读属性的 Invoker
public Class<?> getGetterType(String propertyName)// 获取对应属性的类型
public boolean hasGetter(String propertyName)// 对应属性是否有相应的getter

对应的也有 setter 相关的方法

  public String[] getSetablePropertyNames() // 获取所有的可读属性
public Invoker getSetInvoker(String propertyName)// 获取所有可读属性的 Invoker
public Class<?> getSetterType(String propertyName)// 获取对应属性的类型
public boolean hasSetter(String propertyName)// 对应属性是否有相应的 setter

2.3 Reflector 私有方法

2.3.1 方法相关

每个 Relector 对应缓存一个类的元反射信息, 通过 Map 进行缓存, 后续我们在使用时就不需要再去遍历查找, 可通过键查找即可。

因此, 就涉及到几个方法

获取方法签名: 根据函数名称、参数和返回值类型来取得签名, 保证方法的唯一性

 private String getSignature(Method method)

该方法获取每个方法的签名。 获取得到的签名

返回值类型#方法名:参数1,参数2,参数3...

很显然, 签名的目的是唯一性。 那使用语言本身的特性来保证唯一性是最好的:

  1. 方法名不一致, 则方法就不一致
  2. 返回值不一致或者不是其子类, 则方法不一致
  3. 参数数量, 参数类型顺序不一致方法也会不一样

因此, 以上的签名方式可以保证方法的唯一性。

获取类的所有方法

  private Method[] getClassMethods(Class<?> cls)

注意, 由于在获取方法时, 通过调用当前类及其除 Object 之外的所有父类的 getDeclaredMethods 方法及 getInterfaces() 方法, 因此, 其获取到的方法是该类及其父类的所有方法。

由此, 产生了一个问题, 如果子类重写了父类中的方法, 如果返回值相同, 则可以通过键重复来去掉。 但是, 如果方法返回值是父类相同实体方法返回值类型的子类, 则就会导致两个方法是同一个方法, 但是签名不同。 因此, 需要解决此类冲突。

解决方法冲突:getter方法冲突解决

  private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters)

到了此步骤的时候, 属性已经以 propName->List<Method> 的形式存在于内存中。 此时, 需要Map<String, List>转换为Map<String, Invoker>

该方法只需要明白下面两个条件就能很清晰了:

  1. 返回值类型不同

返回值类型不同, 则哪个方法的返回值是另一个方法返回值类型子类, 就把 propName 指向该方法包装成的 Invoker。 这个很好理解, 毕竟重新(override)重写时, 重写方法的返回值类型可以是被重写方法的子类。

  1. 返回值类型相同

按理来说不会出现这种情况, 因为在获取方法的时候已经使用签名去除掉了, 因此此时可以抛出异常。 但是有一种特殊的情况(这个卡了我一段时间):

public boolean isBool() {return true;}// 方法1
public boolean getBool() {return false;}// 方法2

以上情况在 JavaBean 规范中是允许的(但是, 其实方法2几乎大家都不会这么用)。 因此, mybatis 通过以下的方式进行了过滤。

    if (!boolean.class.equals(candidateType)) {
throw new ReflectionException(
"Illegal overloaded getter method with ambiguous type for property "
+ propName + " in class " + winner.getDeclaringClass()
+ ". This breaks the JavaBeans specification and can cause unpredictable results.");
} else if (candidate.getName().startsWith("is")) {
winner = candidate;
}

也就是说, mybatis 承认的是方法1这种。 方法2的忽略掉。

解决方法冲突:setter方法冲突解决

刚开始, 我也想不明白, 为什么setter也会出现冲突。毕竟没有返回值类型, 也没有上面的 boolean 特殊情况。 最后发现了, 还有一个情况, 泛型!!

 void setId(T id);// 父类
public void setId(String id) {// 子类
// Do nothing
}

显然, 遇到此类情况, 显然, 子类中的方法才是我们想要的:

    if (paramType1.isAssignableFrom(paramType2)) {
return setter2;
} else if (paramType2.isAssignableFrom(paramType1)) {
return setter1;
}

参数中, 父类方法泛型经过类型擦除后, 变成了 Object。 因此, 通过以上的方法, 那个是子类, 我们就获取哪一个。

3 ReflectorFactory

看名称, 工厂方法, 是为了创建和缓存 Reflector 的。

只有三个方法: 是否缓存, 设置要不要缓存, 根据类型查找 Reflector 对象(找不到则创建)。

其与 Reflector 的关系

mybatis 为我们提供了该方法的默认实现 DefaultReflectorFactory。 该类的实现很简单, 就是通过ConcurrentMap<Class<?>, Reflector>Reflector进行缓存。

4 MetaClass

MetaClass 通过与属性工具类的结合, 实现了对复杂表达式的解析,实现了获取指定描述信息的功能。

4.1 成员变量

MetaClass 有两个成员变量, 分别是 ReflectorFactoryReflector

4.2 创建

MetaClass 的构造函数是私有的。

  /**
* MetaClass 构造函数
*/
private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
this.reflectorFactory = reflectorFactory;
this.reflector = reflectorFactory.findForClass(type);
}

但是, 其提供了两个创建的方法。 这两个方法也是通过该方法进行创建对象的。 该方法通过 Class 对象进行了 Reflector 对象的创建, 并赋值给成员变量。

  /**
* 跟上面的是一样的
*/
public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
return new MetaClass(type, reflectorFactory);
}

通过属性进行创建

  /**
* 通过属性名称, 获取属性的 MetaClass
*/
public MetaClass metaClassForProperty(String name) {
Class<?> propType = reflector.getGetterType(name);
return MetaClass.forClass(propType, reflectorFactory);
}

4.3 方法

该类中, 最重要的方法是:

    /**
* 解析属性表达式
* 会去寻找reflector中是否有对应的的属性
* @param name
* @param builder
* @return
*/
private StringBuilder buildProperty(String name, StringBuilder builder) {
// 解析属性表达式
PropertyTokenizer prop = new PropertyTokenizer(name);
// 是否有子表达式
if (prop.hasNext()) {
// 查找对应的属性
String propertyName = reflector.findPropertyName(prop.getName());
if (propertyName != null) {
// 追加属性名
builder.append(propertyName);
builder.append(".");
// 创建对应的 MetaClass 对象
MetaClass metaProp = metaClassForProperty(propertyName);
// 解析子表达式, 递归
metaProp.buildProperty(prop.getChildren(), builder);
}
} else {
// 根据名称查找属性
String propertyName = reflector.findPropertyName(name);
if (propertyName != null) {
builder.append(propertyName);
}
}
return builder;
}

理解了这个方法(递归, 该类中有很多类似的), 就可以很好的对这个类进行理解, 以查找(richType.richProperty)为例:

  1. 通过 PropertyTokenizer 对表达式进行解析, 得到当前的 name=richType, children=richProperty
  2. 从 reflector 中查找该 richType 属性
  3. 将 richType 添加到 builder 中
  4. 使用 metaClassForProperty 创建 richType 的 MetaClass
  5. 递归调用自身来处理子表达式

退出的条件就是没有子表达式。 这个就是为了, 我们类中有成员变量是类, 我们可以通过其找到他们的所有类及其属性。

注意, 在此过程中, ReflectorFactory 一直是同一个, 而其内部缓存了多个 Reflector 对象。

5 总结

类的关系:

Reflector 实现了实体类元信息的封装, 但对类中的成员变量是类的情况没有进行处理。 而 MetaClass 通过 ReflectorFactory 类型的成员变量, 实现了实体类中成员变量是类情况的处理。从而结合属性工具类实现了对复杂表达式的处理。


一起学 mybatis

你想不想来学习 mybatis? 学习其使用和源码呢?那么, 在博客园关注我吧!!

我自己打算把这个源码系列更新完毕, 同时会更新相应的注释。快去我的github star 吧!!

mybatis源码- 反射模块一(跟着MyBatis学反射):类级别信息的封装的更多相关文章

  1. Spring mybatis源码篇章-sql mapper配置文件绑定mapper class类

    前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-MybatisDAO文件解析(二) 背景知识 MappedStatement是mybatis操作sql ...

  2. springboot集成mybatis源码分析-启动加载mybatis过程(二)

    1.springboot项目最核心的就是自动加载配置,该功能则依赖的是一个注解@SpringBootApplication中的@EnableAutoConfiguration 2.EnableAuto ...

  3. mybatis源码学习(四)--springboot整合mybatis原理

    我们接下来说:springboot是如何和mybatis进行整合的 1.首先,springboot中使用mybatis需要用到mybatis-spring-boot-start,可以理解为mybati ...

  4. MyBatis 源码篇-插件模块

    本章主要描述 MyBatis 插件模块的原理,从以下两点出发: MyBatis 是如何加载插件配置的? MyBatis 是如何实现用户使用自定义拦截器对 SQL 语句执行过程中的某一点进行拦截的? 示 ...

  5. MyBatis 源码篇-日志模块2

    上一章的案例,配置日志级别为 debug,执行一个简单的查询操作,会将 JDBC 操作打印出来.本章通过 MyBatis 日志部分源码分析它是如何实现日志打印的. 在 MyBatis 的日志模块中有一 ...

  6. MyBatis 源码篇-日志模块1

    在 Java 开发中常用的日志框架有 Log4j.Log4j2.Apache Common Log.java.util.logging.slf4j 等,这些日志框架对外提供的接口各不相同.本章详细描述 ...

  7. MyBatis 源码篇-整体架构

    MyBatis 的整体架构分为三层, 分别是基础支持层.核心处理层和接口层,如下图所示. 基础支持层 反射模块 该模块对 Java 原生的反射进行了良好的封装,提供了更加简洁易用的 API ,方便上层 ...

  8. MyBatis源码分析-SQL语句执行的完整流程

    MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...

  9. MyBatis 源码分析 - 插件机制

    1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...

随机推荐

  1. Glide开源库的使用

    关于Glide Glide是一款快速高效的Android图像加载库,注重于平滑的滚动.Glide提供了易用的API,高性能.可扩展的图片解码管道(decode pipeline),以及自动的资源池技术 ...

  2. (后端)SpringBoot中Mybatis打印sql(转)

    原文地址:https://www.cnblogs.com/expiator/p/8664977.html 如果使用的是application.properties文件,加入如下配置: logging. ...

  3. Android 线程交互

    在Android开发过程中,耗时操作是不允许写在主线程(UI线程)中的,以免由于等待时间过长而发生ANR.所以耗时操作需要创建子线程来完成,然而往往这些操作都需要与主线程进行通讯交互(例如更新主线程的 ...

  4. celery任务进程关闭

    方法1: ps auxww|grep 方法2: Ctrl+C 方法3: celery multi 管理 celery multi start w1 -A proj -l info celery mul ...

  5. JavaWeb入门笔记

    Java web笔记 一.HTTP协议 HTTP(超文本传输协议),它是一种主流B/S架构中应用的通信协议.具有以下特点: 1.无状态 服务端不会记录客户端每次提交的请求,服务器一旦相应客户端之后,就 ...

  6. 代码管理工具:Git 和 Svn 的简单操作

    1. git 先注册git config --global user.name "name" git config --global user.email "email& ...

  7. Ubuntu下 MySQL的“ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)”

    今天闲来无事,在Ubuntu上掏鼓一下mysql.但尴尬的是,当我输入mysql -u root -p的时候,抛出了一个错误:ERROR 1045 (28000): Access denied for ...

  8. LeetCode算法题-Valid Anagram(Java实现)

    这是悦乐书的第198次更新,第205篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第61题(顺位题号是242).给定两个字符串s和t,写一个函数来确定t是否是s的anag ...

  9. March 07th, 2018 Week 10th Wednesday

    Better later than never. 亡羊补牢,时犹未晚. Time and again all of us are told to complete the tasks assigned ...

  10. HDFS的dfs.replication不同验证

    对于上传文件到hdfs上时,当时hadoop的副本系数是几,这个文件的块数副本数就会有几份,无论以后你怎么更改系统副本系统,这个文件的副本数都不会改变,也就说上传到分布式系统上的文件副本数由当时的系统 ...