MyBatis(技术NeiMu):基础支持层(解析器模块与反射模块)

回顾

前面我们已经认识了MyBatis的整体架构,下面就从下往上去了解一下具体的细节

基础支持层

解析器模块

解析器模块是来解析配置文件的,而我们MyBatis的配置文件是XML,而解析XML常见的方式有三种

  • DOM:Document Object Model
  • SAX:Simple API For Xml
  • StAX:StreamAPI For Xml

而MyBatis使用的是DOM的解析方式,并结合使用XPath解析XML配置文件,XPath是一种为查询XML文档而设计的语言,有了XPath之后就可以来查询XML了,就好像使用SQL去查数据库一样

XPathParser

MyBatis提供的XPathParser类封装了Xpath的调用
在这里插入图片描述
可以看到,XPathParser组装了需要用到的类,比如Document、EntityResolver、Xpath和Properties,下面来说下这几个字段是干什么的

  • Document:解析XML出来的DOM树
  • validation:是否要开启验证
  • EntityResolver:用于加载本地DTD文件
  • Properties:MyBatis配置文件中对于properties标签定义的键值对集合
  • Xpath:Xpath对象,可以查询XML的

针对Properties属性讲一下

在这里插入图片描述
这个属性其实就是在配置文件里面在一处专门的地方统一去管理常量而已,DTD文件可以理解成是命名空间,即可以使用哪些标签(跟Spring的一致)

一般对于DTD文件都是直接联网加载的,当需要本地加载DTD文件的时候(当网络慢,可以进行本地加载),才会使用到EntityResolver这个属性

下面是我之前学习MyBatis的配置文件的笔记,可以看到里面的dtd文件从哪里加载

在这里插入图片描述
来看一下XPathParser是如何构建的
在这里插入图片描述
可以看到里面有一堆的构建方法

但这些构建方法其实逻辑都是一样的

在这里插入图片描述
分为两步

  • 调用commonConstructor方法
  • 实例化document
    • 直接注入
    • 使用createDocument方式实例
commonConstructor

这个方法里面涵盖了一般的构造逻辑

源码如下

在这里插入图片描述
可以看到,里面的逻辑就是很简单的注入validation、Properties和EntityResolver而已,而对于Xpath则是采用XPathFactory来创建(这是简单实例XPath的代码)

之前我们提到过,解析器模块是解析XML文件的,所以其肯定有一个方法是用来解析XML文档来生成DOM树的,对应的方法就是createDocument,而这个方法在调用XPathParser一般会调用,除非外部直接传来了一个Document进来

createDocument

该方法就是去根据XML文件生成对应的DOM树的

源码如下

private Document createDocument(InputSource inputSource) {
    // important: this must only be called AFTER common constructor
    try {
        //可以看到下面就是解析的逻辑
        //里面的仅仅只是使用DocumentBuilderFactory来创建出DOM树而已
        //这里的使用并不是MyBatis自身实现的,是其他依赖
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
      factory.setValidating(validation);

      factory.setNamespaceAware(false);
      factory.setIgnoringComments(true);
      factory.setIgnoringElementContentWhitespace(false);
      factory.setCoalescing(false);
      factory.setExpandEntityReferences(true);

      DocumentBuilder builder = factory.newDocumentBuilder();
      builder.setEntityResolver(entityResolver);
      builder.setErrorHandler(new ErrorHandler() {
        @Override
        public void error(SAXParseException exception) throws SAXException {
          throw exception;
        }

        @Override
        public void fatalError(SAXParseException exception) throws SAXException {
          throw exception;
        }

        @Override
        public void warning(SAXParseException exception) throws SAXException {
          // NOP
        }
      });
      return builder.parse(inputSource);
    } catch (Exception e) {
      throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    }
  }

可以看到这个createDocument方法只是封装了调用依赖去解析XML来生成DOM树的细节而已

eval方法

XPathParser还比较令人关注的点是,提供了一堆的eval方法用于解析boolean、shory、long、int、String、Node等类型的信息

