MyBatis源码分析

MyBatis故事:

  官方网站:http://www.mybatis.org

  官方文档:http://www.mybatis.org/mybatis-3/ 

  GitHub:https://github.com/mybatis

通过在MyBatis的官方网站,我们会看到和MyBatis相关的一些软件产品:

  MyBatis Migrations 是一款数据库迁移工具 http://www.mybatis.org/migrations

  MyBatipse (Eclipse plugin) ,Eclipse插件提供在编写xml配置文件时的内容提示和验证;

  MyBatis Generator 代码生成工具;

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

MyBatis SQL mapper framework for Java可以将其称之为Java持久层框架.

MyBatis 的前身是apache下的一个开源项目iBatis,  iBATIS是一个由Clinton Begin在2002年发起的开源项目,是一个基于Java的持久层框架。

iBATIS:http://ibatis.apache.org

iBATIS这个项目于2010年6月16号由apache迁移到了google code,iBatis3.x并且改名为MyBatis 。

MyBatis: http://code.google.com/p/mybatis/ (现在已经无法打开)

2013年11月MyBatis又从google code将代码迁移到Github。

MyBatis和Hibernate

MyBatis的作者:Clinton Begin 加拿大人  https://www.linkedin.com/in/clintonbegin 

Hibernate 2001年推出的Java持久层框架,澳大利亚人 Gavin King  https://www.linkedin.com/in/GavinKing 

人们经常喜欢比较这两款优秀的持久层框架的特点:

技术 优点 缺点
jdbc 简单,纯粹,一切均可见,最基础的一种技术

1:需要手动关闭连接

2:结果集不能自动映射为对象

3:SQL夹杂在代码中,耦合度高,导致硬编码内伤

4:实际开发中SQL经常随需求变动,导致频繁修改,不易维护

jdbcTemplate

简单、纯粹、自动会话管理、结果集映射

需要手动拼装SQL,SQL与Java代码混合在一起,长的SQL管理混乱

Hibernate

JPA

编程效率高,无需编写SQL。
数据库更换成本低

较完善的二级缓存、自动防SQL注入

完全掌握的门槛高;
复杂SQL、复杂映射处理困难;
性能优化较麻烦、不易做特殊优化;
基于全映射的全自动框架,大量字段的POJO进行部分映射时比较困难,导致数据库性能下降;

MyBatis

学习成本低、可以进行更为细腻的SQL优化,减少查询字段、统一的SQL管理
SQL和Java编码分开,功能边界清晰

需要手动编写维护SQL、表结构变更之后需要手动维护SQL与映射;

 MyBatis的定位

MyBatis专注于SQL本身,其为SQL映射,而非完整的SQL映射,它是一个半自动的ORM框架,需要自己编写SQL语句,这是其优点,也是缺点.

优点:SQL语句单独维护,便于SQL优化,便于发挥SQL的最大性能.

缺点:当数据库表和字段更改后,实体和数据库的映射关系需要手动维护,耗费时间长.

使用场景:是用于性能要求高,有大量的查询操作,适用于互联网项目,如:电商,O2O

互联网项目对持久层的需求:

1:对数据库的访问更加纯粹

2:尽可能不要使用数据库做运算

3:SQL语句尽可能命中索引(字段排序,查询字段,查询条件,尽可能命中索引)

MyBatis实战应用:

  Mybatis框架就一个jar包;要使用 MyBatis, 只需将 mybatis-3.5.1.jar 文件置于 classpath 中即可。

  使用Maven构建项目

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.1</version>
</dependency>

下载MyBatis源码,构建MyBatis项目;https://codeload.github.com/mybatis/mybatis-3/zip/mybatis-3.5.1

如果不出意外,我们解压下载的压缩包,然后通过pom.xml导入IDEA中,通过Maven的compiler就可以正常编译成功,首次编译可能需要一定时间。

为了验证我么编译的项目是否可用,我们可以写一个简单的案例项目去测试一下,也方便后续的断点跟踪源码分析。

