Mybatis源码分析

一、Mybatis环境快速入门

1、maven依赖

<dependencies>
    <!-- mybatis核心包 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.3.0</version>
    </dependency>
    <!-- mysql驱动包 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.29</version>
    </dependency>
    <!-- junit测试包 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2、创建mybatis配置文件 configuration

<?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>
    <!-- 环境配置 -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <!-- 数据库连接相关配置 ,这里动态获取config.properties文件中的内容-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <!-- mapping文件路径配置 -->
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>

</configuration>

3、Mapper配置文件

<?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,namespace的值习惯上设置成包名+sql映射文件名,这样就能够保证namespace的值是唯一的
例如namespace="com.mayikt.mapper.UserMapper"就是com.mayikt.mapper(包名)+userMapper(userMapper.xml文件去除后缀)
 -->
<mapper namespace="com.mayikt.mapper.UserMapper">
    <!-- 在select标签中编写查询的SQL语句, 设置select标签的id属性为getUser,id属性值必须是唯一的,不能够重复
    使用parameterType属性指明查询时使用的参数类型,resultType属性指明查询返回的结果集类型
    resultType="com.mayikt.entity.User"就表示将查询结果封装成一个User类的对象返回
    User类就是users表所对应的实体类
    -->
    <!--
        根据id查询得到一个user对象
     -->
    <select id="getUser" parameterType="int"
            resultType="com.mayikt.entity.UserEntity">
        select * from user where id=#{id}
    </select>
</mapper>

4、实体类

public class UserEntity {
    private Integer id;
    private Date birdate;
    private String name;
}

5、mapper接口

public interface UserMapper {
    public UserEntity getUser(int id);
}

6、运行Mybatis代码

public class TestMyBatis {

