2.4.7.标准的属性名称解析器 PropertyTokenizer

优质
小牛编辑
129浏览
2023-12-01

好了,到这里,终于完成了在settings元素的解析工作中涉及到知识点的学习工作了。

现在我们继续回到XMLConfigBuildersettingsAsProperties方法中:

校验setting子元素配置的属性名称在Configuration对象中是否有对应的setter方法定义:

// 获取Configuration类的描述对象
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
// 检查Settings节点下的配置是否被支持
for (Object key : props.keySet()) {
    if (!metaConfig.hasSetter(String.valueOf(key))) {
        // 校验 setting 配置的属性是否支持
        throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
    }
}

前面我们已经知道MetaClass用于保存指定类的可读/可写的属性、方法以及构造器等信息,他的hasSetter方法接受一个String类型的name参数:

/**
  * 判断当前Reflector维护的类中是否指定名称的属性的setter方法
  *
  * @param name 属性描述符
  * @return 是否有指定属性的setter方法
  */
 public boolean hasSetter(String name) {
     // 包装属性描述符
     PropertyTokenizer prop = new PropertyTokenizer(name);
     if (prop.hasNext()) {
         // 嵌套的属性描述符
         if (reflector.hasSetter(prop.getName())) {
             // 递归生成属性类型的MetaClass对象,并进行setter方法的判断
             MetaClass metaProp = metaClassForProperty(prop.getName());
             return metaProp.hasSetter(prop.getChildren());
         } else {
             // 未包含该属性,返回false
             return false;
         }
     } else {
         // 简单的属性描述符
         // 直接判断是否有指定属性的setter方法
         return reflector.hasSetter(prop.getName());
     }
 }

name参数是一个属性名称描述语句,他支持级联多层的属性嵌套,多层属性描述使用.作为分隔符,比如:panda.name.first表示取当前对象的panda属性的name属性的first属性。

同时name参数还可以表达对集合内具体某一元素的引用,比如:names[0].firstnames[key].first这种语法也是被支持的。

hasSetter方法的逻辑也比较简单,在将属性名称描述符name包装成PropertyTokenizer之后,如果该属性名称描述符不包含多层引用的话(不含.),直接返回当前reflector对象中是否含有名称为name的属性的setter方法 ,如果该属性名称描述符包含多层引用的话(含有.符号),则按照.做为分隔符,生成新的MetaClass对象,递归调用hasSetter方法完成属性的判断,直到中间某一属性不存在或属性描述符被处理完成。

比如:假设name属性的值为:panda.name.first

因为panda.name.first中包含了.,他表示一个多层的属性调用,因此hasSetter方法会尝试从当前MetaClass维护的Reflector对象中, 判断其维护的类型是否有panda属性,如果没有直接返回false, 如果有,则调用metaClassForProperty方法为panda属性生成一个MetaClass对象,并将name.first作为name参数继续执行hasSetter方法的调用,直到解析到first属性。 因为first属性不包含.所以hasSetter方法会直接判断Reflector对象中是否有该属性的setter方法。

metaClassForProperty方法的实现比较简单,他的实现依赖于Reflector对象的getGetterType方法:

/**
 * 获取指定属性的类型元数据
 *
 * @param name 指定字段
 * @return 指定字段的类型元数据
 */
public MetaClass metaClassForProperty(String name) {
    // 从反射元数据中获取该名称的getter方法的类型
    Class<?> propType = reflector.getGetterType(name);
    // 生成元数据
    return MetaClass.forClass(propType, reflectorFactory);
}

Reflector对象的getGetterType方法:

/**
 * 获取指定名称的属性的可读类型,该类型取决于指定属性的getter方法和属性定义本身
 *
 * @param propertyName 属性名称
 * @return 指定名称的属性的可读类型
 */
public Class<?> getGetterType(String propertyName) {
    // 获取属性类型
    Class<?> clazz = getTypes.get(propertyName);
    if (clazz == null) {
        throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
    }
    // 返回
    return clazz;
}

整两个方法的实现都比较简单,就不赘述了,我们继续看关于属性描述符的解析工作。

属性描述符的解析操作并不复杂,它借助于PropertyTokenizer来实现,PropertyTokenizer是一个标准的迭代器实现。

迭代器是一种常见的行为性设计模式,他的作用是可以顺序的访问一个聚合对象中的各种元素,而又不暴露该对象的内部标识。 迭代器又被称为游标模式,游标是处理数据的一种方法,为了查看或者处理结果集中的数据,游标提供了在结果集中一次一行或者多行前进/后退的能力。

