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进行重写增强。