在这里插入图片描述
而对于这些eval方法基本上都是调用evaluate方法,然后进行强转而已
在这里插入图片描述
而该evaluate方法仅仅只是调用了XPath的API而已,通过XPath去获取expression表达式里面指定的位置的值,然后转化成returnType,然后再返回上一层再来一次强转
在这里插入图片描述
基本数据类型都是这样转化的,除了String类型
在这里插入图片描述
可以看到,对于evalString做了额外的处理,并不是直接强转之后返回,还需要PropertyParser.parse来进行处理

针对evalString的特殊处理

在这里插入图片描述
可以看到,这个特殊的额处理交由了PropertyParser去完成

从原马上可以看到,其首先创建了两个对象

  • VariableTokenHandler:使用了Properties来创建,也就是properties属性里面的那些值,并且有解析占位符的功能
  • GenericTokenParser:将配置文件里面的值并转化成对应的数据类型,调用VariableTokenHandler来解析占位符

不过最后调用的还是GenericTokenParser的parse方法(而实际上GenericTokenParser只有parse方法),GenericTokenParser是一个通用的字占位符解析器,我们从传进来的openToken和closeToken大概就能摸清楚要干什么了吧。。。。。。这一看就是用来解析${}占位符的,并且也能说明VariableTokenHandler的用处了,就是取出了占位符里面的表达式,然后从VariableTokenHandler里面进行映射获取真正的值

源码如下

public String parse(String text) {
   //先进行判空,String还有empty方法,我也是惊了。。。。
    //String的empty方法返回底层的字符数组的长度是否为0
  if (text == null || text.isEmpty()) {
    return "";
  }
  // search open token
    //去搜索open token,open token其实就是构造方法传过来的${
  int start = text.indexOf(openToken);
    //如果没有找到,直接return源文本
  if (start == -1) {
    return text;
  }
    //如果不是最后一个,就可能会出现close token
    //要进行下面的处理
    //转化成字符数组
  char[] src = text.toCharArray();
    //记录偏移量
  int offset = 0;
    //使用StringBuilder
  final StringBuilder builder = new StringBuilder();
    //创建expression来承载${}里面的表达式
  StringBuilder expression = null;
  do {
      //如果出现了转义符
    if (start > 0 && src[start - 1] == '\\') {
      // 只添加转义符之后的字符
      builder.append(src, offset, start - offset - 1).append(openToken);
        //修改偏移量
      offset = start + openToken.length();
    } else {
      // 如果没有转义符出现
        //要去搜索close token了
      if (expression == null) {
        expression = new StringBuilder();
      } else {
        expression.setLength(0);
      }
        //将当前偏移量包含的字符添加进builder中
      builder.append(src, offset, start - offset);
        //更新偏移量
      offset = start + openToken.length();
        //找到close token的位置
      int end = text.indexOf(closeToken, offset);
      while (end > -1) {
          //判断有没有转义符
        if (end > offset && src[end - 1] == '\\') {
          // this close token is escaped. remove the backslash and continue.
          expression.append(src, offset, end - offset - 1).append(closeToken);
          offset = end + closeToken.length();
          end = text.indexOf(closeToken, offset);
        } else {
          expression.append(src, offset, end - offset);
          break;
        }
      }
        //如果没有找打close token
      if (end == -1) {
        // close token was not found.
        builder.append(src, start, src.length - start);
        offset = src.length;
      } else {
          //此时expression已经解析完了
          //也就是占位符中的表达式已经解析出来了
          //接下来交由handler去解析这个token
        builder.append(handler.handleToken(expression.toString()));
        offset = end + closeToken.length();
      }
    }
    start = text.indexOf(openToken, offset);
  } while (start > -1);
  if (offset < src.length) {
    builder.append(src, offset, src.length - offset);
  }
  return builder.toString();
}

可以看到,一大串的字符串处理。。。。。。

抛开解析的细节,我们可以看到,解析表达式(映射properties属性里面的property)交由了VariableTokenHandler去完成了

下面就来看看是如何解析的

这里要说明一下,VariableTokenHandler是PropertyParser的一个内部类实现了TokenHandler接口,实现了handleToken方法

在这里插入图片描述
该内部类的handleToken方法如下

@Override
    public String handleToken(String content) {
        //判断Properties对象是否为空
        //前面提到过,这个对象存储properties标签里面的property
      if (variables != null) {
          //content就是传进来的${}里面的表达式
        String key = content;
          //判断是否支持默认值功能
        if (enableDefaultValue) {
            //可以看到,这里取出了默认值分隔符的索引位置
            //也就是说,默认值是在${}表达式里面给出的
          final int separatorIndex = content.indexOf(defaultValueSeparator);
          String defaultValue = null;
            //如果分隔符的位置不是在首位
          if (separatorIndex >= 0) {
              //此时的表达式为0~分隔符的位置
            key = content.substring(0, separatorIndex);
              //获取默认值
              //默认值是从分割符的下一位到最后一位
            defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
          }
            //如果存在默认值
          if (defaultValue != null) {
              ///调用getProperty方法获取
            return variables.getProperty(key, defaultValue);
          }
        }
        //从variables中取出该表达式映射的值
        if (variables.containsKey(key)) {
            //返回表达式映射的值
          return variables.getProperty(key);
        }
      }
        //这里有点意料之外
      	//如果并不是在variables里面的,就拼接回去
      return "${" + content + "}";
    }
  }

步骤大概如下

  • 判断properties属性解析出来的Properties对象是否为空
    • 如果不为空,判断是否支持默认值
      • 如果直接默认值,计算出默认值的分隔符的索引位置,使用该分隔符的索引位置得出表达式与默认值
      • 然后使用getProperty方法去获取
    • 如果不为空,但不支持默认值
      • 调用重载的getProperty方法获取
  • 如果properties属性为空,或者不支持默认值,并且properties属性中根本没有这个键的映射,礼貌地给当前表达式拼接回${}占位符

下面我们来看看默认值的分隔符到底是什么

从构造方法就可以看到这两个参数的设置了
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们先来看看enableDefaultValue,这个值标识着是否开启了默认值,其中可以看到其传进去的ENABLE_DEFAULT_VALUE为false,也就是说明了默认值为false,但如何开启呢?我们看接下来的KEY_ENABLE_DEFAULT_VALUE可以看到其是一个常量值
在这里插入图片描述
从该方法上可以看到,这个开启默认值的标识也是存储我们的variables中的,如果没有设置KEY_ENABLE_DEFAULT_VALUE的化,默认是不开启的,而且默认值分隔符也是一样的,可以看到默认的默认值分隔符是一个冒号

所以现在已经看懂了VariableTokenHandler与GenericTokenHandler的作用了吧

  • VariableTokenHandler:用于对占位符进行解析,获取里面的表达式,并且根据之前解析得到的properties属性里面的键值对映射去获取映射的值
  • GenericTokenHandler:用于对非占位符的值进行类型转化,并且对于字符串类型会调用VariableTokenHandler去对占位符进行解析

并且这里也看出了默认值和默认值分隔符,并且大概也直到了默认值是如何设置的,格式大概如下**${content:默认值}**(采用默认的分隔符)

XPathParser的作用到此就结束了,总的来说XPathParser提供有如下作用

  • 使用DocumentFactory去解析配置文件,生成DOM树对象
  • parser方法可以对配置文件里面的字符串值进行解析,判断是不是要进行映射处理(映射到properties属性中)

反射模块

前面也提到过,反射模块对Java的反射进行了封装,并且对反射做了一系列的优化

MyBatis在进行参数处理、结果映射等操作时,会涉及到大量的反射操作,因此有必要进行自定义的封装来进行优化

Reflector和ReflectorFactory
Reflector

Reflector是MyBatis反射的基础,每个Reflector的实例就负责对应一个类,下面就来看看这个Reflector

Reflector里面会缓存一个类对象的要使用的信息

