学会自己编写Mybatis插件(拦截器)实现自定义需求1:https://developer.aliyun.com/article/1394844
三、拦截器接口介绍
MyBatis 插件可以用来实现拦截器接口 Interceptor ,在实现类中对拦截对象和方法进行处理
public interface Interceptor { // 执行拦截逻辑的方法 Object intercept(Invocation invocation) throws Throwable; //这个方法的参数 target 就是拦截器要拦截的对象,该方法会在创建被拦截的接口实现类时被调用。 //该方法的实现很简单 ,只需要调用 MyBatis 提供的 Plug 类的 wrap 静态方法就可以通过 Java 动态代理拦截目标对象。 default Object plugin(Object target) { return Plugin.wrap(target, this); } //这个方法用来传递插件的参数,可以通过参数来改变插件的行为 default void setProperties(Properties properties) { // NOP } }
有点懵没啥事,一个一个展开说:
intercept 方法
Object intercept(Invocation invocation) throws Throwable;
简单说就是执行拦截逻辑的方法
,但不得不说这句话是个高度概括~
首先我们要明白参数Invocation
是个什么东东:
public class Invocation { private final Object target; // 拦截的对象信息 private final Method method; // 拦截的方法信息 private final Object[] args; // 拦截的对象方法中的参数 public Invocation(Object target, Method method, Object[] args) { this.target = target; this.method = method; this.args = args; } // get... // 利用反射来执行拦截对象的方法 public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); } }
联系我们之前实现的自定义拦截器上的注解:
@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) })
target
对应我们拦截的Executor
对象method
对应Executor#update
方法args
对应Executor#update#args
参数
plugin方法
这个方法其实也很好说:
那就是Mybatis在创建拦截器代理时候会判断一次,当前这个类 Interceptor 到底需不需要生成一个代理进行拦截,如果需要拦截,就生成一个代理对象,这个代理就是一个 {@link Plugin},它实现了jdk的动态代理接口 {@link InvocationHandler},如果不需要代理,则直接返回目标对象本身 加载时机:该方法在 mybatis 加载核心配置文件时被调用
default Object plugin(Object target) { return Plugin.wrap(target, this); }
public class Plugin implements InvocationHandler { // 利用反射,获取这个拦截器 MyInterceptor 的注解 Intercepts和Signature,然后解析里面的值, // 1 先是判断要拦截的对象是哪一个 // 2 然后根据方法名称和参数判断要对哪一个方法进行拦截 // 3 根据结果做出决定,是返回一个对象呢还是代理对象 public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); // 这边就是判断当前的interceptor是否包含在 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } //如果不需要代理,则直接返回目标对象本身 return target; } //.... }
setProperties方法
在拦截器中可能需要使用到一些变量参数,并且这个参数是可配置的,这个时候我们就可以使用这个方法啦,加载时机:该方法在 mybatis 加载核心配置文件时被调用
default void setProperties(Properties properties) { // NOP }
关于如何使用:
javaConfig方式设置:
@Bean public ConfigurationCustomizer configurationCustomizer() { return new ConfigurationCustomizer() { @Override public void customize(org.apache.ibatis.session.Configuration configuration) { // 开启驼峰命名映射 configuration.setMapUnderscoreToCamelCase(true); MybatisMetaInterceptor mybatisMetaInterceptor = new MybatisMetaInterceptor(); Properties properties = new Properties(); properties.setProperty("param1","javaconfig-value1"); properties.setProperty("param2","javaconfig-value2"); mybatisMetaInterceptor.setProperties(properties); configuration.addInterceptor(mybatisMetaInterceptor); } }; }
通过mybatis-config.xml
文件进行配置
<configuration> <plugins> <plugin interceptor="com.nzc.interceptor.MybatisMetaInterceptor"> <property name="param1" value="value1"/> <property name="param2" value="value2"/> </plugin> </plugins> </configuration>
测试效果就是测试案例上那般,通过了解拦截器接口的信息,对于之前的案例不再是那般模糊啦
接下来再接着聊一聊拦截器上面那一坨注解信息是用来干嘛的吧,
注意
当配置多个拦截器时, MyBatis 会遍历所有拦截器,按顺序执行拦截器的 plugin 口方法, 被拦截的对象就会被层层代理。
在执行拦截对象的方法时,会一层层地调用拦截器,拦截器通 invocation proceed()调用下层的方法,直到真正的方法被执行。
方法执行的结果 从最里面开始向外 层层返回,所以如果存在按顺序配置的三个签名相同的拦截器, MyBaits 会按照 C>B>A>target.proceed()>A>B>C 的顺序执行。如果签名不同, 就会按照 MyBatis 拦截对象的逻辑执行.
这也是我们最开始谈到的Mybatis插件模块所使用的设计模式-责任链模式。
四、拦截器注解介绍
上一个章节,我们只说明如何实现Interceptor
接口来实现拦截,却没有说明要拦截的对象是谁,在什么时候进行拦截.就关系到我们之前编写的注解信息啦.
@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) })
这两个注解用来配置拦截器要拦截的接口的方法。
@Intercepts({})
注解中是一个@Signature()
数组,可以在一个拦截器中同时拦截不同的接口和方法。
MyBatis 允许在己映射语句执行过程中的某一点进行拦截调用。默认情况下, MyBatis 允许使用插件来拦截的接口包括以下几个。
- Executor
- ParameterHandler
- ResultSetHandler
- StatementHandler
@Signature
注解包含以下三个属性。
- type 设置拦截接口,可选值是前面提到的4个接口
- method 设置拦截接口中的方法名 可选值是前面4个接口中所对应的方法,需要和接口匹配
- args 设置拦截方法的参数类型数组 通过方法名和参数类型可以确定唯一一个方法
Executor 接口
下面就是Executor接口的类信息
public interface Executor { int update(MappedStatement ms, Object parameter) throws SQLException; <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; List<BatchResult> flushStatements() throws SQLException; void commit(boolean required) throws SQLException; void rollback(boolean required) throws SQLException; CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql); boolean isCached(MappedStatement ms, CacheKey key); void clearLocalCache(); void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType); Transaction getTransaction(); void close(boolean forceRollback); boolean isClosed(); void setExecutorWrapper(Executor executor); }
我只会简单说一些最常用的~
1、update
int update(MappedStatement ms, Object parameter) throws SQLException;
该方法会在所有的 INSERT、UPDATE、DELETE 执行时被调用,因此如果想要拦截这类操作,可以拦截该方法。接口方法对应的签名如下。
@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) })
2、query
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
该方法会在所有 SELECT 查询方法执行时被调用 通过这个接口参数可以获取很多有用的信息,这也是最常被拦截的方法。
@Intercepts({@Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ), @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class} )})
3、queryCursor:
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
该方法只有在查询 的返回值类型为 Cursor 时被调用 。接口方法对应的签名类似于之前的。
//该方法只在通过 SqlSession 方法调用 commit 方法时才被调用 void commit(boolean required) throws SQLException; //该方法只在通过 SqlSessio口方法调用 rollback 方法时才被调用 void rollback(boolean required) throws SQLException; //该方法只在通过 SqlSession 方法获取数据库连接时才被调用, Transaction getTransaction(); //该方法只在延迟加载获取新的 Executor 后才会被执行 void close(boolean forceRollback); //该方法只在延迟加载执行查询方法前被执行 boolean isClosed();
注解的编写方法都是类似的。
ParameterHandler 接口
public interface ParameterHandler { //该方法只在执行存储过程处理出参的时候被调用 Object getParameterObject(); //该方法在所有数据库方法设置 SQL 参数时被调用。 void setParameters(PreparedStatement ps) throws SQLException; }
我都写一块啦,如果要拦截某一个的话只写一个即可
@Intercepts({ @Signature(type = ParameterHandler.class, method = "getParameterObject", args = {}), @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}) })
ResultSetHandler 接口
public interface ResultSetHandler { //该方法会在除存储过程及返回值类型为 Cursor 以外的查询方法中被调用。 <E> List<E> handleResultSets(Statement stmt) throws SQLException; //只会在返回值类型为 ursor 查询方法中被调用 <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException; //只在使用存储过程处理出参时被调用 , void handleOutputParameters(CallableStatement cs) throws SQLException; }
@Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}), @Signature(type = ResultSetHandler.class, method = "handleCursorResultSets", args = {Statement.class}), @Signature(type = ResultSetHandler.class, method = "handleOutputParameters", args = {CallableStatement.class}) })
StatementHandler 接口
public interface StatementHandler { //该方法会在数据库执行前被调用 优先于当前接口中的其他方法而被执行 Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException; //该方法在 prepare 方法之后执行,用于处理参数信息 void parameterize(Statement statement) throws SQLException; //在全局设置配置 defaultExecutorType BATCH 时,执行数据操作才会调用该方法 void batch(Statement statement) throws SQLException; //执行UPDATE、DELETE、INSERT方法时执行 int update(Statement statement) throws SQLException; //执行 SELECT 方法时调用,接口方法对应的签名如下。 <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException; <E> Cursor<E> queryCursor(Statement statement) throws SQLException; //获取实际的SQL字符串 BoundSql getBoundSql(); ParameterHandler getParameterHandler(); }
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class}), @Signature(type = StatementHandler.class, method = "parameterize", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "query", args = {Statement.class,ResultHandler.class}), @Signature(type = StatementHandler.class, method = "queryCursor", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "getBoundSql", args = {}), @Signature(type = StatementHandler.class, method = "getParameterHandler", args = {}) }
如果有时间的话,我会更加建议看了的小伙伴,自己去实现接口做个测试,验证一番,也能了解的更彻底些。看会了,很多时候知识的记忆还是浅的。
五、进一步思考
看完这篇文章后,不知道你有没有什么收获。
再次看看这张文章大纲的图吧
试着思考思考下面几个问题:
- Mybatis插件适用于哪些场景?回忆一下你做过的项目,是否有可以使用Mybatis插件来实现的呢?
- 你可以编写一个Mybatis插件了吗?
- 感兴趣的话,你可以试着去了解一下Mybatis分页插件的实现方式。
最后留下一个遇到的问题,也是下一篇文章可能会写的吧,同时也使用到了今天所谈到了的拦截器。
在项目中,你们都是如何针对表中某些字段进行加解密的呢?
后语
其实也是因为上面留下的那个问题,才让我去拜读了《Mybatis技术内幕》和《Mybatis从入门到精通》两本书,收获还是有不少的。
其中《Mybatis技术内幕》内容相对更进阶一些,如果是已经会使用Mybatis的小伙伴,可以看一看,通过书籍再去看源码,能够温习到之前许多不太清楚的地方。
下次再见吧。