MyBatis运行原理

Posted JKerving

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis运行原理相关的知识,希望对你有一定的参考价值。

这篇文章是自己工作中记录下来的,由于工作比较忙,一直留存于本地忘了传到自己的博客中,现在优化了一下文章结构传上来,请各位看官批评指正。
另外,后续会有很多自己工作中记录的知识点文章做二次优化传上来。

在开发大数据平台调度系统过程中,我们通过spring boot快速构建调度平台,持久化框架采用Mybatis。这也是初次使用Mybatis,当然工作中只是构建一些Dao、Mapper,使用一些增删改查完成调度任务的历史记录、任务间的Dependency的记录。但是Mybatis的基本运行原理还是需要结合源码来梳理一下的。
这里我举一个简单的Demo,跟着Demo一步一步的去看源码,认识到Mybatis的运行原理。

第一部分:Demo样例

创建User实体类:

@Data
public class MyUser {
	private Integer uid;
	private String uname;
	private String usex;
}

创建UserDao接口:

import java.util.List;
import gzc.entity.MyUser;
public interface UserDao {
	// 接口定义的方法名与Mapper映射id一致
	public MyUser selectUserById(Integer uid);
	public List<MyUser> selectAllUser();
	public int addUser(MyUser user);
	public int updateUser(MyUser user);
	public int deleteUser(Integer uid);
}

