MyBatis攔截器的原理與使用
一、攔截對象和接口實現示例
MyBatis攔截器的作用是在於Dao到DB中間進行額外的處理。大部分情況下通過mybatis的xml配置sql都可以達到想要的DB操作效果,然而存在一些類似或者相同的查詢條件或者查詢要求,這些可以通過攔截器的實現可以提升開發效率,比如:分頁、插入和更新時間/人、數據權限、SQL監控日志等。
- Mybatis支持四種對象攔截Executor、StatementHandler、PameterHandler和ResultSetHandler
- Executor:攔截執行器的方法。
- StatementHandler:攔截Sql語法構建的處理。
- ParameterHandler:攔截參數的處理。
- ResultHandler:攔截結果集的處理。
public interface Executor { ResultHandler NO_RESULT_HANDLER = null; int update(MappedStatement var1, Object var2) throws SQLException; <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException; <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException; <E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException; List<BatchResult> flushStatements() throws SQLException; void commit(boolean var1) throws SQLException; void rollback(boolean var1) throws SQLException; CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4); boolean isCached(MappedStatement var1, CacheKey var2); void clearLocalCache(); void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class<?> var5); Transaction getTransaction(); void close(boolean var1); boolean isClosed(); void setExecutorWrapper(Executor var1); } public interface StatementHandler { Statement prepare(Connection var1, Integer var2) throws SQLException; void parameterize(Statement var1) throws SQLException; void batch(Statement var1) throws SQLException; int update(Statement var1) throws SQLException; <E> List<E> query(Statement var1, ResultHandler var2) throws SQLException; <E> Cursor<E> queryCursor(Statement var1) throws SQLException; BoundSql getBoundSql(); ParameterHandler getParameterHandler(); } public interface ParameterHandler { Object getParameterObject(); void setParameters(PreparedStatement var1) throws SQLException; } public interface ResultHandler<T> { void handleResult(ResultContext<? extends T> var1); }
攔截的執行順序是Executor->StatementHandler->ParameterHandler->ResultHandler
- MyBatis提供的攔截器接口:
public interface Interceptor { Object intercept(Invocation var1) throws Throwable; default Object plugin(Object target) { return Plugin.wrap(target, this); } default void setProperties(Properties properties) {} }
Object intercept方法用於攔截器的實現;
Object plugin方法用於判斷執行攔截器的類型;
void setProperties方法用於獲取配置項的屬性。
- 攔截對象和攔截器接口的結合,自定義的攔截器類需要實現攔截器接口,並通過註解@Intercepts和參數@Signature來聲明要攔截的對象。
@Signature參數type是攔截對象,method是攔截的方法,即上面的四個類對應的方法,args是攔截方法對應的參數(方法存在重載因此需要指明參數個數和類型)
@Intercepts可以有多個@Signature,即一個攔截器實現類可以同時攔截多個對象及方法,示例如下:
- Executor->intercept
- StatementHandler->intercept
- ParameterHandler->intercept
- ResultHandler->intercept
@Intercepts({ @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ) }) public class SelectPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { if (invocation.getTarget() instanceof Executor) { System.out.println("SelectPlugin"); } return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof Executor) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties properties) {} } @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class StatementPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { if (invocation.getTarget() instanceof StatementHandler) { System.out.println("StatementPlugin"); } return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties properties) {} } @Intercepts({@Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class})}) public class ParameterPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { if (invocation.getTarget() instanceof ParameterHandler) { System.out.println("ParameterPlugin"); } return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof ParameterHandler) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties properties) {} } @Intercepts({@Signature(type = ResultHandler.class,method = "handleResult",args = {ResultContext.class})}) public class ResultPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { if (invocation.getTarget() instanceof ResultHandler) { System.out.println("ResultPlugin"); } return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof ResultHandler) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties properties) {} }
二、攔截器註冊的三種方式
前面介紹瞭Mybatis的攔截對象及其接口的實現方式,那麼在項目中如何註冊攔截器呢?本文中給出三種註冊方式。
1.XML註冊
xml註冊是最基本的方式,是通過在Mybatis配置文件中plugins元素來進行註冊的。一個plugin對應著一個攔截器,在plugin元素可以指定property子元素,在註冊定義攔截器時把對應攔截器的所有property通過Interceptor的setProperties方法註入給攔截器。因此攔截器註冊xml方式如下:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- ...... --> <plugins> <plugin interceptor="com.tiantian.mybatis.interceptor.MyInterceptor"> <property name="prop1" value="prop1"/> <property name="prop2" value="prop2"/> </plugin> </plugins> <!-- ...... --> </configuration>
2.配置類註冊
配置類註冊是指通過Mybatis的配置類中聲明註冊攔截器,配置類註冊也可以通過Properties類給Interceptor的setProperties方法註入參數。具體參考如下:
@Configuration public class MyBatisConfig { @Bean public String MyBatisInterceptor(SqlSessionFactory sqlSessionFactory) { UpdatePlugin executorInterceptor = new UpdatePlugin(); Properties properties = new Properties(); properties.setProperty("prop1", "value1"); // 給攔截器添加自定義參數 executorInterceptor.setProperties(properties); sqlSessionFactory.getConfiguration().addInterceptor(executorInterceptor); sqlSessionFactory.getConfiguration().addInterceptor(new StatementPlugin()); sqlSessionFactory.getConfiguration().addInterceptor(new ResultPlugin()); sqlSessionFactory.getConfiguration().addInterceptor(new ParameterPlugin()); // sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin()); return "interceptor"; } // 與sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin());效果一致 @Bean public SelectPlugin SelectInterceptor() { SelectPlugin interceptor = new SelectPlugin(); Properties properties = new Properties(); // 調用properties.setProperty方法給攔截器設置自定義參數 interceptor.setProperties(properties); return interceptor; } }
3.註解方式
通過@Component註解方式是最簡單的方式,在不需要轉遞自定義參數時可以使用,方便快捷。
@Component @Intercepts({ @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ) }) public class SelectPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { if (invocation.getTarget() instanceof Executor) { System.out.println("SelectPlugin"); } return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof Executor) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties properties) { } }
三、ParameterHandler參數改寫-修改時間和修改人統一插入
針對具體的攔截器實現進行描述。日常編碼需求中會碰到修改時需要插入修改的時間和人員,如果要用xml的方式去寫非常麻煩,而通過攔截器的方式可以快速實現全局的插入修改時間和人員。先看代碼:
@Component @Intercepts({ @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}), }) public class MyBatisInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 參數代理 if (invocation.getTarget() instanceof ParameterHandler) { System.out.println("ParameterHandler"); // 自動添加操作員信息 autoAddOperatorInfo(invocation); } return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } /** * 自動添加操作員信息 * * @param invocation 代理對象 * @throws Throwable 異常 */ private void autoAddOperatorInfo(Invocation invocation) throws Throwable { System.out.println("autoInsertCreatorInfo"); // 獲取代理的參數對象ParameterHandler ParameterHandler ph = (ParameterHandler) invocation.getTarget(); // 通過MetaObject獲取ParameterHandler的反射內容 MetaObject metaObject = MetaObject.forObject(ph, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory()); // 通過MetaObject反射的內容獲取MappedStatement MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("mappedStatement"); // 當sql類型為INSERT或UPDATE時,自動插入操作員信息 if (mappedStatement.getSqlCommandType() == SqlCommandType.INSERT || mappedStatement.getSqlCommandType() == SqlCommandType.UPDATE) { // 獲取參數對象 Object obj = ph.getParameterObject(); if (null != obj) { // 通過反射獲取參數對象的屬性 Field[] fields = obj.getClass().getDeclaredFields(); // 遍歷參數對象的屬性 for (Field f : fields) { // 如果sql是INSERT,且存在createdAt屬性 if ("createdAt".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) { // 設置允許訪問反射屬性 f.setAccessible(true); // 如果沒有設置createdAt屬性,則自動為createdAt屬性添加當前的時間 if (null == f.get(obj)) { // 設置createdAt屬性為當前時間 f.set(obj, LocalDateTime.now()); } } // 如果sql是INSERT,且存在createdBy屬性 if ("createdBy".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) { // 設置允許訪問反射屬性 f.setAccessible(true); // 如果沒有設置createdBy屬性,則自動為createdBy屬性添加當前登錄的人員 if (null == f.get(obj)) { // 設置createdBy屬性為當前登錄的人員 f.set(obj, 0); } } // sql為INSERT或UPDATE時均需要設置updatedAt屬性 if ("updatedAt".equals(f.getName())) { f.setAccessible(true); if (null == f.get(obj)) { f.set(obj, LocalDateTime.now()); } } // sql為INSERT或UPDATE時均需要設置updatedBy屬性 if ("updatedBy".equals(f.getName())) { f.setAccessible(true); if (null == f.get(obj)) { f.set(obj, 0); } } } // 通過反射獲取ParameterHandler的parameterObject屬性 Field parameterObject = ph.getClass().getDeclaredField("parameterObject"); // 設置允許訪問parameterObject屬性 parameterObject.setAccessible(true); // 將上面設置的新參數對象設置到ParameterHandler的parameterObject屬性 parameterObject.set(ph, obj); } } } }
攔截器的接口實現參考前文,這裡著重介紹autoAddOperatorInfo方法裡的相關類。
1.ParameterHandler
接口源碼:
public interface ParameterHandler { Object getParameterObject(); void setParameters(PreparedStatement var1) throws SQLException; }
提供兩個方法:
getParameterObject是獲取參數對象,可能存在null,需要註意null指針。
setParameters是控制如何設置SQL參數,即sql語句中配置的java對象和jdbc類型對應的關系,例如#{id,jdbcType=INTEGER},id默認類型是javaType=class java.lang.Integer。
該接口有一個默認的實現類,源碼如下:
public class DefaultParameterHandler implements ParameterHandler { private final TypeHandlerRegistry typeHandlerRegistry; private final MappedStatement mappedStatement; private final Object parameterObject; private final BoundSql boundSql; private final Configuration configuration; public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { this.mappedStatement = mappedStatement; this.configuration = mappedStatement.getConfiguration(); this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry(); this.parameterObject = parameterObject; this.boundSql = boundSql; } public Object getParameterObject() { return this.parameterObject; } public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings(); if (parameterMappings != null) { for(int i = 0; i < parameterMappings.size(); ++i) { ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { String propertyName = parameterMapping.getProperty(); Object value; if (this.boundSql.hasAdditionalParameter(propertyName)) { value = this.boundSql.getAdditionalParameter(propertyName); } else if (this.parameterObject == null) { value = null; } else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) { value = this.parameterObject; } else { MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject); value = metaObject.getValue(propertyName); } TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { jdbcType = this.configuration.getJdbcTypeForNull(); } try { typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (SQLException | TypeException var10) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10); } } } } } }
通過DefaultParameterHandler實現類我們知道通過ParameterHandler可以獲取到哪些屬性和方法,其中包括我們下面一個重要的類MappedStatement。
2.MappedStatement
MyBatis的mapper文件中的每個select/update/insert/delete標簽會被解析器解析成一個對應的MappedStatement對象,也就是一個MappedStatement對象描述一條SQL語句。MappedStatement對象屬性如下:
// mapper配置文件名 private String resource; // mybatis的全局信息,如jdbc private Configuration configuration; // 節點的id屬性加命名空間,如:com.example.mybatis.dao.UserMapper.selectByExample private String id; private Integer fetchSize; private Integer timeout; private StatementType statementType; private ResultSetType resultSetType; private SqlSource sqlSource; private Cache cache; private ParameterMap parameterMap; private List<ResultMap> resultMaps; private boolean flushCacheRequired; private boolean useCache; private boolean resultOrdered; // sql語句的類型:select、update、delete、insert private SqlCommandType sqlCommandType; private KeyGenerator keyGenerator; private String[] keyProperties; private String[] keyColumns; private boolean hasNestedResultMaps; private String databaseId; private Log statementLog; private LanguageDriver lang; private String[] resultSets;
在本例中通過MappedStatement對象的sqlCommandType來判斷當前的sql類型是insert、update來進行下一步的操作。
四、通過StatementHandler改寫SQL
StatementHandler是用於封裝JDBC Statement操作,負責對JDBC Statement的操作,如設置參數,並將Statement結果集轉換成List集合。
實現代碼如下:
刪除註解標記
@Target({ElementType.METHOD}) //表示註解的使用范圍 @Retention(RetentionPolicy.RUNTIME) //註解的保存時間 @Documented //文檔顯示 public @interface DeletedAt { boolean has() default true; }
Dao層添加刪除註解,為false時不添加刪除標志
@Mapper public interface AdminProjectDao { @DeletedAt(has = false) List<AdminProjectPo> selectProjects(AdminProjectPo po); }
攔截器通過刪除註解標記判斷是否添加刪除標志
@Component @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}), }) public class MyBatisInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { if (invocation.getTarget() instanceof StatementHandler) { System.out.println("StatementHandler"); checkHasDeletedAtField(invocation); } return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } /** * 檢查查詢是否需要添加刪除標志字段 * * @param invocation 代理對象 * @throws Throwable 異常 */ private void checkHasDeletedAtField(Invocation invocation) throws Throwable { System.out.println("checkHasDeletedAtField"); StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); // 通過MetaObject訪問對象的屬性 MetaObject metaObject = MetaObject.forObject( statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory()); // 獲取成員變量mappedStatement MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); // 如果sql類型是查詢 if (mappedStatement.getSqlCommandType() == SqlCommandType.SELECT) { // 獲取刪除註解標志 DeletedAt annotation = null; String id = mappedStatement.getId(); String className = id.substring(0, id.lastIndexOf(".")); String methodName = id.substring(id.lastIndexOf(".") + 1); Class<?> aClass = Class.forName(className); Method[] declaredMethods = aClass.getDeclaredMethods(); for (Method declaredMethod : declaredMethods) { declaredMethod.setAccessible(true); //方法名相同,並且註解是DeletedAt if (methodName.equals(declaredMethod.getName()) && declaredMethod.isAnnotationPresent(DeletedAt.class)) { annotation = declaredMethod.getAnnotation(DeletedAt.class); } } // 如果註解不存在或者註解為true(默認為true) 則為mysql語句增加刪除標志 if (annotation == null || annotation.has()) { BoundSql boundSql = statementHandler.getBoundSql(); //獲取到原始sql語句 String sql = boundSql.getSql(); //通過反射修改sql語句 Field field = boundSql.getClass().getDeclaredField("sql"); field.setAccessible(true); String newSql = sql.replaceAll("9=9", "9=9 and deleted_at is null "); field.set(boundSql, newSql); } } } }
在SQL語句替換上需要能識別到要被替換的內容,因此在xml的sql語句中加入特殊標志”9=9″,該標志不影響原來SQL的執行結果,不同的過濾條件可以設置不同的標志,是一個比較巧妙的替換方式。
以上就是MyBatis攔截器的原理與使用的詳細內容,更多關於MyBatis攔截器的資料請關註WalkonNet其它相關文章!