MyBatis学习笔记 ---- Mybatis配置文件解析、mapper动态代理加载、注解映射

本文深入探讨了MyBatis如何通过注解解析参数,详细阐述了从生成动态代理类到解析参数列表,再到形成占位符的整个过程。同时,解析了MyBatis配置加载的步骤,包括JDBC属性装配、Mapper加载。最后,讲解了Mapper方法执行的细节,涉及SQL创建、执行及参数装填。通过这些步骤,理解MyBatis内部工作原理。
摘要由CSDN通过智能技术生成

(一)注解方式如何解析参数思路:

如果让我们自己解析这样的一个接口签名,当调用时,生成对应的sql指令,我们会怎么做?

在这里插入图片描述
我们可以分为解析编译至sql 两个大过程;

第一步:生成对象的动态代理类,这样可以获取到对应的参数列表以及被修饰的注释。

在这里插入图片描述

第二步:将解析出来的参数以及参数列表形成对应关系

在这里插入图片描述

1.2.1 填充占位符

形成子方法,用于获取 {id} 这样的占位符;
思路识别#后,截取{,} 之间的内容。

在这里插入图片描述

1.2.2 解析结束

直至解析到},视为当前参数解析完毕;

在这里插入图片描述

1.2.3 装填解析类型

获取mapper.java 的返回值 , 该步骤也是在代理mapper.java中利用反射获取。

在这里插入图片描述
占位符的解析实际上mybatis使用的ognl表达式;

在这个流程完毕后,对mapper.java文件生成代理类。mybatis自己维护的一个Map,K为mapper.java中的method , value 为构建结束的statement。

org.apache.ibatis.session.Configuration
//映射的语句,存在Map里
protected final Map<String, MappedStatement> mappedStatements = new StrictMap(“Mapped Statements collection”);
//缓存,存在Map里
protected final Map<String, Cache> caches = new StrictMap(“Caches collection”);

当用户调用相关方法的时候,进行相关获取statement与调用参数的装填,后文详细介绍。


(二)Mybatis如何加载配置

SqlSessionFactoryBuilder 时,具体构建相关的sessionFactory ,而当构建SqlSessionFactoryBuilder时,将会对配置内容(org.apache.ibatis.session.Configuration 进行属性构建)进行加载

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
        //委托XMLConfigBuilder来解析xml文件,并构建;与SpringResource 的做法很相似
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
        //这里是捕获异常,包装成自己的异常并抛出的idiom?,最后还要reset ErrorContext
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

org.apache.ibatis.builder.xml#XMLConfigBuilder中进行属性装填的相关方法;

