Mybatis3源码分析(20)-Mapper实现-配置加载

撸了今年阿里、腾讯和美团的面试,我有一个重要发现…….

作者:ashan_li

出处:https://blog.csdn.net/ashan_li/article/category/6047775/1?


整体加载过程

  1. mybatis-config.xml中可以在包(package)和class的方法让Mybatis加载一个Mapper
  2. 通过包的方式是从包加载所有的class,最条还是通过class方法来实现加载
  3. 加载过程中主要通过读取方法中的注解来生成MappedStatement对象,再加入到Configuration中。这个过程跟解析mapper.xml配置文件中的select/insert/update/delete节点的过程差不多。一个是从xml里读取信息,一个是从注解里读取信息。
  4. 通过注解生成的MappedStatement的Id生成规则为接口全限定名名加方法的名字,所以在定义Mapper接口类时千万不要重载方法,否则会发生预想不到的问题。
  5. Mapper接口中的只有包含如下八种注解中的一种才会被Mybatis解析成一个MappedStatement对象。

    1. Select/Insert/Update/Delete
    2. SelectProvider/InsertProvider/UpdateProvider/DeleteProvider

    一个方法如果没有这些注解中的其中一个,执行时Mybatis会报MappedStatement找不到

  6. Mybatis加载一个Mapper接口时会为其生成一个MapperProxyFactory对象,由这个对象来创建Mapper接口的实例。MapperProxyFactory这个类的实现在下一节详细分析。

Mapper的配置加载是从XmlConfigBuilder.mapperElement()方法中触发的

    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 {
              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);
                InputStream inputStream = Resources.getResourceAsStream(resource);
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                mapperParser.parse();
              } else if (resource == null && url != null && mapperClass == null) {
                ErrorContext.instance().resource(url);
                InputStream inputStream = Resources.getUrlAsStream(url);
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                mapperParser.parse();
              } else if (resource == null && url == null && mapperClass != null) {
                Class<?> mapperInterface = Resources.classForName(mapperClass);
                //指定class的方式
                configuration.addMapper(mapperInterface);
              } else {
                throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
              }
            }
          }
        }
      }

通过package方式加载,最终也是找出所有符合条件的mapper类,再通过class的方法加载。

先来看configuration.addMaper()方法

     public <T> void addMapper(Class<T> type) {
        //最终是由MapperRegistry对象来完成加载的
        mapperRegistry.addMapper(type);
      }
     public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
          if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
          }
          boolean loadCompleted = false;
          try {
            //这里为一个Mapper生成一个代理工厂,这个代理工厂通过JDK动态代理生成一个对象,稍后再详细分析。
            knownMappers.put(type, new MapperProxyFactory<T>(type));
            // It's important that the type is added before the parser is run
            // otherwise the binding may automatically be attempted by the
            // mapper parser. If the type is already known, it won't try.
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            //Mapper.xml是交由XMLMapperBuilder来解析的
            //同样这里使用了MapperAnnotationBuilder来解析注解
            parser.parse();
            loadCompleted = true;
          } finally {
            if (!loadCompleted) {
              knownMappers.remove(type);
            }
          }
        }

