为了账号安全,请及时绑定邮箱和手机立即绑定

mybatis 拦截器学习

标签:
Java Python API

目录

最近项目中需要开发对指定规则的一组sql进行拦截操作,如果添加在业务层中会增加耦合性,后期的扩展和维护也会很麻烦,因此想到之前用过的mybatis 拦截器,这里学习记录一番。

MyBatis拦截器介绍

官方介绍

从官网我们介绍我们可知:

MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能。

MyBatis允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • ParameterHandler (getParameterObject, setParameters)

  • ResultSetHandler (handleResultSets, handleOutputParameters)

  • StatementHandler (prepare, parameterize, batch, update, query)

可以拦截Executor接口的部分方法,比如update,query,commit,rollback等方法,还有其他接口的一些方法等。

总体概括为:

  • 拦截执行器的方法

  • 拦截参数的处理

  • 拦截结果集的处理

  • 拦截Sql语法构建的处理


拦截器的使用

mybatis拦截器的使用步骤是比较简单的

step1 在mybatis-config.xml中添加如下配置

<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

step2 实现org.apache.ibatis.plugin.Interceptor 接口 并重写三个方法

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  public void setProperties(Properties properties) {
  }
}


三个方法分别为:

  • intercept(Invocation invocation)

    此方法是拦截目标对象的目标方法执行

  • plugin(Object target)

    包装目标对象,为目标对象创建一个代理对象

  • setProperties(Properties properties)

    将插件注册时的property属性设置进来



MyBatis拦截器源码解析

MyBatis的插件机制,实际就是Java动态代理实现的责任链模式实现。

我们先来看一张图了解下整个的流程


一个QUERY的执行流程:

图中红色圈住的地方是可以被代理拦截的点

从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个

红色部分为Mapper初始化,语法解析阶段的产出物.

紫色部分为sql语句执行时的关键类.

  • SqlSession            作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能

  • Executor              MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护

  • StatementHandler   封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。

  • ParameterHandler   负责对用户传递的参数转换成JDBC Statement 所需要的参数,

  • ResultSetHandler    负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;

  • MappedStatement   MappedStatement维护了一条<select|update|delete|insert>节点的封装, 包含sqlSource , resultMap

  • SqlSource            负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回

  • BoundSql             表示动态生成的SQL语句以及相应的参数信息


接下来我们从源头分析源码

  • SqlsessionFactoryBuilder

    public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
        try {
          XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
          return build(parser.parse());
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
          ErrorContext.instance().reset();
          try {
            reader.close();
          } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
          }
        }
      }

    XMLConfigBuilder 读取xml 配置文件中的配置


  • XMLConfigBuilder  

    private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
          }
        }
      }


public void addInterceptor(Interceptor interceptor) {
       interceptorChain.addInterceptor(interceptor);
     }