在这里插入图片描述
总共有9个属性来缓存类对象的信息

  1. type:类对应的class类型
  2. readablePropertyNames:存在get方法的所有字段名称
  3. writablePropertyNames:存在set方法的所有字段名称
  4. setMethods:一个HashMap容器,Key为字段的名称,Value为该字段的set方法,也就是存储所有的set方法并用字段名称来作key来进行映射
  5. getMethods:一个HashMap容器,Key为字段的名称,Value为该字段的get方法,也就是存储所有的get方法并用字段名称来作key来进行映射
  6. setTypes:一个HashMap容器,Key为字段的名称,Value为该字段的set方法的参数的class类型
  7. getTypes:一个HashMap容器,Key为字段的名称,Value为该字段的get方法的返回值的class类型
  8. Constructor:默认的构造方法
  9. caseInsensitivePropertyMap:记录了拥有get方法或拥有set方法(或者两个都有)的属性名称的集合,这里使用Map来记录是因为存储了大小写,Key为大写的名称,Value为小写的名称,并且这个是根据上面的readablePropertyNames和writablePropertyNames来生成的,hashMap还能进行去重

下面来看看其构造方法

可以看到,只有一个构造器,并且参数仅仅只有一个Class类型的参数,所以可知,那几个存储类信息的属性都是通过反射从该Class类型的参数通过反射来得到的

在这里插入图片描述
源码如下

