学会自己编写Mybatis插件(拦截器)实现自定义需求2

简介: 学会自己编写Mybatis插件(拦截器)实现自定义需求

学会自己编写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})
})
  1. target对应我们拦截的Executor对象
  2. method对应Executor#update方法
  3. 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 注解包含以下三个属性。

  1. type 设置拦截接口,可选值是前面提到的4个接口
  2. method 设置拦截接口中的方法名 可选值是前面4个接口中所对应的方法,需要和接口匹配
  3. 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 = {})
}

如果有时间的话,我会更加建议看了的小伙伴,自己去实现接口做个测试,验证一番,也能了解的更彻底些。看会了,很多时候知识的记忆还是浅的。

五、进一步思考

看完这篇文章后,不知道你有没有什么收获。

再次看看这张文章大纲的图吧

image.png

试着思考思考下面几个问题:

  • Mybatis插件适用于哪些场景?回忆一下你做过的项目,是否有可以使用Mybatis插件来实现的呢?
  • 你可以编写一个Mybatis插件了吗?
  • 感兴趣的话,你可以试着去了解一下Mybatis分页插件的实现方式。

最后留下一个遇到的问题,也是下一篇文章可能会写的吧,同时也使用到了今天所谈到了的拦截器。

在项目中,你们都是如何针对表中某些字段进行加解密的呢?

后语

其实也是因为上面留下的那个问题,才让我去拜读了《Mybatis技术内幕》和《Mybatis从入门到精通》两本书,收获还是有不少的。

其中《Mybatis技术内幕》内容相对更进阶一些,如果是已经会使用Mybatis的小伙伴,可以看一看,通过书籍再去看源码,能够温习到之前许多不太清楚的地方。

下次再见吧。



目录
相关文章
|
1月前
|
SQL Java 数据库连接
|
2月前
|
Java 数据库连接 Maven
使用mybatis插件generator生成实体类,dao层和mapper映射
使用mybatis插件generator生成实体类,dao层和mapper映射
52 0
|
19天前
|
SQL Java 数据库连接
【mybatis】第一篇,Springboot中使用插件PageHelper不生效解决方案
【mybatis】第一篇,Springboot中使用插件PageHelper不生效解决方案
|
3月前
|
SQL Oracle 关系型数据库
整合Mybatis-Plus高级,Oracle 主键Sequence,Sql 注入器实现自定义全局操作
整合Mybatis-Plus高级,Oracle 主键Sequence,Sql 注入器实现自定义全局操作
89 0
|
3月前
|
SQL 数据库
在mybatis-plus怎么使用自定义的sql语句
在mybatis-plus怎么使用自定义的sql语句
66 0
|
3天前
|
SQL XML Java
Mybatis-Plus插件扩展MybatisX
Mybatis-Plus插件扩展MybatisX
14 0
|
19天前
|
存储 关系型数据库 MySQL
【mybatis-plus】Springboot+AOP+自定义注解实现多数据源操作(数据源信息存在数据库)
【mybatis-plus】Springboot+AOP+自定义注解实现多数据源操作(数据源信息存在数据库)
|
19天前
|
Java 关系型数据库 MySQL
【mybatis-plus】自定义多数据源,动态切换数据源事务失效问题
【mybatis-plus】自定义多数据源,动态切换数据源事务失效问题
【mybatis-plus】自定义多数据源,动态切换数据源事务失效问题
|
1月前
|
SQL Java 数据库连接
Mybatis拦截器实现带参数SQL语句打印
Mybatis拦截器实现带参数SQL语句打印
|
2月前
|
XML 监控 druid
【Java专题_02】springboot+mybatis+pagehelper分页插件+druid数据源详细教程
【Java专题_02】springboot+mybatis+pagehelper分页插件+druid数据源详细教程