美文网首页MyBatis源码解析
(二)Mybatis-获取SqlSession相关

(二)Mybatis-获取SqlSession相关

作者: 骑着乌龟去看海 | 来源:发表于2018-01-27 11:18 被阅读34次

    概述

      通过上篇文章,我们已经大概了解了Mybatis的大致使用过程。从这篇文章开始,我们将通过源码的形式来更深入的了解Mybatis的使用。

    注:我们目前学习的是基于不使用spring的情况下mybatis的源码。

    流程

    首先,我们先看一下我们上次使用的例子:

    public static void main(String[] args) {
            String resource = "config/mybatis-config.xml";
            InputStream inputStream;
            SqlSession session = null;
            try {
                inputStream = Resources.getResourceAsStream(resource);
                // 构建sqlSession工厂
                SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
                // 获取sqlSession
                session = sqlSessionFactory.openSession();
                // 方法1
                String statement = "com.mapper.IStudentMapper.getAll";
                List<Student> student = session.selectList(statement);
                System.out.println(student);
    
                // 方法2
                IStudentMapper sessionMapper = session.getMapper(IStudentMapper.class);
                List<Student> list = sessionMapper.getAll();
                System.out.println(list);
    
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                session.close();
            }
        }
    

    上述代码基本上就是Mybatis的整个流程了,接下来,我们来结合源码来看下,Mybatis是如何初始化,如何获取SqlSession的。

    1. 通过mybatis的Resources来读取全局配置文件,然后以流的形式作为SqlSessionFactoryBuilder的build方法的参数传入;
    2. SqlSessionFactoryBuilder根据参数创建XMLConfigBuilder对象,然后调用XMLConfigBuilder的parse方法生成Configuration对象;
    3. 然后SqlSessionFactoryBuilder根据Configuration对象作为参数,调用重载的另一个方法build,生成DefaultSqlSessionFactory对象。
    4. 最终,返回DefaultSqlSessionFactory对象。

    我们来简单看下代码:

    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        try {
          // 根据输入流等参数生成XMLConfigBuilder对象,而XMLConfigBuilder用来解析全局配置
          XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
          // 通过parser.parse方法生成Configuration对象,然后再调用重载方法
          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.
          }
        }
    }
    /**
     * 最后调用该方法生成factory,所以这里我们也可以手动传入Configuration对象来构建SessionFactory
     */
    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
    

    上述过程中,涉及到以下几个对象:

    1. XMLConfigBuilder 该对象的目的就是负责解析配置文件的数据,解析成Configuration对象;
    2. Configutation,该对象是全局配置文件mybatis-config.xml所对应的Java对象;
    3. SqlSessionFactoryBuilder,该对象很明显就是为了构建sessionFactory的构造器。
    Conguration对象

      这里可以注意下,XMLConfigBuilder在这里使用了设计模式中的建造者模式,基本建造者基类是BaseBuilder,XMLConfigBuilder是BaseBuilder类的实现之一,就是具体建造者的角色,所负责的就是解析mybatis-config.xml这个配置文件。

    public class XMLConfigBuilder extends BaseBuilder {
        //标识是否已经解析过配置文件 
        private boolean parsed;
        // 用于解析配置文件的解析对象
        private XPathParser parser;
        // 对应配置文件中的<environment>配置
        private String environment;
        private ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
    }
    

    而XMLConfigBuilder的解析是通过parseConfiguration来进行的。

    public Configuration parse() {
        // 判断是否已经解析过
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        // 在配置文件中查找<configuration>节点,然后通过调用parseConfiguration方法来解析
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
    }
    
    private void parseConfiguration(XNode root) {
        try {
            // 解析<settings>节点
            Properties settings = settingsAsPropertiess(root.evalNode("settings"));
            // 解析<properties>节点
            propertiesElement(root.evalNode("properties"));
            loadCustomVfs(settings);
            // 解析<typeAliases>节点
            typeAliasesElement(root.evalNode("typeAliases"));
            // 解析<plugins>节点
            pluginElement(root.evalNode("plugins"));
            // 解析<objectFactory>节点
            objectFactoryElement(root.evalNode("objectFactory"));
            // 解析<objectWrapperFactory>节点
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            // 解析<reflectionFactory>节点
            reflectionFactoryElement(root.evalNode("reflectionFactory"));
            settingsElement(settings);
            // 解析<environments>节点
            // read it after objectFactory and objectWrapperFactory issue #631
            environmentsElement(root.evalNode("environments"));
            // 解析<databaseIdProvider>节点
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            // 解析<typeHandlers>节点
            typeHandlerElement(root.evalNode("typeHandlers"));
            // 解析<mappers>节点
            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
    

      可以看到,parseConfiguration方法将每个节点的解析都封装成来一个个具体的方法,还是很好理解的,不过有一点,我没看太明白,就是对reflectionFactory节点的解析,我记得<configuration>节点中是没有该节点的,但这里解析的是哪个节点呢???

    我们拿其中的一个 typeAliases 解析的方法来看一下实现即可,剩余的如有兴趣,大家可自行查看其源码。先看一下配置:

    <typeAliases>
        <typeAlias type="com.entity.Student" alias="Student"/>
        <package name="com.entity"/>
    </typeAliases>
    
    private void typeAliasesElement(XNode parent) {
        if (parent != null) {
          // 遍历全部子节点
          for (XNode child : parent.getChildren()) {
            // 处理package节点
            if ("package".equals(child.getName())) {
              // 获取包名
              String typeAliasPackage = child.getStringAttribute("name");
              // 通过TypeAliasRegistry扫描指定包中的所有的类,并解析@Alias注解,完成别名注册
              configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else {
              // 处理typeAlias节点,获取指定别名
              String alias = child.getStringAttribute("alias");
              // 获取别名对应的类型
              String type = child.getStringAttribute("type");
              try {
                // 通过名称加载对应的类
                Class<?> clazz = Resources.classForName(type);
                if (alias == null) {
                  // 扫描@Alias注解,完成别名注册
                  typeAliasRegistry.registerAlias(clazz);
                } else {
                  // 注册别名
                  typeAliasRegistry.registerAlias(alias, clazz);
                }
              } catch (ClassNotFoundException e) {
                throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
              }
            }
          }
        }
    }
    

    至于typeAliases中的配置规则,大家可参考我最开始发出来的官方文档,这里我大致截个图来说明下:

    typeAliases使用说明.png
    这里面涉及到了一种设计模式,就是建造者模式,大家有兴趣的可以去查看我的另一篇设计模式的文章。
    SqlSession

      在获取到Configuration对象之后,SqlSessionFactoryBuilder就可以通过build方法进行构建工厂对象了。
      其实SqlSessionFactoryBuilder很简单,目的就是构建SqlSessionFactory工厂对象,然后从工厂里取出SqlSession,该类提供了各种参数的build的重载方法,大家可以根据需要自行调用来学习。
    接下来,我们首先来了解一下SqlSession及SqlSessionFactory的各自的继承关系。

    继承关系图.png

    从继承关系上我们可以看到,这里使用了工厂方法的设计模式。我们所使用的SqlSession的实现大多是DefaultSqlSession,同样,SqlSessionFactory的实现也是DefaultSqlSessionFactory。我们先看下SessionFactory的源码:

    public interface SqlSessionFactory {
    
      SqlSession openSession();
    
      SqlSession openSession(boolean autoCommit);
      SqlSession openSession(Connection connection);
      SqlSession openSession(TransactionIsolationLevel level);
    
      SqlSession openSession(ExecutorType execType);
      SqlSession openSession(ExecutorType execType, boolean autoCommit);
      SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
      SqlSession openSession(ExecutorType execType, Connection connection);
    
      Configuration getConfiguration();
    }
    
    1. 首先,SqlSeesionFactory目的就是创建SqlSession,从源码中我们可以看到,SqlSeesionFactory包含了多个openSession的重载方法。可以通过其参数来指定事务级别,底层使用的Executor的类型,是否自动提交事务等等。
    2. 而在SqlSession中我们可以看到,SqlSession中定义的全是对数据库增删改查的各种方法。这些方法我们稍后会挨个学习使用。
    DefaultSqlSession
    public class DefaultSqlSession implements SqlSession {
      // 全局配置解析后的对象
      private Configuration configuration;
      // 底层依赖的Executor对象
      private Executor executor;
      // 是否自动提交事务
      private boolean autoCommit;
      // 当前缓存中是否有脏数据
      private boolean dirty;
      private List<Cursor<?>> cursorList;
    
      public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
        this.configuration = configuration;
        this.executor = executor;
        this.dirty = false;
        this.autoCommit = autoCommit;
      }
    }
    

    DefaultSqlSession实现类SqlSession的方法,并且提供了多种重载方法。但真正对数据库操作却是通过Executor对象来进行的。并且这里使用到了策略模式。
    还有一点需要注意,DefaultSqlSession不是线程安全的。

    DefaultSqlSessionFactory

    DefaultSqlSessionFactory是工厂类的具体实现,该类实现了多种openSession重载方法,但最终都是基于以下两种方式来创建DefaultSqlSession:

    1. 一种是通过数据源获取数据库连接,并创建Executor对象及DefaultSqlSession对象。
    2. 另一种是用户提供数据库连接对象,然后DefaultSqlSessionFactory使用该连接进行创建操作。

    我们分别来看下源码实现。

    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
          // 获取mybati-config.xml中配置的environment对象
          final Environment environment = configuration.getEnvironment();
          // 获取事务工厂对象
          final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
          // 创建事务
          tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
          // 根据配置创建Executor对象
          final Executor executor = configuration.newExecutor(tx, execType);
          // 创建DefaultSqlSession对象
          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();
        }
    }
    

    来看第二种方式:

    private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
        try {
          boolean autoCommit;
          try {
            // 当前事务提交方式是否是自动提交
            autoCommit = connection.getAutoCommit();
          } catch (SQLException e) {
            // Failover to true, as most poor drivers
            // or databases won't support transactions
            // 如果当前数据库驱动提供的连接不支持事务
            autoCommit = true;
          }
          final Environment environment = configuration.getEnvironment();
          final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
          // 根据connction创建事务Transaction
          final Transaction tx = transactionFactory.newTransaction(connection);
          final Executor executor = configuration.newExecutor(tx, execType);
          return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
    }
    
    SqlSessionManager

    SqlSessionManager同时实现类SqlSessionFactory和SqlSession两个接口,那么它也就同时具有来创建SqlSession以及使用SqlSession处理数据库的功能。
    我们先看一下继承结构及属性:

    public class SqlSessionManager implements SqlSessionFactory, SqlSession {
      // 底层封装的SqlSessionFactory对象
      private final SqlSessionFactory sqlSessionFactory;
      // SqlSession的代理对象,在该类初始化时,会使用JDK动态代理的方式为localSqlSession创建代理对象
      private final SqlSession sqlSessionProxy;
      // 与当前线程绑定的SqlSession对象
      private ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<SqlSession>();
    
      private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
        // 创建代理类,通过内部类SqlSessionInterceptor来实现
        this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
            SqlSessionFactory.class.getClassLoader(),
            new Class[]{SqlSession.class},
            new SqlSessionInterceptor());
      }
    }
    

    SqlSessionManager提供了两种模式:

    1. 第一种与DefaultSqlSessionFactory相同,同一线程每次通过SqlSessionManager对象访问数据库时,都会创建新的DefaultSqlSession对象来完成对数据库的操作;
    2. 第二种是SqlSessionManager通过localSqlSession变量,记录来与当前线程绑定的SqlSession对象,供当前线程循环使用,从而避免在同一线程多次创建SqlSession对象带来的性能损失。

    SqlSessionManager特性:

    1. 构造方法私有,我们只能通过newInstance来获取对象;
    2. SqlSessionManager的openSession方法全是借助于底层的sqlSessionFactory的openSession方法来创建SqlSession的。
    3. 该类所实现的SqlSession的方法,如select,update等方法全是借助于内部的SqlSession的代理类来实现的。

    我们来看一下SqlSessionInterceptor的invoke方法:

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 获取当前线程绑定的SqlSession对象
      final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
      // 如果不等于空,则是第二种模式
      if (sqlSession != null) {
        try {
          // 调用真正的sqlSession来完成相应的操作
          return method.invoke(sqlSession, args);
        } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
        }
      // 否则是第一种模式
      } else {
        // 如果当前线程未绑定SqlSession对象,则创建新的SqlSession对象
        final SqlSession autoSqlSession = openSession();
        try {
          // 通过新建的对象来完成对应的操作
          final Object result = method.invoke(autoSqlSession, args);
          // 提交事务
          autoSqlSession.commit();
          return result;
        } catch (Throwable t) {
          // 回滚事务
          autoSqlSession.rollback();
          throw ExceptionUtil.unwrapThrowable(t);
        } finally {
          // 关闭连接
          autoSqlSession.close();
        }
      }
    }
    

      通过对以上代码分析可知,第一种模式中的SqlSession在使用完成后会立即进行关闭。而第二种模式中,与当前线程绑定的sqlSession对象需要先通过startManagedSession方法进行设置:

    public void startManagedSession() {
        this.localSqlSession.set(openSession());
      }
    

      当需要提交,回滚事务或是关闭localSqlSession中记录的SqlSession对象时,需要通过SqlSessionManager.commit,rollback以及close方法来完成。
      所以说,SqlSessionManager可以看作是DefaultSqlSessionFactory的一个优化类了,它所做的就是通过SqlSessionManager自己维护的一个ThreadLocal,实现session的复用,所以也是一种线程安全的实现方式。所以我们前面的例子也可以改成如下:

    inputStream = Resources.getResourceAsStream(resource);
    // 构建sqlSession工厂
    //SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSessionManager sessionManager = SqlSessionManager.newInstance(inputStream);
    sessionManager.startManagedSession();
    String statement = "com.mapper.IStudentMapper.getAll";
    List<Student> student = sessionManager.selectList(statement);
    

    总结

    对于获取SqlSession,我们可以稍微总结一下:

    1. 获取SessionFactory的方式有两种,一种是通过SqlSessionFactoryBuilder来进行构建,获取DefaultSessionFactory,另一种是通过SqlSessionManager来进行获取,其实,SqlSessionManager获取SessionFactory底层也是通过SqlSessionFactoryBuilder的build方法来获取的,不过多封装了一层。SqlSessionManager优于Builder的方面在于session复用和线程安全,所以我们还是建议使用后者;
    2. 无论使用哪种方式获取SessionFactory,获取SqlSession的方式都只有一种,就是通过SessionFactory的openSession方法来获取;
    3. 仅仅获取SqlSession这一块就使用了工厂方法模式,建造者模式,代理模式,策略模式等多种设计模式,值得我们了解和学习。

    本文参考自:
    终结篇:MyBatis原理深入解析(一)
    《MyBatis技术内幕》一书

    相关文章

      网友评论

        本文标题:(二)Mybatis-获取SqlSession相关

        本文链接:https://www.haomeiwen.com/subject/awknaxtx.html