Mybatis外掛功能

2023-08-26 18:00:40

1 外掛的作用

在Mybatis執行SQL的生命週期中,會使用外掛進行埋點,主要包括Executor、StatementHandler、ParameterHandler和ResultSetHandler等。在執行到這些特殊節點時,就會觸發攔截器的攔截方法。

通過自定義外掛,我們可以對這些核心的節點中進行特殊處理,主要應用場景包括分頁、記錄紀錄檔、加解密等。

2 外掛的工作原理

Mybatis外掛的核心類包括:

  • Interceptor:攔截器
  • Interceptors和Signature:攔截資訊註解,指定需要攔截的類和方法
  • InterceptorChain:攔截器集合
  • Plugin:建立動態代理物件的工具類
  • Invocation:封裝被代理物件、執行方法和引數資訊

Mybatis外掛的工作流程如下:

  1. 通過實現Interceptor介面自定義攔截器,並使用@Interceptors和@Signature註解指定需要攔截的類和方法
  2. 將自定義攔截器新增到Mybatis設定
  3. 在Mybatis啟動時,會使用InterceptorChain儲存設定的所有攔截器
  4. 在Mybatis執行SQL時,會讀取InterceptorChain,使用Plugin對Executor/StatementHandler/ParameterHandler/ResultSetHandler進行動態代理
  5. 在執行代理物件方法時,會將被代理物件、當前執行方法和引數資訊封裝成Invocation,傳遞給攔截器

建立Executor/StatementHandler/ParameterHandler/ResultSetHandler代理物件的方法都位於Configuration:

// org.apache.ibatis.session.Configuration#newExecutor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor 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;
}
// org.apache.ibatis.session.Configuration#newStatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  // 使用攔截器進行動態代理
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}
// org.apache.ibatis.session.Configuration#newParameterHandler
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
  // 使用攔截器進行動態代理
  parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}
// org.apache.ibatis.session.Configuration#newResultSetHandler
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
    ResultHandler resultHandler, BoundSql boundSql) {
  ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
  // 使用攔截器進行動態代理
  resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
  return resultSetHandler;
}

接下來我們來看建立Executor/StatementHandler/ParameterHandler/ResultSetHandler物件&進行代理的節點。

在使用SqlSessionFactory#openSession建立SqlSEession時,會建立Executor物件,並進行代理:

// org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
	// 建立Executor
    final Executor executor = configuration.newExecutor(tx, execType);
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(tx); // may have fetched a connection so lets call close()
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

在Executor執行SQL時,會建立StatementHandler,並進行代理:

// org.apache.ibatis.executor.SimpleExecutor#doQuery
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    Configuration configuration = ms.getConfiguration();
	// 建立StatementHandler
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

ParameterHandler和ResultSetHandler作為StatementHandler的成員變數存在,會在其建構函式中進行建立和代理:

// BaseStatementHandler建構函式
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  // ……
  this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
  this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}

接下來我們來看InterceptorChain如何使用攔截器集合對Executor/StatementHandler/ParameterHandler/ResultSetHandler物件進行代理。

InterceptorChain會遍歷攔截器集合,進行一層一層代理:

// org.apache.ibatis.plugin.InterceptorChain#pluginAll
public Object pluginAll(Object target) {
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
  }
  return target;
}

每一次代理都會呼叫Plugin#wrap,它只是對JDK動態代理進行了簡單應用:

// org.apache.ibatis.plugin.Interceptor#plugin
default Object plugin(Object target) {
  return Plugin.wrap(target, this);
}
// org.apache.ibatis.plugin.Plugin#wrap
public static Object wrap(Object target, Interceptor interceptor) {
  // 獲取攔截設定資訊
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  Class<?> type = target.getClass();
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}
  • target:被代理物件,Executor、StatementHandler、ParameterHandler或ResultSetHandler物件
  • interfaces:被代理物件所實現的介面,Executor、StatementHandler、ParameterHandler或ResultSetHandler介面
  • interceptor:攔截器
  • signatureMap:攔截方法資訊(哪些方法需要攔截)

Plugin本身實現了InvocationHandler方法,其中就定義了代理邏輯,主要會根據設定判斷是否需要進行攔截,並執行對應方法:

// org.apache.ibatis.plugin.Plugin#invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
  	// 從當前攔截器設定資訊中獲取當前方法的攔截資訊
    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);
  }
}

上述攔截設定資訊來自於Interceptor實現類上的@Intercepts和@Signature註解,通過Signature#type指定需要攔截的類,通過Signature#method和Signature#args共同指定需要攔截的方法:

