從?PageHelper?到?MyBatis?Plugin執(zhí)行概要及實(shí)現(xiàn)原理
一、背景
在很多業(yè)務(wù)場景下我們需要去攔截 SQL,達(dá)到不入侵原有代碼業(yè)務(wù)處理一些東西,比如:歷史記錄、分頁操作、數(shù)據(jù)權(quán)限過濾操作、SQL 執(zhí)行時(shí)間性能監(jiān)控等等,這里我們就可以用到 MyBatis 的插件 Plugin。下面我們來了解一下 Plugin 到底是如何工作的。
使用過 MyBatis 框架的朋友們肯定都聽說過 PageHelper 這個(gè)分頁神器吧,其實(shí) PageHelper 的底層實(shí)現(xiàn)就是依靠 plugin。下面我們來看一下 PageHelper 是如何利用 plugin 實(shí)現(xiàn)分頁的。
二、MyBatis 執(zhí)行概要圖
首先我們先看一下 MyBatis 的執(zhí)行流程圖,對其執(zhí)行流程有一個(gè)大體的認(rèn)識(shí)。

三、MyBatis 核心對象介紹
從 MyBatis 代碼實(shí)現(xiàn)的角度來看,MyBatis 的主要的核心部件有以下幾個(gè):
Configuration:初始化基礎(chǔ)配置,比如MyBatis的別名等,一些重要的類型對象,如,插件,映射器,ObjectFactory和typeHandler對象,MyBatis所有的配置信息都維持在Configuration對象之中。SqlSessionFactory:SqlSession工廠,用于生產(chǎn)SqlSession。SqlSession: 作為MyBatis工作的主要頂層API,表示和數(shù)據(jù)庫交互的會(huì)話,完成必要數(shù)據(jù)庫增刪改查功能Executor:MyBatis執(zhí)行器,是MyBatis調(diào)度的核心,負(fù)責(zé)SQL語句的生成和查詢緩存的維護(hù)StatementHandler:封裝了JDBC Statement操作,負(fù)責(zé)對JDBC Statement的操作,如設(shè)置參數(shù)、將Statement結(jié)果集轉(zhuǎn)換成List集合。ParameterHandler:負(fù)責(zé)對用戶傳遞的參數(shù)轉(zhuǎn)換成JDBC Statement所需要的參數(shù),ResultSetHandler:負(fù)責(zé)將JDBC返回的ResultSet結(jié)果集對象轉(zhuǎn)換成List類型的集合;TypeHandler:負(fù)責(zé)java數(shù)據(jù)類型和jdbc數(shù)據(jù)類型之間的映射和轉(zhuǎn)換MappedStatement:MappedStatement維護(hù)了一條<select|update|delete|insert>節(jié)點(diǎn)的封裝,SqlSource:負(fù)責(zé)根據(jù)用戶傳遞的parameterObject,動(dòng)態(tài)地生成SQL語句,將信息封裝到BoundSql對象中,并返回BoundSql:表示動(dòng)態(tài)生成的SQL語句以及相應(yīng)的參數(shù)信息
說了這么多,怎么還沒進(jìn)入正題啊,別急,下面就開始講解 Plugin 的實(shí)現(xiàn)原理。
四、Plugin 實(shí)現(xiàn)原理
MyBatis 支持對 Executor、StatementHandler、PameterHandler和ResultSetHandler 接口進(jìn)行攔截,也就是說會(huì)對這4種對象進(jìn)行代理。
下面我們結(jié)合 PageHelper 來講解 Plugin 是怎樣實(shí)現(xiàn)的。
1、定義 Plugin
要使用自定義 Plugin 首先要實(shí)現(xiàn) Interceptor 接口。可以通俗的理解為一個(gè) Plugin 就是一個(gè)攔截器。
public interface Interceptor {
// 實(shí)現(xiàn)攔截邏輯
Object intercept(Invocation invocation) throws Throwable;
// 獲取代理類
Object plugin(Object target);
// 初始化配置
void setProperties(Properties properties);
}
現(xiàn)在我們來看一下 PageHelper 是如何通過 Plugin 實(shí)現(xiàn)分頁的。
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class PageInterceptor implements Interceptor {
//緩存count查詢的ms
protected Cache<CacheKey, MappedStatement> msCountMap = null;
private Dialect dialect;
private String default_dialect_class = "com.github.pagehelper.PageHelper";
private Field additionalParametersField;
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于邏輯關(guān)系,只會(huì)進(jìn)入一次
if(args.length == 4){
//4 個(gè)參數(shù)時(shí)
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 個(gè)參數(shù)時(shí)
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
List resultList;
//調(diào)用方法判斷是否需要進(jìn)行分頁,如果不需要,直接返回結(jié)果
if (!dialect.skip(ms, parameter, rowBounds)) {
//反射獲取動(dòng)態(tài)參數(shù)
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
//判斷是否需要進(jìn)行 count 查詢
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//創(chuàng)建 count 查詢的緩存 key
CacheKey countKey = executor.createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql);
countKey.update(MSUtils.COUNT);
MappedStatement countMs = msCountMap.get(countKey);
if (countMs == null) {
//根據(jù)當(dāng)前的 ms 創(chuàng)建一個(gè)返回值為 Long 類型的 ms
countMs = MSUtils.newCountMappedStatement(ms);
msCountMap.put(countKey, countMs);
}
//調(diào)用方言獲取 count sql
String countSql = dialect.getCountSql(ms, boundSql, parameter, rowBounds, countKey);
countKey.update(countSql);
BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
//當(dāng)使用動(dòng)態(tài) SQL 時(shí),可能會(huì)產(chǎn)生臨時(shí)的參數(shù),這些參數(shù)需要手動(dòng)設(shè)置到新的 BoundSql 中
for (String key : additionalParameters.keySet()) {
countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//執(zhí)行 count 查詢
Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
Long count = (Long) ((List) countResultList).get(0);
//處理查詢總數(shù)
//返回 true 時(shí)繼續(xù)分頁查詢,false 時(shí)直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//當(dāng)查詢總數(shù)為 0 時(shí),直接返回空的結(jié)果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
//判斷是否需要進(jìn)行分頁查詢
if (dialect.beforePage(ms, parameter, rowBounds)) {
//生成分頁的緩存 key
CacheKey pageKey = cacheKey;
//處理參數(shù)對象
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
//調(diào)用方言獲取分頁 sql
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
//設(shè)置動(dòng)態(tài)參數(shù)
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//執(zhí)行分頁查詢
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不執(zhí)行分頁的情況下,也不執(zhí)行內(nèi)存分頁
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
} else {
//rowBounds用參數(shù)值,不使用分頁插件處理時(shí),仍然支持默認(rèn)的內(nèi)存分頁
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
dialect.afterAll();
}
}
@Override
public Object plugin(Object target) {
//TODO Spring bean 方式配置時(shí),如果沒有配置屬性就不會(huì)執(zhí)行下面的 setProperties 方法,就不會(huì)初始化,因此考慮在這個(gè)方法中做一次判斷和初始化
//TODO https://github.com/pagehelper/Mybatis-PageHelper/issues/26
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
//緩存 count ms
msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
String dialectClass = properties.getProperty("dialect");
if (StringUtil.isEmpty(dialectClass)) {
dialectClass = default_dialect_class;
}
try {
Class<?> aClass = Class.forName(dialectClass);
dialect = (Dialect) aClass.newInstance();
} catch (Exception e) {
throw new PageException(e);
}
dialect.setProperties(properties);
try {
//反射獲取 BoundSql 中的 additionalParameters 屬性
additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
additionalParametersField.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new PageException(e);
}
}
}
代碼太長不看系列: 其實(shí)這段代碼最主要的邏輯就是在執(zhí)行 Executor 方法的時(shí)候,攔截 query 也就是查詢類型的 SQL, 首先會(huì)判斷它是否需要分頁,如果需要分頁就會(huì)根據(jù)查詢參數(shù)在 SQL 末尾加上 limit pageNum, pageSize來實(shí)現(xiàn)分頁。
2、注冊攔截器
- 通過
SqlSessionFactoryBean去構(gòu)建Configuration添加攔截器并構(gòu)建獲取SqlSessionFactory。
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
// ... 此處省略部分源碼
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
// ... 此處省略部分源碼
// 查看是否注入攔截器,有的話添加到Interceptor集合里面
if (!isEmpty(this.plugins)) {
for (Interceptor plugin : this.plugins) {
configuration.addInterceptor(plugin);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registered plugin: '" + plugin + "'");
}
}
}
// ... 此處省略部分源碼
return this.sqlSessionFactoryBuilder.build(configuration);
}
// ... 此處省略部分源碼
}
- 通過原始的
XMLConfigBuilder構(gòu)建configuration添加攔截器
public class XMLConfigBuilder extends BaseBuilder {
//解析配置
private void parseConfiguration(XNode root) {
try {
//省略部分代碼
pluginElement(root.evalNode("plugins"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
//調(diào)用InterceptorChain.addInterceptor
configuration.addInterceptor(interceptorInstance);
}
}
}
}
上面是兩種不同的形式構(gòu)建 configuration 并添加攔截器 interceptor,上面第二種一般是以前 XML 配置的情況,這里主要是解析配置文件的 plugin 節(jié)點(diǎn),根據(jù)配置的 interceptor 屬性實(shí)例化 Interceptor 對象,然后添加到 Configuration 對象中的 InterceptorChain 屬性中。
如果定義多個(gè)攔截器就會(huì)它們鏈起來形成一個(gè)攔截器鏈,初始化配置文件的時(shí)候就把所有的攔截器添加到攔截器鏈中。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
//循環(huán)調(diào)用每個(gè)Interceptor.plugin方法
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
// 添加攔截器
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
3、執(zhí)行攔截器
從以下代碼可以看出 MyBatis 在實(shí)例化 Executor、ParameterHandler、ResultSetHandler、StatementHandler 四大接口對象的時(shí)候調(diào)用 interceptorChain.pluginAll() 方法插入進(jìn)去的。
其實(shí)就是循環(huán)執(zhí)行攔截器鏈所有的攔截器的 plugin() 方法, MyBatis 官方推薦的 plugin 方法是 Plugin.wrap() 方法,這個(gè)就會(huì)生成代理類。
public class Configuration {
protected final InterceptorChain interceptorChain = new InterceptorChain();
//創(chuàng)建參數(shù)處理器
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
//創(chuàng)建ParameterHandler
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
//插件在這里插入
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
//創(chuàng)建結(jié)果集處理器
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
//創(chuàng)建DefaultResultSetHandler
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
//插件在這里插入
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
//創(chuàng)建語句處理器
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
//創(chuàng)建路由選擇語句處理器
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
//插件在這里插入
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public Executor newExecutor(Transaction transaction) {
return newExecutor(transaction, defaultExecutorType);
}
//產(chǎn)生執(zhí)行器
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
//這句再做一下保護(hù),囧,防止粗心大意的人將defaultExecutorType設(shè)成null?
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//然后就是簡單的3個(gè)分支,產(chǎn)生3種執(zhí)行器BatchExecutor/ReuseExecutor/SimpleExecutor
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);
}
//如果要求緩存,生成另一種CachingExecutor(默認(rèn)就是有緩存),裝飾者模式,所以默認(rèn)都是返回CachingExecutor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//此處調(diào)用插件,通過插件可以改變Executor行為
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
}
4、Plugin 的動(dòng)態(tài)代理
我們首先看一下Plugin.wrap() 方法,這個(gè)方法的作用是為實(shí)現(xiàn)Interceptor注解的接口實(shí)現(xiàn)類生成代理對象的。
// 如果是Interceptor注解的接口的實(shí)現(xiàn)類會(huì)產(chǎn)生代理類
public static Object wrap(Object target, Interceptor interceptor) {
//從攔截器的注解中獲取攔截的類名和方法信息
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//取得要改變行為的類(ParameterHandler|ResultSetHandler|StatementHandler|Executor)
Class<?> type = target.getClass();
//取得接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//產(chǎn)生代理,是Interceptor注解的接口的實(shí)現(xiàn)類才會(huì)產(chǎn)生代理
if (interfaces.length > 0) {
return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));
}
return target;
}
Plugin 中的 getSignatureMap、 getAllInterfaces 兩個(gè)輔助方法,來幫助判斷是否為是否Interceptor注解的接口實(shí)現(xiàn)類。
//取得簽名Map,就是獲取Interceptor實(shí)現(xiàn)類上面的注解,要攔截的是那個(gè)類(Executor
//,ParameterHandler, ResultSetHandler,StatementHandler)的那個(gè)方法
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
//取Intercepts注解
Intercepts interceptsAnnotation =interceptor.getClass().getAnnotation(Intercepts.class);
//必須得有Intercepts注解,沒有報(bào)錯(cuò)
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
//value是數(shù)組型,Signature的數(shù)組
Signature[] sigs = interceptsAnnotation.value();
//每個(gè)class里有多個(gè)Method需要被攔截,所以這么定義
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
//取得接口
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
//攔截其他的無效
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
我們來看一下代理類的 query 方法,其實(shí)就是調(diào)用了 Plugin.invoke() 方法。代理類屏蔽了 intercept 方法的調(diào)用。
public final List query(MappedStatement mappedStatement, Object object, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException {
try {
// 這里的 h 就是一個(gè) Plugin
return (List)this.h.invoke(this, m5, new Object[]{mappedStatement, object, rowBounds, resultHandler, cacheKey, boundSql});
}
catch (Error | RuntimeException | SQLException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
最后 Plugin.invoke() 就是判斷當(dāng)前方法是否攔截,如果需要攔截則會(huì)調(diào)用 Interceptor.intercept() 對當(dāng)前方法執(zhí)行攔截邏輯。
public class Plugin implements InvocationHandler {
...
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//獲取需要攔截的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//是Interceptor實(shí)現(xiàn)類注解的方法才會(huì)攔截處理
if (methods != null && methods.contains(method)) {
//調(diào)用Interceptor.intercept,即調(diào)用自己寫的邏輯
return interceptor.intercept(new Invocation(target, method, args));
}
//最后執(zhí)行原來邏輯
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
...
總結(jié)
我們以 PageHelper 為切入點(diǎn)講解了 MyBatis Plugin 的實(shí)現(xiàn)原理,其中 MyBatis 攔截器用到責(zé)任鏈模式+動(dòng)態(tài)代理+反射機(jī)制。 通過上面的分析可以知道,所有可能被攔截的處理類都會(huì)生成一個(gè)代理類,如果有 N 個(gè)攔截器,就會(huì)有 N 個(gè)代理,層層生成動(dòng)態(tài)代理是比較耗性能的。而且雖然能指定插件攔截的位置,但這個(gè)是在執(zhí)行方法時(shí)利用反射動(dòng)態(tài)判斷的,初始化的時(shí)候就是簡單的把攔截器插入到了所有可以攔截的地方。所以盡量不要編寫不必要的攔截器,并且攔截器盡量不要寫復(fù)雜的邏輯。
以上就是從 PageHelper 到 MyBatis Plugin執(zhí)行概要及實(shí)現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于PageHelper MyBatis Plugin的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java?關(guān)鍵字break和continue的使用說明
這篇文章主要介紹了Java?關(guān)鍵字break和continue的使用,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
使用Java構(gòu)造和解析Json數(shù)據(jù)的兩種方法(詳解一)
JSON(JavaScript Object Notation) 是一種輕量級(jí)的數(shù)據(jù)交換格式,采用完全獨(dú)立于語言的文本格式,是理想的數(shù)據(jù)交換格式。接下來通過本文給大家介紹使用Java構(gòu)造和解析Json數(shù)據(jù)的兩種方法,需要的朋友參考下吧2016-03-03

