Mybatis源码初探——优雅精良的骨架
@
前言
Mybatis是一款半自动的ORM框架,是目前国内Java web开发的主流ORM框架,因此作为一名开发者非常有必要掌握其实现原理,才能更好的解决我们开发中遇到的问题;同时,Mybatis的架构和源码也是很优雅的,使用了大量的设计模式实现解耦以及高扩展性,所以对其设计思想,我们也非常有必要好好理解掌握。(PS:本系列文章基于3.5.0版本分析)
精良的Mybatis骨架
宏观设计
Mybatsi的源码相较于Spring源码无论是架构还是实现都简单了很多,它所有的代码都在一个工程里面,在这个工程下分了很多包,每个包分工都很明确:
别看模块有这么多,实际上只需要分为三层:
这样分层后,是不是就很清晰了,基础支撑层是一些通用组件的封装,如日志、缓存、反射、数据源等等,这些模块支撑着核心业务逻辑的实现,并且如果需要我们可以将其直接用于到我们项目中,像反射模块就是对JDK的反射进行了封装,使其更加方便易用;核心处理层就是Mybatis的核心业务的实现了,通过底层支撑模块,实现了配置文件和SQL解析、参数映射和绑定、SQL执行和返回结果的映射以及扩展插件的执行等等;最后接口层则是对外提供的服务,我们使用Mybatis时只需要通过该接口进行操作,对底层的实现无需关注。这样分层的好处不用多说,让我们的代码更加简洁易读,同时可维护性和可扩展性也大大提高,另外从整个架构设计中我们可以看到一个设计模式的体现——门面模式,因为门面模式的设计思想就是对外提供一个统一的的接口,屏蔽掉内部系统实现的复杂性,使得用户无需关注内部实现就能轻松使用所有功能,而这里的架构设计就是采用的这样一个思想。举一反三,再想想看其它的开源框架是不是都是这样的设计?
基础支撑
在了解了Mybatis的宏观架构设计后,下面就是对源码的详细分析,首先先来看几个重点的基础支撑模块:
- 日志
- 数据源
- 缓存
- 反射
日志
日志的加载
Mybatis本身是没有实现日志功能的,而是引入第三方日志,但第三方日志都有自己的log级别,Mybatis需要解决的就是如何兼容这些日志组件。如何兼容呢?Mybatis使用了适配器模式来解决,在logging模块下提供了一个统一的日志接口Log接口:
public interface Log {
boolean isDebugEnabled();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
可以看到在这个接口中统一定义了各个日志级别,引入的第三方日志组件只需要实现该接口,在各个级别接口中调用各组件自身对应的API即可。从下面的类图我们可以看到Mybatis支持了哪些三方日志组件:
看到这里你是否会有疑问,这些第三方日志组件是怎么加载的?加载顺序又是怎样的呢?难道是在需要用的地方才实例化么?当然不是,Mybatis这里又使用了一个设计模式——工厂模式。在日志模块下有一个类LogFactory,日志的加载就是由该类实现的,通过这个类解耦了日志的实例化和日志的使用:
public final class LogFactory {
public static final String MARKER = "MYBATIS";
//被选定的第三方日志组件适配器的构造方法
private static Constructor<? extends Log> logConstructor;
//自动扫描日志实现,并且第三方日志插件加载优先级如下:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog
static {
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
private LogFactory() {
// disable construction
}
public static Log getLog(Class<?> aClass) {
return getLog(aClass.getName());
}
public static Log getLog(String logger) {
try {
return logConstructor.newInstance(logger);
} catch (Throwable t) {
throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
}
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
setImplementation(clazz);
}
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
public static synchronized void useCommonsLogging() {
setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}
public static synchronized void useLog4JLogging() {
setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}
public static synchronized void useLog4J2Logging() {
setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
}
public static synchronized void useJdkLogging() {
setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
public static synchronized void useStdOutLogging() {
setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
}
public static synchronized void useNoLogging() {
setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {//当构造方法不为空才执行方法
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
//通过指定的log类来初始化构造方法
private static void setImplementation(Class<? extends Log> implClass) {
try {
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
}
通过上面的代码我们可以清楚的看到日志的加载顺序是怎样的,并且只要加载成功了任何一个日志组件,其它的日志组件就不会被加载。
日志的使用
日志加载完成后,自然而然的我们就该思考的是哪些地方需要打印日志?Mybatis本身是对JDK原生的JDBC的包装和增强,所以在以下几个关键地方都应该打印日志:
- 创建PreparedStatement和Statement时打印SQL语句和参数信息
- 获取到查询结果后打印结果信息
问题是应该怎么优雅地增强这些方法呢?Mybatis使用了动态代理来实现。在日志模块下的JDBC包就是代理类的实现,先来看看类图:
见名知义,看到这些类名我们应该就能清楚这些类的作用,它们就是对原生的JDBC API的增强,在调用相关的方法时,首先会进入到这些代理类的invoke方法里面,按照执行顺序,首先进入调用的肯定是ConnectionLogger:
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
//真正的连接对象
private final Connection connection;
private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.connection = conn;
}
@Override
//对连接的增强
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {
try {
//如果是从Obeject继承的方法直接忽略
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
//如果是调用prepareStatement、prepareCall、createStatement的方法,打印要执行的sql语句
//并返回prepareStatement的代理对象,让prepareStatement也具备日志能力,打印参数
if ("prepareStatement".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);//打印sql语句
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);//创建代理对象
return stmt;
} else if ("prepareCall".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);//打印sql语句
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);//创建代理对象
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("createStatement".equals(method.getName())) {
Statement stmt = (Statement) method.invoke(connection, params);
stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);//创建代理对象
return stmt;
} else {
return method.invoke(connection, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
public Connection getConnection() {
return connection;
}
}
从invoke方法里我们可以看到主要对Connection的prepareStatement、prepareCall、createStatement方法进行了增强,打印日志并创建了对应的代理类返回。其它几个类实现原理都是一样,这里不再赘述。
但还有个问题,其它几个类的调用都是在创建连接之后,所以对应的代理类是由上一个阶段的代理类创建的,那ConnectionLogger是在哪里创建的呢?自然是在获取连接时,而获取连接都是在我们的业务代码执行阶段的时候,Mybatis对执行阶段又封装了一个个Excutor执行器,详细代码后文分析。
数据源
数据源的创建
数据源都需要实现JDK的DataSource接口,Mybatis自己本身实现了数据源接口,同时也支持第三方的数据源。这里主要看看Mybatis内部的实现,同样先来看一张类图:
从图中我们可以看到DataSource的初始化同样是通过工厂模式实现的,而其本身提供了三种数据源:
- PooledDataSource:带连接池的数据源
- UnpooledDataSource:不带连接池的数据源
- JNDI数据源
最后一种此处不分析。UnpooledDataSource就是一个普通的数据源,实现了基本的数据源接口;而PooledDataSource是基于UnpooledDataSource实现的,只是在此之上提供了连接池功能。另外还需要注意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;
//根据数据库url、用户名、密码生成一个hash值,唯一标识一个连接池
private int connectionTypeCode;
//连接是否有效
private boolean valid;
/*
* Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in
*
* @param connection - the connection that is to be presented as a pooled connection
* @param dataSource - the dataSource that the connection is from
*/
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;
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
......省略
/*
* 此方法专门用来增强数据库connect对象,使用前检查连接是否有效,关闭时对连接进行回收
*
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {//如果是调用连接的close方法,不是真正的关闭,而是回收到连接池
dataSource.pushConnection(this);//通过pooled数据源来进行回收
return null;
} else {
try {
//使用前要检查当前连接是否有效
if (!Object.class.equals(method.getDeclaringClass())) {
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();//
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
private void checkConnection() throws SQLException {
if (!valid) {
throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
}
}
}
属性和方法上都已经有了详细的注释,主要关注realConnection真实连接的引用和invoke方法增强。接着再看连接池的实现,这个类包含了很多属性:
private final PoolState state = new PoolState(this);
//真正用于创建连接的数据源
private final UnpooledDataSource dataSource;
// OPTIONAL CONFIGURATION FIELDS
//最大活跃连接数
protected int poolMaximumActiveConnections = 10;
//最大闲置连接数
protected int poolMaximumIdleConnections = 5;
//最大checkout时长(最长使用时间)
protected int poolMaximumCheckoutTime = 20000;
//无法取得连接是最大的等待时间
protected int poolTimeToWait = 20000;
//最多允许几次无效连接
protected int poolMaximumLocalBadConnectionTolerance = 3;
//测试连接是否有效的sql语句
protected String poolPingQuery = "NO PING QUERY SET";
//是否允许测试连接
protected boolean poolPingEnabled;
//配置一段时间,当连接在这段时间内没有被使用,才允许测试连接是否有效
protected int poolPingConnectionsNotUsedFor;
//根据数据库url、用户名、密码生成一个hash值,唯一标识一个连接池,由这个连接池生成的连接都会带上这个值
private int expectedConnectionTypeCode;
相信上面大部分属性读者们都不会陌生,在进行开发时应该都有配置过。其中有一个关键的属性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;
获取连接
了解了这些关键的属性后,再来看看如何从连接池获取连接,在PooledDataSource中有一个popConnection用于获取连接:
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 (state) {//获取连接必须是同步的
if (!state.idleConnections.isEmpty()) {//检测是否有空闲连接
// Pool has available connection
//有空闲连接直接使用
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {// 没有空闲连接
if (state.activeConnections.size() < poolMaximumActiveConnections) {//判断活跃连接池中的数量是否大于最大连接数
// 没有则可创建新的连接
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++;//超时连接次数+1
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;//累计超时时间增加
state.accumulatedCheckoutTime += longestCheckoutTime;//累计的使用连接的时间增加
state.activeConnections.remove(oldestActiveConnection);//从活跃队列中删除
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {//如果超时连接未提交,则手动回滚
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {//发生异常仅仅记录日志
/*
Just log a message for debug and continue to execute the following
statement like nothing happend.
Wrap the bad connection with a new PooledConnection, this will help
to not intterupt current executing thread and give current thread a
chance to join the next competion for another valid/good database
connection. At the end of this loop, bad {@link @conn} will be set as null.
*/
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++;//连接池累计等待次数加1
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) {//获取连接成功的,要测试连接是否有效,同时更新统计数据
// ping to server and check the connection is valid or not
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++;//累计的获取无效连接次数+1
localBadConnectionCount++;//当前获取无效连接次数+1
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;
}
这里的逻辑相对比较复杂,我总结了整个步骤并画了一张图帮助理解:循环获取连接,首先判断是否还存在空闲连接,如果存在,则直接使用,并删除一个空闲连接;如果不存在,优先判断是否已经达到最大活跃连接数量。如果没有则直接创建一个新的连接;如果已经达到最大活跃连接数,则从活跃连接池中取出最早的连接,判断是否超时。如果没有超时,则调用wait方法阻塞;如果超时,则统计超时连接信息,并根据超时连接的真实连接创建新的连接,同时让旧连接失效。经过以上步骤后,如果获取到一个连接,则还需要判断连接是否有效,有效连接需要回滚之前未提交的事务并添加到活跃连接池,无效连接则统计信息并判断是否已经超过重试次数,若没有则继续循环下一次获取连接,否则抛出异常。循环完成后返回获取到的连接。
回收连接
普通的连接是直接关闭,需要用的时候重新创建,而连接池则需要将连接回收到池中复用,避免重复创建连接提高效率,在PooledDataSource中的pushConnection就是用于回收连接的:
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++;
}
}
}
回收连接的逻辑就比较简单了,不过还是有几个地方需要注意:首先从活跃连接池移除掉该连接,然后判断是否是有效连接以及空闲连接池是否还有位置,如果是有效连接且空闲连接池还有位置的话,则需要基于当前回收连接的真实连接并创建新的连接放入到空闲连接中,然后唤醒等待的线程;如果没有则直接关闭真实连接。这两个分支都需要将回收的连接中未提交的事务回滚并将连接置为无效。如果本来就是无效连接则只需要记录获取无效连接的次数。
以上就是Mybatis数据源以及连接池的实现原理,其中池化技术是非常重要的。
缓存
缓存的实现
Mybatis有一级缓存和二级缓存,一级缓存是SqlSession级别的,只能存在于同一个SqlSession生命周期中;二级缓存则是跨SqlSession,以namespace为单位的。但实际上Mybatis的二级缓存非常鸡肋,有可能出现脏读的情况,一般不会使用。
但Mybatis对缓存做了大量的扩展,提供了防止缓存击穿、缓存清空策略、序列化、定时清空、日志等功能,设计非常优雅,所以此处主要领略这一模块的设计思想。先来看看包的结构:
从上图中我们可以看到,Mybatis提供了统一的缓存接口,impl和decorators包中都是它的实现类,从包的名字我们可以想到缓存这里又是使用了一个设计模式——装饰者模式,利用该模式动态得为缓存添加功能。真正的实现就是impl包 下的PerpetualCache,通过HashMap来缓存数据的(会不会出现并发安全问题?),key是CacheKey对象,value是缓存的数据,为什么key是CacheKey对象,而不是一个字符串呢?读者可以想想,怎样才能确定不会读取到错误的缓存,这个类最后来分析。而decorators包下的都是进行功能增强的装饰者类,这里主要来看看BlockingCache是如何防止缓存击穿的。
public class BlockingCache implements Cache {
//阻塞的超时时长
private long timeout;
//被装饰的底层对象,一般是PerpetualCache
private final Cache delegate;
//锁对象集,粒度到key值
private final ConcurrentHashMap<Object, ReentrantLock> locks;
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
acquireLock(key);//根据key获得锁对象,获取锁成功加锁,获取锁失败阻塞一段时间重试
Object value = delegate.getObject(key);
if (value != null) {//获取数据成功的,要释放锁
releaseLock(key);
}
return value;
}
@Override
public Object removeObject(Object key) {
// despite of its name, this method is called only to release locks
releaseLock(key);
return null;
}
private ReentrantLock getLockForKey(Object key) {
ReentrantLock lock = new ReentrantLock();//创建锁
ReentrantLock previous = locks.putIfAbsent(key, lock);//把新锁添加到locks集合中,如果添加成功使用新锁,如果添加失败则使用locks集合中的锁
return previous == null ? lock : previous;
}
//根据key获得锁对象,获取锁成功加锁,获取锁失败阻塞一段时间重试
private void acquireLock(Object key) {
//获得锁对象
Lock lock = getLockForKey(key);
if (timeout > 0) {//使用带超时时间的锁
try {
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {//如果超时抛出异常
throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} catch (InterruptedException e) {
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
} else {//使用不带超时时间的锁
lock.lock();
}
}
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
在调用getObject获取数据时,首先调用acquireLock根据key获取锁,如果获取到锁,则从PerpetualCache缓存中获取数据,如果没有则去数据库查询数据,返回结果后添加到缓存中并释放锁,注意去数据库查询数据时是根据key加了锁的,因此相同key只会有一个线程到达数据库查询,也就不会出现缓存击穿的问题,这个思路也可以用到我们的项目中去。
以上就是Mybatis解决缓存击穿的思路,另外再来看一个装饰者SynchronizedCache,提供同步的功能,该装饰器就是在对缓存的增删API上加上了synchronized关键字,这个装饰器就是用来防止二级缓存出现并发安全问题的,而一级缓存根本不存在并发安全问题。其余的装饰者这里就不赘述了,感兴趣的读者可自行分析。
CacheKey
因为Mybatis中存在动态SQL,所以缓存的key没法仅用一个字符串来表示,所以通过CacheKey来封装所有可能影响缓存的因素,那么哪些因素会影响到缓存呢?
- namespace + id
- 查询的sql
- 查询的参数
- 分页信息
而在CacheKey中有以下属性:
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier; //参与hash计算的乘数
private int hashcode; //CacheKey的hash值,在update函数中实时运算出来的
private long checksum; //校验和,hash值的和
private int count; //updateList的中元素个数
private List<Object> updateList; //该集合中的元素决定两个CacheKey是否相等
其中updateList就是用来存储所有可能影响缓存的因素,其它几个则是根据该属性中的对象计算出来的值,每次构造CacheKey对象时都会调用update方法:
public void update(Object object) {
//获取object的hash值
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
//更新count、checksum以及hashcode的值
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
//将对象添加到updateList中
updateList.add(object);
}
而判断两个CacheKey对象是否相同则是通过equals方法:
public boolean equals(Object object) {
if (this == object) {//比较是不是同一个对象
return true;
}
if (!(object instanceof CacheKey)) {//是否类型相同
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {//hashcode是否相同
return false;
}
if (checksum != cacheKey.checksum) {//checksum是否相同
return false;
}
if (count != cacheKey.count) {//count是否相同
return false;
}
//以上都不相同,才按顺序比较updateList中元素的hash值是否一致
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
可以看到这里比较相等的方法是非常严格的,并且效率极高,我们在项目中重写equals方法时也可以参照该方法的实现。
反射
反射是Mybatis的重中之重,通过反射Mybatis才能实现对象的实例化和属性的赋值,并且Mybatis的反射是对JDK的封装和增强,使其更易于使用,性能更高。其中关键的几个类如下:
- ObjectFactory:通过该对象创建POJO类的实例。
- ReflectorFactory:创建Reflector的工厂类。
- Reflector:MyBatis反射模块的基础,每个Reflector对象都对应一个类,在其中缓存了反射操作所需要的类元信息。
- ObjectWrapper:对象的包装,抽象了对象的属性信息,他定义了一系列查询对象属性信息的方法,以及更新属性的方法。
- ObjectWrapperFactory:创建ObjectWrapper的工厂类。
- MetaObject:包含了原始对象、ObjectWrapper、ObjectFactory、ObjectWrapperFactory、ReflectorFactory的引用,通过该类可以进行核心反射类的所有操作,也是门面模式的实现。
由于该模块只是对JDK的封装,虽然代码和类非常多,但并不是很复杂,这里就不详细阐述了。
总结
本篇讲解了Mybatis最核心的四大模块,可以看到使用了大量的设计模式使得代码优雅简洁,可读性高,同时便于扩展,这也是我们在做项目时首先需要考虑的,代码都是给人读的,如何降低阅读代码的成本,提高代码的质量,减少BUG的数量,只有多学习优秀代码的设计思想才能提高我们自身的水平。
Mybatis源码初探——优雅精良的骨架的更多相关文章
- MyBatis源码骨架分析
源码包分析 MyBatis 源码下载地址:https://github.com/MyBatis/MyBatis-3 MyBatis源码导入过程: 下载MyBatis的源码 检查maven的版本,必须是 ...
- Mybatis源码学习第六天(核心流程分析)之Executor分析
今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...
- 【转】Mybatis源码解读-设计模式总结
原文:http://www.crazyant.net/2022.html?jqbmtw=b90da1&gsjulo=kpzaa1 虽然我们都知道有26个设计模式,但是大多停留在概念层面,真实开 ...
- MyBatis源码解析之日志记录
一 .概述 MyBatis没有提供日志的实现类,需要接入第三方的日志组件,但第三方日志组件都有各自的Log级别,且各不相同,但MyBatis统一提供了trace.debug.warn.error四个级 ...
- Mybatis源码解读-设计模式总结
虽然我们都知道有26个设计模式,但是大多停留在概念层面,真实开发中很少遇到,Mybatis源码中使用了大量的设计模式,阅读源码并观察设计模式在其中的应用,能够更深入的理解设计模式. Mybatis至少 ...
- MyBatis 源码分析
MyBatis 运行过程 传统的 JDBC 编程查询数据库的代码和过程总结. 加载驱动. 创建连接,Connection 对象. 根据 Connection 创建 Statement 或者 Prepa ...
- Mybatis源码详解系列(四)--你不知道的Mybatis用法和细节
简介 这是 Mybatis 系列博客的第四篇,我本来打算详细讲解 mybatis 的配置.映射器.动态 sql 等,但Mybatis官方中文文档对这部分内容的介绍已经足够详细了,有需要的可以直接参考. ...
- 精尽MyBatis源码分析 - MyBatis 的 SQL 执行过程(一)之 Executor
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
- 精尽MyBatis源码分析 - SQL执行过程(二)之 StatementHandler
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
随机推荐
- Linux 工作管理
把进程放入后台 进程后加&,这时,进程在后台是运行的 tar -zcf etc.tar.gz /etc & 在执行程序时,快速按下ctrl+z快捷键,这时,进程在后台是停止的 查看后台 ...
- 两种方法设置MMDVM静态组
方法一.进入BM页面设置静态组 1.仪表盘配置页面点击下图所示进入BM 2.或是点击链接进入https://brandmeister.network 3..进入页面后点击My hotspots,显示你 ...
- python自学Day06(自学书籍python编程从入门到实践)
第7章 用户输入和while循环 我们设计的程序大多是为了解决用户最终的问题,所以我们大多需要在用户那里获取一些信息. 学习用户输入的获取与处理,学习while循环让程序不断运行直到达到指定的条件不满 ...
- 是时候拥抱.NET CORE了
微软和社区已经做了大量艰苦的工作,使.net core成为市场上具有竞争力的框架,帮助开发人员快速开发具有最佳性能和可扩展性的强大应用程序.做的最棒的事情使.net framework开发人员不需要任 ...
- [每日一题2020.06.07]codeforces Round #627 (Div. 3)
problem A /* * Author: RoccoShi * Time: 2020-06-07 19:37:51 */ #include <bits/stdc++.h> using ...
- (九)HttpClient获取cookies
原文链接:https://blog.csdn.net/cheny1p1ng/article/details/90780024 旧版本DefaultHttpClient 使用getCookieStore ...
- skfpdb.db、cc3268.dll、system_V2.dat、JI60JS.dat文件内容、发票数据查询
cc3268.dll.skfpdb.db.xxxxx_V2.dat,system.dat,JI60JS.dat,log.dat,system_V2.dat,JI60JS_V2.dat,log_V2.d ...
- OO第四单元——终章
一.架构设计 这一单元的作业主要是围绕UML来对我们的面向对象思维进行训练,刚开始接触的时候或许因为些许陌生而觉得有一定难度,但随着一次一次的代码阅读再加上思考,逐渐地也变得得心应手了起来. 1.第一 ...
- IDEA+Maven+Tomcat构建Web项目的三种方法
[本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究.若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!] 本文将介绍三种方 ...
- 手写React的Fiber架构,深入理解其原理
熟悉React的朋友都知道,React支持jsx语法,我们可以直接将HTML代码写到JS中间,然后渲染到页面上,我们写的HTML如果有更新的话,React还有虚拟DOM的对比,只更新变化的部分,而不重 ...