mybatis运行原理,这个面试造大炮的年代,应该有点用

时间:2021-7-3 作者:qvyue

mybatis运行分为两部分,第一部分读取配置文件缓存到Configuration对象中。用以创建SqlSessionFactory,第二部分是SqlSession的执行过程。

动态代理:

Mybatis就是通过动态代理实现的。关于动态代理我们常用的有Jdk动态代理和cglib动态代理。关于CGLIB代理在框架中使用的比较多。

关于动态代理就是所有的请求有一个入口,由这个入口进行分发。在开发领域的一个用途就是【负载均衡】

关于Mybatis的动态代理是使用了两种的结合。

JDK实现:

我们传统的调用方式是通过java提供的new 机制创造一个对象出来。而通过动态代理是通过java.lang.reflect.Proxy对象创建对象调用实际方法的。

通过newProxyInstance方法获取接口对象的。而这个方法需要三个参数

ClassLoader loader : 通过实际接口实例对象获取ClassLoader

Class>[] interfaces : 我们抽象的接口

InvocationHandler h  : 对我们接口对象方法的调用。在调用节点我们可以进行我们的业务拦截

JavaDeveloper jDeveloper = new JavaDeveloper();

Developer developer = (Developer) Proxy.newProxyInstance(jDeveloper.getClass().getClassLoader(), jDeveloper.getClass().getInterfaces(), (proxy, method, params) -> {

    if (method.getName().equals(“code”)) {

        System.out.println(“我是一个特殊的人,code之前先分析问题”);

        return method.invoke(jDeveloper, params);

    }

    if (method.getName().equals(“debug”)) {

        System.out.println(“我没有bug”);

    }

    return null;

});

developer.code();

developer.debug();

CGLIB动态代理

cglib动态代理优点在于他不需要我们提前准备接口。

只需要实现cglib提供的MethodInterceptor接口,在初始化设置cglib的时候加载这个实例化对象就可以

public class MyMethodInterceptor implements MethodInterceptor {

    @Override

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        System.out.println(“======插入前置通知======”);

        Object object = methodProxy.invokeSuper(o, objects);

        System.out.println(“======插入后者通知======”);

        return object;

    }

}

初始化设置cglib

public static void main(String[] args) {

    //代理类class文件存入本地磁盘方便我们反编译查看源代码

    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, “/root/code”);

    //通过CGLIB动态代理获取代理对象过程

    Enhancer enhancer = new Enhancer();

    //设置enhancer对象的父类

    enhancer.setSuperclass(HelloService.class);

    // 设置enhancer的回调对象

    enhancer.setCallback(new MyMethodInterceptor());

    //创建代理对象

    HelloService helloService = (HelloService) enhancer.create();

    //通过代理对象调用目标方法

    helloService.sayHello();

}

类似于spring aop ,进行切面拦截控制;

通过对比两种动态代理我们很容易发现,mybatis就是通过JDK代理实现Mapper调用的。我们Mapper接口实现通过代理到xml中对应的sql执行逻辑

反射

通过反射我们就摆脱了对象的限制我们调用方法不再需要通过对象调用了。可以通过Class对象获取方法对象。从而通过invoke方法进行方法的调用了。

Configuration对象作用

Configuration对象存储了所有Mybatis的配置。主要初始化一下参数

properties

settings

typeAliases

typeHandler

ObjectFactory

plugins

environment

DatabaseIdProvider

Mapper映射器

结构如下:

mybatis运行原理,这个面试造大炮的年代,应该有点用

1,BoundSql提供三个主要的属性 parameterMappings 、parameterObject、sql

2,parameterObject参数本身。我们可以传递java基本类型、POJO、Map或者@Param标注的参数。

3, 当我们传递的是java基本类型mybatis会转换成对应的包装对象 int -> Integer 

4, 如果我们传递POJO、Map。就是对象本身我们传递多个参数且没有@Param指定变量名则parameterObject 类似{“1″:p1,”2″:p2,”param1″:p1,”param2”:p2} 

 我们传递多个参数且@Param指定变量名 则parameterObject类似{“key1″:p1,”key2″:p2,”param1″:p1,”param2”:p2} 

5, parameterMapping 是记录属性、名称、表达式、javaType,jdbcType、typeHandler这些信息 

6,sql 属性就是我们映射器中的一条sql. 正常我们在常见中对sql进行校验。正常不需要修改sql。

sqlsession执行流程(源码跟踪)

首先我们看看我们平时开发的Mapper接口是如何动态代理的。这就需要提到MapperProxyFactory这个类了。该类中的newInstance方法

protected T newInstance(MapperProxy mapperProxy) {

    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);

  }

通过上满代码及上述对jdk动态代理的表述。我们可以知道mapperProxy是我们代理的重点。

MapperProxy是InvocationHandler的实现类。他重写的invoke方法就是代理对象执行的方法入口。

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

try {

    if (Object.class.equals(method.getDeclaringClass())) {

    return method.invoke(this, args);

    } else if (isDefaultMethod(method)) {

    return invokeDefaultMethod(proxy, method, args);

    }

} catch (Throwable t) {

    throw ExceptionUtil.unwrapThrowable(t);

}

final MapperMethod mapperMethod = cachedMapperMethod(method);

return mapperMethod.execute(sqlSession, args);

}

private boolean isDefaultMethod(Method method) {

return (method.getModifiers()

    & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC

    && method.getDeclaringClass().isInterface();

}

通过源码。invoke内部首先判断对象是否是类 。 通过打断点发现会走到cacheMapperMethod这个方法去创建MapperMethod对象。

查看MapperMethod中execute方法我们可以了解到内部实现其实是一个命令行模式开发。通过判断命令从而执行不同的语句。判断到具体执行语句然后将参数传递给sqlsession进行sql调用并获取结果。到了sqlsession就和正常jdbc开发sql进行关联了。sqlsession中Executor、StatementHandler、ParameterHandler、Resulthandler    

Executor

Mybatis提供了三种执行器。

Configuration.class中newExecutor源码

mybatis运行原理,这个面试造大炮的年代,应该有点用

根据uml我们不难看出mybatis中提供了三类执行器分别SimpleExecutor、ReuseExecutor、BatchExecutor

public SqlSession openSession() {

  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);

}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {

    Transaction tx = null;

    try {

      // 得到configuration 中的environment

      final Environment environment = configuration.getEnvironment();

      // 得到configuration 中的事务工厂

      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);

      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

      // 获取执行器

      final Executor executor = configuration.newExecutor(tx, execType);

      // 返回默认的SqlSession

      return new DefaultSqlSession(configuration, executor, autoCommit);

    } catch (Exception e) {

      closeTransaction(tx); // may have fetched a connection so lets call close()

      throw ExceptionFactory.wrapException(“Error opening session.  Cause: ” + e, e);

    } finally {

      ErrorContext.instance().reset();

    }

  }

sqlsession获取一个数据库session对象时我们或根据我们的settings配置加载一个Executor对象。在settings中配置也很简单

通过java代码设置

factory.openSession(ExecutorType.BATCH);

StatementHandler

顾名思义,StatementHandler就是专门处理数据库回话的。这个对象的创建还是在Configuration中管理的。

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;

  }

Mybatis中StatementHandler使用的是RoutingStatementHandler这个class

关于StatementHandler和RoutingStatementHandler之间的关系我们通过源码可以看出这里和Executor一样都是适配器模式。采用这种模式的好处是方便我们对这些对象进行代理。

mybatis运行原理,这个面试造大炮的年代,应该有点用

在查看BaseStatementHandler结构我们会发现和Executor一模一样。同样的Mybatis在构造RoutingStatementHandler的时候会根据setting中配置来加载不同的具体子类。这些子类都是继承了BaseStatementHandler.    

Mybatis默认的是SimpleExecutor。 StatementHandler我们跟踪了Mybaits默认的是PrePareStatementHandler。在SimpleExecutor执行查询的源码如下

mybatis运行原理,这个面试造大炮的年代,应该有点用

我们发现在executor查询钱会先让statementHandler构建一个Statement对象。最终就是StatementHandler中prepare方法。这个方法在抽象类BaseStatmentHandler中已经封装好了。

mybatis运行原理,这个面试造大炮的年代,应该有点用

这个方法的逻辑是初始化statement和设置连接超时等一些辅助作用

然后就是设置一些参数等设置。最后就走到了执行器executor的doquery

mybatis运行原理,这个面试造大炮的年代,应该有点用

PrepareStatement在我们jdbc开发时是常见的一个类 。 这个方法执行execute前我们需要设置sql语句,设置参数进行编译。这一系列步骤就是刚才我们说的流程也是PrepareStatementHandler.prepareStatement做的事情。那么剩下的就是我们对数据结果的封装。

结果处理器(ResultSetHandler)

@Override

  public List handleResultSets(Statement stmt) throws SQLException {

    ErrorContext.instance().activity(“handling results”).object(mappedStatement.getId());

    final List multipleResults = new ArrayList();

    int resultSetCount = 0;

    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List resultMaps = mappedStatement.getResultMaps();

    int resultMapCount = resultMaps.size();

    validateResultMapsCount(rsw, resultMapCount);

    while (rsw != null && resultMapCount > resultSetCount) {

      ResultMap resultMap = resultMaps.get(resultSetCount);

      handleResultSet(rsw, resultMap, multipleResults, null);

      rsw = getNextResultSet(stmt);

      cleanUpAfterHandlingResultSet();

      resultSetCount++;

    }

    String[] resultSets = mappedStatement.getResultSets();

    if (resultSets != null) {

      while (rsw != null && resultSetCount

        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);

        if (parentMapping != null) {

          String nestedResultMapId = parentMapping.getNestedResultMapId();

          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);

          handleResultSet(rsw, resultMap, null, parentMapping);

        }

        rsw = getNextResultSet(stmt);

        cleanUpAfterHandlingResultSet();

        resultSetCount++;

      }

    }

    return collapseSingleResultList(multipleResults);

  }

这个方法我们可以导出来是结果xml中标签配置对结果的一个封装。

总结

SqlSession在一个查询开启的时候会先通过CacheExecutor查询缓存。击穿缓存后会通过BaseExector子类的SimpleExecutor创建StatementHandler。PrepareStatementHandler会基于PrepareStament执行数据库操作。并针对返回结果通过ResultSetHandler返回结果数据

mybatis运行原理,这个面试造大炮的年代,应该有点用
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。