    public static void main(String[] args) {
        try {
            //配置文件
            String configXml = "mybatis_config.xml";
            //加载配置获取流
            Reader reader = Resources.getResourceAsReader(configXml);
            //获取sqlSessionFactory工厂
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            //获取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession();
            //获取对应的mapper
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            //执行方法
            UserEntity user = userMapper.getUser(1);
            System.out.println("name:" + user.getName());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

7、数据表结构

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

二、Mybatis核心配置文件

1、Properties(属性)

    Java属性文件可以配置直观的。

如:

<properties>
        <property name="jdbc.driver" value="com.mysql.jdbc.Driver"/>
        <property name="jdbc.url" value="jdbc:mysql:///mybatis"/>
        <property name="jdbc.username" value="root"/>
        <property name="jdbc.password" value="root"/>
</properties>

或者通过直接引入属性文件,例如:

<properties resource="db.properties"></properties>

然后db.properties文件中的配置就是:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///mybatis
jdbc.username=root
jdbc.password=root

2、typeAliases(类型别名)

类型别名是Java类型的简称。

逐个设置,例如:

<typeAliases>

   <typeAlias type="com.mayikt.entity.User" alias="user"/>

</typeAliases>

3、Plugins

分页插件配置

四、Mybatis大体架构流程分析

1、流程图

      1、读取resources获取对应的Reader对象。

            reader = Resources.getResourceAsReader(resources);

       2、使用SqlSessionFactoryBuilder获取SqlSessionFactory源码分析

           SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader); 

源码分析:

  1. Reader reader = Resources.getResourceAsReader(resources);

       调用javaioAPI  读取resources配置文件,获取InputStreamReader

     2、SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

  3、使用XMLConfigBuilder 解析mybatis配置文件

SqlSessionFactoryBuilder使用XMLConfigBuilder解析配置文件,封装成Configuration对象。

4、因为在构造函数设置了parsed 为fasle,XMLConfigBuilder 只能被使用一次。

调用该方法解析mybatis_config文件

通过反射机制匹配接口

注意:XMLConfigBuilder运行之后,只能被解析一次 否则会抛出异常。

xml转换程bean对象 configuration

XMLConfigBuilder的作用是:解析mybatis配置文件文件 得到configuration

XMLMapperBuilder的作用是什么: 解析mybatisMapper文件

建议很多源码中设置值都是采用构造函数形式

loadedResource 存放都是mybatis映射的文件路由地址 使用set集合存放

1.protected final Set<String> loadedResources = new HashSet<String>();

注意:mapper文件配置的namespace一定要和接口对应 否则情况查找失败!

2mapperRegistry作用存放dao层mapper接口 底层使用过map集合存放。

 

解析配置文件完成了之后,都会装配到configuration

Configuration作用:mybatis核心的配置文件内容 ,使用xml转换bean

Mybatis扫包方式有两种一种是 写package、和resource

 

明白了 mapperRegistry 注册我们的Mapper文件。

使用configuration获取默认的DefaultSqlSessionFactory

五、MybatisMapper接口绑定原理

Mapper既然是接口,没有被初始化如何被调用的?

答案: 使用动态代理技术

public static void main(String[] args) throws IllegalAccessException, InstantiationException {
    System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    UserMapper userMapper = getMapper(UserMapper.class);
    UserEntity user = userMapper.getUser(1);
    System.out.println("user:" + user.toString());
}

//1.获取对应的Mapper接口
public static <T> T getMapper(Class<T> clas)
        throws IllegalArgumentException, InstantiationException, IllegalAccessException {
    return (T) Proxy.newProxyInstance(clas.getClassLoader(), new Class[]{clas},
            new MyBatisJdkInvocationHandler(clas));
}
public class MyBatisJdkInvocationHandler implements InvocationHandler {
    /**
     * 目标对象
     */
    private Object target;

    public MyBatisJdkInvocationHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return new UserEntity(1000l, "蚂蚁课堂", 20);
    }

    /**
     * 获取代理对象接口
     *
     * @param <T>
     * @return
     */
    public <T> T getProxy() {
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

}

接口绑定源码分析:

解析节点信息封装到Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>(),然后添加到configuration里面

转换成Java类就是一个MappedStatement(存sql信息)

使用Configuration的getMappedStatement方法来获取MappedStatement对象

获取的方式key的组成为命名空

 getMapper接口

从mapperRegistry获取查询接口对应的绑定接口

1、检查是否已经注册过Mapper接口
2、使用MapperProxyFactory 创建代理类MapperProxy

 

 

UserMapper.getUser方法的时候 调用MapperProxy的invoke方法

因为mapperRegister中 key:mapper接口 value MapperProxyFactory
使用MapperProxyFacotory创建MapperProxy代理
Mybatis基于多个不同的接口生成代理类 不同接口肯定不同的invoke方法
相同的接口,不同的方法肯定是走同一个invoke方法。

UserMapper.getUser()执行原理分析
1、调用MapperPrxoxy的invoke方法()
2、实现接口方法与配置文件sql语句关联

底层使用的是:result = sqlSession.selectOne(command.getName(), param);

大致原理分析:

  SqlSession提供select/insert/update/delete方法,在旧版本中使用使用SqlSession接口的这些方法,但是新版的Mybatis中就会建议使用Mapper接口的方法。

 射器其实就是一个动态代理对象,进入到MapperMethod的execute方法就能简单找到SqlSession的删除、更新、查询、选择方法,

从底层实现来说:通过动态代理技术,让接口跑起来,之后采用命令模式,最后还是采用了SqlSession的接口方法(getMapper()方法等到Mapper)执行SQL查询

(也就是说Mapper接口方法的实现底层还是采用SqlSession接口方法实现的)。

五、总结:

  1. 获取本地InputStreamReader对象(mybatis配置文件)
  2. 调用SqlSessionFactoryBuilder 
  3. ###在使用XMLConfigBuilder解析mybatis配置文件,装配到Configuration中。
  4. 将配置文件中的Mapper添加到Configuration mapperRegistry实现注册。

        备注:mapperRegistry存放当前所有的mapper文件。

       5.使用Configuration获取默认的DefaultSqlSessionFactory

六、MybatisMapper SQLSession源码分析

1、SQLSession的作用

SqlSession提供select/insert/update/delete方法,在旧版本中使用使用SqlSession接口的这些方法

每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的。

 

2、Executor执行器原理分析

1.openSessionFromDataSource,首先是从 Configuration 中取出相关的配置,生成 Transaction,接着又创建了一个 Executor,最后返回了 DefaultSqlSession 对象。

2.SimpleExecutor: 默认的 Executor,每个 SQL 执行时都会创建新的 Statement

ResuseExecutor: 相同的 SQL 会复用 Statement

BatchExecutor: 用于批处理的 Executor

CachingExecutor: 可缓存数据的 Executor,用代理模式包装了其它类型的 Executor

默认情况下使用缓存的CachingExecutor

创建的openSession源码分析:
1、创建事务管理器
2、创建执行器、
默认是创建简单的执行器会变成缓存执行器呢
最好交给DefaultSqlSession
默认创建SimpleExecutor执行器 ,判断是否开启二级缓存,如果开启了二级缓存
CachingExecutor执行器构造函数传递SimpleExecutor

 如果二级缓存没有,走简单执行器

3、SelectOne底层原理查询分析

  1. 当查询单条数据的时候,最终还是调用selectList查询多个结果集包装程单个对象。

 

  1. 从configuration中获取到MappedStatement(对应的sql语句配置),调用executor的query方法实现执行。

  先查询二级缓存,是否有缓存,没有的话调用delegate.<E> query

如果一级缓存中没有该结果,会调用queryFromDatabase查询数据库得到数据让后在缓存到一级缓存中,下次查询的时候相同的sql语句直接走一级缓存不会查询数据库。

一级(sqlSession缓存)和二级缓存(sessionFactory)

为什么CachingExecutor需要找到SimpleExecutor创建缓存key呢? 方便实现缓存key代码重构

mybatis缓存控制 先查找二级缓存(硬盘、redis)、二级缓存没有的情况在查找一级缓存。

一级缓存绝对是有的 ,但是二缓存可以没有。

PerpetualCache 指的就是我们的一级 一级缓存属于本地缓存 存放在内存中 使用map集合存放

4、Mybatis一级与二级缓存

一级缓存实现原理

相同查询sql语句和参数

第一次查询的时候 会调用数据库的查询 ,缓存到本地内存中
第二次查询的时候 直接走本地内存 不会查询数据库。

sqlSession缓存为了防止脏数据,增加、修改、删除的时候 都会清楚所有本地一级缓存。

mybatis的一级缓存是SqlSession级别的缓存,在操作数据库的时候需要先创建SqlSession会话对象,

在对象中有一个HashMap用于存储缓存数据,此HashMap是当前会话对象私有的,别的SqlSession会话对象无法访问。

具体流程:

1.第一次执行select完毕会将查到的数据写入SqlSession内的HashMap中缓存起来

2.第二次执行select会从缓存中查数据,如果select相同切传参数一样,那么就能从缓存中返回数据,不用去数据库了,从而提高了效率

注意事项:

  1. 如果SqlSession执行了DML操作(insert、update、delete),并commit了,那么mybatis就会清空当前SqlSession缓存中的所有缓存数据,

           这样可以保证缓存中的存的数据永远和数据库中一致,避免出现脏读

  1. 当一个SqlSession结束后那么他里面的一级缓存也就不存在了,mybatis默认是开启一级缓存,不需要配置
  2. mybatis的缓存是基于[namespace:sql语句:参数]来进行缓存的,意思就是,SqlSession的HashMap存储缓存数据时,是使用[namespace:sql:参数]作为key

注意:服务器集群的时候,每个sqlSession有自己独立的缓存相互之间不存在共享,所以在服务器集群的时候容易产生数据冲突问题。

一级存在那些问题呢? 线程安全问题

一级缓存不共享 二级缓存存在共享

配置以下配置可以实现开启日志打印

<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
    <setting name="cacheEnabled" value="false"/>
</settings>

如何禁止一级缓存

方案1  在sql语句上 随机生成 不同的参数 存在缺点:map集合可能爆 内存溢出的问题

方案2  开启二级缓存

方案3  使用sqlSession强制清除缓存

方案4  创建新的sqlSession连接。

二级缓存SessionFactory

 二级缓存是mapper级别的缓存,也就是同一个namespace的mappe.xml,当多个SqlSession使用同一个Mapper操作数据库的时候,得到的数据会缓存在同一个二级缓存区域,二级缓存默认是没有开启的。

需要在setting全局参数中配置开启二级缓存

Config.配置

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

在UserMapper配置

<!-- 以下两个<cache>标签二选一,第一个可以输出日志,第二个不输出日志 -->
<cache type="org.mybatis.caches.ehcache.LoggingEhcache" />
<!-- <cache type="org.mybatis.caches.ehcache.EhcacheCache"/> -->

二级缓存回收策略

 LRU:最近最少使用的策略,移除最长时间不被使用的对象。

 FIFO:先进先出策略,按对象进入缓存的顺序来移除它们。

 SOFT:软引用策略,移除基于垃圾回收器状态和软引用规则的对象。

 WEAK:弱引用策略,更积极地移除基于垃圾收集器状态和弱引用规则的对象。

 软引用与弱引用的区别:

  软引用: 软引用是用来描述一些有用但并不是必需的对象, 对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象

  弱引用: 弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象

TransactionalCache

TransactionalCache:继承自Cache接口,主要作用是保存SqlSession在事务中需要向某个二级缓存提交的缓存数据(因为事务过程中的数据可能会回滚,所以不能直接把数据就提交二级缓存,而是暂存在TransactionalCache中,在事务提交后再将过程中存放在其中的数据提交到二级缓存,如果事务回滚,则将数据清除掉)

TransactionalCacheManager

TransactionalCacheManager:用于管理CachingExecutor使用的二级缓存对象,只定义了一个transactionalCaches字段

private final Cache delegate; //对应的二级缓存对象

private boolean clearOnCommit; //是否在commit时清除二级缓存的标记

// 需要在commit时提交到二级缓存的数据

private final Map<Object, Object> entriesToAddOnCommit;

// 缓存未命中的数据,事务commit时,也会放入二级缓存(key,null)

private final Set<Object> entriesMissedInCache;

StatementHandler

StatementHandler接口的实现大致有四个,其中三个实现类都是和JDBC中的Statement响对应的:
SimpleStatementHandler,这个很简单了,就是对应我们JDBC中常用的Statement接口,用于简单SQL的处理; 存在sql注入攻击问题

PreparedStatementHandler,这个对应JDBC中的PreparedStatement,预编译SQL的接口;

防止sql注入

CallableStatementHandler,这个对应JDBC中CallableStatement,用于执行存储过程相关的接口;

RoutingStatementHandler,这个接口是以上三个接口的路由,没有实际操作,只是负责上面三个StatementHandler的创建及调用。

ResultSetHandler

就是将Statement实例执行之后返回的ResultSet结果集转换成我们需要的List结果集

一级缓存与二级缓存区别

①、一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。

不同的sqlSession之间的缓存数据区域(sqlHashMap)是互相不影响的。

②、二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。

注意:sqlSession缓存底层存在线程安全问题。

 

七、Mybatis使用常用设计模式

 

Builder模式,例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;

 

工厂模式,例如SqlSessionFactory、ObjectFactory、MapperProxyFactory;

 

单例模式,例如ErrorContext和LogFactory;

 

代理模式,Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理;还有executor.loader包使用了cglib或者javassist达到延迟加载的效果;

 

组合模式,例如SqlNode和各个子类ChooseSqlNode等;

 

模板方法模式,例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler;

 

适配器模式,例如Log的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现;

 

装饰者模式,例如Cache包中的cache.decorators子包中等各个装饰者的实现;

 

迭代器模式,例如迭代器模式PropertyTokenizer;

八、流程图总结

 

posted @ 2019-06-25 19:23  吊儿郎当小少年  阅读(895)  评论(0编辑  收藏  举报