MapperAnnotationBuilder.parse()方法

    public void parse() {
        String resource = type.toString();
        if (!configuration.isResourceLoaded(resource)) {
          //这里会先去加载相对应的mapper.xml配置文件
          //也就是说解析UserDao注解时,如果发现有UserDao.xml配置文件会先加载UesrDao.xml配置文件
          loadXmlResource();
          configuration.addLoadedResource(resource);
          assistant.setCurrentNamespace(type.getName());
          parseCache();
          parseCacheRef();
          //获取class中所有的方法
          Method[] methods = type.getMethods();
          for (Method method : methods) {
            try {
              //解析方法
              parseStatement(method);
            } catch (IncompleteElementException e) {
              configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
          }
        }
        parsePendingMethods();
      }
      //这个方法的功能就是读取方法的注解,生成一个MappedStatement对象,然后加入到Configuration中
      void parseStatement(Method method) {
        Class<?> parameterTypeClass = getParameterType(method);
        LanguageDriver languageDriver = getLanguageDriver(method);
        //从注解中获取一个SqlSource,之前已经分析过MappedStatement对应一个SqlSource对象,表示配置的Sql
        SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
        //如果sqlSource为空,这个方法将直接返回
        if (sqlSource != null) {
          //读取方法中的Options注解
          Options options = method.getAnnotation(Options.class);
          //注意这个mappedStatementId的生成规则,Mybatis生成的Mapper代理对象也是根据这个规则来生成一个mappedStatementId,再去Configuration中加载MappedStatement的
          final String mappedStatementId = type.getName() + "." + method.getName();
          Integer fetchSize = null;
          Integer timeout = null;
          StatementType statementType = StatementType.PREPARED;
          ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
          SqlCommandType sqlCommandType = getSqlCommandType(method);
          boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

          //注意这个
          //这两个是设置二级缓存的,如果没有设置options注解,将使用如下默认值
          //如果是update/insert/delete语句,就行里就会刷新缓存,select语句则不刷新
          //如果是select语句是默认使用缓存的
          boolean flushCache = !isSelect;
          boolean useCache = isSelect;

          KeyGenerator keyGenerator;
          String keyProperty = "id";
          String keyColumn = null;

          //自动生成主键,一般在应用中比较少用
          if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
            // first check for SelectKey annotation - that overrides everything else
            SelectKey selectKey = method.getAnnotation(SelectKey.class);
            if (selectKey != null) {
              keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
              keyProperty = selectKey.keyProperty();
            } else {
              if (options == null) {
                keyGenerator = configuration.isUseGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
              } else {
                keyGenerator = options.useGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
                keyProperty = options.keyProperty();
                keyColumn = options.keyColumn();
              }
            }
          } else {
            keyGenerator = new NoKeyGenerator();
          }

          if (options != null) {
            flushCache = options.flushCache();
            useCache = options.useCache();
            fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
            timeout = options.timeout() > -1 ? options.timeout() : null;
            statementType = options.statementType();
            resultSetType = options.resultSetType();
          }

          //获取对应的resultMap,还可以配置多个,用","分隔
          String resultMapId = null;
          ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
          if (resultMapAnnotation != null) {
            String[] resultMaps = resultMapAnnotation.value();
            StringBuilder sb = new StringBuilder();
            for (String resultMap : resultMaps) {
              if (sb.length() > 0) sb.append(",");
              sb.append(resultMap);
            }
            resultMapId = sb.toString();
          } else if (isSelect) {
            //如果没有配置resultMap,自动生成一个加入到Configuration中,就像Mapper.xml配置文件中只配置了resultType属性那样
            resultMapId = parseResultMap(method);
          }

          //这里跟加载mapper.xml配置文件中一样,交由助手去生成一个MappedStatement并加入到Configuration中
          assistant.addMappedStatement(
              mappedStatementId,
              sqlSource,
              statementType,
              sqlCommandType,
              fetchSize,
              timeout,
              null,                             // ParameterMapID
              parameterTypeClass,
              resultMapId,    // ResultMapID
              getReturnType(method),
              resultSetType,
              flushCache,
              useCache,
              false, // TODO issue #577
              keyGenerator,
              keyProperty,
              keyColumn,
              null,
              languageDriver,
              null);
        }
      }

再来看是怎么生成SqlSource对象的

     private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
        try {
          //获取方法的注解,Select/Update/Insert/Delete中的一种
          Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
          //获取方法的注解,SelectProvider/UpdateProvider/InsertProvider/DeleteProvider中的一种
          Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);

          if (sqlAnnotationType != null) {
            if (sqlProviderAnnotationType != null) {
              //不支持两种注解同时存在
              throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
            }

            Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
            //直接读取Select/Update/Insert/Delete中的sql内容
            final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
            //通过sql创建一个SqlSource,这里的逻辑眼从mapper.xml文件读取里的逻辑差不多,不再讨论
            return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
          } else if (sqlProviderAnnotationType != null) {
            Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
            //这个SqlSource主要是通过反射执行SelectProvider/UpdateProvider/InsertProvider/DeleteProvider的方法得到sql内容,也不再讨论
            return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation);
          }
          return null;
        } catch (Exception e) {
          throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
        }
      }
赞(0) 打赏

如未加特殊说明,此网站文章均为原创,转载必须注明出处。Java 技术驿站 » Mybatis3源码分析(20)-Mapper实现-配置加载
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

关注【Java 技术驿站】公众号,每天早上 8:10 为你推送一篇技术文章

扫描二维码关注我!


关注【Java 技术驿站】公众号 回复 “VIP”,获取 VIP 地址永久关闭弹出窗口

免费获取资源

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