//解析配置
  private void parseConfiguration(XNode root) {
    try {
      //分步骤解析
      //issue #117 read properties first
      //1.properties
      propertiesElement(root.evalNode("properties"));
      //2.类型别名
      typeAliasesElement(root.evalNode("typeAliases"));
      //3.插件
      pluginElement(root.evalNode("plugins"));
      //4.对象工厂
      objectFactoryElement(root.evalNode("objectFactory"));
      //5.对象包装工厂
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //6.设置
      settingsElement(root.evalNode("settings"));
      // read it after objectFactory and objectWrapperFactory issue #631
      //7.环境
      environmentsElement(root.evalNode("environments"));
      //8.databaseIdProvider
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //9.类型处理器
      typeHandlerElement(root.evalNode("typeHandlers"));
      //10.映射器
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
2.1 对于JDBC相关属性的装配,发生在第七步environmentsElement中
<environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver/>
                <property name="url" value="jdbc:mysql://#:3306/#?useUnicode=true"/>
                <property name="username" value="#"/>
                <property name="password" value="#"/>
            </dataSource>
        </environment>
    </environments>
private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        //查找匹配的environment
        if (isSpecifiedEnvironment(id)) {
          // 事务配置并创建事务工厂
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          // 数据源配置加载并实例化数据源, 数据源是必备的
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          // 创建Environment.Builder
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }
2.2 Mybatis如何加载xxx.mapper
自动扫描包下所有映射器
//	<mappers>
//	  <package name="org.mybatis.builder"/>
//	</mappers>
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //10.4自动扫描包下所有映射器
          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) {
            //10.1使用类路径
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //映射器比较复杂,调用XMLMapperBuilder
            //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            //10.2使用绝对url路径
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            //映射器比较复杂,调用XMLMapperBuilder
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            //10.3使用java类名
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            //直接把这个映射加入配置
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }
  

其中,
String mapperPackage = child.getStringAttribute(“name”);
configuration.addMappers(mapperPackage);

为mapper的具体引导,

具体入口如下:

//看一下如何添加一个映射
  public <T> void addMapper(Class<T> type) {
    //mapper必须是接口!才会添加
    if (type.isInterface()) {
      if (hasMapper(type)) {
        //如果重复添加了,报错
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        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);
        parser.parse();
        loadCompleted = true;
      } finally {
        //如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之?
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
第一步: 将Configuration转换成MapperAnnotationBuilder为后续sql统一解析做准备;
  public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
   String resource = type.getName().replace('.', '/') + ".java (best guess)";
   this.assistant = new MapperBuilderAssistant(configuration, resource);
   this.configuration = configuration;
   this.type = type;

   sqlAnnotationTypes.add(Select.class);
   sqlAnnotationTypes.add(Insert.class);
   sqlAnnotationTypes.add(Update.class);
   sqlAnnotationTypes.add(Delete.class);

   sqlProviderAnnotationTypes.add(SelectProvider.class);
   sqlProviderAnnotationTypes.add(InsertProvider.class);
   sqlProviderAnnotationTypes.add(UpdateProvider.class);
   sqlProviderAnnotationTypes.add(DeleteProvider.class);
 }
第二步 :parser.parse()包含了具体sql语句层面的解析,我们继续来看。
public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

//注解的扫描解析逻辑
void parseStatement(Method method) {
    Class<?> parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      Options options = method.getAnnotation(Options.class);
      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;
      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();
      }

      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) {
        resultMapId = parseResultMap(method);
      }

      assistant.addMappedStatement(
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          null);
    }
  }

(三)Mybatis如何执行mapper.java中的方法并进行执行的呢?

执行的入口是下述代理类

org.apache.ibatis.binding.MapperProxy

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
    //并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    //这里优化了,去缓存中找MapperMethod
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //执行
    return mapperMethod.execute(sqlSession, args);
  }
  1. 将得到的代理类去methodCache中寻找到mapper执行者
  2. 将获得的执行者执行方法
  3. 进行方法执行

总体是围SqlCommandMethodSignature两个对象

  • SqlCommand 用于构建sql语句。
  • MethodSignature 用于构建该SQL对应的方法签名。
3.1.1 SQL的创建
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    String statementName = mapperInterface.getName() + "." + method.getName();
    MappedStatement ms = null;
    if (configuration.hasStatement(statementName)) {
      ms = configuration.getMappedStatement(statementName);
    } else if (!mapperInterface.equals(method.getDeclaringClass().getName())) { // issue #35
      //如果不是这个mapper接口的方法,再去查父类
      String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
      if (configuration.hasStatement(parentStatementName)) {
        ms = configuration.getMappedStatement(parentStatementName);
      }
    }
    if (ms == null) {
      throw new BindingException("Invalid bound statement (not found): " + statementName);
    }
    name = ms.getId();
    type = ms.getSqlCommandType();
    if (type == SqlCommandType.UNKNOWN) {
      throw new BindingException("Unknown execution method for: " + name);
    }
  }

阐明这条sql是什么类型的语句(select、update、query、delete) 以及根据接口全限定类名进行拼接;

3.1.2: sql对应的Java方法是什么,描述返回值、入参类型等
public MethodSignature(Configuration configuration, Method method) {
     this.returnType = method.getReturnType();
     this.returnsVoid = void.class.equals(this.returnType);
     this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());
     this.mapKey = getMapKey(method);
     this.returnsMap = (this.mapKey != null);
     this.hasNamedParameters = hasNamedParams(method);
     //以下重复循环2遍调用getUniqueParamIndex,是不是降低效率了
     //记下RowBounds是第几个参数
     this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
     //记下ResultHandler是第几个参数
     this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
     this.params = Collections.unmodifiableSortedMap(getParams(method, this.hasNamedParameters));
   }

