当前位置: 首页 > news >正文

Mybatis源码分析(九)Mybatis的PreparedStatement

文章目录

  • 一 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驱动包的源码部分

相关文章:

Mybatis源码分析(九)Mybatis的PreparedStatement

文章目录一 JDBC的PreparedStatement二 prepareStatement的准备阶段2.1 获取Connection2.1.1 **UnpooledDataSource**2.1.2 PooledDataSource2.2 Sql的预编译PreparedStatementHandler2.3 为Statement设置参数2.4 执行具体的语句过程官网&#xff1a;mybatis – MyBatis 3 | 简…...

winfrom ui

http://www.iqidi.com/download/warehouse/Device_DotNetBar.rar http://qiosdevsuite.com/Download https://sourceforge.net/projects/qiosdevsuite/ https://www.cnblogs.com/hcyblogs/p/6758381.html https://www.cnblogs.com/jordonin/p/6484366.html MBTiles地图瓦片管…...

中国国家级地面气象站基本气象要素日值数据集(V3.0)

数据集摘要 数据集包含了中国基本气象站、基准气候站、一般气象站在内的主要2474个站点1951年1月以来本站气压、气温、降水量、蒸发量、相对湿度、风向风速、日照时数和0cm地温要素的日值数据。数据量为21.3GB。 (1)SURF_CLI_CHN_MUL_DAY-TEM-12001-201501.TXT 气温数据TEM, 包…...

【Python语言基础】——Python NumPy 数组副本 vs 视图

Python语言基础——Python NumPy 数组副本 vs 视图 文章目录 Python语言基础——Python NumPy 数组副本 vs 视图一、Python NumPy 数组副本 vs 视图一、Python NumPy 数组副本 vs 视图 副本和视图之间的区别 副本和数组视图之间的主要区别在于副本是一个新数组,而这个视图只是…...

Spring Cloud_OpenFeign服务接口调用

目录一、概述1.OpenFeign是什么2.能干嘛二、OpenFeign使用步骤1.接口注解2.新建Module3.POM4.YML5.主启动类6.业务类7.测试8.小总结三、OpenFeign超时控制1.超时设置&#xff0c;故意设置超时演示出错情况2.是什么3.YML中需要开启OpenFeign客户端超时控制四、OpenFeign日志打印…...

十三、GIO GTask

GTask表示管理一个可取消的“任务task” GCancellable GCancellable是一个线程安全的操作取消栈&#xff0c;用于整个GIO&#xff0c;以允许取消同步和异步操作。 它继承于GObject对象&#xff0c;不是一个单纯的结构体 相关函数 g_task_new GTask* g_task_new (GObject*…...

ch4_1存储器

1. 存储器的类型 1.1 按照存储介质来分类 半导体存储器&#xff1a; TTL&#xff0c; MOS 易失性 磁表面存储器&#xff1a; 磁头&#xff0c; 载磁体&#xff1b; 磁芯存储器&#xff1a; 硬磁材料&#xff0c; 环状元件 光盘存储器: 激光&#xff0c; 磁光材料; 1.2 按…...

Doris通过Flink CDC接入MySQL实战

1. 创建MySQL库表&#xff0c;写入demo数据 登录测试MySQL mysql -u root -pnew_password创建MySQL库表&#xff0c;写入demo数据 CREATE DATABASE emp_1;USE emp_1; CREATE TABLE employees_1 (emp_no INT NOT NULL,birth_date DATE NOT NULL,…...

搭建zookeeper高可用集群详细步骤

目录 一、虚拟机设置 1.新建一台虚拟机并克隆三台&#xff0c;配置自定义 2.修改四台虚拟机的主机名并立即生效 3.修改四台虚拟机的网络信息 4.重启四台虚拟机的网络服务并测试网络连接 5.重启四台虚拟机&#xff0c;启动后关闭四台虚拟机的防火墙 6.在第一台虚拟机的/e…...

Scala 变量和数据类型(第二章)

第二章、变量和数据类型2.1 注释2.2 变量和常量&#xff08;重点&#xff09;2.3 标识符的命名规范2.4 字符串输出2.5 键盘输入2.6 数据类型&#xff08;重点&#xff09;回顾&#xff1a;Java数据类型Scala数据类型2.7 整数类型&#xff08;Byte、Short、Int、Long&#xff09…...

【JVM基础内容速查表】JVM基础知识 默认参数 GC命令 工具使用 JVM参数设置、说明、使用方法、注意事项等(持续更新)

目录一、JVM前置知识1. -X、-XX含义2. JVM参数值的类型和设置方式3. 查看GC时用到的命令和JVM参数4. 查看JVM默认参数二、垃圾收集器选择-XX:UseSerialGC-XX:UseParallelGC-XX:UseParallelOldGC-XX:UseParNewGC-XX:UseConcMarkSweepGC-XX:UseG1GC三、垃圾收集器特有参数1. ParN…...

C语言经典编程题100例(61~80)