public Reflector(Class<?> clazz) {
    //注入type
    type = clazz;
    //通过addDefaultConstructor方法注入默认构造器
    addDefaultConstructor(clazz);
    //通过addGetMethos方法注入get方法
    addGetMethods(clazz);
    //通过addSetMethods方法注入set方法
    addSetMethods(clazz);
    //通过addFields方法注入getTypes和setTypes
    addFields(clazz);
    //通过之前注入的get和Set方法去注入readablePropertyNames和writablePropertyNames
    readablePropertyNames = getMethods.keySet().toArray(new String[0]);
    writablePropertyNames = setMethods.keySet().toArray(new String[0]);
    //遍历readablePropertyNames去将有set方法的属性名称注入进caseInsensitivePropertyMap
    for (String propName : readablePropertyNames) {
      caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
    //遍历writablePropertyNames去将有set方法的属性名称注入进caseInsensitivePropertyMap
    for (String propName : writablePropertyNames) {
      caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
  }

可以看到构造方法其实就是注入各个属性而已,下面就细看一下是MyBatis进行筛选的

getDefaultConstructor方法

该方法是用来筛选出构造器的,因为只需使用一个构造器就够了

该方法源码如下

在这里插入图片描述

private void addDefaultConstructor(Class<?> clazz) {
    //通过反射获取所有声明的构造器,public与非public类型的
    //所以Mybatis也能打破单例
    Constructor<?>[] constructors = clazz.getDeclaredConstructors();
    //可以看到这里,使用JDK1.8的filter特性过滤,并且是过滤出没有参数的构造器
    //也就是说使用的构造器从没有参数的构造器中去取
    Arrays.stream(constructors).filter(constructor -> constructor.getParameterTypes().length == 0)
      .findAny().ifPresent(constructor -> this.defaultConstructor = constructor);
  }

从代码中可以看到,对于构造器的选择会选用第一个无参构造器

addGetMethods方法

该方法用于获取get方法的映射Map的

源码如下

private void addGetMethods(Class<?> clazz) {
    //创建conflictingGetters容器来存放get方法
    //可以看到这个容器的Value为List<Method>,并且从名字大概可以推断出这是存在矛盾的容器
    //也就是说,你一个字段只有一个get方法,但可能匹配出了多个出来
    //后续还需要进行筛选
    Map<String, List<Method>> conflictingGetters = new HashMap<>();
    //获取所有的方法
    Method[] methods = getClassMethods(clazz);
    //使用stream过滤出没有参数并且要满足下面两个条件之一
    //1、方法开头为get,并且方法名称的长度大于3
    //2、方法开头为is,并且方法名称额长度大于2(布尔类型的get方法)
    Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
      .forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
    resolveGetterConflicts(conflictingGetters);
  }
  • 首先获取所有方法
  • 使用filter通过名字过滤出名字上符合get方法的方法,具体匹配的规则如下(对应的方法为PropertyNamer.isGetter)
    • 方法开头为get,且方法名称的长度大于3
    • 方法开始位is,且方法名称的长度大于2
  • 使用foreach循环,将初步筛选出来的方法添加到conflictingGetters中
  • 使用resolveGetterConflicts方法处理conflictingGetters集合

我们先看是如何获取所有方法的

源码如下

private Method[] getClassMethods(Class<?> clazz) {
    //创建uniqueMethods容器来存储
    //有人可能会问了,这里为什么要用Map来存储?
    //这是因为之后会生成一个签名
    Map<String, Method> uniqueMethods = new HashMap<>();
    Class<?> currentClass = clazz;
    //这里要使用循环,当找完本类时要找父类,直到没有父类或者到Object类型
    while (currentClass != null && currentClass != Object.class) {
        //获取到本类当前所有的方法后,使用addUniqueMethods添加进uniqueMethods中
      addUniqueMethods(uniqueMethods, currentClass.getDeclaredMethods());
      // 也要去找接口
      Class<?>[] interfaces = currentClass.getInterfaces();
        // 遍历接口,获取每个接口里面的方法,添加进addUniqueMethods中
        // 因为接口里面有默认方法
      for (Class<?> anInterface : interfaces) {
        addUniqueMethods(uniqueMethods, anInterface.getMethods());
      }
		//找完接口,找完本类,接下来就去找父类
      currentClass = currentClass.getSuperclass();
    }
	//最终将里面的所有values转为集合返回
    Collection<Method> methods = uniqueMethods.values();

    return methods.toArray(new Method[0]);
  }

可以看到,这里找出类的所有方法不仅仅是自身的,还有包括接口和父类的、也包括父类的接口,范围里的方法都会经过,并且这里有一个关键点:先找了本类然后再找了接口(先记住,下面的addUniqueMethods需要这个细节),addUniqueMethods方法存进去uniqueMethods中

下面来看看addUniqueMethods方法做了什么

源码如下

 private void addUniqueMethods(Map<String, Method> uniqueMethods, Method[] methods) {
    for (Method currentMethod : methods) {
      if (!currentMethod.isBridge()) {
          //给方法生成签名
        String signature = getSignature(currentMethod);
        //如果该签名不存在,证明集合里面没有该方法,可以Put进去
         //所以签名的作用是用来去重的,作为方法的唯一标识的,那为什么要这样做呢?
         //Method对象已经重写了hashcode了呀,有必要去生成一个唯一标识吗?
         //这是有必要的,前面提到过,还会将接口中的方法添加进来
          //而且是先找本类的,然后找接口的,那么如果本类重写了接口中的方法呢?
          //接口中的方法和本类中的方法的hashcode是不一样的,此时就会出现重复了
          //所以要给每个方法添加唯一标识,并且为了添加类重写的方法,所以要先添加类再添加接口
          //接口的方法就会由于唯一标识已经存在不会被添加进去了
          //说的更直白一点,这里就把接口里面的抽象方法都过滤出来了,只剩下被重写的方法在里面
        if (!uniqueMethods.containsKey(signature)) {
          uniqueMethods.put(signature, currentMethod);
        }
      }
    }
  }

可以看到,这个方法里面给进来的每个方法都生成了一个签名,先判断uniqueMethods容器里面是不是已经有了该签名,如果有该签名就不会进行添加,说白了,这个签名其实是为了过滤接口里面被重写的方法的(被重写的方法已经添加,那么接口中的抽象方法就不会被添加,其实更明确的是过滤了接口中的抽象方法!!!!!!),因为接口的方法和重写的方法生成的hashcode不一样,不能仅仅依靠HashMap来进行过滤

下面来看看这个签名是如何生成的,对应方法为getSignature

源码如下

private String getSignature(Method method) {
    StringBuilder sb = new StringBuilder();
    //获取返回值类型
    Class<?> returnType = method.getReturnType();
    if (returnType != null) {
        //开头为:返回值类型#
      sb.append(returnType.getName()).append('#');
    }
    //再拼接方法名字,此时变为:返回值类型#方法名称
    sb.append(method.getName());
    //获取参数
    Class<?>[] parameters = method.getParameterTypes();
    for (int i = 0; i < parameters.length; i++) {
        //如果是第一个参数,前缀为:,如果不是后面的参数,用逗号分割
      sb.append(i == 0 ? ':' : ',').append(parameters[i].getName());
    }
    //所以大体的签名形式为:返回值类型#方法名称:参数1,参数2,参数3,....,参数n
    return sb.toString();
  }

从代码上可以看出大体的签名形式为返回值类型#方法名称:参数1,参数2,参数3,…,参数n

接下来看一下PropertyNamer.methodToProperty方法干了什么

源码如下

public static String methodToProperty(String name) {
    //如果方法名字是is开头
    if (name.startsWith("is")) {
        //取方法名字is后面的字符串
        //相当于是筛选出了变量名出来
      name = name.substring(2);
    } 
    //如果方法名字是get和set开头
    else if (name.startsWith("get") || name.startsWith("set")) {
      	//取方法名字的get或者set后面的字符串
        //相当于是筛选出了变量名出来
        name = name.substring(3);
    } else {
        //其他情况就直接抛错处理了
        //因为已经不属于get方法的规范命名了
      throw new ReflectionException("Error parsing property name '" + name + "'.  Didn't start with 'is', 'get' or 'set'.");
    }
	//如果筛选出来的变量名只有1个字符,或者变量名长度大于1,但变量名字的第二位不是大写(第二位不是大写代表第一位很有可能是大写)
    if (name.length() == 1 || (name.length() > 1 && !Character.isUpperCase(name.charAt(1)))) {
        //让变量名第一位变成小写,后面都不变
      name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
    }
	//返回变量名
    return name;
  }

可以看到,这个方法其实就是用来对get/set方法进行处理,从中划分出变量名字出来的,并且做了开头小写处理

下面就可以看addMethodConflict方法了
在这里插入图片描述
源码如下

private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
    if (isValidPropertyName(name)) {
        //computeIfAbsent先判断当前在conflictingMethods的HashMap中是否存在name这个key
        //如果不存在,则会调用后面的lambda去创建然后添加进conflictingMethods中(key为name)
        //如果存在,则直接返回conflictingMethods里面的list
      List<Method> list = MapUtil.computeIfAbsent(conflictingMethods, name, k -> new ArrayList<>());
      //往从conflictingMethods取出的list中添加筛选出来的方法
      list.add(method);
    }
  }

可以看到,这个方法其实就是根据筛选出的变量名来获取在conflictingMethod里面映射的List集合,当然如果不存在则会在conflictingMethod新添加键值对,然后再返回List集合,最后再往这个List集合中进行添加

可以看到,上面这两个方法结合使用,相当于是从get/set方法里面取出了变量名,处理大写问题之后,使用该变量名从conflictingMethod里面去找到对应存储该变量的get方法的List集合,并且往里面添加方法,所以,我们从这里大概可以推测,这个List集合存储的肯定是候选的get方法,后面应该是还要进行过滤的(resolveGetterConflicts方法就是进行下一步的处理矛盾)

拓展:分析一下要处理什么矛盾,前面已经看到了只有是get、is开头才能继续下面的解析然后放入List中,因此出现的矛盾仅仅有is和get方法同时出现的冲突!!!也就是一个变量为a,那么此时有了getA和isA方法

下面就看看resolveGetterConflicts方法

该方法源码如下

private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
    //遍历conflictingGetters的节点
    for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
        //winner来存储最终的选择的Get方法
      Method winner = null;
        //获取从get方法中解析出来的变量名
      String propName = entry.getKey();
      boolean isAmbiguous = false;
        //遍历所有的候选Get方法
      for (Method candidate : entry.getValue()) {
          //如果winner为null
        if (winner == null) {
            //将winner设为当前的候选者
            //所以可以看到,第一个Get方法的候选者将会先被选上
          winner = candidate;
          continue;
        }
          //获取当前候选者的返回类型
        Class<?> winnerType = winner.getReturnType();
          //获取当前被选上的返回类型
        Class<?> candidateType = candidate.getReturnType();
          //如果当前候选者的返回类型与被选上的返回类型相同
        if (candidateType.equals(winnerType)) {
            //如果相同,且返回类型不是布尔类型
          if (!boolean.class.equals(candidateType)) {
              //证明出现了模糊的Get方法,无法选出
              //比如getA和isA两个方法,并且返回值一致并且不为布尔型
              //isA一般都是boolean类型的嘛,所以这里就出现了模糊,不能选出
            isAmbiguous = true;
            break;
          } 
            //类型相同,但是布尔类型的,让is开头的方法被选上
            //说白了,如果是布尔类型,优先选用is方法
            else if (candidate.getName().startsWith("is")) {
            winner = candidate;
          }
        } 
          //如果两个类型不一致
          //判断是不是存在继承关系,并且优先选择父类或接口类型的返回
          //判断当前候选方法的返回值类型是不是被选上方法的返回值的子类
          else if (candidateType.isAssignableFrom(winnerType)) {
              //因为优先选用父类,所以不需要进行更换
          // OK getter type is descendant
        } 
          //判断被选上的方法的返回值类型是不是候选方法的返回值的子类
          else if (winnerType.isAssignableFrom(candidateType)) {
              //如果是,证明候选方法的返回值是父类
              //因此让候选方法被选上(优先选用父类)
          winner = candidate;
        } else {
              //如果两个类型没有任何关联
              //也代表出现了模糊情况了
          isAmbiguous = true;
          break;
        }
      }
        //委派到addGetMethod方法继续实现
      addGetMethod(propName, winner, isAmbiguous);
    }
  }