// org.apache.ibatis.plugin.Plugin#getSignatureMap
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
  // 獲取@Intercepts註解
  Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
  if (interceptsAnnotation == null) {
    throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
  }
  // 獲取@Sinature註解
  Signature[] sigs = interceptsAnnotation.value();
  Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
  // 獲取需要攔截的類(type)、方法(method和args)
  for (Signature sig : sigs) {
    Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
    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;
}

3 自定義攔截器

自定義攔截器主要有兩個步驟:

  1. 建立攔截器:實現Interceptor介面,標註@Intercepts和@Signature註解
  2. 註冊攔截器:新增攔截器到Mybatis設定

3.1 建立攔截器

建立攔截器只需要實現Interceptor介面:

public class CustomInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 攔截業務處理
        return null;
    }
}

如果僅僅是進行切面處理(如記錄紀錄檔),要記得執行代理物件的代理方法:

public Object intercept(Invocation invocation) throws Throwable {
    // before……
    // 獲取代理資訊
    Object target = invocation.getTarget();
    Method method = invocation.getMethod();
    Object[] args = invocation.getArgs();
    Object result = method.invoke(target, args);
    // after……
    return result;
}

如果需要執行自定義邏輯,甚至可以不執行代理物件的代理方法,完全由我們自己定義業務邏輯。

我們還需要指定需要攔截的類和方法,例如如果要攔截org.apache.ibatis.executor.Executor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)方法,我們可以新增如下註解:

@Intercepts(
    {
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
    }
)

我們還可以設定多個攔截類和方法,既可以是同一個類,也可以是不同類。

但是通常不推薦&不會為一個攔截器設定多個攔截類,因為這樣會造成程式碼邏輯混亂,職責不明確。

建立攔截器很簡單,但是最重要的是要選擇適合的需要攔截的類和方法

因為Executor/StatementHandler/ParameterHandler/ResultSetHandler的方法很多,在Mybatis執行SQL過程中,有些方法可能不會被觸發。

這就對開發人員有兩個要求:

  1. 熟悉Mybatis執行SQL流程
  2. 明確攔截業務需求

3.2 註冊攔截器

註冊攔截器,本質上是需要將自定義的攔截器新增到Mybatis的設定資訊中(InterceptorChain)。

對於原生Mybatis或Mybatis-Spring場景中,可以直接使用Configuration#addInterceptor方法:

CustomInterceptor customInterceptor = new CustomInterceptor();
configuration.addInterceptor(customInterceptor);

如果使用Mybatis-SpringBoot框架,則只需要將攔截器註冊為Bean新增到Spring容器中:

  1. 直接新增@Component註解
  2. 使用@Bean新增

在自動設定過程中,會按以下流程註冊攔截器:

  1. 讀取容器中的Interceptor Bean物件
  2. 新增到SqlSessionFactoryBean
  3. 註冊到Configuration
// org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration#MybatisAutoConfiguration
public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider, ObjectProvider<List<SqlSessionFactoryBeanCustomizer>> sqlSessionFactoryBeanCustomizers) {
    // 1、讀取容器中的Interceptor Bean物件
    this.interceptors = (Interceptor[])interceptorsProvider.getIfAvailable();
    // ……
}
// org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration#sqlSessionFactory
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    if (!ObjectUtils.isEmpty(this.interceptors)) {
        // 2、新增到SqlSessionFactoryBean
        factory.setPlugins(this.interceptors);
    }
    // ……
}

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
  if (!isEmpty(this.plugins)) {
    // 3、註冊到Configuration
    Stream.of(this.plugins).forEach(plugin -> {
      targetConfiguration.addInterceptor(plugin);
    });
  }
}

4 開源框架案例

基於Mybatis外掛擴充套件的開源框架比較少,最常用、最熱門的應該是PageHelper。

GitHub:https://github.com/pagehelper/Mybatis-PageHelper

它的原理是自定義了攔截器:com.github.pagehelper.PageInterceptor

在設定分頁資訊時,會將分頁資訊新增到執行緒變數中:

PageHelper.startPage(pageNum, pageSize);
// com.github.pagehelper.page.PageMethod#setLocalPage
protected static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}
// com.github.pagehelper.page.PageMethod#LOCAL_PAGE
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

在執行org.apache.ibatis.executor.Executor#query方法時,會觸發該攔截器,如果執行緒變數中存在分頁資訊,進行分頁邏輯。主要流程如下:

  1. 查詢總數
  2. 查詢分頁
  3. 封裝響應
  4. 清除執行緒變數的分頁資訊