【mybatis】插件开发和源码解析

本篇主要内容涉及mybatis,重点有三个

  • mybatis插件开发
  • mybatis与spring的整合原理和部分源码
  • mybatis管理所有sql,并对所有sql加limit 1

交代一下版本,mybatis使用mybatis为springboot开发的starter

  • springboot:2.4.3
  • mybatis-spring-boot-starter:2.3.0

第一部分:插件开发

参考链接:MyBatis的插件开发_mybatis插件开发_文大奇Quiin的博客-CSDN博客

 这直接是从参考链接里面复制过来的,参考链接里面已经把插件开发讲清楚了,只是有一点是错误的,上面的代码就是为了给所有的sql加上limit 1,经过我的试验在我的mybatis的版本下根本行不通,这就是写这篇文章的初衷,既是为了搞明白为什么行不通,也是为了怎么做才能监控所有sql并且给想要的sql加上limit 1,这个点还是很有价值的,所以研究了一下

为什么上述代码加limit 1,行不通?

查看第一张截图中的33行代码,这个获取boundSql的地方是关键,查看具体实现,截图如下

可以看到BoundSql是通过属性sqlSource调用方法拿到的,这个sqlSource很关键,查看一下它的实现

可以看到一共有4个实现类,其中 DynamicSqlSource、RawSqlSource和StaticSqlSource,是在我调试过程中实际出现的,这边埋个点,这个sqlSource使用了装饰器设计模式,一共有两种装饰方案,分别为动态sql和非动态sql提供能力支持。虽然有4个sqlSource,但是其实经过调试之后发现无论是动态还是非动态最后使用的都是StaticSqlSource,那我们来看StaticSqlSource是怎么获取BoundSql的

看到这大家应该明白了,为啥加了limit 1不会生效,因为每次获取BoundSql都是new了一个新的对象

如何给所有sql加上limit 1

这里先讲一下上面埋的点,也就是sqlSource的两种装饰方案

  • 动态sql:DynamicSqlSource
  • 非动态sql:RawSqlSource->StaticSqlSource

通过调试找到了XMLScriptBuilder类的parseScriptNode方法,直接上图

明白了这两种装饰方案,就能写代码了

import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.scripting.defaults.RawSqlSource;
import org.apache.ibatis.scripting.xmltags.DynamicSqlSource;
import org.apache.ibatis.scripting.xmltags.MixedSqlNode;
import org.apache.ibatis.scripting.xmltags.SqlNode;
import org.apache.ibatis.scripting.xmltags.StaticTextSqlNode;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.lang.reflect.Field;
import java.util.List;

/**
 * @author dfg
 */
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class MybatisPluginTest implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement  mappedStatement = (MappedStatement) invocation.getArgs()[0];
        // mapper的全限定名
        System.out.println(mappedStatement.getId());

        BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
        String sql = boundSql.getSql();

        if (mappedStatement.getSqlSource() instanceof RawSqlSource) {
            // 非动态sql
            RawSqlSource rawSqlSource = (RawSqlSource) mappedStatement.getSqlSource();

            // 获取到rawSqlSource的sqlSource,这是为了获取StaticSqlSource的属性sql
            Field rawSqlSourceField = rawSqlSource.getClass().getDeclaredField("sqlSource");
            rawSqlSourceField.setAccessible(true);
            // 拿到staticSqlSource
            StaticSqlSource staticSqlSource = (StaticSqlSource) rawSqlSourceField.get(rawSqlSource);
            // 获取属性sql
            Field field = staticSqlSource.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            // 给属性sql加上limit 1
            field.set(staticSqlSource, sql + " limit 1");
        } else {
            // 动态sql
            DynamicSqlSource dynamicSqlSource = (DynamicSqlSource) mappedStatement.getSqlSource();
            Field rootSqlNodeField = dynamicSqlSource.getClass().getDeclaredField("rootSqlNode");
            rootSqlNodeField.setAccessible(true);
            // 获取到混合的sqlNode
            MixedSqlNode mixedSqlNode = (MixedSqlNode) rootSqlNodeField.get(dynamicSqlSource);
            // 拿到contents属性
            Field mixedSqlNodeField = mixedSqlNode.getClass().getDeclaredField("contents");
            mixedSqlNodeField.setAccessible(true);
            // contents属性是个list列表
            List<SqlNode> sqlNodes = (List<SqlNode>) mixedSqlNodeField.get(mixedSqlNode);
            // 在列表中尾部加一个静态的sqlNode
            sqlNodes.add(new StaticTextSqlNode("limit 1"));
        }


        return invocation.proceed();
    }
}

 注解已经写明白了,功能其实已经实现了,但是还没有结束,因为通过插件是每一次sql调用都会被执行一次,每次都加limit 1,肯定是不行的

