Mybatis源码解析

目录

传统JDBC的问题如下

mybatis对传统的JDBC的解决方案

Mybaits整体体系图

MyBatis 源码编译

启动流程分析​

1、解析全局配置文件

简单总结

2、Mapper.xml文件解析

3、二级缓存的解析(二级缓存一直是开启的,只是我们调用二级缓存需要条件)

4、sql语句的解析

MyBatis执行Sql的流程分析

1.创建SqlSession

Executor

2.用SqlSession操作数据库

通过getMapper的形式调用的话

Mapper方法的执行流程

重要类

调试主要关注点


传统的JDBC

@Test
public  void test() throws SQLException {
    Connection conn=null;
    PreparedStatement pstmt=null;
    try {
        // 1.加载驱动
        Class.forName("com.mysql.jdbc.Driver");

        // 2.创建连接
        conn= DriverManager.
                getConnection("jdbc:mysql://localhost:3306/mybatis_example", "root", "123456");


        // SQL语句
        String sql="select id,user_name,create_time from t_user where id=?";

        // 获得sql执行者
        pstmt=conn.prepareStatement(sql);
        pstmt.setInt(1,1);

        // 执行查询
        //ResultSet rs= pstmt.executeQuery();
        pstmt.execute();
        ResultSet rs= pstmt.getResultSet();

        rs.next();
        User user =new User();
        user.setId(rs.getLong("id"));
        user.setUserName(rs.getString("user_name"));
        user.setCreateTime(rs.getDate("create_time"));
        System.out.println(user.toString());
    } catch (Exception e) {
        e.printStackTrace();
    }
    finally{
        // 关闭资源
        try {
            if(conn!=null){
                conn.close();
            }
            if(pstmt!=null){
                pstmt.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

传统JDBC的问题如下

1.数据库连接创建,释放频繁造成资源的浪费,从而影响系统性能,使用数据库连接池可以解决问题。

2.sql语句在代码中硬编码,造成代码的不已维护,实际应用中sql的变化可能较大,sql代码和java代码没有分离开来维护不方便。

3.使用preparedStatement向有占位符传递参数存在硬编码问题因为sql中的where子句的条件不确定,同样是修改不方便/

4.对结果集中解析存在硬编码问题,sql的变化导致解析代码的变化,系统维护不方便。

mybatis对传统的JDBC的解决方案

1、数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。

解决:在SqlMapConfig.xml中配置数据连接池,使用连接池管理数据库链接。

2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。

解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。

3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。

解决:Mybatis自动将java对象映射至sql语句,通过statement中的parameterType定义输入参数的类型。

4、对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。

解决:Mybatis自动将sql执行结果映射至java对象,通过statement中的resultType定义输出结果的类型。

Mybaits整体体系图

Mybatis源码解析_第1张图片

Mybatis源码解析_第2张图片

 一个Mybatis最简单的使用列子如下:


public class App {
    public static void main(String[] args) {
        //classpath下的mybatis的全局配置文件
        String resource = "mybatis-config.xml";
        Reader reader;
        try {
            //将XML配置文件构建为Configuration配置类,这里是加载将我们的配置文件加载进来
            reader = Resources.getResourceAsReader(resource);
            // 通过加载配置文件流构建一个SqlSessionFactory  DefaultSqlSessionFactory
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
            // 数据源 执行器  DefaultSqlSession
            SqlSession session = sqlMapper.openSession();
            try {
                // 执行查询 底层执行jdbc
                //User user = (User)session.selectOne("com.tuling.mapper.selectById", 1);

                UserMapper mapper = session.getMapper(UserMapper.class);
                System.out.println(mapper.getClass());
                User user = mapper.selectById(1L);
                System.out.println(user.getUserName());
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                session.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

总结下就是分为下面四个步骤:

  • 从配置文件(通常是XML文件)得到SessionFactory;
  • 从SessionFactory得到SqlSession;
  • 通过SqlSession进行CRUD和事务的操作(底层是使用executor进行sql的操作);
  • 执行完相关操作之后关闭Session。

MyBatis 源码编译

MyBatis的源码编译比较简单, 随便在网上找一篇博客即可,在这里不多说

mybatis 源码导入IDEA - 未亦末 - 博客园

启动流程分析Mybatis源码解析_第3张图片

String resource = "mybatis-config.xml";
//将XML配置文件构建为Configuration配置类
reader = Resources.getResourceAsReader(resource);
// 通过加载配置文件流构建一个SqlSessionFactory  DefaultSqlSessionFactory
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

 通过上面代码发现,创建SqlSessionFactory的代码在SqlSessionFactoryBuilder中,进去一探究竟:

//整个过程就是将配置文件解析成Configration对象,然后创建SqlSessionFactory的过程
//Configuration是SqlSessionFactory的一个内部属性
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.
      }
    }
  }
  

//这里是上面方法return return build(parser.parse());之后返回的一个默认的SqlSessionFactroy 
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

1、解析全局配置文件

 new XMLConfigBuilder()构造函数(在父类中会创建出 configuration,在初始化configuration的时候会创建出很多默认的typehandler)

Mybatis源码解析_第4张图片

 Mybatis源码解析_第5张图片

XPathParser主要是用来解析xml文件的

Mybatis源码解析_第6张图片

 Mybatis源码解析_第7张图片

 Mybatis源码解析_第8张图片

 下面是解析配置文件的核心方法:

 private void parseConfiguration(XNode root) {
    try {
      /**
       * 解析 properties节点
       *     
       *     解析到org.apache.ibatis.parsing.XPathParser#variables
       *           org.apache.ibatis.session.Configuration#variables
       */
      propertiesElement(root.evalNode("properties"));
      /**
       * 解析我们的mybatis-config.xml中的settings节点
       * 具体可以配置哪些属性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings
       * 
            
            
           
           
           
            ..............
           
       *
       */
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      /**
       * 基本没有用过该属性
       * VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。
         Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序
         解析到:org.apache.ibatis.session.Configuration#vfsImpl
       */
      loadCustomVfs(settings);
      /**
       * 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
       * SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
       * 解析到org.apache.ibatis.session.Configuration#logImpl
       */
      loadCustomLogImpl(settings);
      /**
       * 解析我们的别名
       * 
           
        
       
          
       
       解析到oorg.apache.ibatis.session.Configuration#typeAliasRegistry.typeAliases
       */
      typeAliasesElement(root.evalNode("typeAliases"));
      /**
       * 解析我们的插件(比如分页插件)
       * mybatis自带的
       * Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
         ParameterHandler (getParameterObject, setParameters)
         ResultSetHandler (handleResultSets, handleOutputParameters)
         StatementHandler (prepare, parameterize, batch, update, query)
        解析到:org.apache.ibatis.session.Configuration#interceptorChain.interceptors
       */
      pluginElement(root.evalNode("plugins"));

      /**
       * todo
       */
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // 设置settings 和默认值
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631

      /**
       * 解析我们的mybatis环境
         
           
             
             
             
             
             
             
             
           

         
           
           
           
           
           
           
           
         
       
       *  解析到:org.apache.ibatis.session.Configuration#environment
       *  在集成spring情况下由 spring-mybatis提供数据源 和事务工厂
       */
      environmentsElement(root.evalNode("environments"));
      /**
       * 解析数据库厂商
       *     
                
                
                
                
             
       *  解析到:org.apache.ibatis.session.Configuration#databaseId
       */
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      /**
       * 解析我们的类型处理器节点
       * 
            
          
          解析到:org.apache.ibatis.session.Configuration#typeHandlerRegistry.typeHandlerMap
       */
      typeHandlerElement(root.evalNode("typeHandlers"));
      /**
       * 最最最最最重要的就是解析我们的mapper
       *
       resource:来注册我们的class类路径下的
       url:来指定我们磁盘下的或者网络资源的
       class:
       若注册Mapper不带xml文件的,这里可以直接注册
       若注册的Mapper带xml文件的,需要把xml文件和mapper文件同名 同路径
       -->
       
          
          


            
          -->
       
       * package 1.解析mapper接口 解析到:org.apache.ibatis.session.Configuration#mapperRegistry.knownMappers
                 2.
       */
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }




    
    
      
      
    

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    

    

    
        
            
            
            
        
    

    
        
            
            
                
                
                
                
            
        
    

    
        
        
    

    
        
        
        
        
    

Mybatis源码解析_第9张图片

上面解析流程结束后会生成一个Configration对象,包含所有配置信息,然后会创建一个SqlSessionFactory对象,这个对象包含了Configration对象。

简单总结

对于MyBatis启动的流程(获取SqlSession的过程)这边简单总结下:

  • SqlSessionFactoryBuilder解析配置文件,包括属性配置、别名配置、拦截器配置、环境(数据源和事务管理器)、Mapper配置等;解析完这些配置后会生成一个Configration对象,这个对象中包含了MyBatis需要的所有配置,然后会用这个Configration对象创建一个SqlSessionFactory对象,这个对象中包含了Configration对象;

2、Mapper.xml文件解析

核心配置文件中对mapper.xml的读取主要有四种方式

Mybatis源码解析_第10张图片

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      /**
       * 获取我们mappers节点下的一个一个的mapper节点
       */
      for (XNode child : parent.getChildren()) {
        /**
         * 判断我们mapper是不是通过批量注册的
         * 
         */
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          /**
           * 判断从classpath下读取我们的mapper
           * 
           */
          String resource = child.getStringAttribute("resource");
          /**
           * 判断是不是从我们的网络资源读取(或者本地磁盘得)
           * 
           */
          String url = child.getStringAttribute("url");
          /**
           * 解析这种类型(要求接口和xml在同一个包下)
           * 
           *
           */
          String mapperClass = child.getStringAttribute("class");

          /**
           * 我们得mappers节点只配置了
           * 
           */
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            /**
             * 把我们的文件读取出一个流
             */
            InputStream inputStream = Resources.getResourceAsStream(resource);
            /**
             * 创建读取XmlMapper构建器对象,用于来解析我们的mapper.xml文件
             */
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            /**
             * 真正的解析我们的mapper.xml配置文件(说白了就是来解析我们的sql)
             */
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            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.");
          }
        }
      }
    }
  }

Mybatis源码解析_第11张图片

 来看下xmlMapperBuilder是怎么解析我们的mapper.xml文件的

Mybatis源码解析_第12张图片

 private void configurationElement(XNode context) {
    try {
      /**
       * 解析我们的namespace属性
       * 
       */
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      /**
       * 保存我们当前的namespace  并且判断接口完全类名==namespace
       */
      builderAssistant.setCurrentNamespace(namespace);
      /**
       * 解析我们的缓存引用
       * 说明我当前的缓存引用和DeptMapper的缓存引用一致
       * 
            解析到org.apache.ibatis.session.Configuration#cacheRefMap<当前namespace,ref-namespace>
            异常下(引用缓存未使用缓存):org.apache.ibatis.session.Configuration#incompleteCacheRefs
       */
      cacheRefElement(context.evalNode("cache-ref"));
      /**
       * 解析我们的cache节点
       * 
          解析到:org.apache.ibatis.session.Configuration#caches
                 org.apache.ibatis.builder.MapperBuilderAssistant#currentCache
       */
      cacheElement(context.evalNode("cache"));
      /**
       * 解析paramterMap节点(该节点mybaits3.5貌似不推荐使用了)
       */
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      /**
       * 解析我们的resultMap节点
       * 解析到:org.apache.ibatis.session.Configuration#resultMaps
       *    异常 org.apache.ibatis.session.Configuration#incompleteResultMaps
       *
       */
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      /**
       * 解析我们通过sql节点
       *  解析到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments
       *   其实等于 org.apache.ibatis.session.Configuration#sqlFragments
       *   因为他们是同一引用,在构建XMLMapperBuilder 时把Configuration.getSqlFragments传进去了
       */
      sqlElement(context.evalNodes("/mapper/sql"));
      /**
       * 解析我们的select | insert |update |delete节点
       * 解析到org.apache.ibatis.session.Configuration#mappedStatements
       */
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

都是对mapper.xml里面的标签属性的解析(会将我们的每一个节点封装成一个XNode)

3、二级缓存的解析(二级缓存一直是开启的,只是我们调用二级缓存需要条件)

 

 二级缓存的范围是在同一个namespace下,所有的sqlsession范围内有效(必须等sqlSession提交或者是关闭之后才会刷新到二级缓存中)

Mybatis源码解析_第13张图片

 Mybatis源码解析_第14张图片

 Mybatis源码解析_第15张图片

 进入build方法

Mybatis源码解析_第16张图片

Mybatis源码解析_第17张图片

 Mybatis源码解析_第18张图片

Mybatis源码解析_第19张图片

 最后将我们创建出来的二级缓存加入到configuration中

Mybatis源码解析_第20张图片

 key就是我们的namespace,cache就是我们的SynchronizedCache

流程图

Mybatis源码解析_第21张图片

二级缓存结构

Mybatis源码解析_第22张图片

二级缓存在结构设计上采用装饰器+责任链模式

1.SynchronizedCache线程同步缓存区

实现线程同步功能,与序列化缓存区共同保证二级缓存线程安全。若blocking=false关闭则SynchronizedCache位于责任链的最前端,否则就位于BlockingCache后面而BlockingCache位于责任链的最前端,从而保证了整条责任链是线程同步的。

源码分析:只是对于操作缓存的方法进行了线程同步功能

Mybatis源码解析_第23张图片

2.LoggingCache统计命中率以及打印日志

统计二级缓存命中率并输出打印,由以下源码可知:日志中出现了“Cache Hit Ratio”便表示命中了二级缓存。

public class LoggingCache implements Cache {
    private final Log log;
    private final Cache delegate;
    protected int requests = 0;
    protected int hits = 0;
    public LoggingCache(Cache delegate) {
        this.delegate = delegate;
        this.log = LogFactory.getLog(this.getId());
    }

    public Object getObject(Object key) {
        ++this.requests;//执行一次查询加一次
        Object value = this.delegate.getObject(key);//查询缓存中是否已经存在
        if (value != null) {
            ++this.hits;//命中一次加一次
        }

        if (this.log.isDebugEnabled()) {//开启debug日志
            this.log.debug("Cache Hit Ratio [" + this.getId() + "]: " + this.getHitRatio());
        }

        return value;
    }
    private double getHitRatio() {//计算命中率
        return (double)this.hits / (double)this.requests;//命中次数:查询次数

3.ScheduledCache过期清理缓存区

@CacheNamespace(flushInterval=100L)设置过期清理时间默认1个小时,

若设置flushInterval为0代表永远不进行清除。

源码分析:操作缓存时都会进行检查缓存是否过期

public class ScheduledCache implements Cache {
    private final Cache delegate;
    protected long clearInterval;
    protected long lastClear;
    public ScheduledCache(Cache delegate) {
        this.delegate = delegate;
        this.clearInterval = 3600000L;
        this.lastClear = System.currentTimeMillis();
    }
    public void clear() {
        this.lastClear = System.currentTimeMillis();
        this.delegate.clear();
    }
    private boolean clearWhenStale() {
//判断当前时间与上次清理时间差是否大于设置的过期清理时间
        if (System.currentTimeMillis() - this.lastClear > this.clearInterval) {
            this.clear();//一旦进行清理便是清理全部缓存
            return true;
        } else {
            return false;
        }
    }
}

4.LruCache(最近最少使用)防溢出缓存区

内部使用链表(增删比较快)实现最近最少使用防溢出机制

public void setSize(final int size) {
    this.keyMap = new LinkedHashMap(size, 0.75F, true) {
        private static final long serialVersionUID = 4267176411845948333L;

        protected boolean removeEldestEntry(Entry eldest) {
            boolean tooBig = this.size() > size;
            if (tooBig) {
                LruCache.this.eldestKey = eldest.getKey();
            }

            return tooBig;
        }
    };
}
//每次访问都会遍历一次key进行重新排序,将访问元素放到链表尾部。
public Object getObject(Object key) {
    this.keyMap.get(key);
    return this.delegate.getObject(key);
}

 5.FifoCache(先进先出)防溢出缓存区

内部使用队列存储key实现先进先出防溢出机制。

public class FifoCache implements Cache {
    private final Cache delegate;
    private final Deque keyList;
    private int size;
    public FifoCache(Cache delegate) {
        this.delegate = delegate;
        this.keyList = new LinkedList();
        this.size = 1024;
    }
    public void putObject(Object key, Object value) {
        this.cycleKeyList(key);
        this.delegate.putObject(key, value);
    }
    public Object getObject(Object key) {
        return this.delegate.getObject(key);
    }
    private void cycleKeyList(Object key) {
        this.keyList.addLast(key);
        if (this.keyList.size() > this.size) {//比较当前队列元素个数是否大于设定值
            Object oldestKey = this.keyList.removeFirst();//移除队列头元素
            this.delegate.removeObject(oldestKey);//根据移除元素的key移除缓存区中的对应元素
        }
    }
} 
  

Mybatis源码解析_第24张图片

4、sql语句的解析

 这里是对我们的select,insert,update,delete标签的解析

Mybatis源码解析_第25张图片

 循环解析我们的sql的节点Mybatis源码解析_第26张图片

public void parseStatementNode() {
    /**
     * 我们的insert|delte|update|select 语句的sqlId
     */
    String id = context.getStringAttribute("id");
    /**
     * 判断我们的insert|delte|update|select  节点是否配置了
     * 数据库厂商标注
     */
    String databaseId = context.getStringAttribute("databaseId");

    /**
     * 匹配当前的数据库厂商id是否匹配当前数据源的厂商id
     */
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    /**
     * 获得节点名称:select|insert|update|delete
     */
    String nodeName = context.getNode().getNodeName();
    /**
     * 根据nodeName 获得 SqlCommandType枚举
     */
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    /**
     * 判断是不是select语句节点
     */
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    /**
     *  获取flushCache属性
     *  默认值为isSelect的反值:查询:默认flushCache=false   增删改:默认flushCache=true
     */
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    /**
     * 获取useCache属性
     * 默认值为isSelect:查询:默认useCache=true   增删改:默认useCache=false
     */
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);

    /**
     * resultOrdered:  是否需要处理嵌套查询结果 group by (使用极少)
     * 可以将比如 30条数据的三组数据  组成一个嵌套的查询结果
     */
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    /**
     * 解析我们的sql公用片段
     *      解析成sql语句 放在