前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >海康 面试:说说MyBatis 插件机制

海康 面试:说说MyBatis 插件机制

作者头像
田维常
发布2022-11-25 14:51:20
3370
发布2022-11-25 14:51:20
举报
文章被收录于专栏:Java后端技术栈cwnait

大家好,我是田哥

上周末,一位朋友去海康面试,面试中被问到MyBatis插件的问题,如果你还没掌握,那请你认真看完本文。

1 插件概述

问题:什么是 Mybatis 插件?有什么作用?

一般开源框架都会提供扩展点,让开发者自行扩展,从而完成逻辑的增强。

基于插件机制可以实现了很多有用的功能,比如说分页,字段加密,监控等功能,这种通用的功能,就如同 AOP 一样,横切在数据操作上

而通过 Mybatis 插件可以实现对框架的扩展,来实现自定义功能,并且对于用户是无感知的。

2 Mybatis 插件介绍

Mybatis 插件本质上来说就是一个拦截器,它体现了 JDK动态代理和责任链设计模式的综合运用

Mybatis 中针对四大组件提供了扩展机制,这四个组件分别是:

Mybatis 中所允许拦截的方法如下:

  • Executor 【SQL 执行器】【update,query,commit,rollback】
  • StatementHandler 【Sql 语法构建器对象】【prepare,parameterize,batch,update,query 等】
  • ParameterHandler 【参数处理器】【getParameterObject,setParameters 等】
  • ResultSetHandler 【结果集处理器】【handleResultSets,handleOuputParameters 等】
能干什么?
  • 分页功能:mybatis 的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低,不过使用 mybatis 插件可以改变该行为,只需要拦截 StatementHandler 类的 prepare 方法,改变要执行的 SQL 语句为分页语句即可
  • 性能监控:对于 SQL 语句执行的性能监控,可以通过拦截 Executor 类的 update, query 等方法,用日志记录每个方法执行的时间
如何自定义插件?

在使用之前,我们先来看看 Mybatis 提供的插件相关的类,过一遍它们分别提供了哪些功能,最后我们自己定义一个插件。

用于定义插件的类

前面已经知道 Mybatis 插件是可以对 Mybatis 中四大组件对象的方法进行拦截,那拦截器拦截哪个类的哪个方法如何知道,就由下面这个注解提供拦截信息

代码语言:javascript
复制
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {  
  Signature[] value();
}

由于一个拦截器可以同时拦截多个对象的多个方法,所以就使用了 Signture 数组,该注解定义了拦截的完整信息

代码语言:javascript
复制
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  // 拦截的类
  Class<?> type();
  // 拦截的方法
  String method();
  // 拦截方法的参数    
  Class<?>[] args();
  
 } 

已经知道了该拦截哪些对象的哪些方法,拦截后要干什么就需要实现 Intercetor#intercept 方法,在这个方法里面实现拦截后的处理逻辑

代码语言:javascript
复制
public interface Interceptor {
  /**
   * 真正方法被拦截执行的逻辑
   *
   * @param invocation 主要目的是将多个参数进行封装
   */
  Object intercept(Invocation invocation) throws Throwable;
    
  // 生成目标对象的代理对象
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  // 可以拦截器设置一些属性
  default void setProperties(Properties properties) {
    // NOP
  }
}

3 自定义插件

需求:把 Mybatis 所有执行的 sql 都记录下来

步骤

① 创建 Interceptor 的实现类,重写方法

② 使用 @Intercepts 注解完成插件签名 说明插件的拦截四大对象之一的哪一个对象的哪一个方法

③ 将写好的插件注册到全局配置文件中

①. 创建 Interceptor 的实现类

代码语言:javascript
复制
public class MyPlugin implements Interceptor {
 private final Logger logger = LoggerFactory.getLogger(this.getClass());
 // //这里是每次执行操作的时候,都会进行这个拦截器的方法内 
 
 Override
 public Object intercept(Invocation invocation) throws Throwable { 
 //增强逻辑
  StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        logger.info("mybatis intercept sql:{}", sql);
  return invocation.proceed(); //执行原方法 
} 
 
 /**
 *
 * ^Description包装目标对象 为目标对象创建代理对象
 * @Param target为要拦截的对象
 * @Return代理对象
 */
 Override 
 public Object plugin(Object target) {
  System.out.println("将要包装的目标对象:"+target); 
  return Plugin.wrap(target,this);
    }
    
 /**获取配置文件的属性**/
 //插件初始化的时候调用,也只调用一次,插件配置的属性从这里设置进来
 Override
    public void setProperties(Properties properties) {
  System.out.println("插件配置的初始化参数:"+properties );
 }
}

② 使用 @Intercepts 注解完成插件签名

说明插件的拦截四大对象之一的哪一个对象的哪一个方法

代码语言:javascript
复制
@Intercepts({ @Signature(type = StatementHandler.class, 
                         method = "prepare", 
                         args = { Connection.class, Integer.class}) })
public class SQLStatsInterceptor implements Interceptor {

③ 将写好的插件注册到全局配置文件中

代码语言:javascript
复制
<?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>
    <plugins>
        <plugin interceptor="com.itheima.interceptor.MyPlugin">
            <property name="dialect" value="mysql" />
        </plugin>
    </plugins>
</configuration>
核心思想:

就是使用 JDK 动态代理的方式,对这四个对象进行包装增强。具体的做法是,创建一个类实现 Mybatis 的拦截器接口,并且加入到拦截器链中,在创建核心对象的时候,不直接返回,而是遍历拦截器链,把每一个拦截器都作用于核心对象中。这么一来,Mybatis 创建的核心对象其实都是代理对象,都是被包装过的。

4 源码分析 - 插件