第二部分:mybatis在spring环境下的初始化

先来看MybatisAutoConfiguration这个类

这个类中通过@Bean这个注解,把SqlSessionFactory交给spring管理,所以mybatis的初始化就是完善SqlSessionFactory类的过程

 最重要的方法就是这个getObject方法,再来看SqlSessionFactoryBean类

 afterPropertiesSet函数是关键

找到buildSqlSessionFactory

因为是通过xml写的sql所以直接看xml的Mapper解析器,来看XMLMapperBuilder

找到configurationElement

找到buildStatementFromContext,这个方法就是提取出xml配置文件里面select|insert|update|delete这4个标签的内容

找到buildStatementFromContext

找到parseStatementNode

在找到这个位置,调试基本上就已经结束了,这里就是构造sqlSource这个最重要属性的地方,在这个函数里面就有前边提到的关于sqlSouce的两种装饰方式 ,最终把sqlSource跟MappedStatement关联起来,然后在SqlSessionFactory类里面会维护一个关于MappedStatement的map结构,截个图给大家看一下

这个map结构在后面真正调用sql的时候会根据mapper的全限定名获取Mapperstatement类,拿到这个类之后里面有sqlSource,也就是真正的sql存放的地方

放一张断点的截图

第三部分:管理所有sql,并添加limit 1 

在第一部分就已经说明了,通过插件的方式是无法达到目的的,在第二部分也了解了初始化的原理,那就只需要获取spring上下文中的sqlSessionFactory修改其下的sqlSource就能达到目的了

import lombok.SneakyThrows;
import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.scripting.defaults.RawSqlSource;
import org.apache.ibatis.scripting.xmltags.DynamicSqlSource;
import org.apache.ibatis.scripting.xmltags.MixedSqlNode;
import org.apache.ibatis.scripting.xmltags.SqlNode;
import org.apache.ibatis.scripting.xmltags.StaticTextSqlNode;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;

import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.List;

/**
 * mybatis在spring初始化完成之后的监听器
 * @author dfg
 */
@org.springframework.context.annotation.Configuration
public class MybatisApplicationListener implements ApplicationListener<ApplicationEvent> {
    @SneakyThrows
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ContextRefreshedEvent) {
            ContextRefreshedEvent contextRefreshedEvent = (ContextRefreshedEvent) event;
            SqlSessionFactory sqlSessionFactory = contextRefreshedEvent.getApplicationContext().getBean(SqlSessionFactory.class);
            Configuration configuration = sqlSessionFactory.getConfiguration();
            for (MappedStatement mappedStatement : new HashSet<>(configuration.getMappedStatements())) {
                if (mappedStatement.getSqlSource() instanceof RawSqlSource) {
                    RawSqlSource rawSqlSource = (RawSqlSource) mappedStatement.getSqlSource();

                    Field rawSqlSourceField = rawSqlSource.getClass().getDeclaredField("sqlSource");
                    rawSqlSourceField.setAccessible(true);
                    StaticSqlSource staticSqlSource = (StaticSqlSource) rawSqlSourceField.get(rawSqlSource);
                    Field field = staticSqlSource.getClass().getDeclaredField("sql");
                    field.setAccessible(true);
                    field.set(staticSqlSource, field.get(staticSqlSource).toString() + " limit 1");
                } else {
                    DynamicSqlSource dynamicSqlSource = (DynamicSqlSource) mappedStatement.getSqlSource();
                    Field rootSqlNodeField = dynamicSqlSource.getClass().getDeclaredField("rootSqlNode");
                    rootSqlNodeField.setAccessible(true);
                    MixedSqlNode mixedSqlNode = (MixedSqlNode) rootSqlNodeField.get(dynamicSqlSource);
                    Field mixedSqlNodeField = mixedSqlNode.getClass().getDeclaredField("contents");
                    mixedSqlNodeField.setAccessible(true);
                    List<SqlNode> sqlNodes = (List<SqlNode>) mixedSqlNodeField.get(mixedSqlNode);
                    sqlNodes.add(new StaticTextSqlNode("limit 1"));
                }
            }

            System.out.println("init ok");
        }
    }
}

代码跟之前差不多,只是添加方式是通过监听spring的初始化完成事件(ContextRefreshedEvent)搞定的

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值