首頁 > 軟體

Java mybatis 開發自定義外掛

2022-08-17 14:00:49

介紹

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 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com