可以看到,这个方法是用来处理矛盾并选出最优解的,但有些矛盾能解,有些矛盾不能解决,其实说成是判断有没有矛盾产生更合适(一旦发生了矛盾是不会进行添加的),大概流程如下

  • List中存在多个Method,那么此时第一个Method将会被选上

    • 如果后面出现了返回值类型相等的Method,如果不是布尔类型,出现矛盾

    • 如过后面出现了返回值类型相等的Method,但是布尔类型,则优先选用is开头的

    • 如果后面出现了返回值类型不相等的Method,需要判断是不是继承关系

      • 优先选用返回值为父类的方法
  • 最终委派到addGetMethod继续处理

下面就来看看你addGetMethod干了什么

该方法源码如下

private void addGetMethod(String name, Method method, boolean isAmbiguous) {
    //可以看到MyBatis使用了MethodInvoke又进行了一层封装
    //如果出现了矛盾,会使用AmbiguousMethodInvoker
    //如果没有矛盾解决或者没有矛盾,使用就是MethodInvoker
    MethodInvoker invoker = isAmbiguous
        ? new AmbiguousMethodInvoker(method, MessageFormat.format(
            "Illegal overloaded getter method with ambiguous type for property ''{0}'' in class ''{1}''. This breaks the JavaBeans specification and can cause unpredictable results.",
            name, method.getDeclaringClass().getName()))
        : new MethodInvoker(method);
    //并且将invoker存进了getMethods中
    //依然使用map来进行映射,key为变量名字,value为MethodInvoker
    getMethods.put(name, invoker);
    //处理返回值类型,并且添加进getTypes里面
    Type returnType = TypeParameterResolver.resolveReturnType(method, type);
    getTypes.put(name, typeToClass(returnType));
  }

