Mybatis源码分析(九)Mybatis的PreparedStatement

news/2024/4/20 10:09:11/文章来源:https://blog.csdn.net/weixin_44451022/article/details/129040430

文章目录

  • 一 JDBC的PreparedStatement
  • 二 prepareStatement的准备阶段
    • 2.1 获取Connection
      • 2.1.1 **UnpooledDataSource**
      • 2.1.2 PooledDataSource
    • 2.2 Sql的预编译PreparedStatementHandler
    • 2.3 为Statement设置参数
    • 2.4 执行具体的语句过程

  • 官网:mybatis – MyBatis 3 | 简介
  • 参考书籍:《通用源码阅读指导书:MyBatis源码详解》 易哥
  • 参考文章:Mybatis源码解析
  • CSDN:Mybatis源码分析_长安不及十里的博客-CSDN博客
  • 掘金:稀土掘金
  • 语雀文档地址:Mybatis 源码分析
  • 个人博客地址:mybatis – 疯狂的程序员

上一篇文章我们分析了,Select的语句的执行过程,但是在与数据库打交道的那一块,我们没有详细介绍,下面我们来看看Mybatis中是如何与数据库打交道的?
SimpleExecutor

  @Overridepublic <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();// 创建处理器  StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 预编译参数   stmt = prepareStatement(handler, ms.getStatementLog());return handler.query(stmt, resultHandler);} finally {closeStatement(stmt);}}

一 JDBC的PreparedStatement

image.png
文档说明:
表示预编译SQL语句的对象。 SQL语句被预编译并存储在PreparedStatement对象中。然后,可以使用该对象多次有效地执行此语句。 注意:用于设置IN形参值的setter方法(setShort、setString等)必须指定与输入形参定义的SQL类型兼容的类型。例如,如果IN参数的SQL类型为INTEGER,则应该使用方法setInt。 如果需要任意参数类型转换,则应该将方法setObject与目标SQL类型一起使用。 在下面的参数设置示例中,con表示活动连接:

PreparedStatement pstmt = con.prepareStatement("UPDATE EMPLOYEES . 设定工资= ?Where id = ?"); 
pstmt.setBigDecimal (153833.00) 
pstmt.setInt (110592)

JDBC中java.sql.PreparedStatement是java.sql.Statement的子接口,它主要提供了无参数执行方法如executeQuery和executeUpdate等,以及大量形如set{Type}(int, {Type})形式的方法用于设置参数。
PreparedStatement

public interface PreparedStatement extends Statement {// 执行PreparedStatement对象中的SQL查询,并返回由该查询生成的ResultSet对象。ResultSet executeQuery() throws SQLException;// 执行PreparedStatement对象中的SQL语句,该语句必须是SQL数据操作语言(DML)语句,如INSERT、UPDATE或DELETE;或不返回任何结果的SQL语句,如DDL语句。int executeUpdate() throws SQLException;// 其他省略
}

这里我们可以看到executeQuery与executeUpdate方法前者主要是处理Select语句后者处理INSERT、UPDATE或DELETE语句,这里他的子类太多了我们介绍与Mybatis相关的CallableStatement与JdbcPreparedStatement
PreparedStatement.png

  • CallableStatement:用于执行SQL存储过程的接口。JDBC API提供了一种存储过程SQL转义语法,允许以标准方式对所有rdbms调用存储过程。此转义语法有一种形式包含结果形参,另一种形式不包含结果形参。如果使用,结果参数必须注册为OUT参数。其他参数可用于输入、输出或同时用于输入和输出。参数按数字顺序引用,第一个参数为1。 {?= call [( , ,…)]} {call [( , ,…)]}
  • JdbcPreparedStatement:表示预编译SQL语句的对象。 SQL语句被预编译并存储在PreparedStatement对象中。

