ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

从源码层面分析Mybatis中Dao接口和XML文件的SQL是如何关联的

2021-11-06 20:05:50  阅读:93  来源: 互联网

标签:XML Object String Dao 源码 context configuration method getStringAttribute


为了能清楚的说明问题,源码我尽量加上详细的注释。有些大段的源码我只是截取了一部分能说明问题就好。

xml文件解析

我们知道SqlSessionFactory是mybatis非常重要的一个类,它是单个数据库映射关系经过编译后的内存镜像.SqlSessionFactory对象的实例可以通过SqlSessionFactoryBuilder对象类的build方法创建,而xml文件的解析就是在这个方法里调用的。

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());//这里是解析的入口
      ...

接着看下parse方法,

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));//这里开始解析
    return configuration;
  }

接着看parseConfiguration方法,

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //package标签
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          //mapper标签
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
              // 这里就会加载resource,解析mapper文件,构建mapperStatement对象,
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
              mapperParser.parse();
            }
            ...

注意这里还是解析mybatis的配置文件,还没到我们的xml sql文件。有人可能有疑问,这里的package、resource是啥啊,在mybatis的配置文件好像也没看到啊?事实上,mybatis的配置文件是可以这样写的:

<mappers>
    <mapper resource="Mapper xml的路径(相对于classes的路径)"/>
</mappers>

或者,

<mappers>
        <mapper class="接口的完整类名" />
</mappers>

不过我们大部分是用spring+mybatis的方式,这种配置比较少见了,更多的可能是这样的:

mybatis:
    # 配置类型别名
  type-aliases-package: com.xxx.xxx.system.model
    # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapper-locations: 'classpath*:/mybatis/*/**Mapper.xml'
    # 加载全局的配置文件
  config-location: 'classpath:/mybatis/mybatis-config.xml'

作用其实是一样的。

继续往下看,XMLMapperBuilder#parse方法,

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      //xml sql文件都是mapper开始的
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();//尝试通过nameSpace来加载配置文件。
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

configurationElement方法,

private void configurationElement(XNode context) {
    try {
       解析namespace
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      //解析parameterMap,最后添加到 Configuration对象parameterMaps属性里面,全局通用
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //解析resultMap,放在Configuration中,全局通用
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      解析sql
      sqlElement(context.evalNodes("/mapper/sql"));
      //真正的开始解析select|insert|update|delete标签
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

Configuration类是mybatis非常核心的一个类,由很多全局配置都会解析后放在这里,比如parameterMaps、resultMap等。继续看buildStatementFromContext方法,这个方法最终调用org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode,如下:

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    ....

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    //Mybatis会把每个SQL标签封装成SqlSource对象
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    //通过builderAssistant,将组装好的MappedStatement添加到configuration里面维护了statement的map,key就是namespace+sql的id、
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

可以看到,最终会生成MappedStatement对象,并添加到configuration里面维护的一个map,这个map的key就是namespace+sql的id,value是对应的MappedStatement对象。这个MappedStatement对象非常重要,它是连接我们两个部分的关键,记住这个类。

总结下:

XML文件中的每一个SQL标签就对应一个MappedStatement对象,这里面有两个属性很重要。

  • id:全限定类名+方法名组成的ID。
  • sqlSource:当前SQL标签对应的SqlSource对象。

MappedStatement对象会被缓存到Configuration#mappedStatements中,全局有效。Configuration对象就是Mybatis中的核心类,基本所有的配置信息都维护在这里。把所有的XML都解析完成之后,Configuration就包含了所有的SQL信息。

动态代理

了解了解析的流程,接着看另外一个问题:

我们定义的Dao接口并没有实现类,那么在调用它的时候,它是怎样最终执行到我们的SQL语句的呢?我先给出答案,动态代理。下面就来具体分析下。

这里先引入一个非常重要的类MapperProxy,来看看它的定义:

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -4724728412955527868L;
  private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
      | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC;
  private static final Constructor<Lookup> lookupConstructor;
  private static final Method privateLookupInMethod;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethodInvoker> methodCache;
  ...

这个类继承了InvocationHandler,肯定是动态代理了。如果有小伙伴对动态代理不熟悉,可以先补充下这部分知识,下面的内容会更好理解一些

思考一个问题:MapperProxy是什么时候创建的呢?是在SqlSession的getMapper这个抽象方法的实现中调用的,最终调用的是org.apache.ibatis.binding.MapperRegistry#getMapper,代码如下:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      //通过动态代理创建一个Mapper实例
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

当我们声明一个dao接口的时候,通常是这样做:

@MapperScan("com.fcbox.uniorder.system.dao")

这是在springboot中的用法,或者也可以使用xml配置的方式。这个注解的作用是,将路径下的所有类注册到Spring Bean中,并且将它们的beanClass设置为MapperFactoryBean。MapperFactoryBean实现了FactoryBean接口,俗称工厂Bean。那么,当我们注入这个Dao接口的时候,返回的对象就是MapperFactoryBean这个工厂Bean中的getObject()方法对象。(getObject方法返回就是FactoryBean创建的Bean实例)

org.mybatis.spring.mapper.MapperFactoryBean#getObject代码如下:

public T getObject() throws Exception {
        return this.getSqlSession().getMapper(this.mapperInterface);
    }

这个getMapper方法就会一路调用到我们上面说的org.apache.ibatis.binding.MapperRegistry#getMapper方法。

总结下,也就是说我们通过注入Dao接口的时候,注入的就是MapperProxy这个代理对象,那么自然的,根据动态代理的原理,当
我们调用到Dao接口的方法时,则会调用到MapperProxy对象的invoke方法。那我们就来看看这个invoke方法:

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //判断接口是否有实现类(一般我们的dao接口没有实现类)
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        //所以一般会走这个分支
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

然后会继续cachedInvoker的invoke方法,看看cachedInvoker是个啥东西,

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return MapUtil.computeIfAbsent(methodCache, method, m -> {
        if (m.isDefault()) {
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        } else {
          /**
           * 会走到这里来
           * PlainMethodInvoker是封装的一个mapper调用的工具类
           * MapperMethod 对象里面包含了两个对象的引用:
           * SqlCommand 包含了方法名(全限定名)和命令类型(insert、delete等等)
           */
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      ...
  }

所以调用的是org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker#invoke方法,

public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }

这里调用的是MapperMethod的execute方法,继续看,

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      ...

MapperMethod源码发现最终还是调用sqlSession中的相关方法,sqlSession再委托给Excutor去执行,比如我们拿update举例,如下:

@Override
  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

咦?这个MappedStatement怎么看着这么眼熟,这不就是我们第一部分讲的内容吗?是不是有种柳暗花明又一村的感觉。

总结下,当我们调用到Dao接口的方法时,则会调用到MapperProxy对象的invoke方法,最终会通过接口的全路径名从Configuration这个大管家的某个map里找到MappedStatement 对象,然后通过执行器Executor去执行具体SQL并返回。

在这里插入图片描述


参考:

  • https://juejin.cn/post/7004047712664420382

标签:XML,Object,String,Dao,源码,context,configuration,method,getStringAttribute
来源: https://blog.csdn.net/pony_maggie/article/details/121184148

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有