大白話講解Mybatis的plugin(Interceptor)的使用

        mybatis提供了一個入口,可以讓你在語句執行過程中的某一點進行攔截調用。官方稱之為插件plugin,但是在使用的時候需要實現Interceptor介面,默認情況下,MyBatis 允許使用插件來攔截的方法調用包括以下四個對象的方法:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

以上內容在官網包括網上一搜一大把,但是用的時候,應該怎麼選擇,什麼時候用哪種,怎麼入手呢?

        我一開始想用的時候,也不知道什麼時候攔截哪種對象,後來我就寫了一個簡單的demo,大家在用mybatis的時候,無非就是crud操作,那麼我就提供四個plugin,分別來攔截Executor、ParameterHandler、ResultSetHandler、StatementHandler ;然後提供了一個controller暴露了五個介面分別是getUserInfo、listUserInfo、addUser、updateUser、deleteUser,來看下都走了那幾個plugin(demo我會上傳到碼雲上,項目架構是springboot+mybatis+mybatis-plus,資料庫我用的是postgresql-14),我認為這五個介面涵蓋了我們在開發中90%的場景,根據列印的日誌得到的結論是:

  1. 兩種查詢、新增、修改、刪除五個方法都會經過StatementHandler、ParameterHandler
  2. 兩種查詢(單個查詢、列表查詢)都會經過Executor、StatementHandler、ParameterHandler、ResultSetHandler

所以根據上面的結論,我們就可以來確定我們在開發中用哪種plugin,參考場景如下:

  1. 如果想改入參,比如postgresql據庫欄位值大小寫敏感,那麼我可以在ParameterHandler裡面獲取到入參,然後toUpperCase();
  2. 如果想改sql語句,比如改postgresql的schema,那麼我可以在StatementHandler(prepare)裡面獲取到connection修改;若是查詢場景也可以在Executor的query方法中獲取connection修改;
  3. 如果想對數據進行脫敏處理,比如查詢場景下的,查出的結果中身份證顯示前4位後4位中間***填充,那麼我們可以在ResultSetHandler的進行脫敏處理。

下面結合程式碼舉兩個場景的例子:

場景一:對查詢結果數據脫敏處理,首先定義了一個XfactorResultSetHandlerInterceptor,程式碼如下:

package com.lhclab.xfactor.dal.wrapper;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.List;
import org.apache.commons.codec.binary.StringUtils;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Intercepts({
    @Signature(type= ResultSetHandler.class,method = "handleResultSets",args = {Statement.class})
    })
public class XfactorResultSetHandlerInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("===ResultSetHandler===");
        Object resultSet = invocation.proceed();
        List resultList = (List)resultSet;
        for(Object item : resultList) {
            Class<?> sourceClass = item.getClass();
            MetaObject metaObject = SystemMetaObject.forObject(item);
            Field[] fields = sourceClass.getDeclaredFields();
            for(Field field : fields) {
                if(StringUtils.equals(field.getName(), "password")) {
                    metaObject.setValue(field.getName(), "******");
                }
            }
        }
        
        return resultSet;
    }

}

plugin定義好以後,要想讓插件起作用,需要把插件加入到MybatisSqlSessionFactoryBean中,程式碼如下(見標黃的部分)

package com.lhclab.xfactor.dal.config;

import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.lhclab.xfactor.common.exception.XfactorRuntimeException;
import com.lhclab.xfactor.dal.wrapper.XfactorExecutorInterceptor;
import com.lhclab.xfactor.dal.wrapper.XfactorParameterHandlerInterceptor;
import com.lhclab.xfactor.dal.wrapper.XfactorResultSetHandlerInterceptor;
import com.lhclab.xfactor.dal.wrapper.XfactorStatementHandlerInterceptor;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
@MapperScan("com.lhclab.xfactor.dal.dao")
public class DataSourceConfig {
    
    @Autowired
    private DataSourceProperties properties;
    
    @Bean
    public DataSource dataSource() {
        log.info("資料庫連接池創建中......");
        HikariDataSource dataSource = null;
        try {
            dataSource = DataSourceBuilder.create(properties.getClassLoader())
                    .type(HikariDataSource.class)
                    .driverClassName(properties.determineDriverClassName())
                    .url(properties.determineUrl())
                    .username(properties.determineUsername()).password(properties.getPassword())
                    .build();
        } catch (Exception e) {
            throw new XfactorRuntimeException("get password failed!", e);
        }
        return dataSource;
    }
    
    @Bean
    public SqlSessionFactory xfactorSqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource());
//        sqlSessionFactoryBean.setPlugins(mybatisPlusInterceptor(), new AnalyseMybatisPluginsInterceptor());
        sqlSessionFactoryBean.setPlugins(new XfactorResultSetHandlerInterceptor(), 
                new XfactorParameterHandlerInterceptor(), 
                new XfactorStatementHandlerInterceptor(),
                new XfactorExecutorInterceptor());
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/*xml"));
        sqlSessionFactoryBean.setTypeAliasesPackage("com.lhclab.xfactor.dal.dao.entity");
        return sqlSessionFactoryBean.getObject();
    }
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
        return interceptor;
    }
}

場景二:更改查詢庫表的schema(場景類似於修改sql語句),首先定義了一個XfactorStatementHandlerInterceptor,程式碼如下:

package com.lhclab.xfactor.dal.wrapper;

import java.sql.Connection;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import com.zaxxer.hikari.pool.HikariProxyConnection;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Intercepts({
    @Signature(type= StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
})
public class XfactorStatementHandlerInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("===StatementHandler===");
        ((HikariProxyConnection)invocation.getArgs()[0]).setSchema("notes");//這裡改schema
        
        //這裡改sql,但是如果是對select的sql語句進行修改,建議實現Executor.class的plugin中進行,當前方式改select語句insert/update/delete都會走這個判斷
        MetaObject metaObject = SystemMetaObject.forObject(((RoutingStatementHandler)invocation.getTarget()).getBoundSql());
        String execSql = (String) metaObject.getValue("sql");
        if(execSql.startsWith("select ") || execSql.startsWith("SELECT ")) {
            metaObject.setValue("sql", execSql.concat(" order by id desc"));
        }
        return invocation.proceed();
    }

}

結合以上兩個場景可知,有些目的可以通過多個類型的plugin都能實現,但是肯定有一個是最佳方案的(plugin定義好以後,要想讓插件起作用,需要把插件加入到MybatisSqlSessionFactoryBean中,程式碼見加粗的部分)。

Tags: