MyBatis 中 Mapper 生效 解析 MyBatis 中 Mapper 生效的来龙去脉
J3-白起 人气:0最近闲了快有半个多月了,一直提不起兴致再去看一些书籍(没有以前疯狂吸食知识的欲望了😓)。
不过这一两天不知道是什么筋搭错了非常想写点什么,但又不知道写点啥(苦恼)。所以我就结合了一下本人工作中经常用到但没有深入的技术下手了,最后思来想去就选择了 MyBatis 中 Mapper 文件这一块的知识内容入手了。
以前只是知道写一个 Mapper 接口,对应着再去写一个 Mapper.xml 文件然后将 Mapper 接口位置和 Mapper.xml 文件位置通过 MyBatisConfig.xml 的配置文件关联起来就可以非常方便的操作访问数据库,但究其原因确是说不上个所以然来(汗颜)。
那既然搞出了前因,后面就一起往下学咯!
一、MyBatis基本使用
一切都从最简单的开始,所以先来回顾一下其基本的使用(不会吧不会吧,最基本的hello world别忘了)。
步骤:
1、首先我们要创建一个maven工程
2、添加MyBatis的依赖及MySQL依赖,如下:
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency>
3、再添加一个测试单元依赖吧,等会要通过测试单元进行测试
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency>
OK,到这一步项目的基本环境就搭建完毕了,下面就是正式的使用 MyBatis 框架相关的内容了。
1.1 编写配置文件
在资源目录下面创建下面两个配置文件:
这里我们先准备数据库连接信息的配置类:jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/mybatistest?useUnicode=true&characterEncoding=utf-8 jdbc.username=root jdbc.password=root
接着就是最重要的一个配置类了:MyBatisConfig.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 导入数据库配置文件的信息--> <properties resource="jdbc.properties"></properties> <!-- 配置setting属性--> <settings> <!-- 开启了一个驼峰命名规则--> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 日志--> <setting name="logImpl" value="STDOUT_LOGGING"></setting> </settings> <!-- 配置数据库--> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <!-- 配置连接池 --> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <!-- mappers中注册我们所有写的dao接口的实现(映射)文件--> <mappers> <mapper resource="/.../IUserMapper.xml"/> <!-- 如果映射文件有十几百个的话,可以用下面的全局注册 <package name="文件所在包路径"></package> <package name="cn.liuliang.Dao"></package> --> </mappers> </configuration>
1.2 编写Mapper接口及测试方法
Mapper接口类
public interface IUserMapper { /** * 查询所有用户 * @return */ List<User> findAll(); }
开始测试
public class MyBatisTest { @Test public void test01() throws IOException { // 读取配置文件 InputStream in= Resources.getResourceAsStream("MyBatisConfig.xml"); // 创建sqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in); // 获得会话 SqlSession session=sqlSessionFactory.openSession(); // 得到代理 IUserMapper iUserMapper =session.getMapper(IUserMapper.class); // 查询数据库 List<User> userList= iUserMapper.findAll(); for (User user : userList) { System.out.println(user); } } }
1.3 结果
SQL和结果都打印出来了😁。
以后,只要是对数据库的操作,我们就只需要编写 Mapper 接口和其对应的 xml 文件就可以非常快速操作数据库,对比以前原生JDBC操作什么拼接SQL、结果集映射、资源关闭一大堆操作让我们开发人员来处理,也太鸡肋了吧!所以对于这个 MyBatis 持久层框架我只想说(牛逼)。
下面就要全程高能哦!但其实也很简单了,它就只是把原生操作的 JDBC 进行了封装,暴露出按照它所定义的简单规则走而已,多的不说了,你们有资格一睹 MyBatis 源码的芳容了。
二、源码分析
既然要分析源码了,那么从什么地方入手呢!— 测试方法
通过测试方法,我们可以知道 MyBatis 会先加载资源文件(MyBatisConfig.xml),因为这文件是一切的开始,通过这个文件可以知道数据源、特性(日志,驼峰命名…)、Mapper 文件等一系列信息。
2.1 通过配置文件构建出 SqlSessionFactory
第一个类名出现了:SqlSessionFactory ,它的类图如下:
简单熟悉一下图中出现的名字吧:
SqlSessionFactory接口
:SqlSessionFactory 负责创建 SqlSession 对象,其中只包含了多个 openSession() 方法的重载,可以通过其参数指定事务的隔离级别、底层使用 Executor 的类型以及是否自动提交事务等方面的配置。
DefaultSqlSessionFactory类
:一个具体的工厂,实现了 SqlSessionFactory 接口。它主要提供了两种创建 DefaultSqlSession 对象的方式:
- 通过数据源获取数据库连接,并创建 Executor 对象及 DefaultSqlSession 。
- 通过用户提供的数据连接对象,DefaultSqlSessionFactory 会使用该数据库连接对象创建 Executor 对象及 DefaultSqlSession。
SqlSessionManager类
:同时实现了 SqlSession 接口和 SqlSessionFactory 接口 ,也就同时提供了SqlSessionFactory 创建 SqlSession 以及 SqlSession 操纵数据库的功能。
SqlSession接口
:是mybatis的核心操作类,其中对数据库的crud都封装在这个中,是一个顶级接口,其中默认实现类是DefaultSqlSession这个类。
DefaultSqlSession类
:默认 SqlSession 接口的 CRUD 实现类,且 DefaultSqlsession 不是线程安全的(对于线程安全,关注session和connnect的关系就好了)
好了开始分析,从第一行代码入手:
// 读取配置文件 InputStream in= Resources.getResourceAsStream("MyBatisConfig.xml"); // 创建sqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSessionFactoryBuilder # build
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { // ... // 根据文件流,创建 XMLConfigBuilder 对象 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 先解析 配置文件,然后构建出 SqlSessionFactory对象 return build(parser.parse()); // ... }
最终会创建一个 DefaultSqlSessionFactory 对象返回出去
public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
流程如下:
2.2 获取 SqlSession 对象
在获取到会话工厂之后,就是根据工厂获得具体的会话了。
代码入口:
// 获得会话 SqlSession session=sqlSessionFactory.openSession();
调用:DefaultSqlSessionFactory # openSession()
public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }
最终来到:DefaultSqlSessionFactory # openSessionFromDataSource()
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { // 根据配置文件 configuration 获取对应的会话环境(包括事物,数据源) final Environment environment = configuration.getEnvironment(); // 获取事物工厂 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 根据数据源,配置事物,autoCommit:是否自动提交事物 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 根据配置获取执行器(最终都是它执行对应的数据库操作) final Executor executor = configuration.newExecutor(tx, execType); // 准备好上面的信息之后,都封装到默认会话对象中返回出去 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
在获取 SqlSession 对象的过程中,都是根据默认的会话工厂,从工厂中获取对应的会话。这样在我看来非常的不错,因为获取一个数据库的操作会话是需要配置非常多的属性的,包括数据源配置、事物配置等。但是有了这个创建会话工厂类之后,那么一切就变得简单起来了,工厂囊括了所有的细节,只需要我们调一个对外的 API 我们就可以获得对应的 SqlSession 对象(工厂帮我们做了细节),进而操作数据库,读了上面的代码就是一个很好的提现😀。
提一点:
配置文件(MyBatisConfig.xml)构造出默认会话工厂(SqlSessionFactory),工厂再创建出具体的操作数据库会话(SqlSession)
2.3 根据 SqlSession 获取 Mapper 代理
在上面,已经分析了如何获取一个会话的源码,那我们得到一个会话之后,就是要根据具体的 Mapper 接口获得对应的操作数据库代理对象了,就是下面这段代码:
// 得到代理 IUserMapper iUserMapper =session.getMapper(IUserMapper.class);
点进去看看
因为 session 对象是由 DefaultSqlSessionFactory 创建出来的 DefaultSqlSession,所以该代码位于此类中
public <T> T getMapper(Class<T> type) { // 根据配置类,获取 Mapper return configuration.getMapper(type, this); }
点进去:Configuration # getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // 从 mapperRegistry 中获取具体 Mapper return mapperRegistry.getMapper(type, sqlSession); }
MapperRegistry
:可以理解为 Mapper 接口的注册中心,里面存放了所有 Mapper 接口相关属性。
MapperRegistry# getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // knownMappers,一个Map,存放 Mapper 代理工厂 // 在初始化的时候根据配置文件已经将所有配置的 Mapper 接口注册到此了 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { // 具体代理生成 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
点进具体代理:MapperProxyFactory # newInstance
public T newInstance(SqlSession sqlSession) { // 根据 SqlSession 和 Mapper 接口生成代理对象 final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); // 真正代理,如下 return newInstance(mapperProxy); } // 下面就是根据 JDK 原生 API 进行代理了,由此返回代理对象给用户使用 protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }
以上就是 Mapper 接口被代理的全部流程了,其中先是根据会话去获得对应的 Mapper 但其内部调用的是 Mapper 注册中心(MapperRegistry)获取,这里面有所有配置的 Mapper 接口,在 MapperRegistry 中维护了一个 Map 键为 Class 值是 MapperProxyFactory ,这样就可以获得要代理 Mapper 接口的代理工厂,最后通过这个工厂生成我们想要的 Mapper 返回用户。
流程不复杂,就是里面出现了很多 MapperXXX 相关的类,那么下面我梳理一下这些类关系图如下:
对于具体的代理执行类这一步就要到执行这一块了,当用户通过我们返回的代理类(Mapper 接口)执行对应方法时,就会走到图中涉及的类。
按照惯例,来个流程图吧!
2.4 通过 Mapper 代理,执行方法操作数据库
上面的所有分析,都是为了等到一个具体的操作数据库的一个桥梁,那就是 Mapper 代理了(iUserMapper)。
接下来就是分析最后一步了,真正操作数据库,代码如下:
// 查询数据库 List<User> userList= iUserMapper.findAll(); for (User user : userList) { System.out.println(user); }
对于 iUserMapper 对象,我们知道他是代理去执行的,所以直接点进去的话根本行不通,那么我们可以通过 Debug 进去看看。
MapperProxy # invoke
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 方法的类为 object 直接通过原始 JDK 去执行 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else { // 根据方法,获得方法的执行器后再执行代理方法 return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } }
我们先进入 MapperProxy # cachedInvoker
这个方法看看
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable { try { // 先查缓存,有就返回,没有就创建 MapperMethodInvoker invoker = methodCache.get(method); if (invoker != null) { return invoker; } return methodCache.computeIfAbsent(method, m -> { // ... // 返回 PlainMethodInvoker 类型的 Mapper 方法执行器 return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); // ... }); } catch (RuntimeException re) { Throwable cause = re.getCause(); throw cause == null ? re : cause; } }
接着进入 PlainMethodInvoker# invoke
这个方法
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { // 调用 mapperMethod 对象的 execute 方法去真正执行了 return mapperMethod.execute(sqlSession, args); }
真正执行的开始 execute
MapperMethod # execute
public Object execute(SqlSession sqlSession, Object[] args) { // 这里面内容比较多,我简单分析一下 // 1)封装参数 // 2)根据对应的执行类型(INSERT,UPDATE,DELETE,SELECT),执行对应的方法 // 3)根据参数,执行类型封装对应的 sql // 4)操作原生 JDBC API 执行数据库操作 // 5)封装结果集,返回出去 }
我们 Debug 这个方法最后一步,看看结果:
到此,我们的 Mapper 接口及文件生效的原理,就全部过了一边,是不是觉得不是很难呢!
在分析这一块源码时,本人理解的步骤就是:
- 一步步点进源码看。
- 画出流程图,不清楚的就 Debug。
- 很重要一点,对很多出现类似名字的类,一定要画出类图,搞清楚关系在往下走(助于理解每个类的职责)。
- 最后,那就是写点笔记了,毕竟好记性不如烂笔头。
2.5 整体流程图
加载全部内容