创建普通的Maven项目,添加Maven依赖

 

 <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>

        <!--mysql的jdbc驱动jar包依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <!--lombok代码生成工具的jar包依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>

        <!--slf4j日志门面-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.26</version>
        </dependency>

        <!--log4j2日志实现-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.11.2</version>
        </dependency>

        <!-- cglib -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
        </dependency>

        <!-- javassist -->
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.27.0-GA</version>
        </dependency>

 

添加resources编译,个人建议添加

  <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>

添加jdbc.properties,根据自身配置

jdbc.username=xxx
jdbc.password=xxxxx
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://xxxx:3306/xxx?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false

最后贴上mybatis的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">

<!--dtd的xml约束是有顺序,标签顺序不能错-->
<configuration>

    <!--mybatis框架的核心功能可以通过该配置进行设置-->

    <!--配置属性-->
    <properties resource="D:\mybatis-3-mybatis-3.5.1\mybatis3-test\src\main\resources\jdbc.properties"/>

    <settings>
        <!--mybatis输出日志,采用何种组件输出-->
        <setting name="logImpl" value="SLF4J"/>
        <!--开启二级缓存-->
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <!--类型别名-->
    <typeAliases>
        <typeAlias type="com.xxx.model.User" alias="User"/>
    </typeAliases>

    <typeHandlers>
        <!--自定义的类型转换器-->
        <typeHandler handler="com.xxx.type.CryptHandlerType"
                javaType="com.xxx.model.IdCardType"/>
    </typeHandlers>

    <!--插件-->
    <!--
    <plugins>
        <plugin interceptor="com.xxx.plugin.xxxx"></plugin>
    </plugins>
    -->

    <!--多环境配置,默认开发-->
    <environments default="development">
        <environment id="development">
            <transactionManager type="org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory"/>
            <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>

        <environment id="test">
            <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>

        <environment id="product">
            <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>
      
        <mapper resource="com/xxx/mapper/UsersMapper.xml"/>
    </mappers>

</configuration>

日志配置

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug">

    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>

    <Loggers>
        <Root level="debug">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

贴上本人的项目结构,仅供参考

 

MyBatis独立使用  

     //通过配置文件获取输入流
        InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");

        //构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        //打开session
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //第四步 获取Mapper接口对象
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        //第五步 调用Mapper接口对象的方法操作数据库
        User user = mapper.selectByPrimaryKey(1);

        //获取结果,处理业务
        log.info("查询结果:",user.getId());

 

当我们数据库配置等都设置完毕,正常来说查询数据是不会出现异常的,这样就说明我们的源码编译是成功的。

MyBatis Generator (MBG)

MyBatis Generator 简称MBG,是用Java语言开发的一个代码生成工具,可以深入分析你的数据库和表,帮助你生成基本的对数据库的CRUD操作代码以及QBC风格的条件查询,但是表连接、存储过程等这些复杂SQL的定义需要我们手工编写。

 

MyBatis Generator 生成3个东西:

1、POJO

2、Mapper XML Files

3、Mapper interface

MyBatis Generator 生成代码不需要依赖任何第三方jar包,仅仅需要一个jdbc驱动包;

文档:http://www.mybatis.org/generator/

Github:https://github.com/mybatis/generator

原来MyBatis Generator老版本:ibator,用于iBatis代码生成;

