Mybatis源码系列7-原来你是这样的插件

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用版 2核4GB 50GB
简介: Mybatis源码系列7-原来你是这样的插件

Mybatis通过插件机制,提供扩展性。

Mybatis的插件机制,是拦截器的思想,不同于Filter,interceptor之类的拦截器。Mybatis插件使用动态代理+责任链模式来实现。

  • 动态代理: 负责对目标对象,进行某一个方面的增强
  • 责任链模式: 负责组织所有的代理增强链式调用。


1.Mybaits 插件


1.1拦截目标

插件的插入点在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) {
    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) interceptorChain.pluginAll(executor);
    return executor;
  }

拦截的目标对象:

  • Executor:  执行增删改查操作
  • StatementHandler:处理sql语句预编译,设置参数等相关工作;
  • ParameterHandler: 参数处理器
  • ResultSetHandler:结果处理器


1.2原理

插件两个重要点

  • 要有一个 实现了org.apache.ibatis.plugin.Interceptor接口的拦截器
  • plugin方法里,需要调用Plugin.wrap(Object target, Interceptor interceptor)把目标类与拦截器包装成一个插件

Mybaits通过责任链的方式将,插件层层作用在目标对象上。

executor = (Executor) interceptorChain.pluginAll(executor);
public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
}
public Object plugin(Object target) {
        return Plugin.wrap(target, this);
}

作用过程发生在Plugin.wrap方法上,将目标对象与Mybatis拦截器包装成一个插件。

public class Plugin implements InvocationHandler {
  private Object target;
  private Interceptor interceptor;
  private Map<Class<?>, Set<Method>> signatureMap;
public static Object wrap(Object target, Interceptor interceptor) {
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 获取签名Map
  Class<?> type = target.getClass(); // 拦截目标 (ParameterHandler|ResultSetHandler|StatementHandler|Executor)
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);  // 获取目标接口
  if (interfaces.length > 0) {
    return Proxy.newProxyInstance(  // 生成代理
        type.getClassLoader(),
        interfaces,//目标接口
        new Plugin(target, interceptor, signatureMap));//创建一个插件
  }
  return target;
}
}

JDK动态代理两要素:

  • 目标接口
  • InvocationHandler 增强

Plugin类实现了InvocationHandler接口,说明Plugin其实就是这个增强器。

所以Plugin.wrap方法其实就是把目标对象与Mybatis拦截器包装成一个JDK动态代理增强器

我们看生成代理的那个地方

return Proxy.newProxyInstance(  // 生成代理
        type.getClassLoader(),
        interfaces,//目标接口
        new Plugin(target, interceptor, signatureMap));//增强

根据目标对象,插件interceptor,签名方法集,三个参数创建一个Plugin 作为一个增强器,去增强target的代理对象。

当执行代理对象的方法时,执行增强器Plugin 的invoke方法。在invoke方法中调用Mybatis拦截器。

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);
    }
  }

这样插件就借助JDK动态代理,完美的实现了对目标对象的拦截与增强。

执行流程

代理对象方法---> (InvocationHandler)代理增强Plugin对象#invoke方法--->Mybaits拦截器#intercept方法--->....--->目标对象方法

小结:

  • Mybatis插件Plugin的本质是一个JDK动态增强器InvocationHandler
  • Mybatis 拦截器作为Plugin的一个属性,借助Plugin(即JDK动态代理增强)实现对目标方法的拦截


2.PageHelper 插件


PageHelper是我们平时常用的分页插件。下面我们看看其原理


2.1使用

PageHelper.startPage(1, 10);
List<User> list = userMapper.selectIf(1);


2.2原理

PageHelper 根据参数在 ThreadLocal 中设置了 Page 对象,能取到就代表需要分页,在分页完成后在移除,这样就不会导致其他方法分页。

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
  Page<E> page = new Page<E>(pageNum, pageSize, count);
  page.setReasonable(reasonable);
  page.setPageSizeZero(pageSizeZero);
  //当已经执行过orderBy的时候
  Page<E> oldPage = getLocalPage();
  if (oldPage != null && oldPage.isOrderByOnly()) {
    page.setOrderBy(oldPage.getOrderBy());
  }
  setLocalPage(page);
  return page;
}

