前言:
最近几天通过看源码来研究了一下mybatis的运行步骤,本篇文章以最基础的mybatis利用sqlSession操作数据库来讲解。我相信只要各位跟着我的节奏看完这篇文章就会对mybatis的运行步骤有个大体的认识(希望大家像我一样编写一个简单例子,然后跟着我的步骤看源码,不明白的地方debug)。这篇绝对是全网最详细的介绍mybatis运行原理的文章,当然如果你没有耐心看完的话,我也没任何办法,毕竟研究源码是一种枯燥的行为。如果你真的想去了解mybatis背后是怎么运行的,我相信这篇文章肯定会帮助到你。(文章如果有欠缺的地方,欢迎各位大佬指正)
第一部分:项目结构
user_info表:没什么好说的就3个字段
User实体类:
@Data
public class User {
private Long id;
private String name;
private Integer age;
}
mapper:UserMapper 为根据id查询用户信息
public interface UserMapper {
User selectUserById(Integer id);
}
UserMapper.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">
<mapper namespace="com.wsdsg.spring.boot.analyze.mapper.UserMapper">
<select id="selectUserById" resultType="com.wsdsg.spring.boot.analyze.entity.User"
parameterType="java.lang.Integer">
select * from user_info where id = #{id}
</select>
</mapper>
mybaitis的主配置文件:
<?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="dbconfig.properties"/>
<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>
<!--resource-->
<mapper resource="UserMapper.xml"/>
<!--class-->
<!-- <mapper class="com.wsdsg.spring.boot.analyze.mapper.UserMapper"/>-->
<!--url-->
<!--<mapper url="D:\coder_soft\idea_workspace\ecard_bus\spring-boot-analyze\target\classes\UserMapper.xml"/>-->
<!--package-->
<!--<package name="com.wsdsg.spring.boot.analyze.mapper" />-->
</mappers>
</configuration>
数据库连接的属性文件:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mysql?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
测试类:
@Component
public class TestMybatis implements CommandLineRunner{
@Override
public void run(String... args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
/*UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectUserById(1);
System.out.println(user);*/
Object o = session.selectOne("com.wsdsg.spring.boot.analyze.mapper.UserMapper.selectUserById", 1);
System.out.println("我是第一次查询的"+o);
System.out.println("-------------------------------我是分割线---------------------");
Object z = session.selectOne("com.wsdsg.spring.boot.analyze.mapper.UserMapper.selectUserById", 1);
System.out.println("我是第二次查询的"+z);
/*User user = new User();
user.setAge(15);
user.setName("achuan");
int insert = session.insert("com.wsdsg.spring.boot.analyze.mapper.UserMapper.addOneUser", user);
session.commit();
System.out.println(insert);*/
} finally {
session.close();
}
}
}
结构图:
说明:我这虽然是个springboot项目,但是我并没有整个mybatis,依然用的最原始的方式,其中application.yaml中未配置任何东西,logback.xml为显示日志的配置。
第二部分: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语句以及相应的参数信息
这个运行流程图先看不懂没关系,大致了解一下,下面会分析。
第三部分:初始化源码分析
首先我把测试类粘贴过来方便一点。
@Component
public class TestMybatis implements CommandLineRunner{
@Override
public void run(String... args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
/*UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectUserById(1);
System.out.println(user);*/
Object o = session.selectOne("com.wsdsg.spring.boot.analyze.mapper.UserMapper.selectUserById", 1);
System.out.println("我是第一次查询的"+o);
System.out.println("-------------------------------我是分割线---------------------");
Object z = session.selectOne("com.wsdsg.spring.boot.analyze.mapper.UserMapper.selectUserById", 1);
System.out.println("我是第二次查询的"+z);
/*User user = new User();
user.setAge(15);
user.setName("achuan");
int insert = session.insert("com.wsdsg.spring.boot.analyze.mapper.UserMapper.addOneUser", user);
session.commit();
System.out.println(insert);*/
} finally {
session.close();
}
}
}
这是mybatis操作数据库最基本的步骤,前两行代码没什么好说的就是资源加载mybatis的主配置文件获取输入流对象,我们看第三行代码。
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
这行代码的意思就是根据主配置文件的流对象构建一个会话工厂对象。而且还用到了建造者模式--->大致意思就是要创建某个对象不直接new这个对象而是利用其它的类来创建这个对象。mybatis的所有初始化工作都是这行代码完成,那么我们进去一探究竟。
第一步:进入build方法。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
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对象,这个对象的作用就是解析主配置文件用的。先说明一下,我们可以看出主配置文件的最外层节点是<configuration>标签,mybatis的初始化就是把这个标签以及他的所有子标签进行解析,把解析好的数据封装在Configuration这个类中。
第二步:进入parse()方法
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
XMLConfigBuilder维护一个parsed属性默认为false,这个方法一开始就判断这个主配置文件是否已经被解析,如果解析过了就抛异常。
第三步:进入parseConfiguration(...)方法
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
我们可以看出这个方法是对<configuration>的所有子标签挨个解析。比如常在配置文件中出现的settings属性配置,在settings会配置缓存,日志之类的。还有typeAliases是配置别名。environments是配置数据库链接和事务。这些子节点会被一个个解析并且把解析后的数据封装在Configuration 这个类中,可以看第二步方法的返回值就是Configuration对象。在这里我们重点分析的解析mappers这个子标签,这个标签里面还会有一个个的mapper标签去映射mapper所对应的mapper.xml,可以回头看看主配置文件。
第四步:进入mapperElement()方法。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getSt