目录61、练习7-7 矩阵运算62、练习7-8 方阵循环右移63、习题6-1 分类统计字符个数64、习题6-2 使用函数求特殊a串数列和65、习题6-4 使用函数输出指定范围内的Fibonacci数66、习题6-5 使用函数验证哥德巴赫猜想67、习题6-6 使用函数输出一个整数的逆序数68、练习8-2 计算两数的…...

toxssin:一款功能强大的XSS漏洞扫描利用和Payload生成工具

关于toxssin toxssin是一款功能强大的XSS漏洞扫描利用和Payload生成工具&#xff0c;这款渗透测试工具能够帮助广大研究人员自动扫描、检测和利用跨站脚本XSS漏洞。该工具由一台HTTPS服务器组成&#xff0c;这台服务器将充当一个解释器&#xff0c;用于处理恶意JavaScript Pay…...

Keepalived与HaProxy的协调合作原理分析

Keepalived与HaProxy的协调合作原理分析keepalived与haproxy合作场景更好的理解方式协调合作中考虑的问题一、Keepalived以TCP/IP模型角度来分析&#xff1a;二、HaProxy总结&#xff1a;协调合作中考虑的问题的答案虚拟ip&#xff1a;虚拟IP技术&#xff0c;就是一个未分配给客…...

抖音如何找到博主视频推广?筛选博主要看那些数据

近年来抖音视频推广越来越成为企业宣传的热门选择&#xff0c;今天就来和大家聊聊抖音如何找到博主视频推广&#xff0c;以及几种主流的对接方式。一、什么是抖音博主视频推广?抖音博主视频推广就是通过博主的影响力和粉丝量&#xff0c;吸引用户到短视频平台进行观看相关合作…...

Win11的两个实用技巧系列之如何关闭登录密码?

Win11如何关闭登录密码?Win11关闭登录密码的两种解决方法win11是电脑更新后的全新系统&#xff0c;每次开启需要输入密码。有的用户嫌麻烦想要关闭&#xff0c;下面小编就为大家带来了关闭的方法&#xff0c;一起来看看吧有不少用户在升级或者第一次使用Win11系统的时候&#…...

润普挂卷失败之老卷宗对接NP无法获取案件信息问题排查

润普挂卷失败之老卷宗对接NP无法获取案件信息问题排查 写在最前面 根因&#xff1a;NP的dzjzzzfw与老卷宗dzjz服务用的zookeeper不是同一个&#xff0c;且老卷宗指向的zookeeper没有任何一个匹配的dzjzzzfw。仅有消费者&#xff0c;没有任何生产者&#xff0c;导致老卷宗通过…...

产品经理面试题思考及回答思路(一)

求职产品助理/经理岗位&#xff0c;转行产品岗面试真题 关于产品经理岗位能力的思考&#xff1a; 什么是产品经理&#xff1f;为什么要当/选择做产品经理&#xff1f;怎么理解产品经理&#xff1f;如何理解产品经理的价值&#xff1f;产品日常工作有哪些&#xff1f;工作流程…...

Routability-Driven Macro Placement with Embedded CNN-Based Prediction Model

Routability-Driven Macro Placement with Embedded CNN-Based Prediction Model 2019 Design, Automation & Test in Europe Conference & Exhibition (DATE) DOI: 10.23919/DATE.2019.8715126 目录Abstract一、Introduction二、PROBLEM FORMULATION AND PRELIMINARIE…...

论一个上班族如何一次性通过PMP考试

PMP是我工作后考取的一个证书。从准备到通过&#xff0c;花了大约三个月的时间。我之前在某家互联网公司从事程序员的工作&#xff0c;工作一段时间后&#xff0c;天天敲着代码&#xff0c;改着bug&#xff0c;感觉比较迷茫&#xff0c;不知道未来的发展在哪里&#xff0c;都说…...

Web前端:使用Angular CLI时的最佳实践和专业技巧

在web开发业务中&#xff0c;构建高性能的应用程序是首要因素。此外&#xff0c;用开发人员最流行的语言开发一个健壮的网站将始终为构建高功能的网站提供适当的基础网站。相比之下&#xff0c;不可否认&#xff0c;Angular CLI是建立得最好且正在成长的框架之一。Angular CLI简…...

从0到1一步一步玩转openEuler--15 openEuler使用DNF管理软件包

文章目录15.1 搜索软件包15.2 列出软件包清单15.3 显示RPM包信息15.4 安装RPM包15.5 下载软件包15.6 删除软件包DNF是一款Linux软件包管理工具&#xff0c;用于管理RPM软件包。DNF可以查询软件包信息&#xff0c;从指定软件库获取软件包&#xff0c;自动处理依赖关系以安装或卸…...

【java】Spring Boot --spring boot项目整合xxl-job

文章目录1、源码下载地址2.文档地址3.源码结构4.初始化数据库脚本5.配置调度中心xxl-job-admin5.1 修改调度中心配置文件&#xff1a;/xxl-job/xxl-job-admin/src/main/resources/application.properties5.2 启动调度中心5.3 访问调度中心管理界面6.创建执行器项目6.3 载入配置…...

视图、索引、存储过程、触发器

