Mybaits 源码解析 ----- Select 语句的执行过程分析(下篇)(Mapper方法是如何调用到XML中的SQL的?)全网最详细,没有之一

Posted chen_hao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybaits 源码解析 ----- Select 语句的执行过程分析(下篇)(Mapper方法是如何调用到XML中的SQL的?)全网最详细,没有之一相关的知识,希望对你有一定的参考价值。

我们上篇文章讲到了查询方法里面的doQuery方法,这里面就是调用JDBC的API了,其中的逻辑比较复杂,我们这边文章来讲,先看看我们上篇文章分析的地方

SimpleExecutor

 1 public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
 2     Statement stmt = null;
 3     try {
 4         Configuration configuration = ms.getConfiguration();
 5         // 创建 StatementHandler
 6         StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
 7         // 创建 Statement
 8         stmt = prepareStatement(handler, ms.getStatementLog());
 9         // 执行查询操作
10         return handler.<E>query(stmt, resultHandler);
11     } finally {
12         // 关闭 Statement
13         closeStatement(stmt);
14     }
15 }

上篇文章我们分析完了第6行代码,在第6行处我们创建了一个PreparedStatementHandler,我们要接着第8行代码开始分析,也就是创建 Statement,先不忙着分析,我们先来回顾一下 ,我们以前是怎么使用jdbc的

jdbc

public class Login {
    /**
     *    第一步,加载驱动,创建数据库的连接
     *    第二步,编写sql
     *    第三步,需要对sql进行预编译
     *    第四步,向sql里面设置参数
     *    第五步,执行sql
     *    第六步,释放资源 
     * @throws Exception 
     */
     
    public static final String URL = "jdbc:mysql://localhost:3306/chenhao";
    public static final String USER = "liulx";
    public static final String PASSWORD = "123456";
    public static void main(String[] args) throws Exception {
        login("lucy","123");
    }
    