1、配置MyBatis Generator (MBG) 的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>

    <!--在配置文件中使用占位符引用该属性文件的值-->
    <properties resource="jdbc.properties"/>

    <!-- 指定连接数据库的JDBC驱动包所在位置,指定到你本机的完整路径 -->
    <classPathEntry location="F:/mysql-connector-java-5.1.46.jar"/>

    <!-- 配置table表信息内容体,targetRuntime指定采用MyBatis3的版本 -->
    <context id="tables" targetRuntime="MyBatis3">

        <!-- 抑制生成注释,由于生成的注释都是英文的,可以不让它生成 -->
        <commentGenerator>
            <property name="suppressAllComments" value="true" />
        </commentGenerator>

        <!-- 配置数据库连接信息 -->
        <connectionFactory>
            <property name="driverClass" value="${jdbc.driver}"/>
            <property name="connectionURL" value="${jdbc.url}"/>
            <property name="userId" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </connectionFactory>

        <!-- 配置数据库连接信息 connectionFactory与jdbcConnection只能配置其一-->
        <!--
        <jdbcConnection driverClass="COM.ibm.db2.jdbc.app.DB2Driver"
                        connectionURL="jdbc:db2:TEST"
                        userId="db2admin"
                        password="db2admin">
        </jdbcConnection>
        -->

        <!-- 生成model类,targetPackage指定model类的包名, targetProject指定生成的model放在eclipse的哪个工程下面-->
        <javaModelGenerator targetPackage="org.mybatis.model" targetProject="src/main/java">
            <property name="enableSubPackages" value="false" />
            <property name="trimStrings" value="false" />
        </javaModelGenerator>

        <!-- 生成MyBatis的Mapper.xml文件,targetPackage指定mapper.xml文件的包名, targetProject指定生成的mapper.xml放在eclipse的哪个工程下面 -->
        <sqlMapGenerator targetPackage="org.mybatis.mapper" targetProject="src/main/java">
            <property name="enableSubPackages" value="false" />
        </sqlMapGenerator>

        <!-- 生成MyBatis的Mapper接口类文件,targetPackage指定Mapper接口类的包名, targetProject指定生成的Mapper接口放在eclipse的哪个工程下面 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="org.mybatis.mapper" targetProject="src/main/java">
            <property name="enableSubPackages" value="false" />
        </javaClientGenerator>

        <!-- 数据库表名及对应的Java模型类名 -->
        <table tableName="user"
                domainObjectName="User"
                enableCountByExample="false"
                enableUpdateByExample="false"
                enableDeleteByExample="false"
                enableSelectByExample="false"
                selectByExampleQueryId="false"/>
    </context>

</generatorConfiguration>

1、生成代码

   --通过命令行生成 java -jar mybatis-generator-core-1.3.7.jar -configfile generatorConfig.xml (用得比较少)

   --通过Ant生成 (ant几乎被淘汰了)

   --通过java程序生成

   --通过maven生成 

 <plugins>
            <!--mybatis代码自动生成插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.7</version>
                <configuration>
                    <!--配置文件的位置-->
                    <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
                    <!--生成代码过程中是否打印日志-->
                    <verbose>true</verbose>
                    <!--生成时是否覆盖java文件,xml文件总是合并-->
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
        </plugins>

   --通过Eclipse生成

建议不要用mysql驱动8.0, 用5.1.x系列的驱动;

使用maven插件通过以下方式即可自动生成。

在源码分析的前面,我会分享一些MyBatis的常规应用

MyBatis的xml配置(核心配置)

configuration(配置)
   properties(属性)
   settings(设置)  
   typeAliases(类型别名)
   typeHandlers(类型处理器)
   objectFactory(对象工厂)
   plugins(插件)
   environments(环境配置)
        environment(环境变量)
            transactionManager(事务管理器)
            dataSource(数据源)
  databaseIdProvider(数据库厂商标识)
  mappers(映射器)

MyBatis的Mapper映射

Mapper映射文件只有几个顶级元素(按照应被定义的顺序如下):
cache – 对给定命名空间的缓存配置。
cache-ref – 对其他命名空间缓存配置的引用。
resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
sql – 可被其他语句引用的可重用语句块。
insert – 映射插入语句
update – 映射更新语句
delete – 映射删除语句
select – 映射查询语句

MyBatis也可以使用注解开发

生成代码时候,配置:
<javaClientGenerator type="ANNOTATEDMAPPER"...
生成注解代码,不生成xml代码了

MyBatis开发使用注解还是xml?

方式 优点 缺点
Xml Xml与接口分离,方便管理,复杂的SQL不影响代码的可读性 过多的Xml配置文件
Annotation 接口就能看到SQL语句,可读性强,不需要去找xml文件,方便 代码和SQL混杂,复杂一点过于混乱

 

MyBatis批量插入

1 普通for循环(此种方式对于数据库的I/O过于频繁,不适合大数据量的操作)

