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.");
}
}
}
}
}
- 这方法开始是一个循环。因为一个mappers节点下面可能会有很多mapper节点。在应用中肯定不止一个mapper.xml。所以他会去遍历每一个mappers节点去解析该节点所映射的xml文件。
- 循环下面是一个if else判断。它先判断mappers下面的子节点是不是package节点。因为在实际开发中很多的xml文件,不可能每一个xml文件都用一个mapper节点去映射,我们直接用一个package节点去映射一个包下面的所有的xml,这是多文件映射。
- 如果不是package节点,那肯定就是mapper节点做单文件映射。我们通过下面的代码发现单文件映射有3种方式,第一种使用mapper节点的
以上是关于MyBatis运行原理的主要内容,如果未能解决你的问题,请参考以下文章