    public static void login(String username , String password) throws Exception{
        Connection conn = null; 
        PreparedStatement psmt = null;
        ResultSet rs = null;
        try {
            //加载驱动程序
            Class.forName("com.mysql.jdbc.Driver");
            //获得数据库连接
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            //编写sql
            String sql = "select * from user where name =? and password = ?";//问号相当于一个占位符
            //对sql进行预编译
            psmt = conn.prepareStatement(sql);
            //设置参数
            psmt.setString(1, username);
            psmt.setString(2, password);
            //执行sql ,返回一个结果集
            rs = psmt.executeQuery();
            //输出结果
            while(rs.next()){
                System.out.println(rs.getString("user_name")+" 年龄:"+rs.getInt("age"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            //释放资源
            conn.close();
            psmt.close();
            rs.close();
        }
    }
}

上面代码中注释已经很清楚了,我们来看看mybatis中是怎么和数据库打交道的。

SimpleExecutor

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 获取数据库连接
    Connection connection = getConnection(statementLog);
   // 创建 Statement,
    stmt = handler.prepare(connection, transaction.getTimeout());
   // 为 Statement 设置参数
    handler.parameterize(stmt);
    return stmt;
}

在上面的代码中我们终于看到了和jdbc相关的内容了,大概分为下面三个步骤:

  1. 获取数据库连接
  2. 创建PreparedStatement
  3. 为PreparedStatement设置运行时参数

我们先来看看获取数据库连接,跟进代码看看

BaseExecutor

protected Connection getConnection(Log statementLog) throws SQLException {
    //通过transaction来获取Connection
    Connection connection = this.transaction.getConnection();
    return statementLog.isDebugEnabled() ? ConnectionLogger.newInstance(connection, statementLog, this.queryStack) : connection;
}

我们看到是通过Executor中的transaction属性来获取Connection,那我们就先来看看transaction,根据前面的文章中的配置 <transactionManager type="jdbc"/>,则MyBatis会创建一个JdbcTransactionFactory.class 实例,Executor中的transaction是一个JdbcTransaction.class 实例,其实现Transaction接口,那我们先来看看Transaction

JdbcTransaction

我们先来看看其接口Transaction

Transaction

public interface Transaction {
    //获取数据库连接
    Connection getConnection() throws SQLException;
    //提交事务
    void commit() throws SQLException;
    //回滚事务
    void rollback() throws SQLException;
    //关闭事务
    void close() throws SQLException;
    //获取超时时间
    Integer getTimeout() throws SQLException;
}

接着我们看看其实现类JdbcTransaction

JdbcTransaction

public class JdbcTransaction implements Transaction {
  
  private static final Log log = LogFactory.getLog(JdbcTransaction.class);
  
  //数据库连接
  protected Connection connection;
  //数据源信息
  protected DataSource dataSource;
  //隔离级别
  protected TransactionIsolationLevel level;
  //是否为自动提交
  protected boolean autoCommmit;
  
  public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
    dataSource = ds;
    level = desiredLevel;
    autoCommmit = desiredAutoCommit;
  }
  
  public JdbcTransaction(Connection connection) {
    this.connection = connection;
  }
  
  public Connection getConnection() throws SQLException {
    //如果事务中不存在connection,则获取一个connection并放入connection属性中
    //第一次肯定为空
    if (connection == null) {
      openConnection();
    }
    //如果事务中已经存在connection,则直接返回这个connection
    return connection;
  }
  
    /**
     * commit()功能 
     * @throws SQLException
     */
  public void commit() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Committing JDBC Connection [" + connection + "]");
      }
      //使用connection的commit()
      connection.commit();
    }
  }
  
    /**
     * rollback()功能 
     * @throws SQLException
     */
  public void rollback() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Rolling back JDBC Connection [" + connection + "]");
      }
      //使用connection的rollback()
      connection.rollback();
    }
  }
  
    /**
     * close()功能 
     * @throws SQLException
     */
  public void close() throws SQLException {
    if (connection != null) {
      resetAutoCommit();
      if (log.isDebugEnabled()) {
        log.debug("Closing JDBC Connection [" + connection + "]");
      }
      //使用connection的close()
      connection.close();
    }
  }
  
  protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
      log.debug("Opening JDBC Connection");
    }
    //通过dataSource来获取connection,并设置到transaction的connection属性中
    connection = dataSource.getConnection();
   if (level != null) {
      //通过connection设置事务的隔离级别
      connection.setTransactionIsolation(level.getLevel());
    }
    //设置事务是否自动提交
    setDesiredAutoCommit(autoCommmit);
  }
  
  protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
    try {
        if (this.connection.getAutoCommit() != desiredAutoCommit) {
            if (log.isDebugEnabled()) {
                log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + this.connection + "]");
            }
            //通过connection设置事务是否自动提交
            this.connection.setAutoCommit(desiredAutoCommit);
        }

    } catch (SQLException var3) {
        throw new TransactionException("Error configuring AutoCommit.  Your driver may not support getAutoCommit() or setAutoCommit(). Requested setting: " + desiredAutoCommit + ".  Cause: " + var3, var3);
    }
  }
  
}

我们看到JdbcTransaction中有一个Connection属性和dataSource属性,使用connection来进行提交、回滚、关闭等操作,也就是说JdbcTransaction其实只是在jdbc的connection上面封装了一下,实际使用的其实还是jdbc的事务。我们看看getConnection()方法

//数据库连接
protected Connection connection;
//数据源信息
protected DataSource dataSource;

public Connection getConnection() throws SQLException {
//如果事务中不存在connection,则获取一个connection并放入connection属性中
//第一次肯定为空
if (connection == null) {
  openConnection();
}
//如果事务中已经存在connection,则直接返回这个connection
return connection;
}

protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
  log.debug("Opening JDBC Connection");
}
//通过dataSource来获取connection,并设置到transaction的connection属性中
connection = dataSource.getConnection();
if (level != null) {
  //通过connection设置事务的隔离级别
  connection.setTransactionIsolation(level.getLevel());
}
//设置事务是否自动提交
setDesiredAutoCommit(autoCommmit);
}

