一、攔截物件和介面實現示例
MyBatis攔截器的作用是在於Dao到DB中間進行額外的處理。大部分情況下通過mybatis的xml配置sql都可以達到想要的DB操作效果,然而存在一些類似或者相同的查詢條件或者查詢要求,這些可以通過攔截器的實現可以提升開發效率,比如:分頁、插入和更新時間/人、資料許可權、SQL監控日誌等。
-
Mybatis支援四種物件攔截Executor、StatementHandler、PameterHandler和ResultSetHandler
-
Executor:攔截執行器的方法。
-
StatementHandler:攔截Sql語法構建的處理。
-
ParameterHandler:攔截引數的處理。
-
ResultHandler:攔截結果集的處理。
1 public interface Executor { 2 ResultHandler NO_RESULT_HANDLER = null; 3 int update(MappedStatement var1, Object var2) throws SQLException; 4 <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException; 5 <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException; 6 <E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException; 7 List<BatchResult> flushStatements() throws SQLException; 8 void commit(boolean var1) throws SQLException; 9 void rollback(boolean var1) throws SQLException; 10 CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4); 11 boolean isCached(MappedStatement var1, CacheKey var2); 12 void clearLocalCache(); 13 void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class<?> var5); 14 Transaction getTransaction(); 15 void close(boolean var1); 16 boolean isClosed(); 17 void setExecutorWrapper(Executor var1); 18 } 19 public interface StatementHandler { 20 Statement prepare(Connection var1, Integer var2) throws SQLException; 21 void parameterize(Statement var1) throws SQLException; 22 void batch(Statement var1) throws SQLException; 23 int update(Statement var1) throws SQLException; 24 <E> List<E> query(Statement var1, ResultHandler var2) throws SQLException; 25 <E> Cursor<E> queryCursor(Statement var1) throws SQLException; 26 BoundSql getBoundSql(); 27 ParameterHandler getParameterHandler(); 28 } 29 public interface ParameterHandler { 30 Object getParameterObject(); 31 void setParameters(PreparedStatement var1) throws SQLException; 32 } 33 public interface ResultHandler<T> { 34 void handleResult(ResultContext<? extends T> var1); 35 }
攔截的執行順序是Executor->StatementHandler->ParameterHandler->ResultHandler
-
MyBatis提供的攔截器介面:
1 public interface Interceptor { 2 Object intercept(Invocation var1) throws Throwable; 3 default Object plugin(Object target) { 4 return Plugin.wrap(target, this); 5 } 6 default void setProperties(Properties properties) {} 7 }
Object intercept方法用於攔截器的實現;
Object plugin方法用於判斷執行攔截器的型別;
void setProperties方法用於獲取配置項的屬性。
-
攔截物件和攔截器介面的結合,自定義的攔截器類需要實現攔截器介面,並通過註解@Intercepts和引數@Signature來宣告要攔截的物件。
@Signature引數type是攔截物件,method是攔截的方法,即上面的四個類對應的方法,args是攔截方法對應的引數(方法存在過載因此需要指明引數個數和型別)
@Intercepts可以有多個@Signature,即一個攔截器實現類可以同時攔截多個物件及方法,示例如下:
-
Executor->intercept
-
StatementHandler->intercept
-
ParameterHandler->intercept
-
ResultHandler->intercept
1 @Intercepts({ 2 @Signature( 3 type = Executor.class, 4 method = "query", 5 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} 6 ) 7 }) 8 public class SelectPlugin implements Interceptor { 9 @Override 10 public Object intercept(Invocation invocation) throws Throwable { 11 if (invocation.getTarget() instanceof Executor) { 12 System.out.println("SelectPlugin"); 13 } 14 return invocation.proceed(); 15 } 16 @Override 17 public Object plugin(Object target) { 18 if (target instanceof Executor) { 19 return Plugin.wrap(target, this); 20 } 21 return target; 22 } 23 @Override 24 public void setProperties(Properties properties) {} 25 } 26 @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) 27 public class StatementPlugin implements Interceptor { 28 @Override 29 public Object intercept(Invocation invocation) throws Throwable { 30 if (invocation.getTarget() instanceof StatementHandler) { 31 System.out.println("StatementPlugin"); 32 } 33 return invocation.proceed(); 34 } 35 @Override 36 public Object plugin(Object target) { 37 if (target instanceof StatementHandler) { 38 return Plugin.wrap(target, this); 39 } 40 return target; 41 } 42 @Override 43 public void setProperties(Properties properties) {} 44 } 45 @Intercepts({@Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class})}) 46 public class ParameterPlugin implements Interceptor { 47 @Override 48 public Object intercept(Invocation invocation) throws Throwable { 49 if (invocation.getTarget() instanceof ParameterHandler) { 50 System.out.println("ParameterPlugin"); 51 } 52 return invocation.proceed(); 53 } 54 @Override 55 public Object plugin(Object target) { 56 if (target instanceof ParameterHandler) { 57 return Plugin.wrap(target, this); 58 } 59 return target; 60 } 61 @Override 62 public void setProperties(Properties properties) {} 63 } 64 @Intercepts({@Signature(type = ResultHandler.class,method = "handleResult",args = {ResultContext.class})}) 65 public class ResultPlugin implements Interceptor { 66 @Override 67 public Object intercept(Invocation invocation) throws Throwable { 68 if (invocation.getTarget() instanceof ResultHandler) { 69 System.out.println("ResultPlugin"); 70 } 71 return invocation.proceed(); 72 } 73 @Override 74 public Object plugin(Object target) { 75 if (target instanceof ResultHandler) { 76 return Plugin.wrap(target, this); 77 } 78 return target; 79 } 80 @Override 81 public void setProperties(Properties properties) {} 82 }
二、攔截器註冊的三種方式
前面介紹了Mybatis的攔截物件及其介面的實現方式,那麼在專案中如何註冊攔截器呢?本文中給出三種註冊方式。
1.XML註冊
xml註冊是最基本的方式,是通過在Mybatis配置檔案中plugins元素來進行註冊的。一個plugin對應著一個攔截器,在plugin元素可以指定property子元素,在註冊定義攔截器時把對應攔截器的所有property通過Interceptor的setProperties方法注入給攔截器。因此攔截器註冊xml方式如下:
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE configuration 3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-config.dtd"> 5 <configuration> 6 <!-- ...... --> 7 <plugins> 8 <plugin interceptor="com.tiantian.mybatis.interceptor.MyInterceptor"> 9 <property name="prop1" value="prop1"/> 10 <property name="prop2" value="prop2"/> 11 </plugin> 12 </plugins> 13 <!-- ...... --> 14 </configuration>
2.配置類註冊
配置類註冊是指通過Mybatis的配置類中宣告註冊攔截器,配置類註冊也可以通過Properties類給Interceptor的setProperties方法注入引數。具體參考如下:
1 @Configuration 2 public class MyBatisConfig { 3 @Bean 4 public String MyBatisInterceptor(SqlSessionFactory sqlSessionFactory) { 5 UpdatePlugin executorInterceptor = new UpdatePlugin(); 6 Properties properties = new Properties(); 7 properties.setProperty("prop1", "value1"); 8 // 給攔截器新增自定義引數 9 executorInterceptor.setProperties(properties); 10 sqlSessionFactory.getConfiguration().addInterceptor(executorInterceptor); 11 sqlSessionFactory.getConfiguration().addInterceptor(new StatementPlugin()); 12 sqlSessionFactory.getConfiguration().addInterceptor(new ResultPlugin()); 13 sqlSessionFactory.getConfiguration().addInterceptor(new ParameterPlugin()); 14 // sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin()); 15 return "interceptor"; 16 } 17 18 // 與sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin());效果一致 19 @Bean 20 public SelectPlugin SelectInterceptor() { 21 SelectPlugin interceptor = new SelectPlugin(); 22 Properties properties = new Properties(); 23 // 呼叫properties.setProperty方法給攔截器設定自定義引數 24 interceptor.setProperties(properties); 25 return interceptor; 26 } 27 }
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的方式去寫非常麻煩,而通過攔截器的方式可以快速實現全域性的插入修改時間和人員。先看程式碼:
1 @Component 2 @Intercepts({ 3 @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}), 4 }) 5 public class MyBatisInterceptor implements Interceptor { 6 @Override 7 public Object intercept(Invocation invocation) throws Throwable { 8 // 引數代理 9 if (invocation.getTarget() instanceof ParameterHandler) { 10 System.out.println("ParameterHandler"); 11 // 自動新增操作員資訊 12 autoAddOperatorInfo(invocation); 13 } 14 return invocation.proceed(); 15 } 16 17 @Override 18 public Object plugin(Object target) { 19 return Plugin.wrap(target, this); 20 } 21 22 @Override 23 public void setProperties(Properties properties) { 24 25 } 26 27 /** 28 * 自動新增操作員資訊 29 * 30 * @param invocation 代理物件 31 * @throws Throwable 異常 32 */ 33 private void autoAddOperatorInfo(Invocation invocation) throws Throwable { 34 System.out.println("autoInsertCreatorInfo"); 35 // 獲取代理的引數物件ParameterHandler 36 ParameterHandler ph = (ParameterHandler) invocation.getTarget(); 37 // 通過MetaObject獲取ParameterHandler的反射內容 38 MetaObject metaObject = MetaObject.forObject(ph, 39 SystemMetaObject.DEFAULT_OBJECT_FACTORY, 40 SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, 41 new DefaultReflectorFactory()); 42 // 通過MetaObject反射的內容獲取MappedStatement 43 MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("mappedStatement"); 44 // 當sql型別為INSERT或UPDATE時,自動插入操作員資訊 45 if (mappedStatement.getSqlCommandType() == SqlCommandType.INSERT || 46 mappedStatement.getSqlCommandType() == SqlCommandType.UPDATE) { 47 // 獲取引數物件 48 Object obj = ph.getParameterObject(); 49 if (null != obj) { 50 // 通過反射獲取引數物件的屬性 51 Field[] fields = obj.getClass().getDeclaredFields(); 52 // 遍歷引數物件的屬性 53 for (Field f : fields) { 54 // 如果sql是INSERT,且存在createdAt屬性 55 if ("createdAt".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) { 56 // 設定允許訪問反射屬性 57 f.setAccessible(true); 58 // 如果沒有設定createdAt屬性,則自動為createdAt屬性新增當前的時間 59 if (null == f.get(obj)) { 60 // 設定createdAt屬性為當前時間 61 f.set(obj, LocalDateTime.now()); 62 } 63 } 64 // 如果sql是INSERT,且存在createdBy屬性 65 if ("createdBy".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) { 66 // 設定允許訪問反射屬性 67 f.setAccessible(true); 68 // 如果沒有設定createdBy屬性,則自動為createdBy屬性新增當前登入的人員 69 if (null == f.get(obj)) { 70 // 設定createdBy屬性為當前登入的人員 71 f.set(obj, 0); 72 } 73 } 74 // sql為INSERT或UPDATE時均需要設定updatedAt屬性 75 if ("updatedAt".equals(f.getName())) { 76 f.setAccessible(true); 77 if (null == f.get(obj)) { 78 f.set(obj, LocalDateTime.now()); 79 } 80 } 81 // sql為INSERT或UPDATE時均需要設定updatedBy屬性 82 if ("updatedBy".equals(f.getName())) { 83 f.setAccessible(true); 84 if (null == f.get(obj)) { 85 f.set(obj, 0); 86 } 87 } 88 } 89 90 // 通過反射獲取ParameterHandler的parameterObject屬性 91 Field parameterObject = ph.getClass().getDeclaredField("parameterObject"); 92 // 設定允許訪問parameterObject屬性 93 parameterObject.setAccessible(true); 94 // 將上面設定的新引數物件設定到ParameterHandler的parameterObject屬性 95 parameterObject.set(ph, obj); 96 } 97 } 98 } 99 }
攔截器的介面實現參考前文,這裡著重介紹autoAddOperatorInfo方法裡的相關類。
1.ParameterHandler
介面原始碼:
1 public interface ParameterHandler { 2 Object getParameterObject(); 3 void setParameters(PreparedStatement var1) throws SQLException; 4 }
提供兩個方法:
getParameterObject是獲取引數物件,可能存在null,需要注意null指標。
setParameters是控制如何設定SQL引數,即sql語句中配置的java物件和jdbc型別對應的關係,例如#{id,jdbcType=INTEGER},id預設型別是javaType=class java.lang.Integer。
該介面有一個預設的實現類,原始碼如下:
1 public class DefaultParameterHandler implements ParameterHandler { 2 private final TypeHandlerRegistry typeHandlerRegistry; 3 private final MappedStatement mappedStatement; 4 private final Object parameterObject; 5 private final BoundSql boundSql; 6 private final Configuration configuration; 7 8 public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { 9 this.mappedStatement = mappedStatement; 10 this.configuration = mappedStatement.getConfiguration(); 11 this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry(); 12 this.parameterObject = parameterObject; 13 this.boundSql = boundSql; 14 } 15 16 public Object getParameterObject() { 17 return this.parameterObject; 18 } 19 20 public void setParameters(PreparedStatement ps) { 21 ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId()); 22 List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings(); 23 if (parameterMappings != null) { 24 for(int i = 0; i < parameterMappings.size(); ++i) { 25 ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i); 26 if (parameterMapping.getMode() != ParameterMode.OUT) { 27 String propertyName = parameterMapping.getProperty(); 28 Object value; 29 if (this.boundSql.hasAdditionalParameter(propertyName)) { 30 value = this.boundSql.getAdditionalParameter(propertyName); 31 } else if (this.parameterObject == null) { 32 value = null; 33 } else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) { 34 value = this.parameterObject; 35 } else { 36 MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject); 37 value = metaObject.getValue(propertyName); 38 } 39 40 TypeHandler typeHandler = parameterMapping.getTypeHandler(); 41 JdbcType jdbcType = parameterMapping.getJdbcType(); 42 if (value == null && jdbcType == null) { 43 jdbcType = this.configuration.getJdbcTypeForNull(); 44 } 45 46 try { 47 typeHandler.setParameter(ps, i + 1, value, jdbcType); 48 } catch (SQLException | TypeException var10) { 49 throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10); 50 } 51 } 52 } 53 } 54 55 } 56 }
通過DefaultParameterHandler實現類我們知道通過ParameterHandler可以獲取到哪些屬性和方法,其中包括我們下面一個重要的類MappedStatement。
2.MappedStatement
MyBatis的mapper檔案中的每個select/update/insert/delete標籤會被解析器解析成一個對應的MappedStatement物件,也就是一個MappedStatement物件描述一條SQL語句。MappedStatement物件屬性如下:
1 // mapper配置檔名 2 private String resource; 3 // mybatis的全域性資訊,如jdbc 4 private Configuration configuration; 5 // 節點的id屬性加名稱空間,如:com.example.mybatis.dao.UserMapper.selectByExample 6 private String id; 7 private Integer fetchSize; 8 private Integer timeout; 9 private StatementType statementType; 10 private ResultSetType resultSetType; 11 private SqlSource sqlSource; 12 private Cache cache; 13 private ParameterMap parameterMap; 14 private List<ResultMap> resultMaps; 15 private boolean flushCacheRequired; 16 private boolean useCache; 17 private boolean resultOrdered; 18 // sql語句的型別:select、update、delete、insert 19 private SqlCommandType sqlCommandType; 20 private KeyGenerator keyGenerator; 21 private String[] keyProperties; 22 private String[] keyColumns; 23 private boolean hasNestedResultMaps; 24 private String databaseId; 25 private Log statementLog; 26 private LanguageDriver lang; 27 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時不新增刪除標誌
1 @Mapper 2 public interface AdminProjectDao { 3 @DeletedAt(has = false) 4 List<AdminProjectPo> selectProjects(AdminProjectPo po); 5 }
攔截器通過刪除註解標記判斷是否新增刪除標誌
1 @Component 2 @Intercepts({ 3 @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}), 4 }) 5 public class MyBatisInterceptor implements Interceptor { 6 @Override 7 public Object intercept(Invocation invocation) throws Throwable { 8 if (invocation.getTarget() instanceof StatementHandler) { 9 System.out.println("StatementHandler"); 10 checkHasDeletedAtField(invocation); 11 } 12 return invocation.proceed(); 13 } 14 15 @Override 16 public Object plugin(Object target) { 17 return Plugin.wrap(target, this); 18 } 19 20 @Override 21 public void setProperties(Properties properties) { 22 23 } 24 25 /** 26 * 檢查查詢是否需要新增刪除標誌欄位 27 * 28 * @param invocation 代理物件 29 * @throws Throwable 異常 30 */ 31 private void checkHasDeletedAtField(Invocation invocation) throws Throwable { 32 System.out.println("checkHasDeletedAtField"); 33 StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); 34 // 通過MetaObject訪問物件的屬性 35 MetaObject metaObject = MetaObject.forObject( 36 statementHandler, 37 SystemMetaObject.DEFAULT_OBJECT_FACTORY, 38 SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, 39 new DefaultReflectorFactory()); 40 // 獲取成員變數mappedStatement 41 MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); 42 // 如果sql型別是查詢 43 if (mappedStatement.getSqlCommandType() == SqlCommandType.SELECT) { 44 // 獲取刪除註解標誌 45 DeletedAt annotation = null; 46 String id = mappedStatement.getId(); 47 String className = id.substring(0, id.lastIndexOf(".")); 48 String methodName = id.substring(id.lastIndexOf(".") + 1); 49 Class<?> aClass = Class.forName(className); 50 Method[] declaredMethods = aClass.getDeclaredMethods(); 51 for (Method declaredMethod : declaredMethods) { 52 declaredMethod.setAccessible(true); 53 //方法名相同,並且註解是DeletedAt 54 if (methodName.equals(declaredMethod.getName()) && declaredMethod.isAnnotationPresent(DeletedAt.class)) { 55 annotation = declaredMethod.getAnnotation(DeletedAt.class); 56 } 57 } 58 // 如果註解不存在或者註解為true(預設為true) 則為mysql語句增加刪除標誌 59 if (annotation == null || annotation.has()) { 60 BoundSql boundSql = statementHandler.getBoundSql(); 61 //獲取到原始sql語句 62 String sql = boundSql.getSql(); 63 //通過反射修改sql語句 64 Field field = boundSql.getClass().getDeclaredField("sql"); 65 field.setAccessible(true); 66 String newSql = sql.replaceAll("9=9", "9=9 and deleted_at is null "); 67 field.set(boundSql, newSql); 68 } 69 } 70 } 71 }
在SQL語句替換上需要能識別到要被替換的內容,因此在xml的sql語句中加入特殊標誌"9=9",該標誌不影響原來SQL的執行結果,不同的過濾條件可以設定不同的標誌,是一個比較巧妙的替換方式。