遥想在13年,还在上学的我,还在辛辛苦苦学习原始的JDBC,每次连接数据库都要自己手动连接数据库.当时就想为啥不写一个框架保持连接,可惜同年mybaits初版出来了,作者的梦想就破灭了。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcExample {
private static final String URL = "jdbc:mysql://localhost:3306/mydatabase";
private static final String USER = "myuser";
private static final String PASSWORD = "mypassword";
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 加载并注册JDBC驱动类
Class.forName("com.mysql.cj.jdbc.Driver");
// 建立数据库连接
conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 创建SQL查询语句
String sql = "SELECT * FROM mytable WHERE id = ?";
// 预编译SQL语句
pstmt = conn.prepareStatement(sql);
// 设置参数
pstmt.setInt(1, 1);
// 执行查询
rs = pstmt.executeQuery();
// 处理查询结果
while (rs.next()) {
System.out.println("ID: " + rs.getInt("id"));
System.out.println("Name: " + rs.getString("name"));
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
// 关闭资源
try {
if (rs != null) {
rs.close();
}
if (pstmt != null) {
pstmt.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
多年以后,在某次面试过程中被问到mybaits的原理,再次想起当年的梦想,决定好好研究一波,下面先将原理精简总结下,后续会细讲原理。
1.使用SqlSessionFactoryBuilder()构建一个sqlSessionFactory
//mybatis全局配置文件名定义
String resource = "mybatis-config.xml";
//读取这个resource表示的文件并将其转化为输入流
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
2.使用sqlSessionFactory子类(DefaultSqlSessionFactory)工程的openSession()方法获取SqlSession(sqlSession里面都是一些select,update的方法)
sqlSessionFactory.openSession();
3.调用DefaultSqlSessionFactory的openSessionFromDataSource,新建一个executor
Executor executor = this.configuration.newExecutor(tx, execType);//type默认是一个SIMPLE,REUSE,BATCH;
4.executor里面的方法会去调用StatementHandler
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = this.prepareStatement(handler, ms.getStatementLog());
var9 = handler.query(stmt, resultHandler);
5.调用了prepareStatement方法赋值parameterHandler
Connection connection = this.getConnection(statementLog);
Statement stmt = handler.prepare(connection, this.transaction.getTimeout());
handler.parameterize(stmt);
6.查询结果映射ResultHandler
PreparedStatement ps = (PreparedStatement)statement;
ps.execute();//执行sql
return this.resultSetHandler.handleCursorResultSets(ps);
7.关闭stament
stmt.closeOnCompletion();
下面来细讲每一步
1、第一步读取配置和mapper,mybaits只是将JDBC隐藏起来了,不是没有,这里以mybatis-config.xml举例。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 这一步为xml的标头,引用的dtd是用来检验xml的格式的,这里我们不做讲解-->
<configuration>
<!-- -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydatabase?useSSL=true&useUnicode=false&
characterEncoding=UTF-8&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="********"/>
</dataSource>
</environment>
</environments>
<!--每一个Mapper.xml都需要在mybatis核心配置文件中注册-->
<mappers>
<!--resource要用 /-->
<mapper resource="mapper/StudentMapper.xml"/>
</mappers>
</configuration>
这个有个配置为<dataSource type="POOLED"> ,很显然这个是一个类型,这个其实就是为了解决我们jdbc里每次都要自己手动连接,使用完后要手动关闭的麻烦,mybaits提供了三种type,分别为“POOLED”,
“UNPOOLED”,“JNID”(这里不做讨论),其中默认为“POOLED”,正如名字所示,这是一个连接池,相反“UNPOOLED”就是非连接池
下面我们来看源码。源码位置在如下位置
先看简单的unpooled
public class UnpooledDataSource implements DataSource {
private ClassLoader driverClassLoader;
private Properties driverProperties;
private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap();
//驱动名
private String driver;
//连接url
private String url;
//用户名
private String username;
//密码
private String password;
//是否自动提交
private Boolean autoCommit;
private Integer defaultTransactionIsolationLevel;
private Integer defaultNetworkTimeout;
}
这里我们看到了jdbc里面的常客driver,username,password,实际UnpooledDataSource也是这样做的,接下来我们看下面四个方法,其中initializeDriver()这个方法是用来加载Driver驱动的,其中核心代码为driverType = Class.forName(this.driver, true, this.driverClassLoader);
这一步和我们jdbs的Class.forName("com.mysql.cj.jdbc.Driver");的作用是一样的。另一个方法configureConnection(),这个方法最总返回了一个conn,这一步和老的jdbc里面的
conn = DriverManager.getConnection(URL, USER, PASSWORD);
是异曲同工的。
//根据用户名和密码创建连接
private Connection doGetConnection(String username, String password) throws SQLException {
Properties props = new Properties();
if (this.driverProperties != null) {
props.putAll(this.driverProperties);
}
if (username != null) {
props.setProperty("user", username);
}
if (password != null) {
props.setProperty("password", password);
}
return this.doGetConnection(props);
}
private Connection doGetConnection(Properties properties) throws SQLException {
this.initializeDriver();
Connection connection = DriverManager.getConnection(this.url, properties);
this.configureConnection(connection);
return connection;
}
private synchronized void initializeDriver() throws SQLException {
if (!registeredDrivers.containsKey(this.driver)) {
try {
Class driverType;
if (this.driverClassLoader != null) {
driverType = Class.forName(this.driver, true, this.driverClassLoader);
} else {
driverType = Resources.classForName(this.driver);
}
Driver driverInstance = (Driver)driverType.getDeclaredConstructor().newInstance();
DriverManager.registerDriver(new DriverProxy(driverInstance));
registeredDrivers.put(this.driver, driverInstance);
} catch (Exception var3) {
Exception e = var3;
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
}
}
}
private void configureConnection(Connection conn) throws SQLException {
if (this.defaultNetworkTimeout != null) {
conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), this.defaultNetworkTimeout);
}
if (this.autoCommit != null && this.autoCommit != conn.getAutoCommit()) {
conn.setAutoCommit(this.autoCommit);
}
if (this.defaultTransactionIsolationLevel != null) {
conn.setTransactionIsolation(this.defaultTransactionIsolationLevel);
}
}
下面来看PooledDataSource这个类这里面我们看到了类似线程池的东西,这里借用下官方文档里面的定义
public class PooledDataSource implements DataSource {
private static final Log log = LogFactory.getLog(PooledDataSource.class);
private final PoolState state = new PoolState(this);
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;
private int expectedConnectionTypeCode;
}
PooledDataSource类里面重要的方法有三个 ,分别是popConnection(获取连接),pushConnection(关闭连接),forceCloseAll(强制关闭所有)这个连接方法有点长,我们一点点来看。
public ConnectionpushConnection getConnection() throws SQLException {
return this.popConnection(this.dataSource.getUsername(), this.dataSource.getPassword()).getProxyConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return this.popConnection(username, password).getProxyConnection();
}
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) {
synchronized(this.state) {
PoolState var10000;
if (!this.state.idleConnections.isEmpty()) {
conn = (PooledConnection)this.state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else if (this.state.activeConnections.size() < this.poolMaximumActiveConnections) {
conn = new PooledConnection(this.dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
PooledConnection oldestActiveConnection = (PooledConnection)this.state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > (long)this.poolMaximumCheckoutTime) {
++this.state.claimedOverdueConnectionCount;
var10000 = this.state;
var10000.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
var10000 = this.state;
var10000.accumulatedCheckoutTime += longestCheckoutTime;
this.state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException var15) {
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) {
++this.state.hadToWaitCount;
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + this.poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
this.state.wait((long)this.poolTimeToWait);
var10000 = this.state;
var10000.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException var16) {
break;
}
}
}
if (conn != null) {
if (conn.isValid()) {
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.setConnectionTypeCode(this.assembleConnectionTypeCode(this.dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
this.state.activeConnections.add(conn);
++this.state.requestCount;
var10000 = this.state;
var10000.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
++this.state.badConnectionCount;
++localBadConnectionCount;
conn = null;
if (localBadConnectionCount > this.poolMaximumIdleConnections + this.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.");
} else {
return conn;
}
}
简单画一个新建连接流程图(新建其实不准确,准确应该叫获取连接)
归还连接的流程图如下。
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized(this.state) {
this.state.activeConnections.remove(conn);
if (conn.isValid()) {
PoolState var10000;
if (this.state.idleConnections.size() < this.poolMaximumIdleConnections && conn.getConnectionTypeCode() == this.expectedConnectionTypeCode) {
var10000 = this.state;
var10000.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
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.");
}
this.state.notifyAll();
} else {
var10000 = this.state;
var10000.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.");
}
++this.state.badConnectionCount;
}
}
}
forceCloseAll(强制关闭所有),顾名思义,关闭所有的连接,这里就不细讲了,感兴趣的可以自己去看看源码。
至此,我们获得了一个conn,其实就是下面两步,其实很多人刚开始学习的时候,会觉得源码羞涩难懂。其实不然,当我们首次使用JDBC连接数据库的时候,你可以没有感觉,但是当你写了第五遍,第十遍的时候,你在想,为什么我不能写一个ConnUtils来创建连接呢,那恭喜你,你就发明了
UnpooledDataSource的基础版。还有在我们学习JDBC的时候,都会告诉你,连接使用完后要close掉,不close会导致内存泄露,但是在学完
池
这个概念后,再来看下面这段代码,你会发现
conn.prepareStatement是可以多次复制的,那我不关,重复使用是否可以。你想到我创建很多个连接,然后每次要用的时候,判断是否有可以空闲的连接,直接取不就可以了吗,那恭喜你,你就发
明了PooledDataSource的基础版。
Class.forName("com.mysql.cj.jdbc.Driver");
// 建立数据库连接
conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 创建SQL查询语句
String sql = "SELECT * FROM mytable WHERE id = ?";
// 预编译SQL语句
pstmt = conn.prepareStatement(sql);
其实很多人觉得源码都是大神写的,其实很多框架的源码的代码量还没有你工作中的项目代码多,静下心来阅读才会学到很多东西。这里在插一句,很多面试会问的你工作中有没有使用过synchronized,很多时候,大部分打工人都用的很少(如果你用的很多,我只能说你厉害)。但是我们在阅读源码的时候,你就会发现PooledDataSource里面用了很多次,这里你可以考虑下使用的原因是什么。
本文参考文档 "https://mybatis.net.cn/" mybaits中文文档(但貌似这个文档是盗版的)
下一篇将细讲 session模块