先是判断当前事务中是否存在connection,如果存在,则直接返回connection,如果不存在则通过dataSource来获取connection,这里我们明白了一点,如果当前事务没有关闭,也就是没有释放connection,那么在同一个Transaction中使用的是同一个connection,我们再来想想,transaction是SimpleExecutor中的属性,SimpleExecutor又是SqlSession中的属性,那我们可以这样说,同一个SqlSession中只有一个SimpleExecutor,SimpleExecutor中有一个Transaction,Transaction有一个connection。我们来看看如下例子

public static void main(String[] args) throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    //创建一个SqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
         EmployeeMapper employeeMapper = sqlSession.getMapper(Employee.class);
         UserMapper userMapper = sqlSession.getMapper(User.class);
         List<Employee> allEmployee = employeeMapper.getAll();
         List<User> allUser = userMapper.getAll();
         Employee employee = employeeMapper.getOne();
    } finally {
        sqlSession.close();
    }
}

我们看到同一个sqlSession可以获取多个Mapper代理对象,则多个Mapper代理对象中的sqlSession引用应该是同一个,那么多个Mapper代理对象调用方法应该是同一个Connection,直到调用close(),所以说我们的sqlSession是线程不安全的,如果所有的业务都使用一个sqlSession,那Connection也是同一个,一个业务执行完了就将其关闭,那其他的业务还没执行完呢。大家明白了吗?我们回归到源码,connection = dataSource.getConnection();,最终还是调用dataSource来获取连接,那我们是不是要来看看dataSource呢?

我们还是从前面的配置文件来看<dataSource type="UNPOOLED|POOLED">,这里有UNPOOLED和POOLED两种DataSource,一种是使用连接池,一种是普通的DataSource,UNPOOLED将会创将new UnpooledDataSource()实例,POOLED将会new pooledDataSource()实例,都实现DataSource接口,那我们先来看看DataSource接口

DataSource

public interface DataSource  extends CommonDataSource,Wrapper {
  //获取数据库连接
  Connection getConnection() throws SQLException;

  Connection getConnection(String username, String password)
    throws SQLException;

}

很简单,只有一个获取数据库连接的接口,那我们来看看其实现类

UnpooledDataSource

UnpooledDataSource,从名称上即可知道,该种数据源不具有池化特性。该种数据源每次会返回一个新的数据库连接,而非复用旧的连接。其核心的方法有三个,分别如下:

  1. initializeDriver - 初始化数据库驱动
  2. doGetConnection - 获取数据连接
  3. configureConnection - 配置数据库连接

初始化数据库驱动

看下我们上面使用JDBC的例子,在执行 SQL 之前,通常都是先获取数据库连接。一般步骤都是加载数据库驱动,然后通过 DriverManager 获取数据库连接。UnpooledDataSource 也是使用 JDBC 访问数据库的,因此它获取数据库连接的过程一样

UnpooledDataSource

public class UnpooledDataSource implements DataSource {
    private ClassLoader driverClassLoader;
    private Properties driverProperties;
    private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap();
    private String driver;
    private String url;
    private String username;
    private String password;
    private Boolean autoCommit;
    private Integer defaultTransactionIsolationLevel;

    public UnpooledDataSource() {
    }

    public UnpooledDataSource(String driver, String url, String username, String password) {
        this.driver = driver;
        this.url = url;
        this.username = username;
        this.password = password;
    }

    private synchronized void initializeDriver() throws SQLException {
        // 检测当前 driver 对应的驱动实例是否已经注册
        if (!registeredDrivers.containsKey(driver)) {
            Class<?> driverType;
            try {
                // 加载驱动类型
                if (driverClassLoader != null) {
                    // 使用 driverClassLoader 加载驱动
                    driverType = Class.forName(driver, true, driverClassLoader);
                } else {
                    // 通过其他 ClassLoader 加载驱动
                    driverType = Resources.classForName(driver);
                }

                // 通过反射创建驱动实例
                Driver driverInstance = (Driver) driverType.newInstance();
                /*
                 * 注册驱动,注意这里是将 Driver 代理类 DriverProxy 对象注册到 DriverManager 中的,而非 Driver 对象本身。
                 */
                DriverManager.registerDriver(new DriverProxy(driverInstance));
                // 缓存驱动类名和实例,防止多次注册
                registeredDrivers.put(driver, driverInstance);
            } catch (Exception e) {
                throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
            }
        }
    }
    //略...
}