for (int i = 0; i < 500; i++) {
    user = new User();
    user.setId("id" + i);
    user.setName("name" + i);
    userMapper.insert(user);
}

2 ExeutorType.BATCH(设置批量模式),把SQL语句发个数据库,数据库预编译好,然后数据库等待需要运行的参数,接收到参数一次性运行

session = sqlSessionFactory.openSession(ExecutorType.BATCH, true);
for (int i=0; i<500; i++) {
    UUserInfo userInfo = new UUserInfo();
    userInfo.setPhone("1346262122" + i);
    userInfo.setUserName("张" + i);
    uUserInfoMapper.insert(userInfo);
}

3 传入一个数组或集合,标签<foreach>插入

 <foreach item="item" collection="strs.split(',')" separator="," open="(" close=")">
            #{item}
  </foreach>
方式 性能 说明
For循环 性能低,IO高  
ExeutorType.BATCH 性能居中  
<foreach>标签拼SQL 性能最高

有SQL长度限制,mysql默认接收SQl的长度为10486576(1M),该方式若超过1M

会抛出异常

 

MyBatis联合查询

  一对一关系

  在一对一关系中,A表中的一行最多只能匹配B表的一行,反之亦然.这种关系并不常见,因为一般来说,这种关系的信息会保存在一张表中,当然也可以利用一对一关系来保存,比如分割具有多列的表.(如:一个人只有一个身份证) 

<association>标签

<resultMap id="OneToOneBaseResultMap" type="com.test.mybatis.model.Person">
  <id column="id" jdbcType="INTEGER" property="id" />
  <result column="nick" jdbcType="VARCHAR" property="nick" />
  <result column="phone" jdbcType="VARCHAR" property="phone" />
  <result column="sex" jdbcType="INTEGER" property="sex" />
  <association property="idCard" javaType="com.test.mybatis.model.IdCard">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="personId" jdbcType="INTEGER" property="personid" />
    <result column="realName" jdbcType="VARCHAR" property="realname" />
    <result column="idCard" jdbcType="VARCHAR" property="idcard" />
  </association>
</resultMap>

  一对多关系

  一对多是最普通的一种关系,在这种关系中,A表的一行可以匹配B表中的多行,但是B表中的一行只能匹配A表中的一行.举例:(一个部门可以有多个员工)

  <collection>标签

<resultMap type="com.mybatis.bean.Department" id="MyDept">
        <id column="did" property="id"/>
        <result column="dept_name" property="departmentName"/>
        <!-- 
            collection定义关联集合类型的属性的封装规则 
            ofType:指定集合里面元素的类型
        -->
        <collection property="emps" ofType="com.mybatis.bean.Employee">
            <!-- 定义这个集合中元素的封装规则 -->
            <id column="eid" property="id"/>
            <result column="last_name" property="lastName"/>
            <result column="email" property="email"/>
            <result column="gender" property="gender"/>
        </collection>
    </resultMap>

  多对多关系

  在多对多的关系中,A表的一行可以匹配B表的多行.反之亦然.要创建这种关系,需要定义第三张表,也可以称之为结合表或者关系表.它的主键是由A表和B表的外部键组成.  举例(一个用户有多个角色,一个角色可以对应多个用户)  

 <collection>标签

MyBatis插入并返回主键

  方式一:(数据库要设置主键自增长)

<insert id="insertSelective" useGeneratedKeys="true" keyProperty="id" keyColumn="id"
 parameterType="com.test.mybatis.model.UUserInfo">
useGeneratedKeys="true"表示使用主键自增  keyProperty="id"表示将主键自增后的主键值赋值给实体类中的id属性
parameterType="com.test.mybatis.model.UUserInfo" 实体类,自增后的主键值将会赋值给该实体类的id属性

  方式二:(一般用在<insert>标签里面)

<selectKey keyProperty="id" resultType="integer" order="AFTER">
  SELECT LAST_INSERT_ID()
</selectKey>
 SELECT LAST_INSERT_ID()表示查询出刚刚插入的的记录自增长id  order='AFTER' 表示先执行插入语句,之后再执行查询语句

