上次發文說到了如何集成分頁插件MyBatis插件原理分析,看完感受本身better了,今天咱們接着來聊mybatis插件的原理。sql
mybatis插件涉及到的幾個類:數據庫
我將以 Executor 爲例,分析 MyBatis 是如何爲 Executor 實例植入插件的。Executor 實例是在開啓 SqlSession 時被建立的,所以,咱們從源頭進行分析。先來看一下 SqlSession 開啓的過程。設計模式
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 中,咱們跟進去看看看。mybatis
// 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) {...}
else if (ExecutorType.REUSE == executorType) {...}
else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 植入插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
如上,newExecutor 方法在建立好 Executor 實例後,緊接着經過攔截器鏈 interceptorChain 爲 Executor 實例植入代理邏輯。那下面咱們看一下 InterceptorChain 的代碼是怎樣的。app
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
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);
}
}
上面的for循環表明了只要是插件,都會以責任鏈的方式逐一執行(別期望它能跳過某個節點),所謂插件,其實就相似於攔截器。ide
這裏就用到了責任鏈設計模式,責任鏈設計模式就至關於咱們在OA系統裏發起審批,領導們一層一層進行審批。源碼分析
以上是 InterceptorChain 的所有代碼,比較簡單。它的 pluginAll 方法會調用具體插件的 plugin 方法植入相應的插件邏輯。若是有多個插件,則會屢次調用 plugin 方法,最終生成一個層層嵌套的代理類。形以下面:ui
當 Executor 的某個方法被調用的時候,插件邏輯會先行執行。執行順序由外而內,好比上圖的執行順序爲 plugin3 → plugin2 → Plugin1 → Executor
。this
plugin 方法是由具體的插件類實現,不過該方法代碼通常比較固定,因此下面找個示例分析一下。spa
// TianPlugin類
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 方法。
到這裏,關於插件植入的邏輯就分析完了。接下來,咱們來看看插件邏輯是怎樣執行的。
Plugin 實現了 InvocationHandler 接口,所以它的 invoke 方法會攔截全部的方法調用。invoke 方法會對所攔截的方法進行檢測,以決定是否執行插件邏輯。該方法的邏輯以下:
//在Plugin類中
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 方法的代碼比較少,邏輯不難理解。首先,invoke 方法會檢測被攔截方法是否配置在插件的 @Signature 註解中,如果,則執行插件邏輯,不然執行被攔截方法。插件邏輯封裝在 intercept 中,該方法的參數類型爲 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 proceed() throws InvocationTargetException, IllegalAccessException {
//反射調用被攔截的方法
return method.invoke(target, args);
}
}
關於插件的執行邏輯就分析到這,整個過程不難理解,你們簡單看看便可。
下面爲了讓你們更好的理解Mybatis的插件機制,咱們來模擬一個慢sql監控的插件。
/**
* 慢查詢sql 插件
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class SlowSqlPlugin implements Interceptor {
private long slowTime;
//攔截後須要處理的業務
@Override
public Object intercept(Invocation invocation) throws Throwable {
//經過StatementHandler獲取執行的sql
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
long start = System.currentTimeMillis();
//結束攔截
Object proceed = invocation.proceed();
long end = System.currentTimeMillis();
long f = end - start;
System.out.println(sql);
System.out.println("耗時=" + f);
if (f > slowTime) {
System.out.println("本次數據庫操做是慢查詢,sql是:");
System.out.println(sql);
}
return proceed;
}
//獲取到攔截的對象,底層也是經過代理實現的,其實是拿到一個目標代理對象
@Override
public Object plugin(Object target) {
//觸發intercept方法
return Plugin.wrap(target, this);
}
//設置屬性
@Override
public void setProperties(Properties properties) {
//獲取咱們定義的慢sql的時間閾值slowTime
this.slowTime = Long.parseLong(properties.getProperty("slowTime"));
}
}
而後把這個插件類注入到容器中。
而後咱們來執行查詢的方法。
耗時28秒的,大於咱們定義的10毫秒,那這條SQL就是咱們認爲的慢SQL。
經過這個插件,咱們就能很輕鬆的理解setProperties()方法是作什麼的了。
也是實現mybatis接口Interceptor。
@SuppressWarnings({"rawtypes", "unchecked"})
@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 {
@Override
public Object intercept(Invocation invocation) throws Throwable {
...
}
intercept方法中
//AbstractHelperDialect類中
@Override
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
String sql = boundSql.getSql();
Page page = getLocalPage();
//支持 order by
String orderBy = page.getOrderBy();
if (StringUtil.isNotEmpty(orderBy)) {
pageKey.update(orderBy);
sql = OrderByParser.converToOrderBySql(sql, orderBy);
}
if (page.isOrderByOnly()) {
return sql;
}
//獲取分頁sql
return getPageSql(sql, page, pageKey);
}
//模板方法模式中的鉤子方法
public abstract String getPageSql(String sql, Page page, CacheKey pageKey);
AbstractHelperDialect類的實現類有以下(也就是此分頁插件支持的數據庫就如下幾種):
咱們用的是MySQL。這裏也有與之對應的。
@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ? ");
} else {
sqlBuilder.append(" LIMIT ?, ? ");
}
pageKey.update(page.getPageSize());
return sqlBuilder.toString();
}
到這裏咱們就知道了,它無非就是在咱們執行的SQL上再拼接了Limit罷了。同理,Oracle也就是使用rownum來處理分頁了。下面是Oracle處理分頁
@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
if (page.getStartRow() > 0) {
sqlBuilder.append("SELECT * FROM ( ");
}
if (page.getEndRow() > 0) {
sqlBuilder.append(" SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( ");
}
sqlBuilder.append(sql);
if (page.getEndRow() > 0) {
sqlBuilder.append(" ) TMP_PAGE WHERE ROWNUM <= ? ");
}
if (page.getStartRow() > 0) {
sqlBuilder.append(" ) WHERE ROW_ID > ? ");
}
return sqlBuilder.toString();
}
其餘數據庫分頁操做相似。關於具體原理分析,這裏就不必贅述了,由於分頁插件源代碼裏註釋基本上全是中文。
Spring-Boot+Mybatis繼承了分頁插件,以及使用案例、插件的原理分析、源碼分析、如何自定義插件。
涉及到技術點:JDK動態代理、責任鏈設計模式、模板方法模式。
Mybatis插件關鍵對象總結: