mybatis 插件_建议收藏,mybatis插件原理详解

上次发文说到了如何集成分页插件MyBatis插件原理分析,看完感觉自己better了,今天我们接着来聊mybatis插件的原理。

插件原理分析

mybatis插件涉及到的几个类:

e30db8e63ad074a48c73b76477fafabe.png

我将以 Executor 为例,分析 MyBatis 是如何为 Executor 实例植入插件的。Executor 实例是在开启 SqlSession 是被创建的,因此,我们从源头进行分析。先来看一下 SqlSession 开启的过程。

public SqlSession openSession() {    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {    Transaction tx = null;    try {        // 省略部分逻辑                // 创建 Executor        final Executor executor = configuration.newExecutor(tx, execType);        return new DefaultSqlSession(configuration, executor, autoCommit);    }     catch (Exception e) {...}     finally {...}}

Executor 的创建过程封装在 Configuration 中,我们跟进去看看看。

// Configuration类中public Executor newExecutor(Transaction transaction, ExecutorType executorType) {    executorType = executorType == null ? defaultExecutorType : executorType;    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;    Executor executor;        // 根据 executorType 创建相应的 Executor 实例    if (ExecutorType.BATCH == executorType) {...}     else if (ExecutorType.REUSE == executorType) {...}     else {        executor = new SimpleExecutor(this, transaction);    }    if (cacheEnabled) {        executor = new CachingExecutor(executor);    }        // 植入插件    executor = (Executor) interceptorChain.pluginAll(executor);    return executor;}

如上,newExecutor 方法在创建好 Executor 实例后,紧接着通过拦截器链 interceptorChain 为 Executor 实例植入代理逻辑。那下面我们看一下 InterceptorChain 的代码是怎样的。

public class InterceptorChain {    private final List interceptors = new ArrayList();    public Object pluginAll(Object target) {        // 遍历拦截器集合        for (Interceptor interceptor : interceptors) {            // 调用拦截器的 plugin 方法植入相应的插件逻辑            target = interceptor.plugin(target);        }        return target;    }    /** 添加插件实例到 interceptors 集合中 */    public void addInterceptor(Interceptor interceptor) {        interceptors.add(interceptor);    }    /** 获取插件列表 */    public List getInterceptors() {        return Collections.unmodifiableList(interceptors);    }}

上面的for循环代表了只要是插件,都会以责任链的方式逐一执行(别指望它能跳过某个节点),所谓插件,其实就类似于拦截器。

这里就用到了责任链设计模式,责任链设计模式就相当于我们在OA系统里发起审批,领导们一层一层进行审批。

以上是 InterceptorChain 的全部代码,比较简单。它的 pluginAll 方法会调用具体插件的 plugin 方法植入相应的插件逻辑。如果有多个插件,则会多次调用 plugin 方法,最终生成一个层层嵌套的代理类。形如下面:

3beb368a932870700ca775fb45bbd40e.png

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

plugin 方法是由具体的插件类实现,不过该方法代码一般比较固定,所以下面找个示例分析一下。

// TianPlugin类public Object plugin(Object target) {    return Plugin.wrap(target, this);}//Pluginpublic static Object wrap(Object target, Interceptor interceptor) {    /*     * 获取插件类 @Signature 注解内容,并生成相应的映射结构。形如下面:     * {     *     Executor.class : [query, update, commit],     *     ParameterHandler.class : [getParameterObject, setParameters]     * }     */    Map, Set> signatureMap = getSignatureMap(interceptor);    Class> type = target.getClass();    // 获取目标类实现的接口    Class>[] interfaces = getAllInterfaces(type, signatureMap);    if (interfaces.length > 0) {        // 通过 JDK 动态代理为目标类生成代理类        return Proxy.newProxyInstance(            type.getClassLoader(),            interfaces,            new Plugin(target, interceptor, signatureMap));    }    return target;}

如上,plugin 方法在内部调用了 Plugin 类的 wrap 方法,用于为目标对象生成代理。Plugin 类实现了 InvocationHandler 接口,因此它可以作为参数传给 Proxy 的 newProxyInstance 方法。

到这里,关于插件植入的逻辑就分析完了。接下来,我们来看看插件逻辑是怎样执行的。

执行插件逻辑

Plugin 实现了 InvocationHandler 接口,因此它的 invoke 方法会拦截所有的方法调用。invoke 方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:

//在Plugin类中public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    try {        /*         * 获取被拦截方法列表,比如:         *    signatureMap.get(Executor.class),可能返回 [query, update, commit]         */        Set 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);    }}

invoke 方法的代码比较少,逻辑不难理解。首先,invoke 方法会检测被拦截方法是否配置在插件的 @Signature 注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在 intercept 中,该方法的参数类型为 Invocation。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;    }    // 省略部分代码    public Object proceed() throws InvocationTargetException, IllegalAccessException {        //反射调用被拦截的方法        return method.invoke(target, args);    }}

关于插件的执行逻辑就分析到这,整个过程不难理解,大家简单看看即可。

自定义插件

下面为了让大家更好的理解Mybatis的插件机制,我们来模拟一个慢sql监控的插件。

/** * 慢查询sql 插件 */@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})public class SlowSqlPlugin implements Interceptor {    private long slowTime;    //拦截后需要处理的业务    @Override    public Object intercept(Invocation invocation) throws Throwable {        //通过StatementHandler获取执行的sql        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();        BoundSql boundSql = statementHandler.getBoundSql();        String sql = boundSql.getSql();        long start = System.currentTimeMillis();        //结束拦截        Object proceed = invocation.proceed();        long end = System.currentTimeMillis();        long f = end - start;        System.out.println(sql);        System.out.println("耗时=" + f);        if (f > slowTime) {            System.out.println("本次数据库操作是慢查询,sql是:");            System.out.println(sql);        }        return proceed;    }    //获取到拦截的对象,底层也是通过代理实现的,实际上是拿到一个目标代理对象    @Override    public Object plugin(Object target) {        //触发intercept方法        return Plugin.wrap(target, this);    }    //设置属性    @Override    public void setProperties(Properties properties) {        //获取我们定义的慢sql的时间阈值slowTime        this.slowTime = Long.parseLong(properties.getProperty("slowTime"));    }}

然后把这个插件类注入到容器中。

e188cd830c3f732fe6ba13f7f874c85e.png

然后我们来执行查询的方法。

4c96713f6ad8a8f7df430be76d507904.png

耗时28秒的,大于我们定义的10毫秒,那这条SQL就是我们认为的慢SQL。

通过这个插件,我们就能很轻松的理解setProperties()方法是做什么的了。

回顾分页插件

也是实现mybatis接口Interceptor。

@SuppressWarnings({"rawtypes", "unchecked"})@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}),    })public class PageInterceptor implements Interceptor {        @Override    public Object intercept(Invocation invocation) throws Throwable {        ...    }

intercept方法中

27fb6a890c62499706930b3ee4bc84a0.png
//AbstractHelperDialect类中@Overridepublic String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {        String sql = boundSql.getSql();        Page page = getLocalPage();        //支持 order by        String orderBy = page.getOrderBy();        if (StringUtil.isNotEmpty(orderBy)) {            pageKey.update(orderBy);            sql = OrderByParser.converToOrderBySql(sql, orderBy);        }        if (page.isOrderByOnly()) {            return sql;        }        //获取分页sql        return getPageSql(sql, page, pageKey); }//模板方法模式中的钩子方法 public abstract String getPageSql(String sql, Page page, CacheKey pageKey);

AbstractHelperDialect类的实现类有如下(也就是此分页插件支持的数据库就以下几种):

e18582276d2221b8d174585ff43392c4.png

我们用的是MySQL。这里也有与之对应的。

    @Override    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();    }