在PageHelper分页插件中,PageInterceptor 是对Mybatis查询分页查询的拦截器。直接看其intercept方法

public class PageInterceptor implements Interceptor {
//方法比较长,取分页查询部分
public Object intercept(Invocation invocation) throws Throwable {
...
//判断是否需要进行分页查询
                if (dialect.beforePage(ms, parameter, rowBounds)) {
                    //生成分页的缓存 key
                    CacheKey pageKey = cacheKey;
                    //处理参数对象
                    parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
                    //调用方言获取分页 sql
                    String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
                    BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);
                    //设置动态参数
                    for (String key : additionalParameters.keySet()) {
                        pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                    //执行分页查询
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
                } else {
                    //不执行分页的情况下,也不执行内存分页
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
                }
}
}

这里只提两个点,具体的可以去看源码


2.2.1处理参数对象

目的就是把设置分页参数,设置到参数集上

parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);

Mysql方言类MySqlDialect

public class MySqlDialect extends AbstractHelperDialect {
    @Override
    public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
        paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());//设置起始页
        paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());//设置分页大小
        //处理pageKey
        pageKey.update(page.getStartRow());
        pageKey.update(page.getPageSize());
        //处理参数配置
        if (boundSql.getParameterMappings() != null) {
            List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>();
            if (boundSql != null && boundSql.getParameterMappings() != null) {
                newParameterMappings.addAll(boundSql.getParameterMappings());
            }
            if (page.getStartRow() == 0) {
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, Integer.class).build());
            } else {
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_FIRST, Integer.class).build());
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, Integer.class).build());
            }
            MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
            metaObject.setValue("parameterMappings", newParameterMappings);
        }
        return paramMap;
    }
}

可以看到:

从线程本地变量中取出当前Page对象,获取到起始行与分页大小设置到参数集中


2.2.2调用方言获取分页 sql

目的把分页语句拼接到SQL上

String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);

Mysql方言类MySqlDialect

public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        if (page.getStartRow() == 0) {
            sqlBuilder.append(" LIMIT ? ");
        } else {
            sqlBuilder.append(" LIMIT ?, ? ");
        }
        pageKey.update(page.getPageSize());
        return sqlBuilder.toString();
    }

可以看出:

将分页语句 LIMIT  拼接到SQL上 。


3.总结


  • Mybatis插件运用了JDK动态代理和责任链设计模式
  • 插件Plugin 本质是一个JDK动态代理增强器
  • Mybatis拦截器借助Plugin 对 Mybatis进行具体的增强。


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4天前
|
Java 数据库连接 Spring
Spring 整合 MyBatis 底层源码解析
Spring 整合 MyBatis 底层源码解析
|
6天前
MyBatis-Plus分页插件基于3.3.2
MyBatis-Plus分页插件基于3.3.2
8 0
MyBatis-Plus分页插件基于3.3.2
|
3天前
|
SQL 缓存 Java
Java框架之MyBatis 07-动态SQL-缓存机制-逆向工程-分页插件
Java框架之MyBatis 07-动态SQL-缓存机制-逆向工程-分页插件
|
3天前
|
SQL Java 数据库连接
IDEA插件(MyBatis Log Free)
IDEA插件(MyBatis Log Free)
10 0
|
3天前
|
Java 数据库连接 mybatis
idea无法下载Mybatis插件怎么办
idea无法下载Mybatis插件怎么办
|
4天前
|
SQL Java 数据库连接
MyBatis插件深度解析:功能、原理、使用、应用场景与最佳实践
MyBatis插件深度解析:功能、原理、使用、应用场景与最佳实践
|
26天前
|
Java 数据库连接 数据库
mybatis自制插件+注解实现数据脱敏
mybatis自制插件+注解实现数据脱敏
24 1
|
6天前
|
Java 数据库连接 mybatis
mybatis自定义插件实现日期自动注入
mybatis自定义插件实现日期自动注入
14 0
|
6天前
|
XML 关系型数据库 数据库
使用mybatis-generator插件生成postgresql数据库model、mapper、xml
使用mybatis-generator插件生成postgresql数据库model、mapper、xml
29 0
|
26天前
|
SQL Java 数据库连接
自定义mybatis插件实现读写分离
自定义mybatis插件实现读写分离
12 0