视图、索引、存储过程、触发器 group by补充&#xff1a; 规范来说&#xff0c;分组查询中&#xff0c;select后的字段只能是group by的字段或者是聚合函数。mysql在这有一个小优化&#xff0c;分组后如果某个字段的所有记录相同&#xff0c;同样可以select。 视图 视图是虚拟…...

ImportError: cannot import name ‘FlattenObservation‘ from ‘gym.wrappers‘ 解决方案

问题描述 今天在运行openai给出的ppo2的baseline的时候遇到了以下bug&#xff1a; File "/root/code/baselines_openai/baselines/common/cmd_util.py", line 12, in <module> from gym.wrappers import FlattenObservation, FilterObservation ImportErr…...

大件传输的9种方法

不知道你有没有试过用电子邮件进行大文件传输&#xff0c;由于文件大小的限制&#xff0c;往往会发送失败。同时&#xff0c;一些文件共享服务对传输的文件有大小限制&#xff0c;使得你无法与朋友分享电影片段或向客户展示你的工作样本。还有一些要求你注册一个账户&#xff0…...

将vue2的项目《后台管理模式》转变为vue3版本 (一)

本篇主要讲了将v2项目转变为v3版本&#xff0c;以本人经验愿于各位分享 希望大家可以一起交流&#xff01;&#xff01;&#xff01;&#xff01; 文章目录一、app 出口位置二 、 index.js 路由配置三、package.json 文件四、 main.js 既然安装插件那就需要引入五、 跨域问题总…...

苹果手机怎么下载手机铃声?图文教程,快速学会

很多小伙伴喜欢使用苹果手机&#xff0c;可是苹果手机里的铃声自己并不是很喜欢听&#xff0c;想要下载一些好听的歌曲更换自己的手机铃声。苹果手机怎么下载手机铃声&#xff1f;别着急&#xff0c;今天小编以图文的方式&#xff0c;教教大家如何使用苹果手机下载手机铃声。 苹…...

AJAX笔记(二)Fetch和axios

1、Fetch 1.1、XMLHttpRequest的缺陷 1.2、fetch的get写法 1.3、fetch的post写法 1.4、fetch的put写法 1.5、fetch的patch写法 1.6、fetch的delete写法 2、axios 2.1、axios的介绍 2.2、axios的get写法 2.3、axios的post写法&#xff08;图一json写法和图二三form写法&#x…...

TOTOLINK NR1800X 系列 CVE 分析

漏洞简介 TOTOLINK NR1800X最近报了一些cve&#xff0c;现主要对其命令注入进行具体分析&#xff0c;以及对其登录绕过进行分析。 固件下载地址&#xff1a;https://www.totolink.net/home/menu/detail/menu_listtpl/download/id/225/ids/36.html 环境搭建 固件提取 binwa…...

中国站长站/泰州seo外包公司

使用Python原生IDLE IDLE是Python软件包自带的一个集成开发环境&#xff0c;点击开始-->Python安装包-->IDLE。启动 IDLE 时&#xff0c;会显示>>>&#xff0c;可以在>>>后面输入代码。在 Python Shell 输入代码回车后会立即执行&#xff0c;并直接在下…...

wordpress 采集教程/营业推广经典案例

方法一&#xff1a;&#xff08;兼容性好&#xff09; h5 活体检测兼容性比较好的做法是通过input去打开手机系统原生的录像界面 <input type"file" accept"video/*" capture"camcorder" ref"videoFile" change"changeVideo…...

深圳较便宜的网站建设/seo行业

获取变量类型 typeof(var) Number String 类型转换 parseInt parseFloat Date() getDate() 1-31日 getTime() 1970-1-1到当前的毫秒数 getDay() 星期几0-6 getHours() getMinutes() getSeconds() setFullYear() setDate() getFullYear()...

网站开发建设/厦门seo排名优化方式

OpenMP --- 线程同步 1. 引言 在OpenMP中&#xff0c;线程同步机制包括互斥锁同步机制和事件同步机制。 2. 互斥锁同步 互斥锁同步的概念类似于Windows中的临界区&#xff08;CriticalSection&#xff09;以及Windows和Linux中的Mutex以及VxWorks中的SemTake和SemGive&#xff…...

怎样给一个网站做专题策划/seo关键词推广方式

1 点击我搭好私有云 2 下载aria2服务 Aria2是个开源下载工具 它可用在 linux windows系统上 通常在linux上使用较多 它在linux上相比wget下载更灵活 它支持下载http、ftp、BT、磁力链接等 下面这个文章写一个完整windows系统下载使用Aria2 下载Aria2 1、下载Aria2源程序 Gi…...

网站建设与设计教程视频/软文发稿系统

缘起 书说前两篇文章《 十五 ║ Vue前篇&#xff1a;JS对象&字面量&this》和 《 十六 ║ Vue前篇&#xff1a;ES6初体验 & 模块化编程》&#xff0c;已经通过对js面向对象&#xff0c;类和模式封装&#xff0c;ES6新特性等多个角度讲解了Vue入门的一些储备知识&…...