到这里我们就知道了,它无非就是在我们执行的SQL上再拼接了Limit罢了。同理,Oracle也就是使用rownum来处理分页了。下面是Oracle处理分页

    @Override    public String getPageSql(String sql, Page page, CacheKey pageKey) {        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);        if (page.getStartRow() > 0) {            sqlBuilder.append("SELECT * FROM ( ");        }        if (page.getEndRow() > 0) {            sqlBuilder.append(" SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( ");        }        sqlBuilder.append(sql);        if (page.getEndRow() > 0) {            sqlBuilder.append(" ) TMP_PAGE WHERE ROWNUM <= ? ");        }        if (page.getStartRow() > 0) {            sqlBuilder.append(" ) WHERE ROW_ID > ? ");        }        return sqlBuilder.toString();    }

其他数据库分页操作类似。关于具体原理分析,这里就没必要赘述了,因为分页插件源代码里注释基本上全是中文。

Mybatis插件应用场景

  • 水平分表
  • 权限控制
  • 数据的加解密

总结

Spring-Boot+Mybatis继承了分页插件,以及使用案例、插件的原理分析、源码分析、如何自定义插件。

涉及到技术点:JDK动态代理、责任链设计模式、模板方法模式。

Mybatis插件关键对象总结:

  • Inteceptor接口:自定义拦截必须实现的类。
  • InterceptorChain:存放插件的容器。
  • Plugin:h对象,提供创建代理类的方法。
  • Invocation:对被代理对象的封装。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MyBatis 是一个持久层框架,它提供了丰富的 SQL 映射配置和执行功能,但是在处理大量数据时,如果没有分页机制,就会带来性能问题。 MyBatis 分页插件就是为了解决这个问题而被开发出来的,它可以通过拦截 Executor 中的 query 方法,实现 SQL 语句的自动分页,并将分页后的结果返回给调用方。 下面是 MyBatis 分页插件原理: 1. 定义分页插件 首先,我们需要定义一个分页插件,这个插件需要实现 Interceptor 接口,并且重写其 intercept 方法,该方法会在 Executor 中的 query 方法被调用时被触发。 2. 拦截 query 方法 在 intercept 方法中,我们需要拦截 Executor 中的 query 方法,并获取其中的参数和 SQL 语句。 3. 自动分页 在获取到参数和 SQL 语句后,我们需要对 SQL 语句进行处理,将其转化为分页 SQL 语句,这里需要根据不同的数据库类型来进行处理。 4. 执行 SQL 语句 处理完分页 SQL 语句后,我们需要调用 Executor 中的 query 方法来执行 SQL 语句,并获取分页后的结果。 5. 将结果返回给调用方 最后,我们需要将分页后的结果返回给调用方,这里需要注意一点,即在调用方使用分页插件时,需要在 SQL 语句中加入类似 “limit 0,10” 这样的分页参数,这样才能正确的分页。 总的来说,MyBatis 分页插件原理就是通过拦截 Executor 中的 query 方法,将 SQL 语句转化为分页 SQL 语句,并执行该语句,最后将分页后的结果返回给调用方。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值