mybatis源码- 反射模块一(跟着MyBatis学反射):类级别信息的封装
@
反射就是在运行的状态中, 对于任意的一个实体类, 都能知道这个类的所有属性和方法。 并将其封装成一个个对象, 对通过这些对象来实现对应实体类的创建, 以及访问该类的方法和属性。
在我们创建了一个Java类之后, 编译出的.class文件在虚拟机中加载, 都会在JVM中创建一个Class对象,通过该对象来创建这个类的所有对象。
在 Mybatis 中, 有对应的反射模块, 本文就是探究 mybatis 是如何进行反射的。 mybatis 中的反射主要与JavaBean相关。
1 JavaBean 规范
JavaBean 具有如下特征:
- 所有的属性都是私有的(通过 getter和setter 访问)
- 拥有公有的无参数构造函数
- 提供 setter/getter
- 实现 Serializable 接口
2 Reflector和ReflectorFactory
mybatis 这种框架, 出于性能等方面的考虑, 必然不是等到使用的时候再去解析XML/再去解析反射类。
mybatis 为每一个类提供了反射器类(Reflector
), 该类中存储了反射需要使用的类的元信息。
2.1 Reflector 属性
2.1.1 属性
从类的属性中, 我们可以看出:
- 一个反射器(
Reflector
)对应着一个Class
对象。 - 记录了默认构造函数
- 其余的是属性及其setter|getter相关
对于一个属性(没错, 属性, 只有有 setter|getter 才能被称之为属性)
- 如果是可读的(有getter方法)则
Reflector
会将其及其方法处理后放入对应的集合中; - 如果是可写的(有setter方法), 则
Reflector
会将其及其方法处理后放入对应的可写相关的集合中。 - 最后使用 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...
很显然, 签名的目的是唯一性。 那使用语言本身的特性来保证唯一性是最好的:
- 方法名不一致, 则方法就不一致
- 返回值不一致或者不是其子类, 则方法不一致
- 参数数量, 参数类型顺序不一致方法也会不一样
因此, 以上的签名方式可以保证方法的唯一性。
获取类的所有方法
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>
。
该方法只需要明白下面两个条件就能很清晰了:
- 返回值类型不同
返回值类型不同, 则哪个方法的返回值是另一个方法返回值类型子类, 就把 propName 指向该方法包装成的 Invoker
。 这个很好理解, 毕竟重新(override)重写时, 重写方法的返回值类型可以是被重写方法的子类。
- 返回值类型相同
按理来说不会出现这种情况, 因为在获取方法的时候已经使用签名去除掉了, 因此此时可以抛出异常。 但是有一种特殊的情况(这个卡了我一段时间):
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
有两个成员变量, 分别是 ReflectorFactory
和 Reflector
。
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)为例:
- 通过 PropertyTokenizer 对表达式进行解析, 得到当前的 name=richType, children=richProperty
- 从 reflector 中查找该 richType 属性
- 将 richType 添加到 builder 中
- 使用 metaClassForProperty 创建 richType 的
MetaClass
。 - 递归调用自身来处理子表达式
退出的条件就是没有子表达式。 这个就是为了, 我们类中有成员变量是类, 我们可以通过其找到他们的所有类及其属性。
注意, 在此过程中, ReflectorFactory
一直是同一个, 而其内部缓存了多个 Reflector
对象。
5 总结
类的关系:
Reflector
实现了实体类元信息的封装, 但对类中的成员变量是类的情况没有进行处理。 而 MetaClass
通过 ReflectorFactory
类型的成员变量, 实现了实体类中成员变量是类情况的处理。从而结合属性工具类实现了对复杂表达式的处理。
一起学 mybatis
你想不想来学习 mybatis? 学习其使用和源码呢?那么, 在博客园关注我吧!!
我自己打算把这个源码系列更新完毕, 同时会更新相应的注释。快去我的github star 吧!!
mybatis源码- 反射模块一(跟着MyBatis学反射):类级别信息的封装的更多相关文章
- Spring mybatis源码篇章-sql mapper配置文件绑定mapper class类
前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-MybatisDAO文件解析(二) 背景知识 MappedStatement是mybatis操作sql ...
- springboot集成mybatis源码分析-启动加载mybatis过程(二)
1.springboot项目最核心的就是自动加载配置,该功能则依赖的是一个注解@SpringBootApplication中的@EnableAutoConfiguration 2.EnableAuto ...
- mybatis源码学习(四)--springboot整合mybatis原理
我们接下来说:springboot是如何和mybatis进行整合的 1.首先,springboot中使用mybatis需要用到mybatis-spring-boot-start,可以理解为mybati ...
- MyBatis 源码篇-插件模块
本章主要描述 MyBatis 插件模块的原理,从以下两点出发: MyBatis 是如何加载插件配置的? MyBatis 是如何实现用户使用自定义拦截器对 SQL 语句执行过程中的某一点进行拦截的? 示 ...
- MyBatis 源码篇-日志模块2
上一章的案例,配置日志级别为 debug,执行一个简单的查询操作,会将 JDBC 操作打印出来.本章通过 MyBatis 日志部分源码分析它是如何实现日志打印的. 在 MyBatis 的日志模块中有一 ...
- MyBatis 源码篇-日志模块1
在 Java 开发中常用的日志框架有 Log4j.Log4j2.Apache Common Log.java.util.logging.slf4j 等,这些日志框架对外提供的接口各不相同.本章详细描述 ...
- MyBatis 源码篇-整体架构
MyBatis 的整体架构分为三层, 分别是基础支持层.核心处理层和接口层,如下图所示. 基础支持层 反射模块 该模块对 Java 原生的反射进行了良好的封装,提供了更加简洁易用的 API ,方便上层 ...
- MyBatis源码分析-SQL语句执行的完整流程
MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...
- MyBatis 源码分析 - 插件机制
1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...
随机推荐
- Android-简单总结一下图片压缩
最近项目需要用到图片压缩,所以简单总结一下.大致分为三种压缩. 图片质量压缩. 意思就是降低图片的质量,针对文件处理,但本身的像素点并不会减少. 本来像素点是这样的,经过算法计算,若一个像素点周围所存 ...
- Android开发--Service和Activity通过广播传递消息
Android的Service也运行在主线程,但是在服务里面是没法直接调用更改UI,如果需要服务传递消息给Activity,通过广播是其中的一种方法: 一.在服务里面发送广播 通过intent传送数据 ...
- Python赋值运算符
赋值运算符 运 算 符 说 明 举 例 展 开 形 式 = 简单的赋值运算 x=y x=y += 加赋值 x+=y x=x+y -= 减赋值 x-=y x=x-y *= 乘 ...
- redis常用命令及结构
##常用结构及命令: keys * #查询所有key randomkey #随机返回key type key #返回key的类型 exists key #判断key是否存在 del key1 key2 ...
- June 3. 2018 Week 23rd Sunday
You only get one shot; do not miss your chance to blow. 机会只有一次,不要错过. From Eminem, "Lose Yoursel ...
- MATLAB一元线性回归分析
MATLAB一元线性回归分析应用举例 作者:凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ huigui.m function [b,bint,r,rint, ...
- servlet温习
servlet是Javaweb的核心,它实质上就是运行在服务器端的Java代码 1.简介 servlet是运行在服务器端的小程序,是sun公司提供的一套规范(接口),用来处理用户的请求,响应给浏览器的 ...
- wangedit
<template> <el-row id="AddRoom"> <el-col :span="5">.</el-co ...
- kafka-connect-hdfs重启,进去RECOVERY状态,从hadoop hdfs拿租约,很正常,但是也太久了吧
虽说这个算是正常现象,等的时间也太久了吧.分钟级了.这个RECOVERY里面的WAL有点多余.有这么久的时间,早从新读取kafka写入hdfs了.纯属个人见解. @SuppressWarnings(& ...
- (转)ElasticSearch教程——汇总篇
https://blog.csdn.net/gwd1154978352/article/details/82781731 环境搭建篇 ElasticSearch教程——安装 ElasticSearch ...