MyBatis的SQL注入攻击
  注入攻击的危害
  1:数据库被拖库(把数据从数据库拉取出来,造成数据泄露)
  2.重要信息被泄露
注入的本质是把用户输入的数据当做有效代码执行.
Mybatis的预编译机制可以有效防止SQL注入攻击.
  '#{}' 和 '${}'的区别:
'#{}':MyBaits会首先对其进行预编译,将#{user_ids}替换成?占位符,然后在执行时替换成实际传入的user_id值,**并在两边加上单引号,以字符串方式处理。
'${}':简单的字符串拼接
栗子:
uUserInfoMapper.selectByIn("select user_name from u_user_info");
where user_name in (${userName})
可使用MyBatis自带循环标签解决SQL语句动态拼接的问题:
select * from news where id in
<foreach collection="ids" item="item" open="("separator="," close=")">
  #{item} 
</foreach>

MyBatis的自定义类型转换器(实现数据类型和数据库数据类型的映射关系 如:String和VARCHAR的对应关系)  需求:实现对数据库身份证存储的加密
1、定义自己的HandlerType实现TypeHandler接口或者继承BaseTypeHandler类
2、覆盖其四个方法;
3、在核心配置文件mybatis-config.xml中配置<typeHandlers>标签或者在映射文件的增、改、查位置单独配置;

<typeHandlers>
    <typeHandler javaType="com.test.mybatis.typehandler.UserIdCard" handler="com.bjpowernode.mybatis.typehandler.CryptTypeHandler"/>
</typeHandlers>

<result column="idCard" jdbcType="VARCHAR" property="userIdCard" javaType="com.bjpowernode.mybatis.typehandler.UserIdCard" 
typeHandler="com.test.mybatis.typehandler.CryptTypeHandler"/> <if test="userIdCard != null"> #{userIdCard, jdbcType=VARCHAR, typeHandler=com.test.mybatis.typehandler.CryptTypeHandler}, </if>
系统自带的类型转换器StringTypeHandler 
public class StringTypeHandler extends BaseTypeHandler<String> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setString(i, parameter);
  }

  @Override
  public String getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    return rs.getString(columnName);
  }

  @Override
  public String getNullableResult(ResultSet rs, int columnIndex)
      throws SQLException {
    return rs.getString(columnIndex);
  }

  @Override
  public String getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    return cs.getString(columnIndex);
  }
}
MyBatis的动态SQL

  if   --判断标签

  choose (when, otherwise)  --多项选择 ,与页面的 jstl 标签非常类似

  trim (where, set)  --修剪、格式化,添加前后缀的一个标签

  foreach  --循环

<select id="dynamicChoose" parameterType="News" resultType="News">
    select * from news where 1 = 1 
    <choose>
        <when test="title != null">
            and title = #{title}
        </when>
        <when test="content != null">
            and content = #{content}
        </when>
        <otherwise>
            and owner = "zhangsan"
        </otherwise>
    </choose>
</select>

MyBatis的SQL片段使用

  <sql id="Base_Column_List">
    id, name
  </sql>
 <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from user
        where id = #{id,jdbcType=INTEGER}
    </select>

MyBatis的一级.二级缓存

  一级缓存是sqlSession级别,默认开启

  二级缓存是mapper级别,默认关闭,缓存命中率低

MyBatis的事务管理:

<transactionManager type="JDBC">
      <property name="..." value="..."/>
</transactionManager>

如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器, 因为 Spring 模块会使用自带的管理器来覆盖前面的配置.

 

之前已经讲了一个MyBatis项目的基本配置,接下来我们来通过断点跟踪,一步一步揭开它的神秘面纱.

@Slf4j
public class App 
{
    public static void main( String[] args ) throws IOException {
        //通过配置文件获取输入流
        InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
        //构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        //打开session
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //第四步 获取Mapper接口对象
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        //第五步 调用Mapper接口对象的方法操作数据库
        User user = mapper.selectByPrimaryKey(1);
        //获取结果,处理业务
        log.info("查询结果:",user.getId());
    }
}

1.如何通过配置文件获取输入流