其中,对于下面两个方法我们直观感受比较熟悉。

  • 解析mapper.java 中的@param标签

this.hasNamedParameters = hasNamedParams(method);

private boolean hasNamedParams(Method method) {
      boolean hasNamedParams = false;
      final Object[][] paramAnnos = method.getParameterAnnotations();
      for (Object[] paramAnno : paramAnnos) {
        for (Object aParamAnno : paramAnno) {
          if (aParamAnno instanceof Param) {
            //查找@Param注解,一般不会用注解吧,可以忽略
            hasNamedParams = true;
            break;
          }
        }
      }
      return hasNamedParams;
    }
  • 将mapper.java中存在@param注解的参数进行关联
	//得到所有参数
    private SortedMap<Integer, String> getParams(Method method, boolean hasNamedParameters) {
      //用一个TreeMap,这样就保证还是按参数的先后顺序
      final SortedMap<Integer, String> params = new TreeMap<Integer, String>();
      final Class<?>[] argTypes = method.getParameterTypes();
      for (int i = 0; i < argTypes.length; i++) {
        //是否不是RowBounds/ResultHandler类型的参数
        if (!RowBounds.class.isAssignableFrom(argTypes[i]) && !ResultHandler.class.isAssignableFrom(argTypes[i])) {
          //==参数名字默认为0,1,2,这就是为什么xml里面可以用#{1}这样的写法来表示参数了==
          String paramName = String.valueOf(params.size());
          if (hasNamedParameters) {
            //还可以用注解@Param来重命名参数
            paramName = getParamNameFromAnnotation(method, i, paramName);
          }
          params.put(i, paramName);
        }
      }
      return params;
    }
3.2. statement的执行

这个代码比较旧,新版的mybatis已经改成swich了…

//执行
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    //可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      if (method.returnsVoid() && method.hasResultHandler()) {
        //如果有结果处理器
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        //如果结果有多条记录
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        //如果结果是map
        result = executeForMap(sqlSession, args);
      } else {
        //否则就是一条记录
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else {
      throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

这个异常,眼熟吗? 熟…

if (ms == null) {throw new BindingException("Invalid bound statement (not found): " + statementName);}

sql的装填与执行,很有DDD的感觉。

public Object convertArgsToSqlCommandParam(Object[] args) {
      final int paramCount = params.size();
      if (args == null || paramCount == 0) {
        //如果没参数
        return null;
      } else if (!hasNamedParameters && paramCount == 1) {
        //如果只有一个参数
        return args[params.keySet().iterator().next().intValue()];
      } else {
        //否则,返回一个ParamMap,修改参数名,参数名就是其位置
        final Map<String, Object> param = new ParamMap<Object>();
        int i = 0;
        for (Map.Entry<Integer, String> entry : params.entrySet()) {
          //1.先加一个#{0},#{1},#{2}...参数
          param.put(entry.getValue(), args[entry.getKey().intValue()]);
          // issue #71, add param names as param1, param2...but ensure backward compatibility
          final String genericParamName = "param" + String.valueOf(i + 1);
          if (!param.containsKey(genericParamName)) {
            //2.再加一个#{param1},#{param2}...参数
            //你可以传递多个参数给一个映射器方法。如果你这样做了, 
            //默认情况下它们将会以它们在参数列表中的位置来命名,比如:#{param1},#{param2}等。
            //如果你想改变参数的名称(只在多参数情况下) ,那么你可以在参数上使用@Param(“paramName”)注解。 
            param.put(genericParamName, args[entry.getKey()]);
          }
          i++;
        }
        return param;
      }
    }

最终参数在

DefaultSqlSession.wrapCollection 进行最后一轮参数的装填

//把参数包装成Collection
  private Object wrapCollection(final Object object) {
    if (object instanceof Collection) {
      //参数若是Collection型,做collection标记
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("collection", object);
      if (object instanceof List) {
        //参数若是List型,做list标记
        map.put("list", object);
      }
      return map;      
    } else if (object != null && object.getClass().isArray()) {
      //参数若是数组型,,做array标记
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("array", object);
      return map;
    }
    //参数若不是集合型,直接返回原来值
    return object;
  }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值