这个interceptorChain是Configuration的内部属性,类型为InterceptorChain,也就是一个拦截器链,我们来看下它的定义:

  • interceptorChain   

    public class InterceptorChain {
    
      private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
    
      public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
          target = interceptor.plugin(target);
        }
        return target;
      }
    
      public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
      }
    
      public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
      }
    
    }


  • Configuration

    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
    }
    
    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
    }
    
    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
    }
    
    public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
          executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
          executor = new ReuseExecutor(this, transaction);
        } else {
          executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
          executor = new CachingExecutor(executor, autoCommit);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }

    以上4个方法都是Configuration的方法。这些方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler(其中ParameterHandler和ResultSetHandler的创建是在创建StatementHandler[3个可用的实现类CallableStatementHandler,PreparedStatementHandler,SimpleStatementHandler]的时候,其构造函数调用的[这3个实现类的构造函数其实都调用了父类BaseStatementHandler的构造函数])。

    这4个方法实例化了对应的对象之后,都会调用interceptorChain的pluginAll方法,InterceptorChain的pluginAll刚才已经介绍过了,就是遍历所有的拦截器,然后调用各个拦截器的plugin方法。注意:拦截器的plugin方法的返回值会直接被赋值给原先的对象

    由于可以拦截StatementHandler,这个接口主要处理sql语法的构建,因此比如分页的功能,可以用拦截器实现,只需要在拦截器的plugin方法中处理StatementHandler接口实现类中的sql即可,可使用反射实现。

    最后我们再看下plugin 这个核心方法吧

  • plugin    

    1 package org.apache.ibatis.plugin;
      2 
      3 import java.lang.reflect.InvocationHandler;
      4 import java.lang.reflect.Method;
      5 import java.lang.reflect.Proxy;
      6 import java.util.HashMap;
      7 import java.util.HashSet;
      8 import java.util.Map;
      9 import java.util.Set;
     10 
     11 import org.apache.ibatis.reflection.ExceptionUtil;
     12 
     13 //这个类是Mybatis拦截器的核心,大家可以看到该类继承了InvocationHandler
     14 //又是JDK动态代理机制
     15 public class Plugin implements InvocationHandler {
     16 
     17   //目标对象
     18   private Object target;
     19   //拦截器
     20   private Interceptor interceptor;
     21   //记录需要被拦截的类与方法
     22   private Map<Class<?>, Set<Method>> signatureMap;
     23 
     24   private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
     25     this.target = target;
     26     this.interceptor = interceptor;
     27     this.signatureMap = signatureMap;
     28   }
     29 
     30   //一个静态方法,对一个目标对象进行包装,生成代理类。
     31   public static Object wrap(Object target, Interceptor interceptor) {
     32     //首先根据interceptor上面定义的注解 获取需要拦截的信息
     33     Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
     34     //目标对象的Class
     35     Class<?> type = target.getClass();
     36     //返回需要拦截的接口信息
     37     Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
     38     //如果长度为>0 则返回代理类 否则不做处理
     39     if (interfaces.length > 0) {
     40       return Proxy.newProxyInstance(
     41           type.getClassLoader(),
     42           interfaces,
     43           new Plugin(target, interceptor, signatureMap));
     44     }
     45     return target;
     46   }
     47 
     48   //代理对象每次调用的方法
     49   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
     50     try {
     51       //通过method参数定义的类 去signatureMap当中查询需要拦截的方法集合
     52       Set<Method> methods = signatureMap.get(method.getDeclaringClass());
     53       //判断是否需要拦截
     54       if (methods != null && methods.contains(method)) {
     55         return interceptor.intercept(new Invocation(target, method, args));
     56       }
     57       //不拦截 直接通过目标对象调用方法
     58       return method.invoke(target, args);
     59     } catch (Exception e) {
     60       throw ExceptionUtil.unwrapThrowable(e);
     61     }
     62   }
     63 
     64   //根据拦截器接口(Interceptor)实现类上面的注解获取相关信息
     65   private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
     66     //获取注解信息
     67     Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
     68     //为空则抛出异常
     69     if (interceptsAnnotation == null) { // issue #251
     70       throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
     71     }
     72     //获得Signature注解信息
     73     Signature[] sigs = interceptsAnnotation.value();
     74     Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
     75     //循环注解信息
     76     for (Signature sig : sigs) {
     77       //根据Signature注解定义的type信息去signatureMap当中查询需要拦截方法的集合
     78       Set<Method> methods = signatureMap.get(sig.type());
     79       //第一次肯定为null 就创建一个并放入signatureMap
     80       if (methods == null) {
     81         methods = new HashSet<Method>();
     82         signatureMap.put(sig.type(), methods);
     83       }
     84       try {
     85         //找到sig.type当中定义的方法 并加入到集合
     86         Method method = sig.type().getMethod(sig.method(), sig.args());
     87         methods.add(method);
     88       } catch (NoSuchMethodException e) {
     89         throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
     90       }
     91     }
     92     return signatureMap;
     93   }
     94 
     95   //根据对象类型与signatureMap获取接口信息
     96   private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
     97     Set<Class<?>> interfaces = new HashSet<Class<?>>();
     98     //循环type类型的接口信息 如果该类型存在与signatureMap当中则加入到set当中去
     99     while (type != null) {
    100       for (Class<?> c : type.getInterfaces()) {
    101         if (signatureMap.containsKey(c)) {
    102           interfaces.add(c);
    103         }
    104       }
    105       type = type.getSuperclass();
    106     }
    107     //转换为数组返回
    108     return interfaces.toArray(new Class<?>[interfaces.size()]);
    109   }
    110 
    111 }
    复制代码


一个完整的例子:

 @Override
    public Object intercept(Invocation invocation) throws Throwable {
        /** step_1 解析拦截器,获取目标对象,方法,参数 */
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        String sqlId = mappedStatement.getId();
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        Configuration configuration = mappedStatement.getConfiguration();
        /** step_2 执行目标方法,记录不同异常,用以判断Spring声明式回滚策略(https://blog.csdn.net/abc19900828/article/details/39497631) */
        StopWatch clock = new StopWatch();
        clock.start();
        Object result = null;
        try {
            result = invocation.proceed();
        } catch (RuntimeException re) {
            log.error("invoke mybatis intercepts RuntimeException(can rollback),process target method. class and method:{},parameter:{}", new Object[]{sqlId, parameter}, re);
            throw re;
        } catch (Exception e) {
            log.error("invoke mybatis intercepts Exception(not rollback),process target method. class and method:{},parameter:{}", new Object[]{sqlId, parameter}, e);
            throw e;
        } catch (Error error) {
            log.error("invoke mybatis intercepts Error(can rollback),process target method. class and method:{},parameter:{}", new Object[]{sqlId, parameter}, error);
            throw error;
        } finally {
            clock.stop();
        }
       //TODO someThing

        return result;
    }


踩坑注意事项:

1.mybatis 拦截器并不会交由spring 容器管理 而是由其自身管理 因此需要在拦截时注入spring 容器中的bean 需要添加如下而外配置:

 <bean id="SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <property name="dataSource" ref="DataSource"/>
        <property name="plugins">
            <ref bean="mybatisInterceptor"/>
        </property>
    </bean>
    
    
     <!-- MyBatis sql拦截器-->
    <bean id="mybatisInterceptor" class="xx.xx.xx.MybatisInterceptor">
        <property name="properties">
            <map>
                <entry key="insert" value="insert"/>
                <entry key="update" value="update"/>
                <entry key="delete" value="delete"/>
            </map>
        </property>
    </bean>



点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
48
获赞与收藏
193

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消