作者:小傅哥
博客:bugstack.cn
沉淀、分享、成长,让自己和他人都能有所收获!😄
一、前言
Mybatis
最核心的原理也是它最便于使用的体现,为什么这说?
因为我们在使用 Mybatis 的时候,只需要定义一个不需要写实现类的接口,就能通过注解或者配置SQL语句的方式,对数据库进行 CRUD 操作。
那么这是怎么做到的呢,其中有一点非常重要,就是在 Spring 中可以把你的代理对象交给 Spring 容器,这个代理对象就是可以当做是 DAO 接口的具体实现类,而这个被代理的实现类就可以完成对数据库的一个操作,也就是这个封装过程被称为 ORM 框架。
说了基本的流程,我们来做点测试,让大家可以动手操作起来!学知识,一定是上手,才能得到!你可以通过以下源码仓库进行练习
二、把Bean塞到Spring容器,分几步
- 关于Bean注册的技术场景,在我们日常用到的技术框架中,MyBatis 是最为常见的。通过在使用 MyBatis 时都只是定义一个接口不需要写实现类,但是这个接口却可以和配置的 SQL 语句关联,执行相应的数据库操作时可以返回对应的结果。那么这个接口与数据库的操作就用到的 Bean 的代理和注册。
- 我们都知道类的调用是不能直接调用没有实现的接口的,所以需要通过代理的方式给接口生成对应的实现类。接下来再通过把代理类放到 Spring 的 FactoryBean 的实现中,最后再把这个 FactoryBean 实现类注册到 Spring 容器。那么现在你的代理类就已经被注册到 Spring 容器了,接下来就可以通过注解的方式注入到属性中。
按照这个实现方式,我们来操作一下,看看一个 Bean 的注册过程在代码中是如何实现的。
1. 定义接口
1 | java复制代码public interface IUserDao { |
- 先定义一个类似 DAO 的接口,基本这样的接口在使用 MyBatis 时还是非常常见的。后面我们会对这个接口做代理和注册。
2. 类代理实现
1 | java复制代码ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); |
- Java 本身的代理方式使用起来还是比较简单的,用法也很固定。
- InvocationHandler 是个接口类,它对应的实现内容就是代理对象的具体实现。
- 最后就是把代理交给 Proxy 创建代理对象,
Proxy.newProxyInstance
。
3. 实现Bean工厂
1 | java复制代码public class ProxyBeanFactory implements FactoryBean { |
- FactoryBean 在 spring 起到着二当家的地位,它将近有70多个小弟(实现它的接口定义),那么它有三个方法;
- T getObject() throws Exception; 返回bean实例对象
- Class<?> getObjectType(); 返回实例类类型
- boolean isSingleton(); 判断是否单例,单例会放到Spring容器中单实例缓存池中
- 在这里我们把上面使用Java代理的对象放到了 getObject() 方法中,那么现在再从 Spring 中获取到的对象,就是我们的代理对象了。
4. Bean 注册
1 | java复制代码public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor { |
在 Spring 的 Bean 管理中,所有的 Bean 最终都会被注册到类 DefaultListableBeanFactory 中,以上这部分代码主要的内容包括:
- 实现 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry方法,获取 Bean 注册对象。
- 定义 Bean,GenericBeanDefinition,这里主要设置了我们的代理类工厂。
- 创建 Bean 定义处理类,BeanDefinitionHolder,这里需要的主要参数;定义 Bean 和名称
setBeanClass(ProxyBeanFactory.class)
。 - 最后将我们自己的bean注册到spring容器中去,registry.registerBeanDefinition()
5. 测试验证
在上面我们已经把自定义代理的 Bean 注册到了 Spring 容器中,接下来我们来测试下这个代理的 Bean 被如何调用。
1. 定义 spring-config.xml
1 | java复制代码<bean id="userDao" class="org.itstack.interview.bean.RegisterBeanFactory"/> |
- 这里我们把 RegisterBeanFactory 配置到 spring 的 xml 配置中,便于启动时加载。
2. 单元测试
1 | java复制代码@Test |
测试结果
1 | java复制代码22:53:14.759 [main] DEBUG o.s.c.e.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source |
- 从测试结果可以看到,我们已经可以通过注入到Spring的代理Bean对象,实现我们的预期结果。
- 其实这个过程也是很多框架中用到的方式,尤其是在一些中间件开发,类似的 ORM 框架都需要使用到。
三、手写个Mybatis
扩展上一篇源码分析工程;itstack-demo-mybatis,增加 like 包,模仿 Mybatis 工程。完整规程下载 github.com/fuzhengwei/…
1 | java复制代码itstack-demo-mybatis |
关于整个 Demo 版本,并不是把所有 Mybatis 全部实现一遍,而是拨丝抽茧将最核心的内容展示给你,从使用上你会感受一模一样,但是实现类已经全部被替换,核心类包括;
- Configuration
- DefaultSqlSession
- DefaultSqlSessionFactory
- Resources
- SqlSession
- SqlSessionFactory
- SqlSessionFactoryBuilder
- XNode
1. 先测试下整个DemoJdbc框架
ApiLikeTest.test_queryUserInfoById()
1 | java复制代码@Test |
一切顺利结果如下(新人往往会遇到各种问题);
1 | java复制代码{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000} |
可能乍一看这测试类完全和 MybatisApiTest.java 测试的代码一模一样呀,也看不出区别。其实他们的引入的包是不一样;
MybatisApiTest.java 里面引入的包
1 | java复制代码import org.apache.ibatis.io.Resources; |
ApiLikeTest.java 里面引入的包
1 | java复制代码import org.itstack.demo.like.Resources; |
好!接下来我们开始分析这部分核心代码。
2. 加载XML配置文件
这里我们采用 mybatis 的配置文件结构进行解析,在不破坏原有结构的情况下,最大可能的贴近源码。mybatis 单独使用的使用的时候使用了两个配置文件;数据源配置、Mapper 映射配置,如下;
mybatis-config-datasource.xml & 数据源配置
1 | java复制代码<?xml version="1.0" encoding="UTF-8"?> |
User_Mapper.xml & Mapper 映射配置
1 | java复制代码<?xml version="1.0" encoding="UTF-8"?> |
这里的加载过程与 mybaits 不同,我们采用 dom4j 方式。在案例中会看到最开始获取资源,如下;
ApiLikeTest.test_queryUserInfoById() & 部分截取
1 | java复制代码String resource = "spring/mybatis-config-datasource.xml"; |
从上可以看到这是通过配置文件地址获取到了读取流的过程,从而为后面解析做基础。首先我们先看 Resources 类,整个是我们的资源类。
Resources.java & 资源类
1 | java复制代码/** |
这段代码方法的入口是getResourceAsReader,直到往下以此做了;
- 获取 ClassLoader 集合,最大限度搜索配置文件
- 通过 classLoader.getResourceAsStream 读取配置资源,找到后立即返回,否则抛出异常
3. 解析XML配置文件
配置文件加载后开始进行解析操作,这里我们也仿照 mybatis 但进行简化,如下;
1 | java复制代码SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader); |
SqlSessionFactoryBuilder.build() & 入口构建类
1 | java复制代码public DefaultSqlSessionFactory build(Reader reader) { |
- 通过读取流创建 xml 解析的 Document 类
- parseConfiguration 进行解析 xml 文件,并将结果设置到配置类中,包括;连接池、数据源、mapper关系
SqlSessionFactoryBuilder.parseConfiguration() & 解析过程
1 | java复制代码private Configuration parseConfiguration(Element root) { |
- 在前面的 xml 内容中可以看到,我们需要解析出数据库连接池信息 datasource,还有数据库语句映射关系 mappers
SqlSessionFactoryBuilder.dataSource() & 解析出数据源
1 | java复制代码private Map<String, String> dataSource(List<Element> list) { |
- 这个过程比较简单,只需要将数据源信息获取即可
SqlSessionFactoryBuilder.connection() & 获取数据库连接
1 | java复制代码private Connection connection(Map<String, String> dataSource) { |
- 这个就是jdbc最原始的代码,获取了数据库连接池
SqlSessionFactoryBuilder.mapperElement() & 解析SQL语句
1 | java复制代码private Map<String, XNode> mapperElement(List<Element> list) { |
- 这个过程首先包括是解析所有的sql语句,目前为了测试只解析 select 相关
- 所有的 sql 语句为了确认唯一,都是使用;namespace + select中的id进行拼接,作为 key,之后与sql一起存放到 map 中。
- 在 mybaits 的 sql 语句配置中,都有占位符,用于传参。where id = #{id} 所以我们需要将占位符设置为问号,另外需要将占位符的顺序信息与名称存放到 map 结构,方便后续设置查询时候的入参。
4. 创建DefaultSqlSessionFactory
最后将初始化后的配置类 Configuration,作为参数进行创建 DefaultSqlSessionFactory,如下;
1 | java复制代码public DefaultSqlSessionFactory build(Reader reader) { |
DefaultSqlSessionFactory.java & SqlSessionFactory的实现类
1 | java复制代码public class DefaultSqlSessionFactory implements SqlSessionFactory { |
- 这个过程比较简单,构造函数只提供了配置类入参
- 实现 SqlSessionFactory 的 openSession(),用于创建 DefaultSqlSession,也就可以执行 sql 操作
5. 开启SqlSession
1 | java复制代码SqlSession session = sqlMapper.openSession(); |
上面这一步就是创建了DefaultSqlSession,比较简单。如下;
1 | java复制代码@Override |
6. 执行SQL语句
1 | java复制代码User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L); |
在 DefaultSqlSession 中通过实现 SqlSession,提供数据库语句查询和关闭连接池,如下;
SqlSession.java & 定义
1 | java复制代码public interface SqlSession { |
接下来看具体的执行过程,session.selectOne
DefaultSqlSession.selectOne() & 执行查询
1 | java复制代码public <T> T selectOne(String statement, Object parameter) { |
- selectOne 就objects.get(0);,selectList 就全部返回
- 通过 statement 获取最初解析 xml 时候的存储的 select 标签信息;
1 | xml复制代码<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User"> |
- 获取 sql 语句后交给 jdbc 的 PreparedStatement 类进行执行
- 这里还需要设置入参,我们将入参设置进行抽取,如下;
1 | java复制代码private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException { |
+ 单个参数比较简单直接设置值即可,Long、Integer、String ...
+ 如果是一个类对象,需要通过获取 Field 属性,与参数 Map 进行匹配设置
- 设置参数后执行查询 preparedStatement.executeQuery()
- 接下来需要将查询结果转换为我们的类(主要是反射类的操作),resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
1 | java复制代码private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) { |
+ 主要通过反射生成我们的类对象,这个类的类型定义在 sql 标签上
+ 时间类型需要判断后处理,Timestamp,与 java 不是一个类型
7. Sql查询补充说明
sql 查询有入参、有不需要入参、有查询一个、有查询集合,只需要合理包装即可,例如下面的查询集合,入参是对象类型;
ApiLikeTest.test_queryUserList()
1 | java复制代码@Test |
**测试结果:
1 | java复制代码[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}] |
四、源码分析(mybatis)
1 | xml复制代码<dependency> |
Mybatis的整个源码还是很大的,以下主要将部分核心内容进行整理分析,以便于后续分析Mybatis与Spring整合的源码部分。简要包括;容器初始化、配置文件解析、Mapper加载与动态代理。
1. 从一个简单的案例开始
要学习Mybatis源码,最好的方式一定是从一个简单的点进入,而不是从Spring整合开始分析。SqlSessionFactory是整个Mybatis的核心实例对象,SqlSessionFactory对象的实例又通过SqlSessionFactoryBuilder对象来获得。SqlSessionFactoryBuilder对象可以从XML配置文件加载配置信息,然后创建SqlSessionFactory。如下例子:
MybatisApiTest.java
1 | java复制代码public class MybatisApiTest { |
dao/IUserDao.java
1 | java复制代码public interface IUserDao { |
spring/mybatis-config-datasource.xml
1 | java复制代码<?xml version="1.0" encoding="UTF-8"?> |
如果一切顺利,那么会有如下结果:
1 | java复制代码{"age":18,"createTime":1571376957000,"id":1,"name":"花花","updateTime":1571376957000} |
从上面的代码块可以看到,核心代码;SqlSessionFactoryBuilder().build(reader),负责Mybatis配置文件的加载、解析、构建等职责,直到最终可以通过SqlSession来执行并返回结果。
2. 容器初始化
从上面代码可以看到,SqlSessionFactory是通过SqlSessionFactoryBuilder工厂类创建的,而不是直接使用构造器。容器的配置文件加载和初始化流程如下:
- 流程核心类
- SqlSessionFactoryBuilder
- XMLConfigBuilder
- XPathParser
- Configuration
SqlSessionFactoryBuilder.java
1 | java复制代码public class SqlSessionFactoryBuilder { |
从上面的源码可以看到,SqlSessionFactory提供三种方式build构建对象;
- 字节流:java.io.InputStream
- 字符流:java.io.Reader
- 配置类:org.apache.ibatis.session.Configuration
那么,字节流、字符流都会创建配置文件解析类:XMLConfigBuilder,并通过parser.parse()生成Configuration,最后调用配置类构建方法生成SqlSessionFactory。
XMLConfigBuilder.java
1 | java复制代码public class XMLConfigBuilder extends BaseBuilder { |
- XMLConfigBuilder对于XML文件的加载和解析都委托于XPathParser,最终使用JDK自带的javax.xml进行XML解析(XPath)
- XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver)
- reader:使用字符流创建新的输入源,用于对XML文件的读取
- validation:是否进行DTD校验
- variables:属性配置信息
- entityResolver:Mybatis硬编码了new XMLMapperEntityResolver()提供XML默认解析器
XMLMapperEntityResolver.java
1 | java复制代码public class XMLMapperEntityResolver implements EntityResolver { |
- Mybatis依赖于dtd文件进行进行解析,其中的ibatis-3-config.dtd主要是用于兼容用途
- getInputSource(String path, String publicId, String systemId)的调用里面有两个参数publicId(公共标识符)和systemId(系统标示符)
XPathParser.java
1 | java复制代码public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) { |
- 从上到下可以看到主要是为了创建一个Mybatis的文档解析器,最后根据builder.parse(inputSource)返回Document
- 得到XPathParser实例后,接下来在调用方法:this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
1 | java复制代码 XMLConfigBuilder.this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props); |
- 其中调用了父类的构造函数
1 | java复制代码public abstract class BaseBuilder { |
- XMLConfigBuilder创建完成后,sqlSessionFactoryBuild调用parser.parse()创建Configuration
1 | java复制代码public class XMLConfigBuilder extends BaseBuilder { |
3. 配置文件解析
这一部分是整个XML文件解析和装载的核心内容,其中包括;
- 属性解析propertiesElement
- 加载settings节点settingsAsProperties
- 载自定义VFS loadCustomVfs
- 解析类型别名typeAliasesElement
- 加载插件pluginElement
- 加载对象工厂objectFactoryElement
- 创建对象包装器工厂objectWrapperFactoryElement
- 加载反射工厂reflectorFactoryElement
- 元素设置settingsElement
- 加载环境配置environmentsElement
- 数据库厂商标识加载databaseIdProviderElement
- 加载类型处理器typeHandlerElement
- (核心)加载mapper文件mapperElement
1 | java复制代码parseConfiguration(parser.evalNode("/configuration")); |
所有的root.evalNode()底层都是调用XML DOM方法:Object evaluate(String expression, Object item, QName returnType),表达式参数expression,通过XObject resultObject = eval( expression, item )返回最终节点内容,可以参考mybatis.org/dtd/mybatis…
1 | xml复制代码<!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)> |
mybatis-3-config.dtd 定义文件中有11个配置文件,如下;
- properties?,
- settings?,
- typeAliases?,
- typeHandlers?,
- objectFactory?,
- objectWrapperFactory?,
- reflectorFactory?,
- plugins?,
- environments?,
- databaseIdProvider?,
- mappers?
以上每个配置都是可选。最终配置内容会保存到org.apache.ibatis.session.Configuration,如下;
1 | java复制代码public class Configuration { |
以上可以看到,Mybatis把所有的配置;resultMap、Sql语句、插件、缓存等都维护在Configuration中。这里还有一个小技巧,在Configuration还有一个StrictMap内部类,它继承于HashMap完善了put时防重、get时取不到值的异常处理,如下;
1 | java复制代码protected static class StrictMap<V> extends HashMap<String, V> { |
(核心)加载mapper文件mapperElement
Mapper文件处理是Mybatis框架的核心服务,所有的SQL语句都编写在Mapper中,这块也是我们分析的重点,其他模块可以后续讲解。
XMLConfigBuilder.parseConfiguration()->mapperElement(root.evalNode(“mappers”));
1 | java复制代码private void mapperElement(XNode parent) throws Exception { |
- Mybatis提供了两类配置Mapper的方法,第一类是使用package自动搜索的模式,这样指定package下所有接口都会被注册为mapper,也是在Spring中比较常用的方式,例如:
1 | java复制代码<mappers> |
- 另外一类是明确指定Mapper,这又可以通过resource、url或者class进行细分,例如;
1 | java复制代码<mappers> |
4. Mapper加载与动态代理
通过package方式自动搜索加载,生成对应的mapper代理类,代码块和流程,如下;
1 | java复制代码private void mapperElement(XNode parent) throws Exception { |
Mapper加载到生成代理对象的流程中,主要的核心类包括;
- XMLConfigBuilder
- Configuration
- MapperRegistry
- MapperAnnotationBuilder
- MapperProxyFactory
MapperRegistry.java
解析加载Mapper
1 | java复制代码public void addMappers(String packageName, Class<?> superType) { |
生成代理类:MapperProxyFactory
1 | java复制代码public <T> void addMapper(Class<T> type) { |
在MapperRegistry中维护了接口类与代理工程的映射关系,knownMappers;
1 | java复制代码private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>(); |
MapperProxyFactory.java
1 | java复制代码public class MapperProxyFactory<T> { |
如上是Mapper的代理类工程,构造函数中的mapperInterface就是对应的接口类,当实例化时候会获得具体的MapperProxy代理,里面主要包含了SqlSession。
五、综上总结
- 分析过程较长篇幅也很大,不一定一天就能看懂整个流程,但当耐下心来一点点研究,还是可以获得很多的收获的。以后在遇到这类的异常就可以迎刃而解了,同时也有助于面试、招聘!
- 之所以分析Mybatis最开始是想在Dao上加自定义注解,发现切面拦截不到。想到这是被动态代理的类,之后层层往往下扒直到MapperProxy.invoke!当然,Mybatis提供了自定义插件开发。
- 以上的源码分析只是对部分核心内容进行分析,如果希望了解全部可以参考资料;MyBatis 3源码深度解析,并调试代码。IDEA中还是很方便看源码的,包括可以查看类图、调用顺序等。
- mybatis、mybatis-spring中其实最重要的是将Mapper配置文件解析与接口类组装成代理类进行映射,以此来方便对数据库的CRUD操作。从源码分析后,可以获得更多的编程经验(套路)。
- Mybatis相关链接;
本文转载自: 掘金