static SortedMap<Field, Descriptor> buildBeanedDescriptorsMap(
      Class<?> clz, boolean searchParent) {
    List<Field> fieldList = new ArrayList<>();
    Class<?> clazz = clz;
    Map<Tuple2<Class, String>, Method> methodMap = new HashMap<>();
    do {
      Field[] fields = clazz.getDeclaredFields();
      for (Field field : fields) {
        int modifiers = field.getModifiers();
        // final and non-private field validation left to {@link isBean(clz)}
        if (!Modifier.isTransient(modifiers)
            && !Modifier.isStatic(modifiers)
            && !field.isAnnotationPresent(Ignore.class)) {
          fieldList.add(field);
        }
      }
      Arrays.stream(clazz.getDeclaredMethods())
          .filter(m -> !Modifier.isPrivate(m.getModifiers()))
          // if override, use subClass method; getter/setter method won't overload
          .forEach(m -> methodMap.put(Tuple2.of(m.getDeclaringClass(), m.getName()), m));
      clazz = clazz.getSuperclass();
    } while (clazz != null && searchParent);

    for (Class<?> anInterface : clz.getInterfaces()) {
      Method[] methods = anInterface.getDeclaredMethods();
      for (Method method : methods) {
        if (method.isDefault()) {
          methodMap.put(Tuple2.of(method.getDeclaringClass(), method.getName()), method);
        }
      }
    }

    // use TreeMap to sort to fix field order
    TreeMap<Field, Descriptor> descriptorMap = new TreeMap<>(fieldComparator);
    for (Field field : fieldList) {
      Class<?> fieldDeclaringClass = field.getDeclaringClass();
      String fieldName = field.getName();
      String cap = StringUtils.capitalize(fieldName);
      Method getter;
      if ("boolean".equalsIgnoreCase(field.getType().getSimpleName())) {
        getter = methodMap.get(Tuple2.of(fieldDeclaringClass, "is" + cap));
      } else {
        getter = methodMap.get(Tuple2.of(fieldDeclaringClass, "get" + cap));
      }
      if (getter != null) {
        if (getter.getParameterCount() != 0
            || !getter
                .getGenericReturnType()
                .getTypeName()
                .equals(field.getGenericType().getTypeName())) {
          getter = null;
        }
      }
      Method setter = methodMap.get(Tuple2.of(fieldDeclaringClass, "set" + cap));
      if (setter != null) {
        if (setter.getParameterCount() != 1
            || !setter
                .getGenericParameterTypes()[0]
                .getTypeName()
                .equals(field.getGenericType().getTypeName())) {
          setter = null;
        }
      }
      TypeRef fieldType = TypeRef.of(field.getGenericType());
      descriptorMap.put(field, new Descriptor(field, fieldType, getter, setter));
    }
    // Don't cache descriptors using a static `WeakHashMap<Class<?>, SortedMap<Field, Descriptor>>`，
    // otherwise classes can't be gc.
    return descriptorMap;
  }
@Test
  public void testBuildBeanedDescriptorsMap() throws Exception {
    Assert.assertEquals(BeanA.class.getDeclaredField("f1"), BeanA.class.getDeclaredField("f1"));
    Assert.assertNotSame(BeanA.class.getDeclaredField("f1"), BeanA.class.getDeclaredField("f1"));
    SortedMap<Field, Descriptor> map = Descriptor.buildBeanedDescriptorsMap(BeanA.class, true);
    Assert.assertTrue(map.containsKey(BeanA.class.getDeclaredField("f1")));
    Assert.assertEquals(
        map.get(BeanA.class.getDeclaredField("doubleList")).getTypeRef(),
        new TypeRef<List<Double>>() {});
    Assert.assertNotNull(map.get(BeanA.class.getDeclaredField("longStringField")).getReadMethod());
    Assert.assertEquals(
        map.get(BeanA.class.getDeclaredField("longStringField")).getWriteMethod(),
        BeanA.class.getDeclaredMethod("setLongStringField", String.class));

    SortedMap<Field, Descriptor> map2 = Descriptor.buildBeanedDescriptorsMap(B.class, false);
    Assert.assertEquals(map2.size(), 1);
  }