//第一步,调用
  public static InputStream getResourceAsStream(String resource) throws IOException {
    return getResourceAsStream(null, resource);
  }
//第二步调用
  public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
    InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
    if (in == null) {
      throw new IOException("Could not find resource " + resource);
    }
    return in;
  }

通过进入源码,我们可以看到通过配置文件获取输入流最核心的一步  classLoaderWrapper.getResourceAsStream(resource, loader);

那么classLoaderWrapper这个成员变量又是如何来的呢?原来是Resource的静态成员变量

  private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();

在静态变量实例化的时候,我把关键的几行代码粘出来.只是给成员变量systemClassLoader赋值.

public class ClassLoaderWrapper {

  ClassLoader defaultClassLoader;
  ClassLoader systemClassLoader;

  ClassLoaderWrapper() {
    try {
      systemClassLoader = ClassLoader.getSystemClassLoader();
    } catch (SecurityException ignored) {
      // AccessControlException on Google App Engine
    }
  }
  ClassLoader[] getClassLoaders(ClassLoader classLoader) {
    return new ClassLoader[]{
        classLoader,
        defaultClassLoader,
        Thread.currentThread().getContextClassLoader(),
        getClass().getClassLoader(),
        systemClassLoader};
  }

}

我们回到classLoaderWrapper.getResourceAsStream(resource, loader),可以看出最终是通过类加载器去读取配置文件获取输入流

  public InputStream getResourceAsStream(String resource) {
    return getResourceAsStream(resource, getClassLoaders(null));
  }

  InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
    for (ClassLoader cl : classLoader) {
      if (null != cl) {

        // try to find the resource as passed
        InputStream returnValue = cl.getResourceAsStream(resource);

        // now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource
        if (null == returnValue) {
          returnValue = cl.getResourceAsStream("/" + resource);
        }

        if (null != returnValue) {
          return returnValue;
        }
      }
    }
    return null;
  }

类加载器是一个数组,一共五个类加载器,前两个初次是没有赋值的,可以最终的到一个APP-classLoader,根据;类加载机制双亲委派,三种类型加载器

BootStrapClassLoader-ExtClassLoader-APPClassLoader

  ClassLoader[] getClassLoaders(ClassLoader classLoader) {
    return new ClassLoader[]{
        classLoader,
        defaultClassLoader,
        Thread.currentThread().getContextClassLoader(),
        getClass().getClassLoader(),
        systemClassLoader};
  }

第一步,我们可以总结为简单的一句话:通过类加载器读取类的根路径下的配置文件获取输入流,我们以后的开发也可以借鉴这种方式

2.构建SqlSessionFactory

 //构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

这一步主要是build方法,前面的创建对象调用默认无参构造,并没有做什么事情.

  public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }

调用同一个类的重载方法

  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 parser = new XMLConfigBuilder(inputStream, environment, properties);

我们通过传递可知,后两个参数environment为null,properties为null.

  public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
  }
new XPathParser(inputStream, true, props, new XMLMapperEntityResolver())这一步到底做了什么?
首先看下new XMLMapperEntityResolver(),你会在下图看到熟悉的dtd,你会想到什么?这一步也只是初始化了内部成员变量.


我们再看下new XPathParser(inputStream, true, props, new XMLMapperEntityResolver())下一步调用
 public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
    commonConstructor(validation, variables, entityResolver);
    this.document = createDocument(new InputSource(inputStream));
  }
首先进入第一个方法内部一探究竟,给成员变量赋值
  private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
    this.validation = validation;
    this.entityResolver = entityResolver;
    this.variables = variables;
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();
  }

然后是通过输入流,构建Document对象,赋值给成员变量document


  private Document createDocument(InputSource inputSource) {
    // important: this must only be called AFTER common constructor
    try {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setValidating(validation);

      factory.setNamespaceAware(false);
      factory.setIgnoringComments(true);
      factory.setIgnoringElementContentWhitespace(false);
      factory.setCoalescing(false);
      factory.setExpandEntityReferences(true);

      DocumentBuilder builder = factory.newDocumentBuilder();
      builder.setEntityResolver(entityResolver);
      builder.setErrorHandler(new ErrorHandler() {
        @Override
        public void error(SAXParseException exception) throws SAXException {
          throw exception;
        }

        @Override
        public void fatalError(SAXParseException exception) throws SAXException {
          throw exception;
        }

        @Override
        public void warning(SAXParseException exception) throws SAXException {
        }
      });
      return builder.parse(inputSource);
    } catch (Exception e) {
      throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    }
  }
