Java mybatis 開發自定義插件

介紹

MyBatis 允許你在映射語句執行過程中的某一點進行攔截調用。比如執行前、執行後或者對SQL結果集處理、sql入參處理等,這樣就可以在不修改mybatis源碼的情況下對sql執行的過程或結果進行修改,實現瞭解耦。mybatis 是在動態代理的基礎上實現的。

使用場景

如果業務中需要設置一些通用數據庫操作,比如創建時間、創建人等通用字段又或者是分頁操作等,這類都可以使用插件開發方式,PageHelper就是基於Interceptor的一個mybatis插件。

Interceptor攔截器

public interface Interceptor {

  /**
   * 子類攔截器必須要實現的方法,
   * 在該方法對內自定義攔截邏輯
   * @param invocation
   * @return
   * @throws Throwable
   */
  Object intercept(Invocation invocation) throws Throwable;

  /**
   生成目標類的代理對象
   * 也可以根據需求不返回代理對象,這種情況下這個攔截器將不起作用
   * 無特殊情況使用默認的即可
   * @param target
   * @return
   */
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  /**
   * 設置變量
   * 在註冊攔截器的時候設置變量,在這裡可以獲取到
   * @param properties
   */
  default void setProperties(Properties properties) {
    // NOP
  }

}

InterceptorChain攔截器鏈

在org.apache.ibatis.plugin包下有個InterceptorChain類,該類有個interceptors屬性,所有實現瞭Interceptor接口的攔截器都會被存儲到interceptors中。

源碼如下:

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();
  /**
   *  讓目標類在所有的攔截器中生成代理對象,並返回代理對象
   * @param target
   * @return
   */
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  /**
   * 添加過濾器
   * @param interceptor
   */
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

攔截方法

默認情況下,MyBatis 允許使用插件來攔截Executor 、ParameterHandler 、ResultSetHandler 、StatementHandler 接口下面的方法。如果系統中有配置自定義插件,默認情況下,系統會把上面四個類的默認子類都作為目標類來讓所有的攔截器進行攔截, 以保證所有的攔截器都能對Executor 、ParameterHandler 、ResultSetHandler 、StatementHandler子類進行攔截。

源碼如下: 在org.apache.ibatis.session.Configuration類中

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 使用攔截器進行攔截
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

  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;
  }

  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;
  }

  public Executor newExecutor(Transaction transaction) {
    return newExecutor(transaction, defaultExecutorType);
  }

  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;
  }

註解

Intercepts

Intercepts的作用是攔截Signature註解數組中指定的類的方法。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  /**
   * Returns method signatures to intercept.
   * Signature註解列表
   * @return method signatures
   */
  Signature[] value();
}

Signature

Signature註解作用是攔截指定類的方法。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  /**
   * Returns the java type.
   * 要攔截的類
   * @return the java type
   */
  Class<?> type();

  /**
   * Returns the method name.
   * 要攔截的類的方法
   * @return the method name
   */
  String method();

  /**
   * Returns java types for method argument.
   * 要攔截的類的方法的參數列表
   * @return java types for method argument
   */
  Class<?>[] args();
}

示例

步驟

  • 1、實現org.apache.ibatis.plugin.Interceptor接口
  • 2、添加Intercepts和Signature註解
  • 3、根據需求實現Interceptor方法邏輯

入門使用

這裡會寫兩個使用示例,一個是動態給屬性賦值,一個是打印SQL。

表結構:

CREATE TABLE `users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `gender` varchar(20) DEFAULT NULL,
  `userName` text NOT NULL,
  `create_date` datetime DEFAULT NULL COMMENT '創建日期',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

實體類:

public class UserInfo {
    private Long id;

    private String gender;
    private String userName;
    private Date createDate;
    // 省略get、set方法
}

動態給屬性賦值

在創建表時,有些是每個表都有的參數,比如創建時間、修改時間等,這類參數如果在每個類進行保存或修改的時候都進行設值的話就有點重復操作瞭,所以可以通過mybatis插件進行處理。

1、Interceptor 實現類InsertInterceptor:

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class InsertInterceptor implements Interceptor {

    private Properties properties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        final Object[] args = invocation.getArgs();

        MappedStatement mappedStatement= (MappedStatement) args[0];
        Object parameter = args[1];

        Executor executor = (Executor) invocation.getTarget();
        final Class<?> parameterClass = parameter.getClass();

        final String createDate = properties.getProperty("createDate");

        //獲取createDate 屬性描述器
        final PropertyDescriptor propertyDescriptor = new PropertyDescriptor(createDate , parameterClass);
        //獲取createDate 寫方法
        final Method writeMethod = propertyDescriptor.getWriteMethod();
        //調用createDate 寫方法
        writeMethod.invoke(parameter , new Date());

        return executor.update(mappedStatement, parameter);
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target , this);
    }

    /**
     * 設置變量
     *
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

2、mybatis配置文件中註冊InsertInterceptor

<plugins>
    <plugin interceptor="plugin.PrintSqlPlugin"/>
    <plugin interceptor="plugin.InsertInterceptor">
      <property name="createDate" value="createDate"/>
    </plugin>
  </plugins>

3、測試

public class UserTest {

  private final static SqlSessionFactory sqlSessionFactory;
  static {
    String resource = "mybatis-config.xml";
    Reader reader = null;
    try {
      reader = Resources.getResourceAsReader(resource);
    } catch (IOException e) {
      System.out.println(e.getMessage());
    }
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
  }

  @Test
  public void insert(){
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserInfo userInfo = new UserInfo();
    userInfo.setUserName("test1");
    userInfo.setGender("male");
    UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class);
    mapper.insertUser(userInfo);
    sqlSession.commit();
    sqlSession.close();
  }

}

查看數據庫,可以看到在沒有給createDate屬性收到賦值的情況下,通過攔截器進行賦值,最後是保存到數據庫中瞭。

打印SQL

Interceptor 實現類PrintSqlPlugin:

@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}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class PrintSqlPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //被代理對象
        Object target = invocation.getTarget();
        //代理方法
        Method method = invocation.getMethod();
        //方法參數
        Object[] args = invocation.getArgs();

        MappedStatement mappedStatement= (MappedStatement) args[0];
        Object parameter = args[1];

        final BoundSql mappedStatementBoundSql = mappedStatement.getBoundSql(parameter);
        System.err.println("BoundSql="+mappedStatementBoundSql.getSql());

        final Configuration configuration = mappedStatement.getConfiguration();

        final String showSql = showSql(configuration, mappedStatementBoundSql);
        System.err.println("sql="+showSql);

        //方法執行
        final Object returnValue = invocation.proceed();
        return returnValue;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 獲取參數
     * @param obj
     * @return
     */
    private static String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
            value = value.replaceAll("\\\\", "\\\\\\\\");
            value = value.replaceAll("\\$", "\\\\\\$");
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(obj) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }

        }
        return value;
    }

    /**
     * 打印SQL
     * @param configuration
     * @param boundSql
     * @return
     */
    public static String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));

            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }
}

使用同樣的方法進行測試,查看控制臺打印結果:

BoundSql=insert into users (gender,  userName ,create_date) values(? , ?, ?)
sql=insert into users (gender, userName ,create_date) values('male' , 'test2', '2022-1-14 18:40:08')

mybatis自定義插就到這裡瞭,其實操作也簡單,用好瞭也很強大。

到此這篇關於Java mybatis 開發自定義插件的文章就介紹到這瞭,更多相關Java mybatis 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: