Mybatis只能攔截ParameterHandler、ResultSetHandler、StatementHandler、Executor共4個接口對象內的方法。
重新審視interceptorChain.pluginAll()方法:該方法在創建上述4個接口對象時調用,其含義為給這些接口對象注冊攔截器功能,注意是注冊,而不是執行攔截。
攔截器執行時機:plugin()方法注冊攔截器后,那么,在執行上述4個接口對象內的具體方法時,就會自動觸發攔截器的執行,也就是插件的執行。
所以,一定要分清,何時注冊,何時執行。切不可認為pluginAll()或plugin()就是執行,它只是注冊。
(1)方法plugin(Object target)
plugin方法是攔截器用於封裝目標對象的,通過該方法我們可以返回目標對象本身,也可以返回一個它的代理。當返回的是代理的時候我們可以對其中的方法進行攔截來調用intercept方法,當然也可以調用其他方法。
(2)方法setProperties(Properties properties)
setProperties方法是用於在Mybatis配置文件中指定一些屬性的。
(3)方法intercept(Invocation invocation)
定義自己的Interceptor最重要的是要實現plugin方法和intercept方法,在plugin方法中我們可以決定是否要進行攔截進而決定要返回一個什么樣的目標對象。而intercept方法就是要進行攔截的時候要執行的方法。
這章我們來講解下如何使用 mybatis 的 plugin,自定義插件,通過這章我們將講述如何使用 plugin 插件。
基於 XML 方式我們實現一個 給入參的對象上增加一個 update_date 數值
1. 基於 xml 方式實現 plugin 方式的插件
1.1 首先,我們要寫一個類,實現 mybatis 的 inteceptor 接口
package root.configure; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.*; import org.apache.log4j.Logger; import java.lang.reflect.Field; import java.util.Date; import java.util.Map; import java.util.Properties; /** * @Auther: lxf * @Date: 2018/11/16 15:13 * @Description: */ @Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}) public class ParamInterceptor implements Interceptor { private static Logger log = Logger.getLogger(ParamInterceptor.class); @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("aaaaaaaaaaaaaaaa=========="); // 這個判斷可以不要,因為 注解當中 只攔截了Executor // 攔截 Executor 的 update 方法 生成sql前將 tenantId 設置到實體中 // 當然 我們也可以不用 instanceof 的方法,我們可以在 類上面的注解的 args 當中指定 if (invocation.getTarget() instanceof Executor && invocation.getArgs().length == 2) { // Executor 的第一個參數是 ms MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); if(SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)){ // Executor 的第二個參數是 param Object object = invocation.getArgs()[1]; // 得到參數 Date currentDate = new Date(System.currentTimeMillis()); // 原對象要聲明 屬性 updateDate if(object instanceof Map){ ((Map)object).put("update_date",currentDate); // 如果是一個 map 類型的入參 ,我們就給其植入一個 update_date 的值 }else { Field fieldModifyTime = object.getClass().getDeclaredField("updateDate"); fieldModifyTime.setAccessible(true); fieldModifyTime.set(object, currentDate); if(SqlCommandType.INSERT.equals(sqlCommandType)){ // 再插入 創建時間 fieldModifyTime = object.getClass().getDeclaredField("creatDate"); fieldModifyTime.setAccessible(true); fieldModifyTime.set(object, currentDate); } } } } return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); // 沒什么特殊要求的話就注冊到插件鏈里面 // return null; } // 對屬性進行配置 @Override public void setProperties(Properties properties) { } }
1.2 緊接着,我們需要把這個 plugin 定義到 configuration.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> <!-- 注意dtd 是引用mybatis的,而不是sprinh的 --> <!-- 指定properties配置文件, 我這里面配置的是數據庫相關 ,注意 文件夾路徑,我這里存放的是同級的這里 --> <properties resource="resource.properties"></properties> <!-- 指定Mybatis使用log4j --> <settings> <setting name="logImpl" value="LOG4J" /> <setting name="cacheEnabled" value="true" /> </settings> <plugins> <!-- 自定義plugin插件 --> <plugin interceptor="root.configure.ParamInterceptor"> <property name="paramInterceptor" value="100" /> </plugin> </plugins> <environments default="development"> <!-- 可以配置多個environment 來達到多數據源的配置,或者說能達到 生產、測試、開發環境的切換 --> <environment id="development"> <!-- 指定jdbc的事務管理,還沒跟spring結合成事務管理 --> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <!-- 上面指定了數據庫配置文件, 配置文件里面也是對應的這四個屬性 一定要跟resource.properties當中的屬性對應上 --> <property name="driver" value="${jdbc.driverClass}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.userName}" /> <property name="password" value="${jdbc.password}" /> </dataSource> </environment> </environments> <!-- 映射文件,mybatis精髓, 后面才會細講 ,掃描mybatis的mapper映射文件,一定要放在resource文件夾下 --> <!-- mapper resource= 的值 必須是resource文件夾下的路徑值 --> <mappers> <mapper resource="UserMapper.xml"/> </mappers> </configuration>
1.3 然后我們編寫 UserMapper 接口和其 Mapper.xml
那么我們需要注意什么嗎? 其實跟普通的編寫 Mapper 接口一樣,只是我們要想在哪處加這個 update_date 那么肯定我們自己加羅 :
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="UserMapper">
<!-- 二級緩存+ ehcache 下面兩個選一個就好,一個有日志,一個沒有 -->
<!-- type="org.mybatis.caches.ehcache.EhcacheCache" -->
<cache type="org.mybatis.caches.ehcache.LoggingEhcache" />
<!--<cache></cache>-->
<select id="findUserById" parameterType="int" resultType="Map">
select user_id,user_name,CREATION_DATE from fnd_user where _id=#{id,jdbcType=INTEGER}
</select>
<!-- 測試能否執行存儲過程 -->
<select id="callProcTest" parameterType="int" resultType="Map" statementType="CALLABLE">
{ call myfuncdictproc2(#{p_dict_id,mode=IN,jdbcType=INTEGER}) }
</select>
<!-- useCache="false" 是用來禁止二級緩存的 ,一定要設置成 true,這個時候才能使二級緩存、ehcache生效 -->
<select id="findUserByIdForCache" parameterType="int" resultType="Map" useCache="true">
select user_id,user_name,CREATION_DATE from fnd_user where _id=#{id,jdbcType=INTEGER}
</select>
<insert id="testUserMapperTypeHandle" parameterType="Map">
insert into test_dict(code,name) values(#{code,typeHandler=root.report.db.TestIntegerTypeHandle},'${name}')
</insert>
<update id="testPluginForUpdate" parameterType="Map"> <!-- 1. 定義成Map,因為我們對map進行處理, 2. sql當中有update_date是因為我這里想看到效果,相對其賦值 -->
update test_dict set name = ${name},update_date=${update_date} where code=#{code}
</update>
</mapper>
import org.apache.ibatis.annotations.Mapper; import java.util.List; import java.util.Map; /** * @Auther: lxf * @Date: 2018/11/7 10:19 * @Description: */ @Mapper public interface UserMapper { Map<String,Object> findUserById(int id); List<Map<String,Object>> callProcTest(int dict_id); Map<String,Object> findUserByIdForCache(int id); void testUserMapperTypeHandle(Map map); void testPluginForUpdate(Map map); }
1.4 main 方法測試 :
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import org.apache.ibatis.cache.Cache; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.cache.TransactionalCacheManager; import org.apache.ibatis.executor.BaseExecutor; import org.apache.ibatis.io.Resources; import org.apache.ibatis.mapping.StatementType; import org.apache.ibatis.session.*; import org.apache.ibatis.transaction.Transaction; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.LinkedCaseInsensitiveMap; import root.configure.ParamInterceptor; import root.report.db.DbFactory; import root.report.db.TestIntegerTypeHandle; import root.report.util.ExecuteSqlUtil; import root.report.util.cache.EhcacheManager; import java.io.IOException; import java.net.URL; import java.sql.Connection; import java.sql.SQLException; import java.util.*; import java.util.concurrent.locks.ReadWriteLock; /** * @Auther: pccw * @Date: 2018/11/7 10:27 * @Description: * 測試自己寫的mybatis方法 */ public class TestMybatis { public static void main(String[] args) throws IOException { // testEhcache(); // testUserMapper(); // executeTest(); // showEhcache(); // testTypeHandle(); testPlugin(); }
public static void testPlugin() throws IOException{
SqlSessionFactory sessionFactory = null;
String resource = "configuration.xml";
sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(resource)); //從類路徑中加載資源
// 這是一個半成品 xml 跟代碼結合的方式
// configuration.getTypeHandlerRegistry().register(GregorianCalendarTypeHandle.class);
// sessionFactory.getConfiguration().getTypeHandlerRegistry().register(TestIntegerTypeHandle.class);
SqlSession sqlSessionOne = sessionFactory.openSession();
UserMapper userMapperOne = sqlSessionOne.getMapper(UserMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("code",1905201314);
map.put("name",String.valueOf("adwadawdwadwadawd11111"));
userMapperOne.testPluginForUpdate(map); // ---》 不管做任何操作,都會被 Executor 執行,我們攔截的就是 Executor,而且是map形式的入參
sqlSessionOne.commit();
}
}
1.5 我們看下測試結果

可以看到我們的plugin已經生效了,所以注意到,我們的plugin是對全局的configuration生效,一定不能亂用。雖然我上面沒寫對 date 格式,但是已經成功了。
2. 基於 代碼方式怎么植入 我們自己寫的 interceptor 呢
這里我就貼出一段代碼
2.1 首先,比如我們自己最常用的 PageRowBound 來自於 分頁插件,先引入坐標
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
2.2 這樣我們就有 分頁插件了

2.3 若是使用xml 的方式,跟我們上述一樣注入就行了,若是代碼方式就要知道注入plugin 的時機
// 初始化 public static void init(String dbName) { long t1 = System.nanoTime(); try { JSONObject dbJson = JSONObject.parseObject(manager.getDBConnectionByName(dbName)); if (dbJson.size() == 0) { return; } String dbtype = dbJson.getString("dbtype"); SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); DruidDataSource dataSource = new DruidDataSource(); dataSource.setUsername(dbJson.getString("username")); dataSource.setPassword(erpUtil.decode(dbJson.getString("password"))); dataSource.setDriverClassName(dbJson.getString("driver")); if ("Mysql".equals(dbtype)) { dataSource.setUrl(dbJson.getString("url") + "?serverTimezone=UTC&useSSL=true&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&rewriteBatchedStatements=true"); } else { dataSource.setUrl(dbJson.getString("url")); } dataSource.setMaxWait(10000);//設置連接超時時間10秒 dataSource.setMaxActive(Integer.valueOf(dbJson.getString("maxPoolSize"))); dataSource.setInitialSize(Integer.valueOf(dbJson.getString("minPoolSize"))); dataSource.setTimeBetweenEvictionRunsMillis(60000);//檢測數據源空連接間隔時間 dataSource.setMinEvictableIdleTimeMillis(300000);//連接空閑時間 dataSource.setTestWhileIdle(true); dataSource.setTestOnBorrow(true); // if ("Oracle".equals(dbtype)) { // dataSource.setPoolPreparedStatements(true); // } // if ("DB2".equals(dbtype)) { // dataSource.setValidationQuery("select 'x' from sysibm.sysdummy1"); // } else { // dataSource.setValidationQuery("select 'x' from dual"); // } // dataSource.setFilters("stat"); // List<Filter> filters = new ArrayList<Filter>(); // filters.add(new SqlFilter()); // dataSource.setProxyFilters(filters); dataSource.init(); //填充數據源 factoryBean.setDataSource(dataSource); // ----------》 注入 dataSource 的時機 //填充SQL文件 factoryBean.setMapperLocations(getMapLocations(dbtype, dbName)); // ---------> 注入 掃描文件的時機 Configuration configuration = new Configuration(); configuration.setCallSettersOnNulls(true); //啟動SQL日志 configuration.setLogImpl(Log4jImpl.class); // ----> 注入日志的時機 configuration.getTypeHandlerRegistry().register(GregorianCalendarTypeHandle.class); // --> 注入 typeHandle 的時機 factoryBean.setConfiguration(configuration); // ---------> 注入 configuration 的時機 factoryBean.setPlugins(getMybatisPlugins(dbtype)); // ------------> 注入 plugin 的時機 mapFactory.put(dbJson.getString("name"), factoryBean.getObject()); // ----> 得到 SqlSessionFactory 的時機 long t2 = System.nanoTime(); log.info("初始化數據庫【" + dbName + "】耗時" + String.format("%.4fs", (t2 - t1) * 1e-9)); } catch (Exception e) { log.error("初始化數據庫【" + dbName + "】失敗!"); e.printStackTrace(); } }
2.3 分頁插件原理
由於Mybatis采用的是邏輯分頁,而非物理分頁,那么,市場上就出現了可以實現物理分頁的Mybatis的分頁插件。
要實現物理分頁,就需要對Stringsql進行攔截並增強,Mybatis通過BoundSql對象存儲String sql,而BoundSql則由StatementHandler對象獲取。
public interface StatementHandler {
List query(Statement statement, ResultHandler resultHandler) throws SQLException;
BoundSql getBoundSql();
}
public class BoundSql {
public String getSql() {
return sql;
}
}
因此,就需要編寫一個針對StatementHandler的query方法攔截器,然后獲取到sql,對sql進行重寫增強。
