Mybatis(六)数据源、缓存机制、插件机制

本系列文章:
  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无需提供连接池功能,因此它的实现非常简单。核心的方法有三个:

  1. initializeDriver:初始化数据库驱动。
  2. doGetConnection:获取数据连接。
  3. 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方法主要包含三步操作:

  1. 加载驱动
  2. 通过反射创建驱动实例
  3. 注册驱动实例

  上面代码中出现了缓存相关的逻辑,这个是用于避免重复注册驱动。因为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;
	}

  上面方法的逻辑:从连接池中获取连接首先会遇到两种情况:

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

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

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

  第一种情况直接创建新的连接即可。至于第二种情况,需要再次进行细分:

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

  第一种情况,直接将超时连接强行中断,并进行回滚,然后复用部分字段重新创建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所允许拦截的方法:

  1. Executor::update、query、flushStatements、commit、rollback、getTransaction、close、isClosed
  2. ParameterHandler: getParameterObject, setParameters
  3. ResultSetHandler::handleResultSets, handleOutputParameters
  4. 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字段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值