解读 MyBatis 源码:探寻数据持久化的奥秘

开发
们将逐步揭开 MyBatis 源码那神秘的面纱,去追寻它高效数据处理、灵活映射机制以及出色性能表现背后的根源。

在当今软件开发的广袤领域中,MyBatis 作为一款备受青睐的持久层框架,以其强大的功能和灵活的特性发挥着重要作用。当我们深入探究 MyBatis 的源码时,就如同开启一扇通往技术奥秘的大门。

在这里,每一行代码都蕴含着智慧与巧思,每一个模块都承载着独特的设计理念。我们将逐步揭开 MyBatis 源码那神秘的面纱,去追寻它高效数据处理、灵活映射机制以及出色性能表现背后的根源。通过对其源码的仔细剖析,我们不仅能更深刻地理解 MyBatis 是如何工作的,更能汲取其中的精髓,为我们自身的技术成长和项目实践提供宝贵的经验和启示。让我们怀揣着对技术的好奇与探索之心,正式踏上 MyBatis 源码解析的精彩旅程……

详解Mybatis的功能架构与核心技术

按照工作层次划分可以分为三层:

  • 接口层:也就是我们用户用到的这一层,提供各种对数据的CRUD以及配置信息维护的API调用。
  • 数据处理层:这层是框架为上层提供的关键,这一层实现参数映射,SQL解析,SQL执行,结果处理。
  • 基础支撑层:负责连接管理、配置加载,事务管理、缓存机制等。

详解Mybatis执行过程

本质上mybatis执行过程大体是:

  • 参数映射
  • sql解析
  • sql执行
  • 结果和处理映射

我们以下面这段查询代码为例,针对该流程进行深入讲解:

Tb1Example tb1Example = new Tb1Example();
        tb1Example.createCriteria().andBirthdayIsNull();
        List<Tb1> tb1List = SpringUtil.getBean(Tb1Mapper.class).selectByExample(tb1Example);
        log.info("tb1List: {}", tb1List);

本质上我们所使用的Tb1Mapper是基于我们的xml配置动态代理生成的一个MapperProxy,在执行查询请求时被本质上就调用这个生成代理对象,以我们的selectByExample为例,在初始配置的时候我们指明了select标签在进行代理创建时该方法就会被标准为SELECT命令请求,执行时就会按照代理的查询逻辑执行。

随后代理的MapperProxy会调用MapperMethod进行参数解析,将参数转换为后续可拼接到xml中所配置sql语句中的参数。

然后SqlSessionTemplate通过内部sqlSessionProxy的selectList着手进行实际查询工作,其内部会拿到当前sql连接的session和xml中配置的sql还有我们上述步骤的参数生成jdbc的Statement然后通过SimpleExecutor执行sql查询,然后通过resultSetHandler将结果解析并返回:

对此我们也给出相应的源码,首先从MapperProxy开始调用mapperMethod进行参数解析。

//MapperProxy的invoke方法调用mapperMethod
@Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }

//MapperMethod解析参数并基于指令匹配SQL操作
```java
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
      //......
      }
      case UPDATE: {
         //......
      }
      case DELETE: {
        //......
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          //......
        } else if (method.returnsMany()) {
        //内部进行参数解析和查询调用
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          //......
        } else {
          //......
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
   //......
    return result;
  }

随后步入MapperMethod进行通过convertArgsToSqlCommandParam参数解析,底层在基于sqlSession着手查询和结果转换:

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    //参数解析
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      //
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectList(command.getName(), param);
    }
   //......
    return result;
  }

最终来到SimpleExecutor的doQuery方法,通过xml配置所得的各种信息生成StatementHandler创建出Statement ,再通过resultSetHandler处理结果并返回:

@Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
    //通过配置信息生成StatementHandler 
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      //基于StatementHandler 生成Statement 
      stmt = prepareStatement(handler, ms.getStatementLog());
      //内部通过Statement 执行sql并将结果交由resultSetHandler转换并返回
      return handler.query(stmt, resultHandler);
    } finally {
      //......
    }
  }

为什么Mybatis不需要实现类

是通过代理生成的,我们不妨通过源码来看看究竟,以下面这段代码作为入口讲解原生mapper创建思路:

SqlSession sqlSession = SpringUtil.getBean(SqlSessionFactory.class).openSession();
 Tb1Mapper mapper = sqlSession.getMapper(Tb1Mapper.class);

本质上getMapper会基于接口和sqlSession信息通过mapper创建工厂mapperProxyFactory ,然后mapperProxyFactory 底层通过反射的方式创建JDK动态代理mapper对象:

对此我们给出getMapper的入口,逻辑和笔者说的一样mapperProxyFactory 传递元信息进行动态代理创建:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
   //......
   
    try {
    //通过mapperProxyFactory创建动态代理
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

查看newInstance方法即可看到我们所说的基于类加载器、接口信息和methodCache内部的MapperMethodInvoker完成动态代理对象的创建:

protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

如何实现Mybatis插件

Mybatis支持对ParameterHandler、ResultSetHandler、StatementHandler、Executor进行拦截,例如我们想对mybatis查询的SQL结果解析阶段进行拦截,我们可以编写下面这样一段代码:

import java.sql.Statement;

//@Intercepts({@Signature(
//        type = Executor.class,  //确定要拦截的对象
//        method = "query",        //确定要拦截的方法
//        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}   //拦截方法的参数
//)})

@Intercepts({@Signature(
        type = ResultSetHandler.class,  //确定要拦截的对象
        method = "handleResultSets",        //确定要拦截的方法
        args = {Statement.class}   //拦截方法的参数
)})
public class MyInterceptor implements Interceptor {


    private static Logger logger = LoggerFactory.getLogger(MyInterceptor.class);


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        logger.info("请求被拦截,拦截类:[{}],请求方法:[{}]请求参数[{}]", invocation.getTarget().getClass().getName(),
                invocation.getMethod().getName(),
                invocation.getArgs());
        //如果当前代理的是一个非代理对象,那么就会调用真实拦截对象的方法
        // 如果不是它就会调用下个插件代理对象的invoke方法
        Object obj = invocation.proceed();
        logger.info("请求被拦截结果:[{}]", obj);
        return obj;
    }
}

然后配置文件,增加对这个拦截类的配置:

<plugins>
        <plugin interceptor="com.sharkchili.mapper.MyInterceptor">
            <property name="dbType"  value="mysql"/>
        </plugin>
    </plugins>

执行我们的请求:

// 可以从配置或者直接编码来创建SqlSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2)通过SqlSessionFactory创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3)通过sqlsession执行数据库操作
        User1Mapper user1Mapper = sqlSession.getMapper(User1Mapper.class);
        User1 user = user1Mapper.select("1");

        logger.info("查询结果:[{}]", user.toString());

        if (sqlSession != null) {
            sqlSession.close();
        }

从输出结果就可以看出,我们的方法拦截到了结果处理的逻辑了。

2022-11-30 10:11:55,389 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==>  Preparing: select * from user1 where id = ?
2022-11-30 10:11:55,526 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Parameters: 1(String)
[main] INFO com.sharkchili.mapper.MyInterceptor - 请求被拦截,拦截类:[org.apache.ibatis.executor.resultset.DefaultResultSetHandler],请求方法:[handleResultSets]请求参数[[org.apache.ibatis.logging.jdbc.PreparedStatementLogger@12d2ce03]]
2022-11-30 10:12:06,928 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - <==      Total: 1
[main] INFO com.sharkchili.mapper.MyInterceptor - 请求被拦截结果:[[User1{id='1', name='小明', user2=null}]]
[main] INFO com.sharkchili.mapper.MyBatisTest - 查询结果:[User1{id='1', name='小明', user2=null}]
2022-11-30 10:12:06,938 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@24959ca4]

Mybatis插件的工作原理

Mybatis如何引入自定义插件?

我们的业务代码如下,创建SqlSessionFactory:

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

步入build逻辑会看到xml解析相关代码有一行,如下所示,可以看出他就是对xml文件中plugins标签进行解析:

//获取标签内容并反射生成拦截器存到某个list中
this.pluginElement(root.evalNode("plugins"));

其内部做的就是解析xml配置,生成拦截器对象MyInterceptor,并存放到interceptorChain中的一个名为interceptors的list中。

private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
            Iterator var2 = parent.getChildren().iterator();

            while(var2.hasNext()) {
            //解析配置生成Interceptor 
                XNode child = (XNode)var2.next();
                String interceptor = child.getStringAttribute("interceptor");
                Properties properties = child.getChildrenAsProperties();
                Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).getDeclaredConstructor().newInstance();
                interceptorInstance.setProperties(properties);
                //存到拦截器链中
                this.configuration.addInterceptor(interceptorInstance);
            }
        }

    }

我们看看addInterceptor,逻辑非常简单,说白了就是存到一个名为interceptors的list集合中,然后进行链式调用:

public void addInterceptor(Interceptor interceptor) {
 
        this.interceptorChain.addInterceptor(interceptor);
    }

执行真正逻辑,调用插件:

//3)通过sqlsession执行数据库操作
        User1Mapper user1Mapper = sqlSession.getMapper(User1Mapper.class);
        User1 user = user1Mapper.select("1");

注意在Plugin的signatureMap插个断点,如下所示:

这时候进行debug,我们可以看到堆栈中停在这样一段代码上。由于我们编写了一个结果解析的拦截插件MyInterceptor,所以在newResultSetHandler时会从上文注册的interceptorChain中取出对应处理器,给我们的resultSetHandler ,这过程中piugin类会通过Plugin.wrap(target, this);对我们的结果处理类进行包装。

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);
        //调用pluginAll将所有插件都引入
        ResultSetHandler resultSetHandler = (ResultSetHandler)this.interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
    }

所以我们步入看看wrap的逻辑,可以看到interceptor即我们自己编写的插件,他会通过getSignatureMap获取这个我们编写插件MyInterceptor注解上的信息,通过反射生成一个新的代理对象,这个对象存放着signatureMap。

public static Object wrap(Object target, Interceptor interceptor) {
//获取自定义插件信息存到signatureMap 中
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        // 使用插件包装我们的目标类
        return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
    }

执行SQL逻辑:

最终我们的结果处理插件会在handleResultSets阶段发现signatureMap里面有值,当前处理器有拦截,执行this.interceptor.intercept(new Invocation(this.target, method, args))拦截相关处理器执行我们的MyInterceptor逻辑。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
        //发现signatureMap有值
            Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
            //methods 中包含我们的这个阶段的方法handleResultSets,故调用this.interceptor.intercept(new Invocation(this.target, method, args))
            return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
        } catch (Exception var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    }

Mybatis的分页插件的实现原理

通过上面工作原理的介绍我们就知道原理了,分页插件就是通过mybatis提供的接口,拦截Executor的query方法,重写执行的sql,例如select * from student,拦截sql后重写为:select t.* from (select * from student) t limit 0, 10;即可。

责任编辑:赵宁宁 来源: 写代码的SharkChili
相关推荐

2010-02-03 16:15:05

Python语言

2010-01-15 10:22:24

C++语言

2011-07-07 09:12:46

智慧运算WatsonPower

2023-10-27 13:59:30

Mybatis占位符

2010-02-04 11:06:14

2020-08-26 10:25:16

智慧

2010-12-01 14:37:00

2023-10-08 08:22:33

2021-03-18 08:18:15

ZooKeeper数据持久化

2022-03-18 15:55:15

鸿蒙操作系统架构

2021-01-21 08:49:52

数据单体架构

2021-07-26 05:33:59

自动化领导CIO

2018-12-14 09:48:23

Redis数据故障

2023-08-17 16:17:00

Docker前端

2017-09-21 08:16:33

数据存储环境

2017-08-17 09:46:42

大数据诸葛io数据挖掘

2021-09-01 07:21:40

ArrayPool源码Bucket

2009-11-11 15:29:15

ADO初始化

2023-09-08 08:42:01

数据场景项目
点赞
收藏

51CTO技术栈公众号