在这里插入图片描述
在这里插入图片描述
可以看到这两个MethodInvoke的区别

  • AmbiguousMethodInvoker:出现矛盾的时候使用的Invoker,在invoker方法里面会直接抛错
  • MethodInvoker:正常反射调用然后处理异常

从这里可以看到,MyBatis对于不为布尔类型出现的多个Get方法,或不相同返回类型的Get方法,反射生成Java Bean的时候会进行抛错

至此,addGetMethods方法就是这样了,要注意的几个点

  • 每个方法都有自己的唯一签名标识
  • 无法解决不为布尔类型的多个Get方法,无法解决不同返回值类型的多个Get方法
addSetMethods方法

在这里插入图片描述
addSetMethods的方法跟addGetMethods的方法几乎是同样的逻辑,唯一的不同就是一开始的选择候选方法时使用的是PropertyNamer.isSetter方法
在这里插入图片描述
该方法就更简单了,只要set开头的,并且方法名称长度大于3的

addFields方法

现在构造器、get和set方法、get和set的返回值类型的容器都已经完成了,接下来到成员属性的容器

该方法源码如下

private void addFields(Class<?> clazz) {
    //获取本类的所有成员属性
    Field[] fields = clazz.getDeclaredFields();
    //遍历
    for (Field field : fields) {
        //判断在setMethods中是否存在该成员属性的set方法
        
      if (!setMethods.containsKey(field.getName())) {
        // 获取属性的修饰符
        int modifiers = field.getModifiers();
          //判断是不是final static类型的
        if (!(Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers))) {
            //如果不是final static类型的,直接添加
            //因为final static类型的是直接初始化了,不需要Set方法注入
            //添加
          addSetField(field);
        }
      }
        //判断在getMethods中是否存在该成员属性的get方法
      if (!getMethods.containsKey(field.getName())) {
			//如果存在,添加
          addGetField(field);
      }
    }
    //递归去找父类的
    if (clazz.getSuperclass() != null) {
      addFields(clazz.getSuperclass());
    }
  }