  • 插件的初始化:插件对象是如何实例化的?插件的实例对象如何添加到拦截器链中的?组件对象的代理对象是如何产生的?
  • 拦截逻辑的执行
插件配置信息的加载

我们定义好了一个拦截器,那我们怎么告诉 Mybatis 呢?Mybatis 所有的配置都定义在 XXx.xml 配置文件中

代码语言:javascript
复制
<?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>
    <plugins>
        <plugin interceptor="com.itheima.interceptor.MyPlugin">
            <property name="dialect" value="mysql" />
        </plugin>
    </plugins>
</configuration>

对应的解析代码如下(XMLConfigBuilder#pluginElement):

代码语言:javascript
复制
private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 获取拦截器
        String interceptor = child.getStringAttribute("interceptor");
        // 获取配置的Properties属性
        Properties properties = child.getChildrenAsProperties();
        // 根据配置文件中配置的插件类的全限定名 进行反射初始化
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        // 将属性添加到Intercepetor对象
        interceptorInstance.setProperties(properties);
        // 添加到配置类的InterceptorChain属性,InterceptorChain类维护了一个List<Interceptor>
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

主要做了以下工作:

  1. 遍历解析 plugins 标签下每个 plugin 标签
  2. 根据解析的类信息创建 Interceptor 对象
  3. 调用 setProperties 方法设置属性
  4. 将拦截器添加到 Configuration 类的 IntercrptorChain 拦截器链中
代理对象的生成

Executor 代理对象(Configuration#newExecutor)

代码语言:javascript
复制
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    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);
    }
    // 生成Executor代理对象逻辑
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

ParameterHandler 代理对象(Configuration#newParameterHandler)

代码语言:javascript
复制
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 生成ParameterHandler代理对象逻辑 
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

ResultSetHandler 代理对象(Configuration#newResultSetHandler)

代码语言:javascript
复制
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 = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

StatementHandler 代理对象(Configuration#newStatementHandler)

代码语言:javascript
复制
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 = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;

}

通过查看源码会发现,所有代理对象的生成都是通过 InterceptorChain#pluginAll 方法来创建的,进一步查看 pluginAll 方法

代码语言:javascript
复制
public Object pluginAll(Object target) {
    //责任链模式
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}

InterceptorChain#pluginAll() 内部通过遍历 Interceptor#plugin() 方法来创建代理对象,并将生成的代理对象又赋值给 target, 如果存在多个拦截器的话,生成的代理对象会被另一个代理对象所代理,从而形成一个代理链,执行的时候,依次执行所有拦截器的拦截逻辑代码,我们再跟进去

代码语言:javascript
复制
default Object plugin(Object target) {
  return Plugin.wrap(target, this);
}

Interceptor#plugin 方法最终将目标对象和当前的拦截器交给 Plugin.wrap 方法来创建代理对象。该方法是默认方法,是 Mybatis 框架提供的一个典型 plugin 方法的实现。让我们看看在 Plugin#wrap 方法中是如何实现代理对象的

代码语言:javascript
复制
public static Object wrap(Object target, Interceptor interceptor) {
    // 1.解析该拦截器所拦截的所有接口及对应拦截接口的方法
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 2.获取目标对象实现的所有被拦截的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 3.目标对象有实现被拦截的接口,生成代理对象并返回
    if (interfaces.length > 0) {
        // 通过JDK动态代理的方式实现
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    // 目标对象没有实现被拦截的接口,直接返回原对象
    return target;
}

最终我们看到其实是通过 JDK 提供的 Proxy.newProxyInstance() 方法来生成代理对象。

拦截逻辑的执行

通过上面的分析,我们知道 Mybatis 框架中执行 Executor、ParameterHandler、ResultSetHandler 和 StatementHandler 中的方法时真正执行的是代理对象对应的方法。而且该代理对象是通过 JDK 动态代理生成的,所以执行方法时实际上是调用 InvocationHandler#invoke 方法(Plugin 类实现 InvocationHandler 接口), 下面是 Plugin#invoke 方法

代码语言:javascript
复制
@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)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
}

注:一个对象被代理很多次

问题:同一个组件对象的同一个方法是否可以被多个拦截器进行拦截?

答案是肯定的,所以我们配置在最前面的拦截器最先被代理,但是执行的时候却是最外层的先执行

具体点:

假如依次定义了三个插件:插件1插件2 和 插件3

那么 List 中就会按顺序存储:插件1插件2插件3

而解析的时候是遍历 list,所以解析的时候也是按照:插件1 , 插件2, 插件3 的顺序。

但是执行的时候就要反过来了,执行的时候是按照:插件3插件2插件1 的顺序进行执行。

当 Executor 的某个方法被调用的时候,插件逻辑会先行执行。执行顺序由外而内,比如上图的执行顺序为 plugin3 → plugin2 → Plugin1 → Executor

我之前整理了一份《MyBatis源码分析-菜鸟版》,菜鸟言外之意就是只要是个正常的java 开发者都能轻松掌握。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java后端技术全栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 插件概述
    • 问题:什么是 Mybatis 插件?有什么作用?
    • 2 Mybatis 插件介绍
      • 能干什么?
        • 如何自定义插件?
        • 3 自定义插件
          • 核心思想:
            • 插件配置信息的加载
            • 代理对象的生成
            • 拦截逻辑的执行
        • 4 源码分析 - 插件
        相关产品与服务
        应用性能监控
        应用性能监控(Application Performance Management,APM)是一款应用性能管理平台,基于实时多语言应用探针全量采集技术,为您提供分布式性能分析和故障自检能力。APM 协助您在复杂的业务系统里快速定位性能问题,降低 MTTR(平均故障恢复时间),实时了解并追踪应用性能,提升用户体验。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档