创建映射文件:MyUserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <!-- namespace属性绑定dao接口 -->
<mapper namespace="gzc.dao.UserDao">
	 <!-- 根据uid 查询一个用户信息 -->
	 <select id="selectUserById" resultType="zjw.entity.MyUser" parameterType="Integer">
	 	select * from myuser where uid = #{uid}
	 </select>
	 <!-- 查询全部用户信息 -->
	 <select id="selectAllUser" resultType="zjw.entity.MyUser">
	 	select * from myuser
	 </select>
	 <!-- 添加一个用户  #{uname}gzc.entity.MyUser的属性值-->
	 <insert id="addUser" parameterType="zjw.entity.MyUser">
	 insert into myuser values(#{uid},#{uname},#{usex})
	 </insert>
	 <!-- 修改一个用户 -->
	 <update id="updateUser" parameterType="zjw.entity.MyUser">
	 	update myuser set uname=#{uname},usex=#{usex} where uid=#{uid}
	 </update>
	 <!-- 删除一个用户 -->
	 <delete id="deleteUser" parameterType="zjw.entity.MyUser">
	 delete from myuser where uid=#{uid}
	 </delete>
</mapper>

创建Mybatis主配置文件mybatis-config.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>
<!--利用mybatis自带环境配置数据源,以后和Spring整合后,这部分数据源可以交由Spring 配置处理-->
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<property name="driver" value="com.mysql.cj.jdbc.Driver" />
				<property name="url" value="jdbc:mysql://localhost:3306/springtestdb?serverTimezone=UTC" />
				<property name="username" value="root" />
				<property name="password" value="123456" />
			</dataSource>
		</environment>
	</environments>
	<mappers>
		<!-- SQL映射文件的位置-->
		<mapper resource="zjw/mapper/MyUserMapper.xml" />
	</mappers>
</configuration>

创建测试类:

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import zjw.dao.UserDao;
import zjw.entity.MyUser;
public class MybatisTest {
	public static void main(String[] args) {
		try {
			// 读取配置文件mybatis-config.xml
			InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
			// 根据配置文件构建SqlSessionFactory
			SqlSessionFactory ssl = new SqlSessionFactoryBuilder().build(config);
			// 通过SqlSessionFactory创建SQLSession对象
			SqlSession ss = ssl.openSession();

			/*
			 * 方法一 : SqlSession执行映射文件中定义的sql,并返回映射结果
			 * gzc.mapper.MyUserMapper.selectUserById为MyUserMapper.xml中的命名空间+SQL语句的id 例如:
			 * MyUser mu = ss.selectOne("gzc.mapper.MyUserMapper.selectUserById", 6);
			 */

			/*
			 * 方法二 : 通过SqlSession对象getMapper方法获得Mapper映射与Dao接口映射
			 * 该方法需要绑定dao的接口到Mapper的namespace中
			 */
			 
			// 将dao接口方法与映射文件关联,返回接口对象
			UserDao userDao = ss.getMapper(UserDao.class);
			// 查询一个用户
			MyUser user = userDao.selectUserById(1);
			System.out.println(user);
			// 添加一个用户
			MyUser newUser = new MyUser(8, "小花", "女");
			userDao.addUser(newUser);
			// 修改一个用户
			MyUser updatemu = new MyUser(7, "小明", "男");
			userDao.updateUser(updatemu);
			// 删除一个用户
			userDao.deleteUser(3);
			// 查找所有用户
			List<MyUser> myUsers = userDao.selectAllUser();
			for (MyUser myUser : myUsers) {
				System.out.println(myUser);
			}
			ss.commit();
			ss.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

Mybatis重要组件和运行流程图

  • Configuration:Mybatis所有的配置信息都保存在Configuration对象中,配置文件中的大部分配置都会存储到该类
  • SqlSession:作为Mybatis工作的顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能
  • Executor:Mybatis执行器,是Mybatis调度的核心,负责SQL语句的生成和查询缓存的维护
  • StatementHandler:封装了JDBC Statement操作,负责对JDBC Statement的操作,如设置参数等
  • ParameterHandler:负责对用户传递的参数转换成JDBC Statement所对应的数据类型
  • ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
  • TypeHandler:负责java数据类型和jdbc数据类型之间的映射和转换
  • MappedStatement:MappedStatement维护一条<select|update|delete|insert>节点的封装
  • SqlSource:负责根据用户传递的parameterObject,动态生成SQL语句,将信息封装到BoundSql对象中,并返回
  • BoundSql:表示动态生成的SQL语句以及相应的参数信息

真正的源码分析来啦

测试类贴过来方便看:

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import zjw.dao.UserDao;
import zjw.entity.MyUser;
public class MybatisTest {
	public static void main(String[] args) {
		try {
			// 读取配置文件mybatis-config.xml
			InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
			// 根据配置文件构建SqlSessionFactory
			SqlSessionFactory ssl = new SqlSessionFactoryBuilder().build(config);
			// 通过SqlSessionFactory创建SQLSession对象
			SqlSession ss = ssl.openSession();

			/*
			 * 方法一 : SqlSession执行映射文件中定义的sql,并返回映射结果
			 * gzc.mapper.MyUserMapper.selectUserById为MyUserMapper.xml中的命名空间+SQL语句的id 例如:
			 * MyUser mu = ss.selectOne("gzc.mapper.MyUserMapper.selectUserById", 6);
			 */

			/*
			 * 方法二 : 通过SqlSession对象getMapper方法获得Mapper映射与Dao接口映射
			 * 该方法需要绑定dao的接口到Mapper的namespace中
			 */
			 
			// 将dao接口方法与映射文件关联,返回接口对象
			UserDao userDao = ss.getMapper(UserDao.class);
			// 查询一个用户
			MyUser user = userDao.selectUserById(1);
			System.out.println(user);
			// 添加一个用户
			MyUser newUser = new MyUser(8, "小花", "女");
			userDao.addUser(newUser);
			// 修改一个用户
			MyUser updatemu = new MyUser(7, "小明", "男");
			userDao.updateUser(updatemu);
			// 删除一个用户
			userDao.deleteUser(3);
			// 查找所有用户
			List<MyUser> myUsers = userDao.selectAllUser();
			for (MyUser myUser : myUsers) {
				System.out.println(myUser);
			}
			ss.commit();
			ss.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

这是Mybatis操作数据库的基本步骤。

InputStream config = Resources.getResourceAsStream("mybatis-config.xml");

资源加载mybatis的主配置文件获取输入流对象。我们重点看下一行代码:

// 根据配置文件构建SqlSessionFactory
SqlSessionFactory ssl = new SqlSessionFactoryBuilder().build(config);

这行代码表示根据主配置文件的流对象构建一个会话工厂对象。这里用到了建造者模式:要创建某个对象不直接new,而是利用其它的类来创建这个对象。mybatis的所有初始化工作都是这行代码完成的,我们跟着源码深入去看。
一:进入build方法

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //委托XMLConfigBuilder来解析xml文件,并构建
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

可以看到会创建一个XMLConfigBuilder对象,这个对象的作用就是解析主配置文件用的。我们可以发现主配置文件的最外层节点是标签,mybatis的初始化就是把这个标签及其所有子标签进行解析,把解析好的数据封装在Configuration这个类中。
二:进入parse()方法

//解析配置
  public Configuration parse() {
    //如果已经解析过了,报错
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //根节点是configuration
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

XMLConfigBuilder维护一个parsed属性默认为false,此方法首先判断主配置文件是否已经被解析,如果解析过了就抛异常。
三:进入parseConfiguration方法

private void parseConfiguration(XNode root) {
    try {
      //分步骤解析
      //issue #117 read properties first
      //1.properties
      propertiesElement(root.evalNode("properties"));
      //2.类型别名
      typeAliasesElement(root.evalNode("typeAliases"));
      //3.插件
      pluginElement(root.evalNode("plugins"));
      //4.对象工厂
      objectFactoryElement(root.evalNode("objectFactory"));
      //5.对象包装工厂
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //6.设置
      settingsElement(root.evalNode("settings"));
      // read it after objectFactory and objectWrapperFactory issue #631
      //7.环境
      environmentsElement(root.evalNode("environments"));
      //8.databaseIdProvider
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //9.类型处理器
      typeHandlerElement(root.evalNode("typeHandlers"));
      //10.映射器
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

此方法很明显是对的所有子标签逐个解析。比如常在配置文件中出现的settings属性配置,在settings会配置缓存,日志等。typeAliases是配置别名。environments是配置数据库链接和事务。这些子节点会被一个个解析并把解析后的数据封装在Configuration类中,可以看到第二部方法的返回值就是Configuration对象。我们重点分析mappers标签,这个标签中还有一个个的mapper标签去映射mapper所对应的mapper.xml。
四:进入mapperElement方法

//	10.4自动扫描包下所有映射器
//	<mappers>
//	  <package name="org.mybatis.builder"/>
//	</mappers>
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //10.4自动扫描包下所有映射器
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            //10.1使用类路径
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //映射器比较复杂,调用XMLMapperBuilder
            //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            //10.2使用绝对url路径
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            //映射器比较复杂,调用XMLMapperBuilder
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            //10.3使用java类名
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            //直接把这个映射加入配置
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }
  1. 这方法开始是一个循环。因为一个mappers节点下面可能会有很多mapper节点。在应用中肯定不止一个mapper.xml。所以他会去遍历每一个mappers节点去解析该节点所映射的xml文件。
  2. 循环下面是一个if else判断。它先判断mappers下面的子节点是不是package节点。因为在实际开发中很多的xml文件,不可能每一个xml文件都用一个mapper节点去映射,我们直接用一个package节点去映射一个包下面的所有的xml,这是多文件映射。
  3. 如果不是package节点,那肯定就是mapper节点做单文件映射。我们通过下面的代码发现单文件映射有3种方式,第一种使用mapper节点的

    以上是关于MyBatis运行原理的主要内容,如果未能解决你的问题,请参考以下文章

    MyBatis原理分析之三:初始化(配置文件读取和解析)

    深入浅出MyBatis:MyBatis解析和运行原理

    Mybatis的解析和运行原理

    MyBatis原理解析之运行原理

    mybatis运行原理

    Hibernate和Mybatis的工作原理以及区别