文章目录
本系列文章:
Mybatis(一)Mybatis的基本使用
Mybatis(二)Mybatis的高级使用
Mybatis(三)配置文件解析流程
Mybatis(四)映射文件解析流程
Mybatis(五)SQL执行流程
Mybatis(六)数据源、缓存机制、插件机制
一、内置数据源
由于创建连接和释放连接都有很大的开销(尤其是数据库服务器不在本地时,每次建立连接都需要进行TCP的三次握手,释放连接需要进行TCP四次握手,造成的开销是不可忽视的),为了提升系统访问数据库的性能,可以事先创建若干连接置于连接池中,需要时直接从连接池获取,使用结束时归还连接池而不必关闭连接,从而避免频繁创建和释放连接所造成的开销,这是典型的用空间换取时间的策略(浪费了空间存储连接,但节省了创建和释放连接的时间)。
池化技术在Java开发中是很常见的,在使用线程时创建线程池的道理与此相同。基于Java的开源数据库连接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid等。
MyBatis支持三种数据源配置,分别为UNPOOLED(不使用连接池的数据源)、POOLED(使用连接池的数据源)、 JNDI(使用JNDI实现的数据源)。并提供了两种数据源实现,分别是UnpooledDataSource和PooledDataSource。在这三种数据源配置中,UNPOOLED和POOLED是最常用的两种配置,JNDI 数据源在日常开发中使用较少。
1.1 内置数据源初始化过程
数据源配置示例:
<dataSource type="UNPOOLED|POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql..."/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</dataSource>
数据源的配置是内嵌在<environment>
节点中的,MyBatis在解析<environment>
节点时,会同时解析数据源的配置。MyBatis会根据具体的配置信息,为不同的数据源创建相应工厂类,通过工厂类即可创建数据源实例。
MyBatis是通过工厂模式来创建数据源DataSource对象的,MyBatis定义了抽象的工厂接口DataSourceFactory,通过其getDataSource()方法返回数据源DataSource:
public interface DataSourceFactory {
void setProperties(Properties props);
//生产DataSource
DataSource getDataSource();
}
数据源工厂类的实现逻辑:
public class UnpooledDataSourceFactory implements DataSourceFactory {
private static final String DRIVER_PROPERTY_PREFIX = "driver.";
private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length();
protected DataSource dataSource;
public UnpooledDataSourceFactory() {
//创建UnpooledDataSource对象
this.dataSource = new UnpooledDataSource();
}
@Override
public void setProperties(Properties properties) {
Properties driverProperties = new Properties();
//为dataSource创建元信息对象
MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
//遍历properties键列表,properties由配置文件解析器传入
for (Object key : properties.keySet()) {
String propertyName = (String) key;
//检测propertyName是否以"driver."开头
if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {
String value = properties.getProperty(propertyName);
//存储配置信息到driverProperties中
driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
} else if (metaDataSource.hasSetter(propertyName)) {
String value = (String) properties.get(propertyName);
//按需转换value类型
Object convertedValue = convertValue(metaDataSource, propertyName, value);
//设置转换后的值到UnpooledDataSourceFactory指定属性中
metaDataSource.setValue(propertyName, convertedValue);
} else {
throw new DataSourceException("……");
}
}
if (driverProperties.size() > 0) {
//设置driverProperties到UnpooledDataSourceFactory的
//driverProperties属性中
metaDataSource.setValue("driverProperties", driverProperties);
}
}
private Object convertValue(MetaObject metaDataSource, String propertyName, String value) {
Object convertedValue = value;
//获取属性对应的setter方法的参数类型
Class<?> targetType = metaDataSource.getSetterType(propertyName);
//按照setter方法的参数类型进行类型转换
if (targetType == Integer.class || targetType == int.class) {
convertedValue = Integer.valueOf(value);
} else if (targetType == Long.class || targetType == long.class) {
convertedValue = Long.valueOf(value);
} else if(targetType == Boolean.class||targetType == boolean.class){
convertedValue = Boolean.valueOf(value);
}
return convertedValue;
}
@Override
public DataSource getDataSource() {
return dataSource;
}
}
PooledDataSourceFactory的源码:
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
public PooledDataSourceFactory() {
//创建PooledDataSource
this.dataSource = new PooledDataSource();
}
}
以上就是PooledDataSource类的所有源码,PooledDataSourceFactory继承自UnpooledDataSourceFactory,复用了父类的逻辑,因此它的实现很简单。
1.2 UnpooledDataSource
该种数据源每次会返回一个新的数据库连接,而非复用旧的连接。由于UnpooledDataSource无需提供连接池功能,因此它的实现非常简单。核心的方法有三个:
- initializeDriver:初始化数据库驱动。
- doGetConnection:获取数据连接。
- configureConnection:配置数据库连接。
UnpooledDataSource调用流程:
1.2.1 初始化数据库驱动
UnpooledDataSource也是使用JDBC访问数据库的,initializeDriver源码:
private synchronized void initializeDriver() throws SQLException {
//检测缓存中是否包含了与driver对应的驱动实例
if (!registeredDrivers.containsKey(driver)) {
Class<?> driverType;
try {
//加载驱动类型
if (driverClassLoader != null) {
//使用driverClassLoader加载驱动
driverType = Class.forName(driver, true, driverClassLoader);
} else {
//通过其他ClassLoader加载驱动
driverType = Resources.classForName(driver);
}
//通过反射创建驱动实例
Driver driverInstance = (Driver) driverType.newInstance();
//注册驱动,注意这里是将Driver代理类DriverProxy对象注册,
//而非Driver对象本身
DriverManager.registerDriver(new DriverProxy(driverInstance));
//缓存驱动类名和实例
registeredDrivers.put(driver, driverInstance);
} catch (Exception e) {
throw new SQLException("……");
}
}
}
initializeDriver方法主要包含三步操作:
- 加载驱动
- 通过反射创建驱动实例
- 注册驱动实例
上面代码中出现了缓存相关的逻辑,这个是用于避免重复注册驱动。因为initializeDriver方法并不是在UnpooledDataSource初始化时被调用的,而是在获取数据库连接时被调用的。因此这里需要做个检测,避免每次获取数据库连接时都重新注册驱动。
1.2.2 获取数据库连接
public Connection getConnection() throws SQLException {
return doGetConnection(username, password);
}
private Connection doGetConnection(String username, String password) throws SQLException {
Properties props = new Properties();
if (driverProperties != null) {
props.putAll(driverProperties);
}
if (username != null) {
//存储user配置
props.setProperty("user", username);
}
if (password != null) {
//存储password配置
props.setProperty("password", password);
}
//调用重载方法
return doGetConnection(props);
}
private Connection doGetConnection(Properties properties) throws SQLException {
//初始化驱动
initializeDriver();
//获取连接
Connection connection = DriverManager.getConnection(url, properties);
//配置连接,包括自动提交以及事务等级
configureConnection(connection);
return connection;
}
private void configureConnection(Connection conn) throws SQLException {
if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
//设置自动提交
conn.setAutoCommit(autoCommit);
}
if (defaultTransactionIsolationLevel != null) {
//设置事务隔离级别
conn.setTransactionIsolation(defaultTransactionIsolationLevel);
}
}
上面的逻辑:将一些配置信息放入到Properties对象中,然后将数据库连接和Properties对象传给DriverManager的getConnection方法即可获取到数据库连接。
1.3 PooledDataSource
PooledDataSource内部实现了连接池功能,用于复用数据库连接。PooledDataSource需要借助一些辅助类帮助它完成连接池的功能。
1.3.1 辅助类
PooledDataSource需要借助两个辅助类帮其完成功能,这两个辅助类分别是PoolState和PooledConnection。PoolState用于记录连接池运行时的状态,比如连接获取次数,无效连接数量等。同时PoolState内部定义了两个PooledConnection集合,用于存储空闲连接和活跃连接。PooledConnection 内部定义了一个Connection类型的变量,用于指向真实的数据库连接。以及一个Connection的代理类,用于对部分方法调用进行拦截。除此之外,PooledConnection内部也定义了一些字段,用于记录数据库连接的一些运行时状态。
class PooledConnection implements InvocationHandler {
private static final String CLOSE = "close";
private static final Class<?>[] IFACES = new Class<?>[]{Connection.class};
private final int hashCode;
private final PooledDataSource dataSource;
//真实的数据库连接
private final Connection realConnection;
//数据库连接代理
private final Connection proxyConnection;
//从连接池中取出连接时的时间戳
private long checkoutTimestamp;
//数据库连接创建时间
private long createdTimestamp;
//数据库连接最后使用时间
private long lastUsedTimestamp;
//connectionTypeCode = (url + username + password).hashCode()
private int connectionTypeCode;
//表示连接是否有效
private boolean valid;
public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
//创建Connection的代理类对象
this.proxyConnection = (Connection) Proxy.newProxyInstance(
Connection.class.getClassLoader(), IFACES, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {...}
}
PoolState:
public class PoolState {
protected PooledDataSource dataSource;
//空闲连接列表
protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
//活跃连接列表
protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();
//从连接池中获取连接的次数
protected long requestCount = 0;
//请求连接总耗时(单位:毫秒)
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;
}
1.3.2 获取连接
PooledDataSource会将用过的连接进行回收,以便可以复用连接。因此从PooledDataSource获取连接时,如果空闲链接列表里有连接时,可直接取用。那如果没有空闲连接怎么办呢?此时有两种解决办法,要么创建新连接,要么等待其他连接完成任务。具体怎么做,需视情况而定。
public Connection getConnection() throws SQLException {
//返回Connection的代理对象
return popConnection(dataSource.getUsername(),dataSource.getPassword()).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 (state) {
//检测空闲连接集合(idleConnections)是否为空
if (!state.idleConnections.isEmpty()) {
//idleConnections不为空,表示有空闲连接可以使用
conn = state.idleConnections.remove(0);
} else {
//暂无空闲连接可用,但如果活跃连接数还未超出限制
//(poolMaximumActiveConnections),则可创建新的连接
if (state.activeConnections.size() < poolMaximumActiveConnections) {
//创建新连接
conn = new PooledConnection(dataSource.getConnection(), this);
} 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) {...}
}
//创建一个新的PooledConnection,注意,此处复用
//oldestActiveConnection的realConnection变量
conn = new PooledConnection(oldestActiveConnection.getRealConnection(),this);
//复用oldestActiveConnection的一些信息,注意
//PooledConnection中的createdTimestamp用于记录
//Connection的创建时间,而非PooledConnection
//的创建时间。所以这里要复用原连接的时间信息。
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
//设置连接为无效状态
oldestActiveConnection.invalidate();
} else { // 运行时间最长的连接并未超时
try {
if (!countedWait) {
state.hadToWaitCount++;
countedWait = true;
}
long wt = System.currentTimeMillis();
//当前线程进入等待状态
state.wait(poolTimeToWait);
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException e) {
break;
}
}
}
}
if (conn != null) {
//检测连接是否有效,isValid方法除了会检测valid是否为true,
//还会通过PooledConnection的pingConnection方法执行SQL语句,
//检测连接是否可用
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 {
//连接无效,此时累加无效连接相关的统计字段
state.badConnectionCount++;
localBadConnectionCount++;
conn = null;
if(localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
throw new SQLException(...);
}
}
}
}
}
if (conn == null) {
throw new SQLException(...);
}
return conn;
}
上面方法的逻辑:从连接池中获取连接首先会遇到两种情况:
- 连接池中有空闲连接
- 连接池中无空闲连接
对于第一种情况,把连接取出返回即可。对于第二种情况,则要进行细分:
- 活跃连接数没有超出最大活跃连接数
- 活跃连接数超出最大活跃连接数
第一种情况直接创建新的连接即可。至于第二种情况,需要再次进行细分:
- 活跃连接的运行时间超出限制,即超时了
- 活跃连接未超时
第一种情况,直接将超时连接强行中断,并进行回滚,然后复用部分字段重新创建PooledConnection即可。对于第二种情况,目前没有更好的处理方式了,只能等待。
这些逻辑对应的伪代码:
if (连接池中有空闲连接) {
1. 将连接从空闲连接集合中移除
} else {
if (活跃连接数未超出限制) {
1. 创建新连接
} else {
1. 从活跃连接集合中取出第一个元素
2. 获取连接运行时长
if (连接超时) {
1. 将连接从活跃集合中移除
2. 复用原连接的成员变量,并创建新的 PooledConnection 对象
} else {
1. 线程进入等待状态
2. 线程被唤醒后,重新执行以上逻辑
}
}
}
1. 将连接添加到活跃连接集合中
2. 返回连接
1.3.3 回收连接
回收连接成功与否只取决于空闲连接集合的状态,所需处理情况很少,因此比较简单:
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
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
state.idleConnections.add(newConn);
//复用时间信息
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
//将原连接置为无效状态
conn.invalidate();
//通知等待的线程
state.notifyAll();
} else { // 空闲连接集合已满
state.accumulatedCheckoutTime += conn.getCheckoutTime();
//回滚未提交的事务
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
//关闭数据库连接
conn.getRealConnection().close();
conn.invalidate();
}
} else {
state.badConnectionCount++;
}
}
}
首先将连接从活跃连接集合中移除,然后再根据空闲集合是否有空闲空间进行后续处理。如果空闲集合未满,此时复用原连接的字段信息创建新的连接,并将其放入空闲集合中即可。若空闲集合已满,此时无需回收连接,直接关闭即可。
回收连接的方法pushConnection是由谁调用的呢?答案是PooledConnection中的代理逻辑。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//检测close方法是否被调用,若被调用则拦截之
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)){
//将回收连接中,而不是直接将连接关闭
dataSource.pushConnection(this);
return null;
} else {
try {
if (!Object.class.equals(method.getDeclaringClass())) {
checkConnection();
}
//调用真实连接的目标方法
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
在上一节中,getConnection方法返回的是Connection代理对象。如果代理对象的close方法被调用,MyBatis并不会直接调用真实连接的close方法关闭连接,而是调用pushConnection方法回收连接。同时会唤醒处于睡眠中的线程,使其恢复运行。
二、缓存机制
在Web应用中,通常都会用Redis等缓存中间件,以减轻数据库压力。MyBatis自然也在内部提供了相应的支持。通过在框架层面增加缓存功能,可减轻数据库的压力,同时又可以提升查询速度,可谓一举两得。MyBatis缓存结构由一级缓存和二级缓存构成,这两级缓存均是使用Cache接口的实现类。
2.1 缓存类
在MyBatis 中,Cache是缓存接口,定义了一些基本的缓存操作。MyBatis内部提供了丰富的缓存实现类,比如具有基本缓存功能的PerpetualCache
,具有LRU策略的缓存LruCache
,以及可保证线程安全的缓存SynchronizedCache
和具备阻塞功能的缓存BlockingCache
等。
MyBatis在实现缓存模块的过程中,使用了装饰模式。
2.1.1 PerpetualCache
PerpetualCache是一个具有基本功能的缓存类,内部使用了HashMap实现缓存功能。
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
//存储键值对到HashMap
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
//查找缓存项
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
//移除缓存项
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
2.1.2 LruCache
LruCache,是一种具有LRU策略的缓存实现类。
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
public void setSize(final int size) {
//初始化keyMap,keyMap的类型继承自LinkedHashMap,
//并覆盖了removeEldestEntry方法
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
//覆盖LinkedHashMap的removeEldestEntry方法
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
//获取将要被移除缓存项的键值
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public void putObject(Object key, Object value) {
//存储缓存项
delegate.putObject(key, value);
cycleKeyList(key);
}
@Override
public Object getObject(Object key) {
//刷新key在keyMap中的位置
keyMap.get(key);
//从被装饰类中获取相应缓存项
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
//从被装饰类中移除相应的缓存项
return delegate.removeObject(key);
}
@Override
public void clear() {
delegate.clear();
keyMap.clear();
}
private void cycleKeyList(Object key) {
//存储key到keyMap中
keyMap.put(key, key);
if (eldestKey != null) {
//从被装饰类中移除相应的缓存项
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
}
LruCache的keyMap属性是实现LRU策略的关键,该属性类型继承自LinkedHashMap,并覆盖了removeEldestEntry方法。LinkedHashMap可保持键值对的插入顺序,当插入一个新的键值对时,LinkedHashMap内部的tail节点会指向最新插入的节点。head节点则指向第一个被插入的键值对,也就是最久未被访问的那个键值对。
默认情况下,LinkedHashMap仅维护键值对的插入顺序。若要基于LinkedHashMap实现LRU缓存,还需通过构造方法将LinkedHashMap的accessOrder属性设为true,此时LinkedHashMap会维护键值对的访问顺序。比如,上面代码中getObject方法中执行了这样一句代码keyMap.get(key),目的是刷新key对应的键值对在LinkedHashMap的位置。
LinkedHashMap会将key对应的键值对移动到链表的尾部,尾部节点表示最久刚被访问过或者插入的节点。除了需将accessOrder设为 true,还需覆盖removeEldestEntry方法。LinkedHashMap在插入新的键值对时会调用该方法,以决定是否在插入新的键值对后,移除老的键值对。
在上面的代码中,当被装饰类的容量超出了keyMap的所规定的容量(由构造方法传入)后,keyMap会移除最长时间未被访问的键,并将该键保存到eldestKey中,然后由cycleKeyList方法将eldestKey传给被装饰类的removeObject方法,移除相应的缓存项目。
2.1.3 BlockingCache
BlockingCache实现了阻塞特性,该特性是基于Java重入锁实现的。同一时刻下,BlockingCache仅允许一个线程访问指定key的缓存项,其他线程将会被阻塞住。
public class BlockingCache implements Cache {
private long timeout;
private final Cache delegate;
private final ConcurrentHashMap<Object, CountDownLatch> locks;
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public void putObject(Object key, Object value) {
try {
//存储缓存项
delegate.putObject(key, value);
} finally {
//释放锁
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
//请求锁
acquireLock(key);
Object value = delegate.getObject(key);
//若缓存命中,则释放锁。需要注意的是,未命中则不释放锁
if (value != null) {
//释放锁
releaseLock(key);
}
return value;
}
@Override
public Object removeObject(Object key) {
//释放锁
releaseLock(key);
return null;
}
@Override
public void clear() {
delegate.clear();
}
private void acquireLock(Object key) {
CountDownLatch newLatch = new CountDownLatch(1);
while (true) {
CountDownLatch latch = locks.putIfAbsent(key, newLatch);
if (latch == null) {
break;
}
try {
if (timeout > 0) {
boolean acquired = latch.await(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new CacheException(
"Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} else {
latch.await();
}
} catch (InterruptedException e) {
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
}
}
private void releaseLock(Object key) {
CountDownLatch latch = locks.remove(key);
if (latch == null) {
throw new IllegalStateException("Detected an attempt at releasing unacquired lock. This should never happen.");
}
latch.countDown();
}
public long getTimeout() {
return timeout;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
}
在查询缓存时,getObject方法会先获取与key对应的锁,并加锁。若缓存命中,getObject方法会释放锁,否则将一直锁定。getObject方法若返回null,表示缓存未命中。此时MyBatis会向数据库发起查询请求,并调用putObject方法存储查询结果。此时,putObject方法会将指定key对应的锁进行解锁,这样被阻塞的线程即可恢复运行。
2.2 CacheKey
MyBatis中,引入缓存的目的是为提高查询效率,降低数据库压力。那么缓存中的key和value的值分别是什么吗?value的内容是SQL的查询结果,key是一种复合对象,能涵盖可影响查询结果的因子。在MyBatis中,这种复合对象就是CacheKey。
public class CacheKey implements Cloneable, Serializable {
private static final int DEFAULT_MULTIPLIER = 37;
private static final int DEFAULT_HASHCODE = 17;
//乘子,默认为37
private final int multiplier;
//CacheKey的hashCode,综合了各种影响因子
private int hashcode;
//校验和
private long checksum;
//影响因子个数
private int count;
//影响因子集合
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLIER;
this.count = 0;
this.updateList = new ArrayList<>();
}
}
除了multiplier是恒定不变的 ,其他变量将在更新操作中被修改。
/** 每当执行更新操作时,表示有新的影响因子参与计算 */
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
//自增count
count++;
//计算校验和
checksum += baseHashCode;
//更新baseHashCode
baseHashCode *= count;
//计算hashCode
hashcode = multiplier * hashcode + baseHashCode;
//保存影响因子
updateList.add(object);
}
当不断有新的影响因子参与计算时,hashcode和checksum将会变得愈发复杂和随机。这样可降低冲突率,使CacheKey可在缓存中更均匀的分布。CacheKey最终要作为键存入HashMap,因此它需要覆盖equals和hashCode方法。下面看一下这两个方法的实现。
public boolean equals(Object object) {
//检测是否为同一个对象
if (this == object) {
return true;
}
//检测object是否为CacheKey
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
//检测hashCode是否相等
if (hashcode != cacheKey.hashcode) {
return false;
}
//检测校验和是否相同
if (checksum != cacheKey.checksum) {
return false;
}
//检测coutn是否相同
if (count != cacheKey.count) {
return false;
}
//如果上面的检测都通过了,下面分别对每个影响因子进行比较
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;
}
public int hashCode() {
return hashcode;
}
equals方法的检测逻辑比较严格,对CacheKey中多个成员变量进行了检测,已保证两者相等。hashCode方法比较简单,返回hashcode变量即可。
2.3 一级缓存*
在进行数据库查询之前,MyBatis首先会检查以及缓存中是否有相应的记录,若有的话直接返回即可。若一级缓存未命中,查询请求将落到数据库上。一级缓存是在BaseExecutor被初始化的:
public abstract class BaseExecutor implements Executor {
protected PerpetualCache localCache;
//...
protected BaseExecutor(Configuration configuration,Transaction transaction) {
this.localCache = new PerpetualCache("LocalCache");
//...
}
}
一级缓存的类型为PerpetualCache,没有被其他缓存类装饰过。一级缓存所存储从查询结果会在MyBatis执行更新操作(INSERT/UPDATE/DELETE),以及提交和回滚事务时被清空。下面看一下访问一级缓存的逻辑。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
//创建CacheKey
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//查询一级缓存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//存储过程相关逻辑
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//缓存未命中,则从数据库中查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
如上,在访问一级缓存之前,MyBatis首先会调用createCacheKey方法创建CacheKey:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//创建CacheKey对象
CacheKey cacheKey = new CacheKey();
//将MappedStatement的id作为影响因子进行计算
cacheKey.update(ms.getId());
//RowBounds用于分页查询,下面将它的两个字段作为影响因子进行计算
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
//获取sql语句,并进行计算
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
//当前大段代码用于获取SQL中的占位符#{xxx}对应的运行时参数
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);
}
//让运行时参数参与计算
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// 获取 Environment id 遍历,并让其参与计算
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
在计算CacheKey的过程中,有很多影响因子参与了计算。比如MappedStatement的id字段、SQL语句、分页参数、运行时变量、Environment的id字段等。通过让这些影响因子参与计算,可以很好的区分不同查询请求。所以,可以简单的把CacheKey看做是一个查询请求的id。有了CacheKey,就可以使用它读写缓存了。
在上面代码中,若一级缓存为命中,BaseExecutor会调用queryFromDatabase查询数据库,并将查询结果写入缓存中。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//向缓存中存储一个占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//查询数据库
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//移除占位符
localCache.removeObject(key);
}
//存储查询结果
localCache.putObject(key, list);
//存储过程相关逻辑
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
2.4 二级缓存
二级缓存构建在一级缓存之上,在收到查询请求时,MyBatis首先会查询二级缓存。若二级缓存未命中,再去查询一级缓存
。与一级缓存不同,二级缓存和具体的命名空间绑定,一级缓存则是和SqlSession绑定
。
在按照MyBatis规范使用SqlSession的情况下,一级缓存不存在并发问题。二级缓存则不然,1)二级缓存可在多个命名空间间共享。这种情况下,会存在并发问题,因此需要针对性的去处理。2)除了并发问题,二级缓存还存在事务问题,相关问题将在接下来进行分析。下面先来看一下CachingExecutor中的访问二级缓存的逻辑。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
//创建CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//从MappedStatement中获取Cache,注意这里的Cache
//并非是在CachingExecutor中创建的
Cache cache = ms.getCache();
//如果配置文件中没有配置<cache>,则cache为空
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
//访问二级缓存
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
//缓存未命中
if (list == null) {
//向一级缓存或者数据库进行查询
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//缓存查询结果
tcm.putObject(cache, key, list);
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
二级缓存是从MappedStatement中获取的,而非由CachingExecutor创建。由于MappedStatement存在于全局配置中,可以被多个CachingExecutor获取到,这样就会出现线程安全问题。
除此之外,若不加以控制,多个事务共用一个缓存实例,会导致脏读问题。线程安全问题可以通过SynchronizedCache装饰类解决,该装饰类会在 Cache 实例构造期间被添加上。至于脏读问题,需要借助其他类来处理,也就是上面代码中tcm变量对应的类型,即TransactionalCacheManager。
/** 事务缓存管理器 */
public class TransactionalCacheManager {
//Cache与TransactionalCache的映射关系表
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
//获取TransactionalCache对象,并调用该对象的clear方法
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
//从映射表中获取TransactionalCache
//TransactionalCache也是一种装饰类,为Cache增加事务功能
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
TransactionalCacheManager内部维护了Cache实例与TransactionalCache实例间的映射关系,该类也仅负责维护两者的映射关系,真正做事的还是TransactionalCache。TransactionalCache是一种缓存装饰器,可以为Cache实例增加事务功能。之前提到的脏读问题正是由该类进行处理的。
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
private final Cache delegate;
private boolean clearOnCommit;
//在事务被提交前,所有从数据库中查询的结果将缓存在此集合中
private final Map<Object, Object> entriesToAddOnCommit;
//在事务被提交前,当缓存未命中时,CacheKey将会被存储在此集合中
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public Object getObject(Object key) {
//查询delegate所代表的缓存
Object object = delegate.getObject(key);
if (object == null) {
//缓存未命中,则将key存入到entriesMissedInCache中
entriesMissedInCache.add(key);
}
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
//将键值对存入到entriesToAddOnCommit中,而非delegate缓存中
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
clearOnCommit = true;
//清空entriesToAddOnCommit,但不清空delegate缓存
entriesToAddOnCommit.clear();
}
public void commit() {
//根据clearOnCommit的值决定是否清空delegate
if (clearOnCommit) {
delegate.clear();
}
//刷新未缓存的结果到delegate缓存中
flushPendingEntries();
//重置entriesToAddOnCommit和entriesMissedInCache
reset();
}
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
//清空集合
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
//将entriesToAddOnCommit中的内容转存到delegate中
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
//存入空值
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
//调用removeObject进行解锁
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter. "
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
}
在TransactionalCache的代码中,要重点关注entriesToAddOnCommit集合,TransactionalCache中的很多方法都会与这个集合打交道。该集合用于存储从查询的结果,那为什么要将结果保存在该集合中,而非 delegate 所表示的缓存中呢?主要是因为直接存到delegate会导致脏数据问题。
entriesMissedInCache集合,这个集合是用于存储未命中缓存的查询请求所对应的CacheKey。单独分析与entriesMissedInCache相关的逻辑没什么意义,要搞清entriesMissedInCache的实际用途,需要把它和BlockingCache的逻辑结合起来进行分析。在BlockingCache,同一时刻仅允许一个线程通过getObject方法查询指定key对应的缓存项。如果缓存未命中,getObject方法不会释放锁,导致其他线程被阻塞住。其他线程要想恢复运行,必须进行解锁,解锁逻辑由BlockingCache的putObject和removeObject方法执行。其中putObject会在TransactionalCache的flushPendingEntries方法中被调用,removeObject方法则由TransactionalCache的unlockMissedEntries方法调用。flushPendingEntries 和unlockMissedEntries最终都会遍历entriesMissedInCache集合,并将集合元素传给BlockingCache的相关方法。这样可以解开指定key对应的锁,让阻塞线程恢复运行。
三、插件机制
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例,可基于MyBatis插件机制实现分页、分表、监控等功能。
3.1 插件机制原理
在编写插件时,除了需要让插件类实现 Interceptor 接口外,还需要通过注解标注该插件的拦截点。所谓拦截点指的是插件所能拦截的方法,MyBatis所允许拦截的方法:
- Executor::update、query、flushStatements、commit、rollback、getTransaction、close、isClosed
- ParameterHandler: getParameterObject, setParameters
- ResultSetHandler::handleResultSets, handleOutputParameters
- StatementHandler::prepare, parameterize, batch, update, query
如果想要拦截Executor的query方法,那么可以这样定义插件:
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args ={MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class}
)
})
public class ExamplePlugin implements Interceptor {
//...
}
除此之外,我们还需将插件配置到相关文件中。这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链)中。待准备工作做完后,MyBatis处于就绪状态。我们在执行SQL时,需要先通过DefaultSqlSessionFactory创建SqlSession 。Executor实例会在创建SqlSession的过程中被创建,Executor实例创建完毕后,MyBatis会通过JDK动态代理为实例生成代理类。这样,插件逻辑即可在Executor相关方法被调用前执行。这就是MyBatis插件机制的基本原理。
3.1.1 植入插件逻辑
以Executor为例,分析MyBatis是如何为Executor实例植入插件逻辑的。Executor实例是在开启SqlSession时被创建的。因此,下面从源头进行分析。先来看一下SqlSession开启的过程。先看DefaultSqlSessionFactory:
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType,
TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//...
//创建Executor
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
catch (Exception e) {...}
finally {...}
}
Executor的创建过程封装在Configuration中:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//根据executorType创建相应的Executor实例
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//植入插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
newExecutor方法在创建好Executor实例后,紧接着通过拦截器链interceptorChain为 Executor实例植入代理逻辑。接下来看下InterceptorChain:
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
//遍历拦截器集合
for (Interceptor interceptor : interceptors) {
//调用拦截器的plugin方法植入相应的插件逻辑
target = interceptor.plugin(target);
}
return target;
}
/** 添加插件实例到interceptors集合中 */
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
/** 获取插件列表 */
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
pluginAll方法会调用具体插件的plugin 方法植入相应的插件逻辑。如果有多个插件,则会多次调用plugin方法,最终生成一个层层嵌套的代理类。形如下面:
当Executor的某个方法被调用的时候,插件逻辑会先行执行。执行顺序由外而内,比如上图的执行顺序为plugin3 → plugin2 → Plugin1 → Executor。
plugin方法是由具体的插件类实现,不过该方法代码一般比较固定,所以下面找个示例分析一下,例如ExamplePlugin:
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
继续看Plugin:
public static Object wrap(Object target, Interceptor interceptor) {
//获取插件类@Signature注解内容,并生成相应的映射结构。形如下面:
//{
// Executor.class : [query, update, commit],
// ParameterHandler.class : [getParameterObject, setParameters]
//}
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
//获取目标类实现的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
//通过JDK动态代理为目标类生成代理类
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
plugin方法在内部调用了Plugin类的wrap方法,用于为目标对象生成代理。Plugin类实现了InvocationHandler接口,因此它可以作为参数传给Proxy的newProxyInstance方法。
关于插件植入的逻辑就分析完了。接下来,来看看插件逻辑是怎样执行的。
3.1.2 执行插件逻辑
Plugin实现了InvocationHandler接口,因此它的invoke方法会拦截所有的方法调用。invoke方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//获取被拦截方法列表,比如:signatureMap.get(Executor.class),
//可能返回 [query, update, commit]
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//检测方法列表是否包含被拦截的方法
if (methods != null && methods.contains(method)) {
//执行插件逻辑
return interceptor.intercept(new Invocation(target, method, args));
}
//执行被拦截的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
invoke方法会检测被拦截方法是否配置在插件的@Signature注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在intercept中,该方法的参数类型为Invocation。Invocation主要用于存储目标类,方法以及方法参数列表。下面简单看一下Invocation:
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
//调用被拦截的方法
return method.invoke(target, args);
}
}
3.2 实现一个分页插件
本节将实现一个MySQL数据库分页插件。相关代码:
@Intercepts({
@Signature(
type = Executor.class, //目标类
method = "query", //目标方法
args ={MappedStatement.class,
Object.class, RowBounds.class, ResultHandler.class}
)
})
public class MySqlPagingPlugin implements Interceptor {
private static final Integer MAPPED_STATEMENT_INDEX = 0;
private static final Integer PARAMETER_INDEX = 1;
private static final Integer ROW_BOUNDS_INDEX = 2;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
RowBounds rb = (RowBounds) args[ROW_BOUNDS_INDEX];
//无需分页
if (rb == RowBounds.DEFAULT) {
return invocation.proceed();
}
//将原RowBounds参数设为RowBounds.DEFAULT,关闭MyBatis内置的分页机制
args[ROW_BOUNDS_INDEX] = RowBounds.DEFAULT;
MappedStatement ms = (MappedStatement) args[MAPPED_STATEMENT_INDEX];
BoundSql boundSql = ms.getBoundSql(args[PARAMETER_INDEX]);
//获取SQL语句,拼接limit语句
String sql = boundSql.getSql();
String limit = String.format("LIMIT %d,%d", rb.getOffset(), rb.getLimit());
sql = sql + " " + limit;
//创建一个StaticSqlSource,并将拼接好的sql传入
SqlSource sqlSource = new StaticSqlSource(
ms.getConfiguration(), sql, boundSql.getParameterMappings());
//通过反射获取并设置MappedStatement的sqlSource字段
Field field = MappedStatement.class.getDeclaredField("sqlSource");
field.setAccessible(true);
field.set(ms, sqlSource);
//执行被拦截方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
上面的分页插件通过RowBounds参数获取分页信息,并生成相应的limit语句。之后拼接 sql,并使用该sql作为参数创建StaticSqlSource。最后通过反射替换MappedStatement对象中的sqlSource字段。