下面:我们来看看在JDBC中使用PreparedStatement

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;public class PreparedStatementTest {public static void main(String[] args) throws Throwable {Class.forName("com.mysql.jdbc.Driver");String url = "jdbc:mysql://localhost/test";// 获取连接try (Connection con = DriverManager.getConnection(url, "root", null)) {String sql = "insert into t select ?,?";// 预编译SqlPreparedStatement statement = con.prepareStatement(sql);// 设置参数statement.setInt(1, 123456);statement.setString(2, "abc");// 执行statement.executeUpdate();// 关闭statement.close();}}
}

通常我们的一条sql在db接收到最终执行完毕返回可以分为下面三个过程:

  1. 词法和语义解析
  2. 优化sql语句,制定执行计划
  3. 执行并返回结果

我们把这种普通语句称作Immediate Statements

二 prepareStatement的准备阶段

让我们回到之前的准备工作?
ReuseExecutor

  @Overridepublic int doUpdate(MappedStatement ms, Object parameter) throws SQLException {// 获取好的解析文件   Configuration configuration = ms.getConfiguration();// 参数与处理器  StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);Statement stmt = prepareStatement(handler, ms.getStatementLog());return handler.update(stmt);}

image.png

  • SimpleStatementHandler,这个很简单了,就是对应我们JDBC中常用的Statement接口,用于简单SQL的处理;
  • PreparedStatementHandler,这个对应JDBC中的PreparedStatement,预编译SQL的接口;
  • CallableStatementHandler,这个对应JDBC中CallableStatement,用于执行存储过程相关的接口;
  • RoutingStatementHandler,这个接口是以上三个接口的路由,没有实际操作,只是负责上面三个StatementHandler的创建及调用。
  1. 首先获取Connection
  2. SQL预编译
  3. 设置参数
  4. 执行Sql,返回结果
  5. 关闭

2.1 获取Connection

首先我们来看看获取Connection,是如何获取的?
ReuseExecutor

 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;BoundSql boundSql = handler.getBoundSql();String sql = boundSql.getSql();if (hasStatementFor(sql)) {stmt = getStatement(sql);applyTransactionTimeout(stmt);} else {Connection connection = getConnection(statementLog);stmt = handler.prepare(connection, transaction.getTimeout());putStatement(sql, stmt);}handler.parameterize(stmt);return stmt;}

BaseExecutor

  /*** 获取一个Connection对象* @param statementLog 日志对象* @return Connection对象* @throws SQLException*/protected Connection getConnection(Log statementLog) throws SQLException {Connection connection = transaction.getConnection();if (statementLog.isDebugEnabled()) { // 启用调试日志// 生成Connection对象的具有日志记录功能的代理对象ConnectionLogger对象return ConnectionLogger.newInstance(connection, statementLog, queryStack);} else {// 返回原始的Connection对象return connection;}}

transaction事务工厂,在DefaultSqlSessionFactory#openSession()创建,在我们的配置中JdbcTransactionFactory
DefaultSqlSessionFactory

 /*** 从数据源中获取SqlSession对象* @param execType 执行器类型* @param level 事务隔离级别* @param autoCommit 是否自动提交事务* @return 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);// 创建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 TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {if (environment == null || environment.getTransactionFactory() == null) {return new ManagedTransactionFactory();}return environment.getTransactionFactory();}

让我们来看看Transaction类的接口信息,他的实现类:JdbcTransaction与ManagedTransaction
image.png
Transaction

public interface Transaction {/*** 获取该事务对应的数据库连接* @return 数据库连接* @throws SQLException*/Connection getConnection() throws SQLException;/*** 提交事务* @throws SQLException*/void commit() throws SQLException;/*** 回滚事务* @throws SQLException*/void rollback() throws SQLException;/*** 关闭对应的数据连接* @throws SQLException*/void close() throws SQLException;/*** 读取设置的事务超时时间* @return 事务超时时间* @throws SQLException*/Integer getTimeout() throws SQLException;}

Transaction接口只是定义了基本的方法,关键在于他的实现,我们来看看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 autoCommit;public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {dataSource = ds;level = desiredLevel;autoCommit = desiredAutoCommit;}public JdbcTransaction(Connection connection) {this.connection = connection;}@Overridepublic Connection getConnection() throws SQLException {if (connection == null) {openConnection();}return connection;}/*** 提交事务* @throws SQLException*/@Overridepublic void commit() throws SQLException {// 连接存在且不会自动提交事务if (connection != null && !connection.getAutoCommit()) {if (log.isDebugEnabled()) {log.debug("Committing JDBC Connection [" + connection + "]");}// 调用connection对象的方法提交事务connection.commit();}}/*** 回滚事务* @throws SQLException*/@Overridepublic void rollback() throws SQLException {if (connection != null && !connection.getAutoCommit()) {if (log.isDebugEnabled()) {log.debug("Rolling back JDBC Connection [" + connection + "]");}connection.rollback();}}// 关闭@Overridepublic void close() throws SQLException {if (connection != null) {resetAutoCommit();if (log.isDebugEnabled()) {log.debug("Closing JDBC Connection [" + connection + "]");}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);}// 其他方法省略  
}

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

  @Overridepublic Connection getConnection() throws SQLException {if (connection == null) {openConnection();}return connection;}protected void openConnection() throws SQLException {if (log.isDebugEnabled()) {log.debug("Opening JDBC Connection");}// 从数据源中获取connection这与上面的案例是一致的connection = dataSource.getConnection();if (level != null) {connection.setTransactionIsolation(level.getLevel());}setDesiredAutoCommit(autoCommit);}
  • 我们再来看看dataSource.getConnection()这个方法获取connection

这里先看看官方文档对dataSource的配置信息吧:虽然数据源配置是可选的,但如果要启用延迟加载特性,就必须配置数据源,有三种内建的数据源类型(也就是 type=“[UNPOOLED|POOLED|JNDI]”):
image.png
对数据源元素的解析,请参考前面的文章对environments元素进行解析,下面贴关键代码
XMLConfigBuilder

  /*** 解析配置信息,获取数据源工厂* 被解析的配置信息示例如下:* <dataSource type="POOLED">*   <property name="driver" value="{dataSource.driver}"/>*   <property name="url" value="{dataSource.url}"/>*   <property name="username" value="${dataSource.username}"/>*   <property name="password" value="${dataSource.password}"/>* </dataSource>** @param context 被解析的节点* @return 数据源工厂* @throws Exception*/private DataSourceFactory dataSourceElement(XNode context) throws Exception {if (context != null) {// 通过这里的类型判断数据源类型,例如POOLED、UNPOOLED、JNDIString type = context.getStringAttribute("type");// 获取dataSource节点下配置的propertyProperties props = context.getChildrenAsProperties();// 根据dataSource的type值获取相应的DataSourceFactory对象DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();// 设置DataSourceFactory对象的属性factory.setProperties(props);return factory;}throw new BuilderException("Environment declaration requires a DataSourceFactory.");}

扩展问题:DefaultSqlSession线程安全吗?:Mybatis(二)SqlSession之线程安全
下面我们仔细来介绍下数据源的创建?首先我们看看DataSource接口信息
DataSource

public interface DataSource  extends CommonDataSource, Wrapper {Connection getConnection() throws SQLException;Connection getConnection(String username, String password)throws SQLException;
}

image.png
我们再来看看他的实现类PooledDataSource与UnpooledDataSource,池化与非池化,首先看看UnpooledDataSource的成员变量,他的步骤如下

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

2.1.1 UnpooledDataSource

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;// 最长等待时间。发出请求后,最长等待该时间后如果数据库还没有回应,则认为失败private Integer defaultNetworkTimeout;static {// 首先将java.sql.DriverManager中的驱动都加载进来Enumeration<Driver> drivers = DriverManager.getDrivers();while (drivers.hasMoreElements()) {Driver driver = drivers.nextElement();registeredDrivers.put(driver.getClass().getName(), driver);}}
}

UnpooledDataSource

// 传入用户密码
@Overridepublic 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) {props.setProperty("user", username);}if (password != null) {props.setProperty("password", password);}return doGetConnection(props);}/*** 建立数据库连接* @param properties 里面包含建立连接的"user"、"password"、驱动配置信息* @return 数据库连接对象* @throws SQLException*/private Connection doGetConnection(Properties properties) throws SQLException {// 初始化驱动initializeDriver();// 通过DriverManager获取连接Connection connection = DriverManager.getConnection(url, properties);// 配置连接,要设置的属性有defaultNetworkTimeout、autoCommit、defaultTransactionIsolationLevelconfigureConnection(connection);return connection;}
  • 我们来看看他初始化数据库驱动方法initializeDriver()来初始化数据库驱动

UnpooledDataSource

/*** 初始化数据库驱动* @throws SQLException*/private synchronized void initializeDriver() throws SQLException {if (!registeredDrivers.containsKey(driver)) { // 如果所需的驱动尚未被注册到registeredDriversClass<?> driverType;try {if (driverClassLoader != null) { // 如果存在驱动类加载器// 优先使用驱动类加载器加载驱动类driverType = Class.forName(driver, true, driverClassLoader);} else {// 使用Resources中的所有加载器加载驱动类driverType = Resources.classForName(driver);}// 实例化驱动Driver driverInstance = (Driver)driverType.newInstance();// 向DriverManager注册该驱动的代理DriverManager.registerDriver(new DriverProxy(driverInstance));// 注册到registeredDrivers,表明该驱动已经加载registeredDrivers.put(driver, driverInstance);} catch (Exception e) {throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);}}}

通过反射机制加载驱动Driver,并将其注册到DriverManager中的一个常量集合中,供后面获取连接时使用,为什么这里是一个List呢?我们实际开发中有可能使用到了多种数据库类型,如Mysql、Oracle等,其驱动都是不同的,不同的数据源获取连接时使用的是不同的驱动。

  • 我们仔细从驱动管理器中来看看他说如何获取理数据库连接的

Connection connection = DriverManager.getConnection(url, properties)
DriverManager

 //  Worker method called by the public getConnection() methods.private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {// 当callerCl为空时,我们应该检查应用程序的(间接调用该类的)类加载器,以便可以从这里加载rt.jar外部的JDBC驱动程序类。ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;synchronized(DriverManager.class) {// synchronize loading of the correct classloader.if (callerCL == null) {callerCL = Thread.currentThread().getContextClassLoader();}}if(url == null) {throw new SQLException("The url cannot be null", "08001");}println("DriverManager.getConnection(\"" + url + "\")");// 遍历已加载的registeredDrivers,试图建立连接。// 记住第一个被引发的异常,这样我们就可以重新引发它。SQLException reason = null;// 遍历已经注册是使用驱动for(DriverInfo aDriver : registeredDrivers) {// 如果调用者没有加载驱动程序的权限,那么跳过它。if(isDriverAllowed(aDriver.driver, callerCL)) {try {println("    trying " + aDriver.driver.getClass().getName());Connection con = aDriver.driver.connect(url, info);if (con != null) {// Success!println("getConnection returning " + aDriver.driver.getClass().getName());return (con);}} catch (SQLException ex) {if (reason == null) {reason = ex;}}} else {println("    skipping: " + aDriver.getClass().getName());}}// if we got here nobody could connect.if (reason != null)    {println("getConnection failed: " + reason);throw reason;}println("getConnection: no suitable driver found for "+ url);throw new SQLException("No suitable driver found for "+ url, "08001");}
  • 从配置文件中来配置连接比如:超时,自动提交,事务隔离级别

configureConnection(connection)
UnpooledDataSource

 private void configureConnection(Connection conn) throws SQLException {if (defaultNetworkTimeout != null) {conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), defaultNetworkTimeout);}if (autoCommit != null && autoCommit != conn.getAutoCommit()) {conn.setAutoCommit(autoCommit);}if (defaultTransactionIsolationLevel != null) {conn.setTransactionIsolation(defaultTransactionIsolationLevel);}}

到最后返回数据库连接对象Connection,从源码可以看到最后返回来JDBCConnection

2.1.2 PooledDataSource

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

public class PooledDataSource implements DataSource {private static final Log log = LogFactory.getLog(PooledDataSource.class);// 池化状态private final PoolState state = new PoolState(this);// 持有一个UnpooledDataSource对象private final UnpooledDataSource dataSource;// 和连接池设置有关的配置项protected int poolMaximumActiveConnections = 10;protected int poolMaximumIdleConnections = 5;protected int poolMaximumCheckoutTime = 20000;protected int poolTimeToWait = 20000;protected int poolMaximumLocalBadConnectionTolerance = 3;protected String poolPingQuery = "NO PING QUERY SET";protected boolean poolPingEnabled;protected int poolPingConnectionsNotUsedFor;// 存储池子中的连接的编码,编码用("" + url + username + password).hashCode()算出来// 因此,整个池子中的所有连接的编码必须是一致的,里面的连接是等价的private int expectedConnectionTypeCode;}

PoolState 用于记录连接池运行时的状态,比如连接获取次数,无效连接数量等。同时 PoolState 内部定义了两个 PooledConnection 集合,用于存储空闲连接和活跃连接。
PoolState

public class PoolState {// 池化数据源protected PooledDataSource dataSource;// 空闲的连接protected final List<PooledConnection> idleConnections = new ArrayList<>();// 活动的连接protected final List<PooledConnection> activeConnections = new ArrayList<>();// 连接被取出的次数protected long requestCount = 0;// 取出请求花费时间的累计值。从准备取出请求到取出结束的时间为取出请求花费的时间protected long accumulatedRequestTime = 0;// 累积被检出的时间protected long accumulatedCheckoutTime = 0;// 声明的过期连接数protected long claimedOverdueConnectionCount = 0;// 过期的连接数的总检出时长protected long accumulatedCheckoutTimeOfOverdueConnections = 0;// 总等待时间protected long accumulatedWaitTime = 0;// 等待的轮次protected long hadToWaitCount = 0;// 坏连接的数目protected long badConnectionCount = 0;public PoolState(PooledDataSource dataSource) {this.dataSource = dataSource;}
}

PooledConnection 内部定义了一个 Connection 类型的变量,用于指向真实的数据库连接。以及一个 Connection 的代理类,用于对部分方法调用进行拦截,至于为什么要拦截,随后将进行分析。除此之外,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;// 真正的Connectionprivate final Connection realConnection;// 代理Connectionprivate final Connection proxyConnection;// 从连接池中取出的时间private long checkoutTimestamp;// 创建时间private long createdTimestamp;// 上次使用时间private long lastUsedTimestamp;// 标志所在连接池的连接类型编码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;// 参数依次是:被代理对象的类加载器  被代理对象的接口 包含代理对象的类(实现InvocationHandler接口的类)this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);}}

获取连接调用了PooledDataSource#popConnection方法

/*** 从池化数据源中给出一个连接* @param username 用户名* @param password 密码* @return 池化的数据库连接* @throws SQLException*/private PooledConnection popConnection(String username, String password) throws SQLException {boolean countedWait = false;PooledConnection conn = null;// 用于统计取出连接花费的时长的时间起点long t = System.currentTimeMillis();int localBadConnectionCount = 0;while (conn == null) {// 给state加同步锁synchronized (state) {if (!state.idleConnections.isEmpty()) { // 池中存在空闲连接// 左移操作,取出第一个连接conn = state.idleConnections.remove(0);if (log.isDebugEnabled()) {log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");}} else { // 池中没有空余连接if (state.activeConnections.size() < poolMaximumActiveConnections) { // 池中还有空余位置// 可以创建新连接,也是通过DriverManager.getConnection拿到的连接conn = new PooledConnection(dataSource.getConnection(), this);if (log.isDebugEnabled()) {log.debug("Created connection " + conn.getRealHashCode() + ".");}} else { // 连接池已满,不能创建新连接// 找到借出去最久的连接PooledConnection oldestActiveConnection = state.activeConnections.get(0);// 查看借出去最久的连接已经被借了多久long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();if (longestCheckoutTime > poolMaximumCheckoutTime) { // 借出时间超过设定的借出时长// 声明该连接超期不还state.claimedOverdueConnectionCount++;state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;state.accumulatedCheckoutTime += longestCheckoutTime;// 因超期不还而从池中除名state.activeConnections.remove(oldestActiveConnection);if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { // 如果超期不还的连接没有设置自动提交事务// 尝试替它提交回滚事务try {oldestActiveConnection.getRealConnection().rollback();} catch (SQLException e) {// 即使替它回滚事务的操作失败,也不抛出异常,仅仅做一下记录log.debug("Bad connection. Could not roll back");}}// 新建一个连接替代超期不还连接的位置conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());oldestActiveConnection.invalidate();if (log.isDebugEnabled()) {log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");}} else { // 借出去最久的连接,并未超期// 继续等待,等待有连接归还到连接池try {if (!countedWait) {// 记录发生等待的次数。某次请求等待多轮也只能算作发生了一次等待state.hadToWaitCount++;countedWait = true;}if (log.isDebugEnabled()) {log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");}long wt = System.currentTimeMillis();// 沉睡一段时间再试,防止一直占有计算资源state.wait(poolTimeToWait);state.accumulatedWaitTime += System.currentTimeMillis() - wt;} catch (InterruptedException e) {break;}}}}if (conn != null) { // 取到了连接// 判断连接是否可用if (conn.isValid()) { // 如果连接可用if (!conn.getRealConnection().getAutoCommit()) { // 该连接没有设置自动提交// 回滚未提交的操作conn.getRealConnection().rollback();}// 每个借出去的连接都到打上数据源的连接类型编码,以便在归还时确保正确conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));// 数据记录操作conn.setCheckoutTimestamp(System.currentTimeMillis());conn.setLastUsedTimestamp(System.currentTimeMillis());state.activeConnections.add(conn);state.requestCount++;state.accumulatedRequestTime += System.currentTimeMillis() - t;} else { // 连接不可用if (log.isDebugEnabled()) {log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");}state.badConnectionCount++;localBadConnectionCount++;// 直接删除连接conn = null;// 如果没有一个连接能用,说明连不上数据库if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {if (log.isDebugEnabled()) {log.debug("PooledDataSource: Could not get a good connection to the database.");}throw new SQLException("PooledDataSource: Could not get a good connection to the database.");}}}}// 如果到这里还没拿到连接,则会循环此过程,继续尝试取连接}if (conn == null) {if (log.isDebugEnabled()) {log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");}throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");}return conn;}

从连接池中获取连接首先会遇到两种情况:

  1. 连接池中有空闲连接
  2. 连接池中无空闲连接

对于第一种情况,把连接取出返回即可。对于第二种情况,则要进行细分,会有如下的情况。

  1. 活跃连接数没有超出最大活跃连接数
  2. 活跃连接数超出最大活跃连接数

对于上面两种情况,第一种情况比较好处理,直接创建新的连接即可。至于第二种情况,需要再次进行细分。

  1. 活跃连接的运行时间超出限制,即超时了
  2. 活跃连接未超时

对于第一种情况,我们直接将超时连接强行中断,并进行回滚,然后复用部分字段重新创建 PooledConnection 即可。对于第二种情况,目前没有更好的处理方式了,只能等待了。

  • 回收连接相比于获取连接,回收连接的逻辑要简单的多。回收连接成功与否只取决于空闲连接集合的状态,所需处理情况很少,因此比较简单。

PooledConnection

 /*** 代理方法* @param proxy 代理对象,未用* @param method 当前执行的方法* @param args 当前执行的方法的参数* @return 方法的返回值* @throws Throwable*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 获取方法名String methodName = method.getName();if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { // 如果调用了关闭方法// 那么把Connection返回给连接池,而不是真正的关闭dataSource.pushConnection(this);return null;}try {// 校验连接是否可用if (!Object.class.equals(method.getDeclaringClass())) {checkConnection();}// 用真正的连接去执行操作return method.invoke(realConnection, args);} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}}

我们来看看pushConnection方法,回收一个连接

 /*** 收回一个连接* @param conn 连接* @throws SQLException*/protected void pushConnection(PooledConnection conn) throws SQLException {synchronized (state) {// 将该连接从活跃连接中删除state.activeConnections.remove(conn);if (conn.isValid()) { // 当前连接是可用的// 判断连接池未满 + 该连接确实属于该连接池if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {state.accumulatedCheckoutTime += conn.getCheckoutTime();if (!conn.getRealConnection().getAutoCommit()) { // 如果连接没有设置自动提交// 将未完成的操作回滚conn.getRealConnection().rollback();}// 重新整理连接PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);// 将连接放入空闲连接池state.idleConnections.add(newConn);newConn.setCreatedTimestamp(conn.getCreatedTimestamp());newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());// 设置连接为未校验,以便取出时重新校验conn.invalidate();if (log.isDebugEnabled()) {log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");}state.notifyAll();} else { // 连接池已满或者该连接不属于该连接池state.accumulatedCheckoutTime += conn.getCheckoutTime();if (!conn.getRealConnection().getAutoCommit()) {conn.getRealConnection().rollback();}// 直接关闭连接,而不是将其放入连接池中conn.getRealConnection().close();if (log.isDebugEnabled()) {log.debug("Closed connection " + conn.getRealHashCode() + ".");}conn.invalidate();}} else { // 当前连接不可用if (log.isDebugEnabled()) {log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");}state.badConnectionCount++;}}}

先将连接从活跃连接集合中移除,如果空闲集合未满,此时复用原连接的字段信息创建新的连接,并将其放入空闲集合中即可;若空闲集合已满,此时无需回收连接,直接关闭即可。
好了,我们已经获取到了数据库连接,接下来要创建PrepareStatement了,我们上面JDBC的例子是怎么获取的? psmt = conn.prepareStatement(sql);,直接通过Connection来获取,并且把sql传进去了,我们看看Mybaits中是怎么创建PrepareStatement的

2.2 Sql的预编译PreparedStatementHandler

让我们回到之前的步骤,我们已经获取数据库连接,接下来来看看Sql的预编译,从Connection中创建一个Statement

 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;BoundSql boundSql = handler.getBoundSql();String sql = boundSql.getSql();if (hasStatementFor(sql)) {stmt = getStatement(sql);applyTransactionTimeout(stmt);} else {Connection connection = getConnection(statementLog);stmt = handler.prepare(connection, transaction.getTimeout());putStatement(sql, stmt);}handler.parameterize(stmt);return stmt;}

BaseStatementHandler

// 从连接中获取一个Statement,并设置事务超时时间
@Overridepublic Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {ErrorContext.instance().sql(boundSql.getSql());Statement statement = null;try {// 获取一个Statement对象statement = instantiateStatement(connection);// 设置设置查询超时时间setStatementTimeout(statement, transactionTimeout);// 获取数据大小限制setFetchSize(statement);return statement;} catch (SQLException e) {closeStatement(statement);throw e;} catch (Exception e) {closeStatement(statement);throw new ExecutorException("Error preparing statement.  Cause: " + e, e);}
}

我们接着来看看instantiateStatemen接口,根据上面的的信息,我们这的默认实现是PreparedStatementHandler
image.png
PreparedStatementHandler

  @Overrideprotected Statement instantiateStatement(Connection connection) throws SQLException {//  //获取sql字符串,比如"select * from user where id= ?"String sql = boundSql.getSql();// / 根据条件调用不同的 prepareStatement 方法创建 PreparedStatement if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {String[] keyColumnNames = mappedStatement.getKeyColumns();if (keyColumnNames == null) {//通过connection获取Statement,将sql语句传进去return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);} else {return connection.prepareStatement(sql, keyColumnNames);}} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {return connection.prepareStatement(sql);} else {return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);}}

我们接着上面connection.prepareStatement来看看他到底是如何创建的
JDBCConnection

// 创建一个默认的PreparedStatement对象,该对象具有检索自动生成的键的功能。
public synchronized PreparedStatement prepareStatement(String sql,int autoGeneratedKeys) throws SQLException {// 检测内部是否关闭checkClosed();try {if (autoGeneratedKeys != ResultConstants.RETURN_GENERATED_KEYS&& autoGeneratedKeys!= ResultConstants.RETURN_NO_GENERATED_KEYS) {throw JDBCUtil.invalidArgument("autoGeneratedKeys");}// 构造生成所请求类型结果的语句。 预处理语句必须是一条SQL语句。return new JDBCPreparedStatement(this, sql,JDBCResultSet.TYPE_FORWARD_ONLY,JDBCResultSet.CONCUR_READ_ONLY, rsHoldability,autoGeneratedKeys, null, null);} catch (HsqlException e) {throw JDBCUtil.sqlException(e);}}

到这我们就返回一个Statement对象,下面我们来看看设置参数

2.3 为Statement设置参数

SimpleExecutor

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;Connection connection = getConnection(statementLog);stmt = handler.prepare(connection, transaction.getTimeout());handler.parameterize(stmt);return stmt;}

我们这里StatementHandler在前面我们已经看到了是PreparedStatementHandler,我们来看看他是如何处理的
PreparedStatementHandler

 @Overridepublic void parameterize(Statement statement) throws SQLException {parameterHandler.setParameters((PreparedStatement) statement);}
 /*** 为语句设置参数* @param ps 语句*/@Overridepublic void setParameters(PreparedStatement ps) {ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());// 取出参数列表List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();if (parameterMappings != null) {for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);// ParameterMode.OUT是CallableStatement的输出参数,已经单独注册。故忽略if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;// 取出属性名称String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) {// 从附加参数中读取属性值value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {// 参数对象是基本类型,则参数对象即为参数值value = parameterObject;} else {// 参数对象是复杂类型,取出参数对象的该属性值MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}// 确定该参数的处理器TypeHandler typeHandler = parameterMapping.getTypeHandler();JdbcType jdbcType = parameterMapping.getJdbcType();if (value == null && jdbcType == null) {jdbcType = configuration.getJdbcTypeForNull();}try {// 此方法最终根据参数类型,调用java.sql.PreparedStatement类中的参数赋值方法,对SQL语句中的参数赋值typeHandler.setParameter(ps, i + 1, value, jdbcType);} catch (TypeException | SQLException e) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);}}}}}

可以看到调用不同的参数处理器来设置参数类型,我们先看看TypeHandler这个接口信息
TypeHandler

public interface TypeHandler<T> {/*** 设置参数* @param ps* @param i* @param parameter* @param jdbcType* @throws SQLException*/void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;/*** 获取结果* @param rs* @param columnName* @return* @throws SQLException*/T getResult(ResultSet rs, String columnName) throws SQLException;T getResult(ResultSet rs, int columnIndex) throws SQLException;T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}

调用不同的参数处理器,来设置不同的参数这里的类型调用来设置参数
image.png
BaseTypeHandler来处理的设置参数类型,而许多类型的处理器继承BaseTypeHandler
image.png
最后根据参数类型,调用java.sql.PreparedStatement类中的参数赋值方法,对SQL语句中的参数赋值
DefaultParameterHandler

typeHandler.setParameter(ps, i + 1, value, jdbcType);

参数也设置好了,下面我们来看看执行具体的查询语句吧

2.4 执行具体的语句过程

回到刚开始的地方
SimpleExecutor

 @Overridepublic int doUpdate(MappedStatement ms, Object parameter) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();// 获取不同的StatementHandler处理器StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);// 获取到不同的 Statement并完成参数绑定stmt = prepareStatement(handler, ms.getStatementLog());// 交给具体的StatementHandler实现执行return handler.update(stmt);} finally {closeStatement(stmt);}}

我们以默认为例:PreparedStatementHandler
PreparedStatementHandler

 @Overridepublic int update(Statement statement) throws SQLException {//ClientPreparedStatement对象PreparedStatement ps = (PreparedStatement) statement;ps.execute();// 返回影响条数int rows = ps.getUpdateCount();// 参数对象Object parameterObject = boundSql.getParameterObject();// 主键自增器KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();// 后置执行主键自增keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);return rows;}

从前面的数据库连接开始,我们就可以知道PreparedStatement返回的对象是ClientPreparedStatement,因此我们来看看他的execute方法
image.png
ClientPreparedStatement
image.png

  @Overridepublic boolean execute() throws SQLException {// 其他代码省略rs = executeInternal(this.maxRows, sendPacket, createStreamingResultSet(),(((PreparedQuery<?>) this.query).getParseInfo().getFirstStmtChar() == 'S'), cachedMetadata, false);// 其他代码省略}

关键在于调用executeInternal方法,感兴趣的朋友可以自己通过Debug去查看
image.png
到最后我们可以拿到ResultSetImpl中拿到执行的结果
image.png
PreparedStatementHandler

   int rows = ps.getUpdateCount();

ClientPreparedStatement

    @Overridepublic int getUpdateCount() throws SQLException {// 调用父类的方法int count = super.getUpdateCount();if (containsOnDuplicateKeyUpdateInSQL() && this.compensateForOnDuplicateKeyUpdate) {if (count == 2 || count == 0) {count = 1;}}return count;}

详细的过程,请参考mysql驱动包的源码部分

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_258434.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【蓝桥杯嵌入式】第十三届蓝桥杯嵌入式 国赛 程序设计试题以及详细题解

文章目录原题展示原题分析详细题解LED模块按键模块串口LCD模块模拟电压读取(ADC)脉冲输入输出文章福利原题展示 原题分析 本届国赛试题主要包含LCD、LED、按键、EEPROM、串口、模拟电压输入、脉冲输入输出七大部分&#xff0c;其中前面三个部分是蓝桥杯嵌入式的“亲儿子”(必考…

chatGPT做了这么一道数学题,我陷入了沉思

已知 2cos23cosasina−3sin2a1,并且a∈(−32π,−π)2cos^{2}3cosasina-3sin^{2}a1 ,并且a\in(-\frac{3}{2}\pi,-\pi) 2cos23cosasina−3sin2a1,并且a∈(−23​π,−π) 求 2sin(π−a)−3sin(π2a))4sina−9cosa\frac{2sin(\pi-a)-3sin(\frac{\pi}{2}a))}{4sina-9cosa} 4sina−…

分布式光伏储能系统的优化配置方法(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

IO知识整理

IO 面向系统IO page cache 程序虚拟内存到物理内存的转换依靠cpu中的mmu映射 物理内存以page&#xff08;4k&#xff09;为单位做分配 多个程序访问磁盘上同一个文件&#xff0c;步骤 kernel将文件内容加载到pagecache多个程序读取同一份文件指向的同一个pagecache多个程…

VMware 修复了三个身份认证绕过漏洞

Bleeping Computer 网站披露&#xff0c;VMware 近期发布了安全更新&#xff0c;以解决 Workspace ONE Assist 解决方案中的三个严重漏洞&#xff0c;分别追踪为 CVE-2022-31685&#xff08;认证绕过&#xff09;、CVE-2022-31686 &#xff08;认证方法失败&#xff09;和 CVE-…

2023想转行软件测试的看过来,你想要了解的薪资、前景、岗位方向、学习路线都讲明白了

在过去的一年中&#xff0c;软件测试行业发展迅速&#xff0c;随着数字化技术应用的广泛普及&#xff0c;业界对于软件测试的要求也在持续迭代与增加。 同样的&#xff0c;有市场就有需求&#xff0c;软件测试逐渐成为企业中不可或缺的岗位&#xff0c;作为一个高薪又需求广的…

RTT 消息邮箱

1.邮箱概念 邮箱服务是实时操作系统中一种典型的线程间通信方法。举一个简单的例子&#xff0c;有两个线程&#xff0c;线程 1 检测按键状态并发送&#xff0c;线程 2 读取按键状态并根据按键的状态相应地改变 LED 的亮灭。这里就可以使用邮箱的方式进行通信&#xff0c;线程 …

软件测试选Python还是Java?

目录 前言 1、先从一门语言开始 2、两个语言的区别 3、两个语言的测试栈技术 4、如何选择两种语言&#xff1f; 总结 前言 对于工作多年的从业者来说&#xff0c;同时掌握java和Python两门语言再好不过&#xff0c;可以大大增加找工作时的选择范围。但是对于转行的人或者…

Spring Cloud Gateway集成Nacos实现负载均衡

&#x1f4a1;Nacas可以用于实现Spring Cloud Gateway中网关动态路由功能&#xff0c;也可以基于Nacos来实现对后端服务的负载均衡&#xff0c;前者利用Nacos配置中心功能&#xff0c;后者利用Nacos服务注册功能。接下来我们来看下Gateway集成Nacos实现负载均衡的架构图一. 环境…

央行数据-一款查逆回购 LPR 货币供应量 资产负债表 Shibor 数据的专业工具

自己开发的APP, App Store搜索"央行数据" 即可下载欢迎大家下载,给修改意见逆回购、正回购、MLF、票据&#xff0c;俗称央行发钱房贷基准利率多少? M2/M1/M0, 资产负债表,Shibor 了解下这款APP是经济,投资理财,股市,房价分析参考利器适用于关注经济、货币政策的用户…

Spring Cloud Alibaba环境搭建

环境依赖 SpringCloud Alibaba 依赖 Java环境来运行。还需要为此配置 Maven环境&#xff0c;请确保是在以下版本环境中安装使用: 1. 64 bit JDK 1.8&#xff1b;下载& 配置。 1.8.0_131 2. Maven 3.2.x&#xff1b;下载& 配置搭建微服务 1.建立微服务项目 1.idea通过…

Python编程 动态爱心

作者简介&#xff1a;一名在校计算机学生、每天分享Python的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.所用库 1.random简介 2.math 简介 3.tkinter库的简介 二.实际图 三.…

机器学习笔记之生成模型综述(五)重参数化技巧(随机反向传播)

机器学习笔记之生成模型综述——重参数化技巧[随机反向传播]引言回顾神经网络的执行过程变分推断——重参数化技巧重参数化技巧(随机反向传播)介绍示例描述——联合概率分布示例描述——条件概率分布总结引言 本节将系统介绍重参数化技巧。 回顾 神经网络的执行过程 上一节…

android 混淆后的异常信息 工具处理

我们可以利用SDK中tools下的proguardgui.bat工具和混淆对应文档进行反混淆处理 D:\Android\sdk\tools\proguard\bin\proguardgui.bat 工具在SDK中的位置&#xff0c;有的SDK版本不同这个工具的具体位置可能有改变&#xff0c;也可以在tools中直接搜索proguardgui.bat&#x…

【并发编程】【2】进程与线程

并发编程 2.进程与线程 2.1 进程与线程 进程 程序由指令和数据组成&#xff0c;但这些指令要运行&#xff0c;数据要读写&#xff0c;就必须将指令加载至 CPU&#xff0c;数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管…

MQ技术选型

RocketMQ部署架构图NameServer&#xff1a;主要是对元数据的管理&#xff0c;包括Topic和路由信息的管理&#xff0c;底层由netty实现&#xff0c;是一个提供路由管理、路由注册和发现的无状态节点&#xff0c;类似于ZooKeeperBroker&#xff1a;消息中转站&#xff0c;负责收发…

chatGPT 配合excel /maxscript使用_初试

EXCEL 配合chatGPT方法一&#xff1a;利用excel的開發工具1打開excel的VB编辑器&#xff1a;如果頂部菜單上沒有看到開發工具&#xff0c;需要先按下面步驟打開開發工具&#xff1a;這樣按上面步驟就可以打開excel的開發工具~ 然後點擊VB~ 2让chatGPT帮忙写脚本在chatGPT上提问…

智慧校园人脸识别系统源码

智慧校园人脸识别系统源码 技术架构&#xff1a; 后端&#xff1a;Java 框架&#xff1a;springboot 前端页面&#xff1a;vue element-ui 小程序&#xff1a;小程序原生开发 电子班牌&#xff1a;Java Android 源码有演示&#xff0c;可正常上线运营可授权。 随着智慧校…

对撞双指针(一) 盛水最多的容器

描述 给定一个数组height&#xff0c;长度为n&#xff0c;每个数代表坐标轴中的一个点的高度&#xff0c;height[i]是在第i点的高度&#xff0c;请问&#xff0c;从中选2个高度与x轴组成的容器最多能容纳多少水 1.你不能倾斜容器 2.当n小于2时&#xff0c;视为不能形成容器&…

【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢

HashMap中为什么引入红黑树&#xff0c;而不是AVL树呢1. 概述 开始学习这个知识点之前我们需要知道&#xff0c;在JDK1.8 以及之前&#xff0c;针对HashMap有什么不同。 JDK 1.7的时候&#xff0c;HashMap的底层实现是数组 链表JDK1.8的时候&#xff0c;HashMap的底层实现是数…