//DriverManager
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();
public static synchronized void registerDriver(java.sql.Driver driver)
    throws SQLException {

    if(driver != null) {
        registeredDrivers.addIfAbsent(new DriverInfo(driver));
    } else {
        // This is for compatibility with the original DriverManager
        throw new NullPointerException();
    }
}
通过反射机制加载驱动Driver,并将其注册到DriverManager中的一个常量集合中,供后面获取连接时使用,为什么这里是一个List呢?我们实际开发中有可能使用到了多种数据库类型,如Mysql、Oracle等,其驱动都是不同的,不同的数据源获取连接时使用的是不同的驱动。
在我们使用JDBC的时候,也没有通过DriverManager.registerDriver(new DriverProxy(driverInstance));去注册Driver啊,如果我们使用的是Mysql数据源,那我们来看Class.forName("com.mysql.jdbc.Driver");这句代码发生了什么
Class.forName主要是做了什么呢?它主要是要求JVM查找并装载指定的类。这样我们的类com.mysql.jdbc.Driver就被装载进来了。而且在类被装载进JVM的时候,它的静态方法就会被执行。我们来看com.mysql.jdbc.Driver的实现代码。在它的实现里有这么一段代码:
static {  
    try {  
        java.sql.DriverManager.registerDriver(new Driver());  
    } catch (SQLException E) {  
        throw new RuntimeException("Can\'t register driver!");  
    }  
}

 很明显,这里使用了DriverManager并将该类给注册上去了。所以,对于任何实现前面Driver接口的类,只要在他们被装载进JVM的时候注册DriverManager就可以实现被后续程序使用。

作为那些被加载的Driver实现,他们本身在被装载时会在执行的static代码段里通过调用DriverManager.registerDriver()来把自身注册到DriverManager的registeredDrivers列表中。这样后面就可以通过得到的Driver来取得连接了。

获取数据库连接

在上面例子中使用 JDBC 时,我们都是通过 DriverManager 的接口方法获取数据库连接。我们来看看UnpooledDataSource是如何获取的。

UnpooledDataSource

public Connection getConnection() throws SQLException {
    return doGetConnection(username, password);
}
    
private Connection doGetConnection(String username, String password) throws SQLException {
    Properties props = new Properties();
    if (driverProperties != null) {
        props.putAll(driverProperties);
    }
    if (username != null) {
        // 存储 user 配置
        props.setProperty("user", username);
    }
    if (password != null) {
        // 存储 password 配置
        props.setProperty("password", password);
    }
    // 调用重载方法
    return doGetConnection(props);
}

private Connection doGetConnection(Properties properties) throws SQLException {
    // 初始化驱动,我们上一节已经讲过了,只用初始化一次
    initializeDriver();
    // 获取连接
    Connection connection = DriverManager.getConnection(url, properties);
    // 配置连接,包括自动提交以及事务等级
    configureConnection(connection);
    return connection;
}

private void configureConnection(Connection conn) throws SQLException {
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
        // 设置自动提交
        conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
        // 设置事务隔离级别
        conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
}

上面方法将一些配置信息放入到 Properties 对象中,然后将数据库连接和 Properties 对象传给 DriverManager 的 getConnection 方法即可获取到数据库连接。我们来看看是怎么获取数据库连接的

private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
    // 获取类加载器
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
      if (callerCL == null) {
        callerCL = Thread.currentThread().getContextClassLoader();
      }
    }
    // 此处省略部分代码 
    // 这里遍历的是在registerDriver(Driver driver)方法中注册的驱动对象
    // 每个DriverInfo包含了驱动对象和其信息
    for(DriverInfo aDriver : registeredDrivers) {

      // 判断是否为当前线程类加载器加载的驱动类
      if(isDriverAllowed(aDriver.driver, callerCL)) {
        try {
          println("trying " + aDriver.driver.getClass().getName());

          // 获取连接对象,这里调用了Driver的父类的方法
          // 如果这里有多个DriverInfo,比喻Mysql和Oracle的Driver都注册registeredDrivers了
          // 这里所有的Driver都会尝试使用url和info去连接,哪个连接上了就返回
          // 会不会所有的都会连接上呢?不会,因为url的写法不同,不同的Driver会判断url是否适合当前驱动
          Connection con = aDriver.driver.connect(url, info);
          if (con != null) {
            // 打印连接成功信息
            println("getConnection returning " + aDriver.driver.getClass().getName());
            // 返回连接对像
            return (con);
          }
        } catch (SQLException ex) {
          if (reason == null) {
            reason = ex;
          }
        }
      } else {
        println("    skipping: " + aDriver.getClass().getName());
      }
    }  
}