可以看到,这里添加成员属性时,还要根据是否有get和set方法来进行添加,并且还是添加到两个不同的集合,并且对于set方法,如果变量是被final static修饰的,不需要之前检查时拥有set方法

下面就来看看addSetField做了什么

在这里插入图片描述
源码如下

private void addGetField(Field field) {
    //进行校验,不是以$开头,不是序列号ID,变量名字不是为class
    //才会进行添加
    if (isValidPropertyName(field.getName())) {
        //符合校验才进行添加
        //可以看到,这里竟然直接put了进去,那之前去addGetMethods的方法有什么意义呢?
        //回想一下,在之前addGetMethods时,此时里面有两种类型
        //一种是模糊的MethodInvoker,一种是精确的MethodInvoker
        //之所以出现了矛盾不直接抛错,是因为可以通过变量类型来进行填充!!!
        //所以对于这里对getMethods进行完善、解决冲突
        //不过前提是,变量要有get方法才能进行完善
      getMethods.put(field.getName(), new GetFieldInvoker(field));
      Type fieldType = TypeParameterResolver.resolveFieldType(field, type);
      getTypes.put(field.getName(), typeToClass(fieldType));
    }
  }

可以看到,说白了addGetField是为了完善之前添加的Get方法,因为之前添加进来的Get方法可能存在模糊的,假如这个变量存在,那么就对GetMethods进行完善(因为反射支持Field直接调用get和set方法)

在这里插入图片描述
而这个GetFieldInvoker其实就是封装了一下而已,本质上的invoke就是通过Field反射去执行get方法

对于addSetField是同样的原理,这里就不再赘述了

在这里插入图片描述
在这里插入图片描述

最后的处理

在这里插入图片描述
最后的几步处理就很简单了,只是去将拥有get、set方法的变量名字添加进caseInsensitivePropertyMap里面中去

ReflectorFactory

上面已经讨论了Reflector了,可以知道一个Reflector对应着缓存一个类的元数据信息,而ReflectorFactory负责的是对应每个类去创建Reflector

在这里插入图片描述
可以看到ReflectorFactory的是一个接口,并且只有一个DefaultReflectorFactory实现类!

先看下这三个方法的作用

  • isClassCacheEnabled:是否开启了缓存
  • setClassCacheEnabled:开启、关闭缓存(从这里可以看到,还是有人喜欢在接口上面添加一些set方法的嘛)
  • findForClass:根据class去创建Reflector,或者从缓存中取Reflector

而且这个DefaultReflectorFactory的实现代码比较少

在这里插入图片描述
并且从源码上可以看到DefaultReflectorFactory默认是使用缓存的,并且缓存是一个ConcurrentHashMap

findForClass

接下来就来看看,他是怎么给Class创建Reflector的

源码如下

public Reflector findForClass(Class<?> type) {
    //判断是否开启了缓存
    if (classCacheEnabled) {
      // 如果开启了缓存,从缓存中去找,如果没找到,就创建
      return MapUtil.computeIfAbsent(reflectorMap, type, Reflector::new);
    } else {
        //没开启缓存,直接new一个
      return new Reflector(type);
    }
  }

相当简单

  • 判断是否开启了缓存
    • 如果开启了缓存,则取缓存中找,没有就新建(computeIfAbsent方法在上面已经看过了)
    • 如果没开启缓存,直接new一个出来

当然对于ReflectorFactory的实现类我们可以在配置文件上自定义,这个只不过是默认的

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值