mybatis源码研究-2019/12/04-2019/12/09
教你一周精通mybatis(调试版本:3.5.3)
从整体到局部,最后再回到整体,从全局把控mybatis各个模块;
1.自底向上分析法,先对基础控件描述并阐述相关实现原理,带着问题一步一步完成封装,最终形成整体。
2.从应用入手,自顶向下,挖掘实现机制,将具体模块一步一步细节拆解,最终形成深入全面的认识。
深挖mybatis所有核心知识点,真正掌握mybatis核心设计思想;
赠送各种流程图,思维导图。
├─java
│ └─org
│ └─pp
│ ├─annotation 注解模拟SQL操作
│ │ └─demo_sql
│ └─mybatis
│ ├─entity
│ ├─foundationsupportlayer
│ │ ├─jdbc JDBC操作
│ │ │ └─cases
│ │ │ └─prepare
│ │ ├─parser XPath-DTD-Schema
│ │ └─reflector
│ │ ├─beans
│ │ └─proxy JDK动态代理,proxy handler 接口与实现分离
│ │ ├─interceptor
│ │ └─jdk
│ └─mapper
└─resources
└─mapper
mybatis整体架构
mybatis整体架构分为三层,分别是基础支持层、核心处理层和接口层。
参考项目代码~~~
全局配置文件 mybatis-config.xml
映射配置文件 UserMapper.xml
方法接口 UserMapper
结果集/实体 UserEntiry
- 读写配置文件,如何封装,提取配置的SQL语句并解析 Configuration MapperProxy MappedStatement
- 接口在哪里实现,怎么实现,SQL如何执行 SqlSession Executor TypeHandler
- 结果集映射返回期望的Java类型 ResultSetHandler
- 对扩展开放 plugins
所有的 XML 文档(以及 HTML 文档)均由以下简单的构建模块构成:
元素
属性
实体
PCDATA
CDATA
名称空间为了避免条目命名冲突
参考资料:
文档类型声明或DOCTYPE告诉解析器,XML文档必须遵循DTD定义。同时它也告诉解析器,到哪里找到文档定义的其余内容。
<!DOCTYPE name [ ]>
引用外部DTD要用下面两种方法之一:系统标识符和公共标识符。
关键字SYSTEM和指向文档位置的URI引用。URI可以是硬盘上的一个文件,也可以是局域网或者internet上的一个文件。
<!DOCTYPE name SYSTEM "name.dtd" [...]>
以PUBLIC关键字开始,其后紧跟一个专用的标识符,但是公共标识不是用来表示对文件的引用,而是表示目录中的一个记录。 根据XML规范,公共标识符可以采用任何格式,但是一种经常使用的格式是正式公共标识符(Formal Public Identifier, FPI)。
FPI的语法要匹配下面的基本结构:
-//Owner//Class Description//Language//Version
从底层的角度看,它与名称空间的作用相似,但是公共标识符不能把两个不同的词汇组合到同一个文档里。所以名称空间比它功能更强大。
在标识符字符串之后,还可以插入一个可选的系统标识符。这样,当处理器不能解析公共标识符时,可以查找这个文档的副本(大多数处理器
不能解析公共标识符)。
<!DOCTYPE name PUBLIC "-//Beginning XML//DTD Name Example//EN" "name.dtd">
通常,DTD由三个基本部分组成:
- 元素声明
- 属性声明
- 实体声明
由三个部分组成:ELEMENT声明 元素名 元素内容模型
<!ELEMENT name (first, middle, last)>
ELEMENT声明告诉解析器当前声明一个元素。
就XML标准而言,有四类内容文档。(FAQ. 重点,请自行查阅相关资料)
- 元素内容
- 混合内容
- 空内容
- 任意内容
指示符 | 说明 |
---|---|
[none] | 内容模型中默认方式,表示这个元素必须且只出现一次 |
? |
表示元素出现一次或零次 |
+ |
表示元素出现一次或多次 |
* |
表示元素出现零次或多次 |
属性声明需要使用ATTLIST关键字。
ATTLIST包含三个部分:ATTLIST关键字 相应元素名 属性列表
<!ELEMENT book (title, author*, publisher,isbn,price)>
<!ATTLIST book year CDATA #IMPLIED>
上面例子指出book元素包含year属性,CDATA用来定义属性类型,#IMPLIED指出该属性不是必须的。
ref: https://www.w3school.com.cn/dtd/dtd_attributes.asp
http://mybatis.org/dtd/mybatis-3-config.dtd
http://mybatis.org/dtd/mybatis-3-mapper.dtd
XML Schema 参考手册:
XML Schema 是基于 XML 的 DTD 替代者。XML Schema也称为XML框架或XML模式。通过Schema可以描述和规范XML文档的数据模式
和组织结构,规定XML文档中可以包含哪些元素、这些元素拥有哪些子元素及其出现的顺序和次数,还规定每个元素和属性的数据
类型。与DTD相比,通过XML Schema可以更好地规范和验证有效的XML文档。
XML Schema总是以独立文档形式存在,其文件扩展名为.xsd。XML Schema文档本身就是一个符合XML规范的、格式良好的XML文
档,该文档通过一套预先定义的XML元素及其属性创建的,正是这些特定的元素和属性规定了XML文档的结构和内容模式。
Schema文档从一个XML文档声明开始,其后的内容是对根元素schema的声明。根据需要,还可以从一个Schema文档中包含或导入其他Schema文档。
eg. http://www.springframework.org/schema/beans/spring-beans-4.1.xsd 文档基本框架如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.springframework.org/schema/beans"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.springframework.org/schema/beans">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<!--元素和属性声明-->
</xsd:schema>
xmlns:指定该schema的默认名称空间
targetNamespace:声明了在该xsd文档下定义的元素属于该命名空间
schemaLocation:告诉验证器去哪里找到可用于验证的XML Schema文档
举个栗子:
/** Default Servlet name used by Tomcat, Jetty, JBoss, and GlassFish */
private static final String COMMON_DEFAULT_SERVLET_NAME = "default";
/** Default Servlet name used by Google App Engine */
private static final String GAE_DEFAULT_SERVLET_NAME = "_ah_default";
/** Default Servlet name used by Resin */
private static final String RESIN_DEFAULT_SERVLET_NAME = "resin-file";
/** Default Servlet name used by WebLogic */
private static final String WEBLOGIC_DEFAULT_SERVLET_NAME = "FileServlet";
/** Default Servlet name used by WebSphere */
private static final String WEBSPHERE_DEFAULT_SERVLET_NAME = "SimpleFileServlet";
如果web.xml中配置了某个servlet拦截了所有的请求,那么一些静态资源的访问如jpg,css,js也会被处理。
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.js</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.css</url-pattern>
</servlet-mapping>
在springMVC-servlet.xml中配置:mvc:default-servlet-handler/
这个元素声明在:http://www.springframework.org/schema/mvc/spring-mvc.xsd(FAQ. 自动匹配xsd版本?)
删除文档注释,可以看到复合类型元素default-servlet-handler有且只有一个default-servlet-name属性,
通过转发请求到Servlet容器的default-servlet从而达到对静态资源的处理。
(具体实现请参考,笔者版本spring4.1.9:org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler)
<!--通过转发到Servlet容器的默认Servlet来配置用于提供静态资源的处理程序。-->
<xsd:element name="default-servlet-handler">
<xsd:complexType>
<xsd:attribute name="default-servlet-name" type="xsd:string">
</xsd:attribute>
</xsd:complexType>
</xsd:element>
最后介绍一个xml/dtd转xsd工具trang
cd C:\Users\45554\Downloads\trang-20030619
java -jar .\trang.jar .\inventory.dtd inventory.xsd
FAQ. 自行比较DTD与XML schema
FAQ. 阅读其他主题XPath、xslt、DOM、Java与xml等
程序员必须建立起在机器模型(位于“解空间”内,这是你对问题建模的地方,例如计算机)和实际待解问题的模型(位于“问题空间”内
这是问题存在的地方,例如一项业务)之间的关联。
面向对象方式将问题空间中的元素及其在解空间中的表示称为“对象”。这种思想的实质是:程序可以通过添加新类型的对象使自身适用与
某个特定问题。这是一种更灵活更强有力的语言抽象。
OOP中,用class表示数据类型;类描述了具有相同特性(数据元素)和行为(功能)的对象集合,一个类实际上就是一个数据类型。
***多态的本质***
前期绑定:编译器将产生对一个具体函数名字的调用,而运行时将这个调用解析到将要被执行的代码的绝对地址。
OOP中,程序直到运行时才能确定代码地址。当消息发送到一个泛化对象时,使用了后期绑定,编译器确保被调用方法的存在,并对
调用参数和返回值致性类型检查(无法提供此类保证的语言被称为是弱类型的),但是并不知道将要执行的确切代码。
Java的动态绑定是默认行为(C++通过virtual关键字明确声明某个方法后期绑定)不需要额外关键字实现多态。
多态的作用是消除类型之间的耦合关系。多态通过分离做什么和怎么做,从另一个角度实现接口和实现分离(FAQ. 查阅OOP三大特
性(ch08-多态))。
“封装”通过合并特征和行为来创建新的数据类型。
继承允许将对象视为它自己本身的类型或其基类型来加以处理。这种能力极为重要,因为它允许将多种类型(从同一基类导出的)视为
同一类型,而同一份代码也就可以毫无差别地运行在这些不同类型之上了。
多态方法调用根据方法行为的不同来表现类型之间的差别,尽管它们都可以通过同一个基类来调用。
FAQ. 方法重载:what(表现形式)?why(为什么要重载方法)?who(怎么设计重载方法,如何查找到正确的方法来调用)?
每当编写并且编译一个新类就会产生一个Class对象,它被保存在一个同名的.class文件中。所有的类都是在对其第一次使用时,动态
加载到JVM中的。无论何时,通过获得相应的Class对象的引用可以运行时使用类型信息。
为使用类而做的准备工作包含三个步骤:
加载,类加载器执行。该步骤将查找字节码,并从字节码中创建一个Class对象。
链接,验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。
初始化,如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。(类初始化是线程安全的)
一种标记,用于对java说明,注释。
Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解:
@Target,@Retention,@Documented,@Inherited
Java语言对注解的支持:
泛型是jdk1.5引入的新特性,允许在定义类和接口的时候使用类型参数。
类型擦除
java的泛型基本是在编译器这个层次上实现的。在生成的java字节码中不包含泛型中的类型信息。
使用泛型时加上的类型参数,会被编译器在编译时擦除。
类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份
字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。
类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。
类型擦除的主要过程如下:
1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
2.移除所有的类型参数。
泛型支持
为了表达泛型类型声明,JavaSE 5.0在java.lang.reflect包中提供了一个新的接口Type。这个接口包含下列子类型:
GenericArrayType (java.lang.reflect)
GenericArrayTypeImpl (com.sun.xml.internal.bind.v2.model.nav)
GenericArrayTypeImpl (sun.reflect.generics.reflectiveObjects)
GenericArrayTypeImpl in TypeParameterResolver (org.apache.ibatis.reflection)
ParameterizedType (java.lang.reflect)
ParameterizedTypeImpl in TypeParameterResolver (org.apache.ibatis.reflection)
ParameterizedTypeImpl (com.sun.xml.internal.bind.v2.model.nav)
ParameterizedTypeImpl (com.alibaba.fastjson.util)
ParameterizedTypeImpl (sun.reflect.generics.reflectiveObjects)
WildcardType (java.lang.reflect)
WildcardTypeImpl (sun.reflect.generics.reflectiveObjects)
WildcardTypeImpl in TypeParameterResolver (org.apache.ibatis.reflection)
WildcardTypeImpl (com.sun.xml.internal.bind.v2.model.nav)
WildcardTypeImpl (com.sun.beans)
TypeVariable (java.lang.reflect)
TypeVariableImpl (sun.reflect.generics.reflectiveObjects)
Class (java.lang)
泛型与数组
泛型擦除会移除参数类型信息,数组必须知道所持有的确切类型,以强制保证类型安全。
参考:package org.pp.mybatis.foundationsupportlayer.reflector.proxy
描述文件 proxy.txt
测试类入口: Client.main
关于模拟spring IOC请参考我的另一个repo:https://github.com/GitJavaProgramming/JavaAdvance/tree/master/src/reflection
mybatis入口: SqlSessionFactoryBuilder读取xml文档,解析并构造Configuration,根据Configuration构造sqlSessionFactory
sqlSessionFactory可以很轻易的拿到sqlSession,sqlSession提供系列操作方法,
mybatis初始化之后Configuration、sqlSessionFactory均已构造完成。
MapperRegistry中维护了实际类型(*Mapper接口,由mapper元素的namespace属性指定)和代理类的map映射
提供两个泛型方法:
public void addMapper(Class type)中传入的Class将会被MapperProxyFactory代理,代理处理器是MapperProxy
public T getMapper(Class type, SqlSession sqlSession) 返回代理类实例(与被代理类是同一类型)
所有配置解析完成之后,mapper映射文件中,一个SQL元素节点被封装成一个MappedStatement对象,在SqlSession中注入全局配置单例
Configuration通过元素id、MappedStatement映射可以找到对应MappedStatement对象,它包含了具体执行的sql字符串。
MapperProxyFactory仅仅只是代理方法,由于接口不提供实现,所以需要自己实现方法处理逻辑,并提供返回值。
final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args);
具体实现在SqlSession中,SqlSession用到了缓存的MappedStatement执行真实的sql字符串;这是接口和实现分离思想的体现。
同时这里用到了命令模式。
sqlSession javadoc:Through this interface you can execute commands, get mappers and manage transactions.。
SqlSession接口对JDBC封装通过Executor接口实现,SqlSession类型负责提供高层对外访问接口。
BaseExecutor的基本实现提供内置的一级缓存管理和事务管理:
缓存由Cache接口定义,一级缓存是SqlSession内部缓存,Executor中localCache,localOutputParameterCache表示一级缓存:
/**
* 为每个namespace提供缓存一个实例
* 缓存实现必须提供字符串(被定义为缓存id属性,提供getter)作为构造参数
*/
public interface Cache {
String getId();
void putObject(Object key/*{@link CacheKey}*/, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
default ReadWriteLock getReadWriteLock() {
return null;
}
}
mybatis提供很多Cache实现,PerpetualCache提供基本实现,Cache装饰器类在org.apache.ibatis.cache.decorators包下面。
如何设计CacheKey,提供全局唯一的key:从上往下找config->mapper->statement(sql, args)
在Executor.createCacheKey中还用到了RowBounds(实现通用分页,个人感觉方法设计冗余,提供插件?)的相关属性offset,limit
对于嵌套查询,BaseExecutor实现提供一个延迟加载队列,在query()方法执行时,会查询该队列并加载命中的缓存。
(参考源码,mybatis 3.5.3在这里有几个issue):
mybatis issues列表: https://github.com/mybatis/mybatis-3/issues
例如 #482 https://github.com/mybatis/mybatis-3/issues/482
这个问题是一个字符编码问题,描述以前的NStringHandler不支持国家字符集类型数据~~~
Java String字符串类型与JdbcType转换
NVARCHAR(Types.NVARCHAR), // JDK6
NCHAR(Types.NCHAR), // JDK6
NCLOB(Types.NCLOB), // JDK6
具体转换的类型会根据参数字符串长度和驱动定义的NVARCHAR值来确定
TypeHandler完成单个参数/列值的类型转换
NStringTypeHandler会在TypeHandlerRegistry中注册,这是已经修复问题了吧,注释没删掉吗???
FAQ. 自行查阅关于char varchar NCHAR NVARCHAR 区别
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
这里不再详述,读者可以结合JDBC自身情况来设计一个查询缓存,Cache接口已为你定义好了...
Mybatis定义Transaction事务操作接口,它有两个实现JdbcTranscation、ManagedTransaction;
二级缓存是事务级别缓存,是一个全局缓存,被所有SqlSession(ref -> sqlSession javadoc)共享。
CachingExecutor通过装饰模式组合Executor添加新功能,提供二级缓存功能。当然Cache的线程安全由自身保证。
JDBC JSR221规范,当前4.3(oracle)草案,4.0(sun)为最终版。
JCP
JSR221
JDBC4.0
JDBC4.0规范中文
DataSource数据源实现 参考规范4.0 ch09---connections
// 1. SqlSessionFactory build from config document and initialize all
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 2. open session and new Executor
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3. get MapperProxyFactory instance from cache for create MapperProxy
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<UserEntity> userList = userMapper.listAllUser();
System.out.println(JSON.toJSONString(userList));
mybatis很好的实践了面向接口编程。通过合理抽象、接口解耦实现了配置式的SQL控制。实践中大量用到面向接口编程、注册延后使用、
动态代理和装饰器模式。
创建 singleton factory builder
结构 decorator
对象行为 command proxy
类行为 template
关于结构:为了实现新功能,如何对一些对象进行组合。这样会形成更大的结构。
关于行为:涉及到算法和对象间职责的分配,同时它描述了对象或类之间的通信模式。
类行为:使用继承机制在类间分派行为。
对象行为: 使用对象复合,这会使依赖关系复杂。所以需要对象解耦或者说进行对象关系维护。
- OGNL
- 高性能SQL与SQL优化
- ResultSetHandler/TypeHandler
- 插件(动态代理与注解)与扩展
- 测试驱动与敏捷开发
- Java8 新特性
- MyBatis技术内幕 徐郡明 2017/07
- [MyBatis 3 源码深度解析 江荣波 2019/10]
- MyBatis从入门到精通 刘增辉 2017/07
- 深入浅出MyBatis 技术原理与实战 杨开振 2016/09
- xml
- dtd
- schema
- xpath
- XML入门经典(第5版) 第I部分-->第III部分
- 疯狂XML讲义
- XML揭秘 入门·应用·精通 Michael Morrison 陆新年
- [《XML简明教程》2009年清华大学出版社出版 张欣毅]
- Java与XML
- Java编程思想 第四版
- Java核心技术I/II卷
- [C++程序设计教程(第二版) 钱能]
- 深入理解Java虚拟机 JVM高级特性与最佳实践 第2版
- Java深度历险
- Java语言规范 基于Java SE 8
- Java虚拟机规范 Java SE 8版
- [设计模式-可复用面向对象软件的基础]