首页 > 其他分享 >浅析Mybatis拦截器

浅析Mybatis拦截器

时间:2024-06-17 17:23:57浏览次数:11  
标签:拦截器 return target plugin Object class Executor Mybatis 浅析

一、背景

最近针对项目中出现的慢sql,我们使用自定义Mybatis拦截器,结合DUCC动态配置慢sql阈值,来监控慢sql并报警,提前发现风险点。借着这个契机,浅析下Mybatis拦截器原理,个人理解,不足之处请指正。

二、Mybatis拦截器

Mybatis使用plugin来拦截方法调用,所以MyBatis plugin也称为:Mybatis拦截器。Mybatis采用责任链模式,通过代理组织多个plugin,对Executor、ParameterHandler、StatementHandler、ResultSetHandler中的方法进行拦截。

- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)

本文使用mybatis版本为3.4.5。

2.1 使用Mybatis拦截器

2.1.1 自定义Plugin

Signature定义具体要拦截的方法信息,type为拦截的对象,method为拦截对象的方法名,考虑到重载的方法,args为拦截对象的方法参数。

@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}),
        @Signature(
                type = Executor.class,
                method = "update",
                args = {MappedStatement.class, Object.class})
})
public class ExamplePlugin implements Interceptor {

    /**
     * 具体拦截逻辑
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // implement pre-processing if needed
        Object result = invocation.proceed();
        // implement post-processing if needed
        return result;
    }

    /**
     * 生成代理对象
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 注册插件时xml配置的property转化,设置一些自定义属性
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {

    }
}

2.1.2 注册Plugin

在mybatis-config.xml中注册插件。

<plugins>
  <plugin interceptor="xx.ExamplePlugin">
    <property name="xxxProperty" value="xxx"/>
  </plugin>
</plugins>

2.1.3 绑定SqlSessionFactory

<bean id="xxxSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="xxxDataSource"/>
        <property name="configLocation" value="classpath:xxx/mybatis-config.xml"/>
</bean>

2.2 源码分析

在Spring容器对SqlSessionFactory初始化时,会解析mybatis-config.xml,注册Interceptor。

/** 
 * org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
 * 解析mybatis-config.xml
 */
private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}
/** 
 * org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement
 * 解析mybatis-config.xml中的plugins元素
 */
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);
      }
    }
}
/**
 * org.apache.ibatis.session.Configuration#addInterceptor
 * 将Interceptor添加到interceptorChain中
 */
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}

2.2.1 生成代理流程

这里以ExamplePlugin为例,对Executor阶段的方法进行代理,跟踪一下流程。

第①步

/**
 * org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
 */
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //创建Executor对象
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}

 

第②步

/**
 * org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
 */
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor 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);
    }
    //处理注册的plugin
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

第③步

/**
 * org.apache.ibatis.plugin.InterceptorChain#pluginAll
 * 如果同一个拦截的方法存在多个拦截器,会按照声明的顺序,循环进行代理。
 */
public Object pluginAll(Object target) {
    //循环处理每个注册的plugin,构成一条代理链。
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
}

前面说了会对Executor、ParameterHandler、StatementHandler、ResultSetHandler中的方法进行拦截,从这一步的引用可以论证,这4个阶段都会通过plugin生成代理对象。

第④步

/**
 * ExamplePlugin#plugin
 * 调用自定义拦截器的plugin方法
 * @param target
 * @return
 */
 @Override
 public Object plugin(Object target) {
      return Plugin.wrap(target, this);
 }

第⑤⑥⑦步

/**
 * org.apache.ibatis.plugin.Plugin#wrap
 */
public static Object wrap(Object target, Interceptor interceptor) {
    //反射获取拦截方法信息
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    //原始对象类信息
    Class<?> type = target.getClass();
    //获取可以生成代理的接口数组
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //判断是否需要生成代理
    if (interfaces.length > 0) {
      //生成代理对象,Plugin对象为该代理对象的InvocationHandler
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
}
/**
 * org.apache.ibatis.plugin.Plugin#getSignatureMap
 */
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    //获取配置的注解信息
    Signature[] sigs = interceptsAnnotation.value();
    //key是拦截的类,value是拦截的方法集合
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        //反射获取对应的方法
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
}

/**
 * org.apache.ibatis.plugin.Plugin#getAllInterfaces
 */
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      //遍历原始对象类的接口信息
      for (Class<?> c : type.getInterfaces()) {
        //判断拦截配置是否存在的匹配的类型
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      //以父类进行匹配,一直匹配到java.lang.Object的父类,为null结束循环
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
}

2.2.2 拦截流程

这里以一个单条查询sql跟踪一下拦截流程。

 

第①步
/**
 * org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
 */
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      //此时的executor是代理后的executor
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}
第②步
/**
 * org.apache.ibatis.plugin.Plugin#invoke
 */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //获取生成代理时构建好的拦截方法信息
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      //判断是否需要拦截
      if (methods != null && methods.contains(method)) {
        //调用自定义plugin实现的intercept方法
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
}
第③步
/**
 * ExamplePlugin#intercept
 * 具体拦截逻辑
 * 如果同一个拦截的方法存在多个拦截器,由于生成层层代理时是顺序生成,拦截时正好相反。
 * @param invocation
 * @return
 * @throws Throwable
 */