代码中循环所有注册的驱动,然后通过驱动进行连接,所有的驱动都会尝试连接,但是不同的驱动,连接的URL是不同的,如Mysql的url是jdbc:mysql://localhost:3306/chenhao,以jdbc:mysql://开头,则其Mysql的驱动肯定会判断获取连接的url符合,Oracle的也类似,我们来看看Mysql的驱动获取连接

由于篇幅原因,我这里就不分析了,大家有兴趣的可以看看,最后由URL对应的驱动获取到Connection返回,好了我们再来看看下一种DataSource

PooledDataSource

PooledDataSource 内部实现了连接池功能,用于复用数据库连接。因此,从效率上来说,PooledDataSource 要高于 UnpooledDataSource。但是最终获取Connection还是通过UnpooledDataSource,只不过PooledDataSource 提供一个存储Connection的功能。

辅助类介绍

PooledDataSource 需要借助两个辅助类帮其完成功能,这两个辅助类分别是 PoolState 和 PooledConnection。PoolState 用于记录连接池运行时的状态,比如连接获取次数,无效连接数量等。同时 PoolState 内部定义了两个 PooledConnection 集合,用于存储空闲连接和活跃连接。PooledConnection 内部定义了一个 Connection 类型的变量,用于指向真实的数据库连接。以及一个 Connection 的代理类,用于对部分方法调用进行拦截。至于为什么要拦截,随后将进行分析。除此之外,PooledConnection 内部也定义了一些字段,用于记录数据库连接的一些运行时状态。接下来,我们来看一下 PooledConnection 的定义。

PooledConnection

class PooledConnection implements InvocationHandler {

    private static final String CLOSE = "close";
    private static final Class<?>[] IFACES = new Class<?>[]{Connection.class};

    private final int hashCode;
    private final PooledDataSource dataSource;
    // 真实的数据库连接
    private final Connection realConnection;
    // 数据库连接代理
    private final Connection proxyConnection;
    
    // 从连接池中取出连接时的时间戳
    private long checkoutTimestamp;
    // 数据库连接创建时间
    private long createdTimestamp;
    // 数据库连接最后使用时间
    private long lastUsedTimestamp;
    // connectionTypeCode = (url + username + password).hashCode()
    private int connectionTypeCode;
    // 表示连接是否有效
    private boolean valid;

    public PooledConnection(Connection connection, PooledDataSource dataSource) {
        this.hashCode = connection.hashCode();
        this.realConnection = connection;
        this.dataSource = dataSource;
        this.createdTimestamp = System.currentTimeMillis();
        this.lastUsedTimestamp = System.currentTimeMillis();
        this.valid = true;
        // 创建 Connection 的代理类对象
        this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {...}
    
    // 省略部分代码
}

下面再来看看 PoolState 的定义。

PoolState 

public class PoolState {

    protected PooledDataSource dataSource;

    // 空闲连接列表
    protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
    // 活跃连接列表
    protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();
    // 从连接池中获取连接的次数
    protected long requestCount = 0;
    // 请求

以上是关于Mybaits 源码解析 ----- Select 语句的执行过程分析(下篇)(Mapper方法是如何调用到XML中的SQL的?)全网最详细,没有之一的主要内容,如果未能解决你的问题,请参考以下文章

又见mybaits之selectall源码解析

Mybaits 源码解析 ----- 全网最详细,没有之一:一级缓存和二级缓存源码分析

Mybaits 源码解析 ----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)

Mybaits 源码解析 ----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?

MyBaits集合的嵌套 Select 查询

mybaits-spring 源码阅读随笔