PropertyTokenizer实现了JAVA自带的迭代器接口定义 Iterator<E>Iterator接口定义了四个方法:

  • hasNext:判断是否有下一个元素
  • next:获取下一个元素
  • remove:移除当前元素
  • forEachRemaining:按照指定的方法处理剩余的元素

PropertyTokenizer完成了对前三个方法的实现,同时定义了四个属性并为其提供了对应的getter方法:

/**
 * 属性名称:
 * 比如names[0].first中的names
 * 比如names.first中的names
 */
private String name;
/**
 * 包含索引的名称,
 * 比如:names[0].first中的names[0]
 * 比如:names.first中的names
 */
private final String indexedName;
/**
 * 索引值
 * 比如:names[0].first中的0
 * 比如:names.first中的null
 */
private String index;
/**
 * 子属性
 * 比如:names[0].first中的first
 * 比如:panda.name.first中的name.first
 */
private final String children;

这四个属性的赋值操作是在PropertyTokenizer的构造方法中完成的,PropertyTokenizer的构造方法接收一个String类型的参数fullnamefullname是一个属性名称描述符.

他支持两种语法,一种是通过.分隔符表示多层属性的引用:name.first,另一种是通过[key]来表示集合中角标或者key值为key的元素:names[0].first

/**
 * 生成PropertyTokenizer对象
 *
 * @param fullname 完整的属性名称描述符
 */
public PropertyTokenizer(String fullname) {
    // 假设入参为names[0].first

    // 获取第一个属性分隔符的位置
    int delim = fullname.indexOf('.');

    if (delim > -1) {
        // 有属性分隔符,表示是一个嵌套的多层属性
        // 获取属性分隔符前面的内容作为属性,此处是names[0]
        name = fullname.substring(0, delim);
        // 属性分隔符后面的内容,则作为子属性保存起来,此处是first
        children = fullname.substring(delim + 1);
    } else {
        // 当前不存属性分隔符,直接赋值即可
        name = fullname;
        children = null;
    }

    // 处理names[0].first这种包含了索引的场景
    // 包含索引内容的属性名称此处是names[0]
    indexedName = name;
    // 获取属性名称中[ 符号的位置
    delim = name.indexOf('[');
    if (delim > -1) {
        // 包含索引引用
        // 获取索引的值,表示从names[1]中获取1。
        index = name.substring(delim + 1, name.length() - 1);
        // 重新赋值,将names[0]转为names
        name = name.substring(0, delim);
    }
}

构造方法里的逻辑也比较简单,分为两步,第一步解析.,第二步解析[

第一步: 首先处理fullnamenamechildren属性赋值:

  • 如果传到构造方法的fullname包含了.,则表示他描述的是一个嵌套的多层属性的引用,对于这种场景,PropertyTokenizer会获取第一个.前面的部分作为name属性,并把.后面的内容赋值为children属性。
  • 如果传到构造方法的fullname不包含.,则表示他是一个简单的属性描述符,直接把fullname赋值给name属性,children属性赋值为null

完成属性namechildren的赋值操作之后,将上一步获取到的name属性的值赋值给indexedName属性,然后处理``

第二步:解析[,重置name属性,并完成index属性的赋值:

  • 如果上一步获取到的name属性中包含了字符[,则获取从[name属性的倒数第二个字符之间的内容赋值给index属性,这里截止到倒数第二位,是默认最后一位的字符为]
  • 如果name属性中不包含字符[,不进行任何操作。

完成了属性的赋值操作之后,再看PropertyTokenizer实现的Iterator的方法:

  • hasNext:判断当前PropertyTokenizer中是否有children的值。
    @Override
    public boolean hasNext() {
     // 是否有children数据
     return children != null;
    }
    
  • next:通过children生成一个新的PropertyTokenizer对象
    @Override
    public PropertyTokenizer next() {
      // 通过child生成新的PropertyTokenizer
      return new PropertyTokenizer(children);
    }
    
  • remove:不被支持
    @Override
    public void remove() {
      throw new UnsupportedOperationException("Remove is not supported, as it has no meaning in the context of properties.");
    }
    

在大致了解了PropertyTokenizer对象之后,我们回到XmlConfigBuildersettingsAsProperties方法上来,此时我们基本已经完成了该方法的解析操作,现在该方法中唯一剩下的疑点就是Configuration都提供了那些属性的setter方法, 这个问题,我们后面在解析settingsElement会详细的介绍。