MyBatis攔截器

飄梧發表於2021-06-16

一、攔截物件和介面實現示例

        MyBatis攔截器的作用是在於Dao到DB中間進行額外的處理。大部分情況下通過mybatis的xml配置sql都可以達到想要的DB操作效果,然而存在一些類似或者相同的查詢條件或者查詢要求,這些可以通過攔截器的實現可以提升開發效率,比如:分頁、插入和更新時間/人、資料許可權、SQL監控日誌等。

  • Mybatis支援四種物件攔截Executor、StatementHandler、PameterHandler和ResultSetHandler

  1. Executor:攔截執行器的方法。

  2. StatementHandler:攔截Sql語法構建的處理。

  3. ParameterHandler:攔截引數的處理。

  4. 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,即一個攔截器實現類可以同時攔截多個物件及方法,示例如下:

    1. Executor->intercept

    2. StatementHandler->intercept

    3. ParameterHandler->intercept

    4. 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的執行結果,不同的過濾條件可以設定不同的標誌,是一個比較巧妙的替換方式。

相關文章