这里面一大堆到底做了什么呢,大致就是设置xml的验证,毕竟我们要保证xml的配置是符合MyBatis的配置书写规则,不然后续的解析也是有问题的.
XPathParser解析完毕之后,XPathParser此时成员变量已经赋值完毕,调用重载方法

 首先看一下new Configuration()做了什么?里面的成员变量响应初始化,我们暂且不看,看看无参构造干了什么.下面的代码是不是很熟悉?好像在哪里见过?

没错,这些我们在配置mybatis-config.xml的时候见过,通过别名可以找到对应的类,而我们配置文件只需要写别名,方便我们的记忆和配置

  public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

    typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);

    typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);

    typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);

    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
    typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
    typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
    typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    languageRegistry.register(RawLanguageDriver.class);
  }
ErrorContext.instance().resource("SQL Mapper Configuration");这个是什么呢,我们也进去瞅一眼

   我们一眼就可以看到ThreadLocal,想到了什么吗?指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据.这个是错误上下文,打印异常使用,可以认为是线程单例,因为它针对的级别是线程.

我们再次回到SqlSessionFactory的build方法

 调用parser.parse()的方法

 XPATH解析/configuration,解析之后我们会把所有解析获取的配置信息防止到成员变量configuration里面

 最终我们获取的SqlSessionFactory的时候,里面的Configuration成员变量就包含了我们xml里面配置的所有解析出来的信息

3.打开session  SqlSession sqlSession = sqlSessionFactory.openSession();

public class DefaultSqlSessionFactory implements SqlSessionFactory {

  private final Configuration configuration;

  public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }

  @Override
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

调用一下方法,这里面顾名思义,根据我我们解析出来的配置信息,拿到数据源,构建出事务对象,然后创建出默认的SqlSession

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      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里面都是诸如此类的方法DefaultSqlSession做了实现

4.获取Mapper接口对象  UserMapper mapper = sqlSession.getMapper(UserMapper.class);

我们去看下getMapper的实现,我们传入了接口类型,但是我们并没有具体的实现类代码,看看Mybatis是如何做的。

  @Override
  public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
  }

再向下断点调试

 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

这个不是核心,接着向下调用,下面这是重点,我们来看一看

  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    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);
    }
  }
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
knownMappers这个就是一个HashMap,这个map是时候放进去值的呢?
其实我们在解析xml的时候,MyBatis已经悄悄地放进去了,不信请看,这个有一点就是,此刻其实也已经把mapper.xml解析数据放入configuration里面了
  <!--映射器-->
    <mappers>
        <mapper resource="com/bjpowernode/mapper/UsersMapper.xml"/>
    </mappers>

通过这种不知不觉的方法,就把我们的mapper接口类型放进了map里面

 

 所以我们最后一定会执行

return mapperProxyFactory.newInstance(sqlSession);
如果我们对于反射和动态代理有了解的话,是不是很熟悉,动态代理实例化。下面的代码也证实了这一点,如果对于动态代理不太了解

 

 我们先来看一下MapperProxy的方法

 

 上面有一个经典面试题:动态代理,投鞭断流

final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);

 我们直接看最后一个地方,前一个地方cachedMapperMethod(method),这个地方字面应该是缓存,略过

 这里根据mapper.xml里面的标签确定是什么操作,最后把结果集返回

5.调用Mapper接口对象的方法操作数据库

当我们调用mapper接口的方法,通过动态代理,底层自动给我们生成动态代理对象,执行exceute方法,返回结果集。

6.获取结果,处理业务

 
 

 

posted @ 2022-03-04 11:14  hanease  阅读(549)  评论(0编辑  收藏  举报