@Override
public Object intercept(Invocation invocation) throws Throwable {
     // implement pre-processing if needed
     //调用代理对象的目标类
     Object result = invocation.proceed();
     // implement post-processing if needed
     return result;
}

2.3 应用场景

1.慢sql监控(拦截Executor对象的query、update方法) 2.分库分表场景主键ID替换(拦截StatementHandler对象的prepare方法) 3.分页查询(拦截Executor对象的query方法)

2.4 优化

在生成代理流程时序图中的第⑤⑥步会进行反射判断是否需要生成代理,这一步可以前置到第④步进行判断,减少反射。

/**
 * 生成代理对象
 * @param target
 * @return
 */
@Override
public Object plugin(Object target) {
    //提前判断是否要生成代理对象
    if (target != null && target instanceof Executor) {
         return Plugin.wrap(target, this);
    }
    return target;
}

三、总结思考

通过自定义plugin,自己也对mybatis拦截器的实现机制有了清晰的认识,希望这篇文章也能帮助到读者。

四、参考文档

https://mybatis.org/mybatis-3/configuration.html#plugins

标签:拦截器,return,target,plugin,Object,class,Executor,Mybatis,浅析
From: https://www.cnblogs.com/zhengbiyu/p/18252816

相关文章

  • 【Mybatis】Mybatis快速入门
    MyBatis是一款优秀的持久层框架,用于简化JDBC的开发。MyBatis本是Apache的一个开源项目iBatis,2010年这个项目由apache迁移到了googlecode,并且改名为MyBatis。2013年11月迁移到Github。官网:https://mybatis.org/mybatis-3/zh/index.htmlMybatis入门Mybatis会把数据库执......
  • MyBatisX插件生成代码
    MyBatisX插件MyBatisPlus提供了一个IDEA插件——MybatisX,使用它可根据数据库快速生成Entity、Mapper、Mapper.xml、Service、ServiceImpl等代码,使用户更专注于业务。下面演示具体用法安装插件在IDEA插件市场搜索MyBatisX,进行在线安装配置数据库连接在IDEA中配置数据......
  • MybatisPlus逻辑删除
    逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:1.在表中添加一个字段标记数据是否被删除2.当删除数据时把标记置为13.查询时只查询标记为0的数据 例如逻辑删除字段为deleted,那么删除操作的sql语句为:UPDATEuserSETdeleted=1WHEREid=1AND......
  • Mybatis框架
    Java中的持久层框架 1.mybatis:最早叫ibatis       开发效率低,执行性能好2.hibernate                开发效率高,执行性能低反射对象.属性=值属性.赋值方法(对象,值)*反射是java进阶的分水岭*不要乱用反射,反射性能......
  • Mybatis-Plus-Join(MPJ连表查询)
    mybatis-plus作为mybatis的增强工具,它的出现极大的简化了开发中的数据库操作,但是长久以来,它的联表查询能力一直被大家所诟病。一旦遇到leftjoin或rightjoin的左右连接,你还是得老老实实的打开xml文件,手写上一大段的sql语句一款叫做mybatis-plus-join的工具(后面就简称mpj了),可以不......
  • 过滤器和拦截器的区别
    一、拦截器和过滤器的区别1、过滤器和拦截器触发时机不一样,过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是,是在servlet处理完后,返回给前端之前。2、拦截器可以获取IOC容器中的各个bean,而过滤器就不行,因为拦截器是spring提供并管理的,spring的功能......
  • 『手写Mybatis』创建简单的映射器代理工厂
    前言在阅读本文之前,我相信你已经是一个MybatisORM框架工具使用的熟练工了,那你是否清楚这个ORM框架是怎么屏蔽我们对数据库操作的细节的?比如我们使用JDBC的时候,需要手动建立数据库链接、编码SQL语句、执行数据库操作、自己封装返回结果等。但在使用ORM框架后,只需要......
  • 『手写Mybatis』实现映射器的注册和使用
    前言如何面对复杂系统的设计?我们可以把Spring、MyBatis、Dubbo这样的大型框架或者一些公司内部的较核心的项目,都可以称为复杂的系统。这样的工程也不在是初学编程手里的玩具项目,没有所谓的CRUD,更多时候要面对的都是对系统分层的结构设计和聚合逻辑功能的实现,再通过层层转换......
  • 带你学习Mybatis之执行器Executor
    执行器ExecutorExecutor定义了数据库操作的基本方法,SqlSession接口中的功能都是基于Executor接口实现的,真正执行java和数据库交互的类,负责维护一级缓存和二级缓存,并提供事务管理的相关操作,会将数据库相关操作委托给StatementHandler完成public enum ExecutorType { ......
  • MyBatis 的缓存机制
    1.MyBatis的缓存机制@目录1.MyBatis的缓存机制2.准备工作3.MyBatis的一级缓存3.1一级缓存失效情况/条件4.MyBatis的二级缓存5.MyBatis集成EhCache第三方缓存6.总结:7.最后:缓存(Cache)缓存的作用:通过减少IO的方式,来提高程序的执行效率。MyBatis的缓存:将Sele......