Generic class type parameter detail at runtime

允我心安 提交于 2021-01-19 22:03:33

问题


Libraries like Jackson can create objects from JSON if we provide enough information about generics.

In Jackson, we can do

    Class<?>[] parameterTypes; 
    JavaType type = objectMapper.getTypeFactory().constructParametricType(ObjectClass, parameterTypes);
    objectMapper.readValue(json, type);

In java, a Generic class can be defined in many ways, like one generic class having another generic class and that can have another generic class, for simple illustration consider these three classes.

  public class MultiLevelGenericTestData<T, V> {
    private GenericTestData<T> tGenericTestData;
    private GenericTestData<V> vGenericTestData;
  }

  public class MultiGenericTestData<K, V> {
    private K key;
    private V value;
  }
 
  public class MultiGenericTestDataSameType<T> {
    private GenericTestData<T> genericTestData;
    private MultiGenericTestData<T, T> multiGenericTestData;
  }

I know about type erasure and other things but is there a way to identify the type T, V from the object of MultiLevelGenericTestData?

One way I thought of check generic types and look at their name and inspect all the fields until you have found all the types. This quickly becomes tricky as soon as we hit the case where there's more than one field with the same generic type, for example in MultiGenericTestDataSameType, we should get only one generic type.

 // This method should find all type's class names in the list
 // that can be used to construct the object without any issue.
void genericFieldClassNames(List<String> types, List<String> classes, Object payload)
      throws IllegalAccessException {
    for (Field field : payload.getClass().getDeclaredFields()) {
      // ignorefield without annotation
      if (!field.isAnnotationPresent(GenericField.class)) {
        continue;
      }
      Type genericType = field.getGenericType();
      // not a generic field
      if (genericType.equals(field.getType())) {
        continue;
      }
      // null value nothing can be done
      Object fieldVal = FieldUtils.readField(field, payload, true);
      if (fieldVal == null) {
        continue;
      }
      String genericFieldType = genericType.getTypeName();
      Class<?> fieldClass = fieldVal.getClass();
      // problematic cases when we start traversing up 
      if (genericFieldType.endsWith(">")) {
        genericFieldClassNames(types, classes, fieldVal);
      } else {
        // here a check can be added to avoid duplicate type name but as soon as  
        // we add type genericFieldType check it will fail when we have used similar  
        // types in construction like MultiGenericTestData<String, String>
        types.add(genericFieldType);
        classes.add(fieldClass.getName());
      }
    }
  }

The number of type parameters can be found via the method getTypeParameters, how can we combine this to get exact type information.

Example

MultiLevelGenericTestData<String, String> data;

In this case, we should get [String, String]

MultiLevelGenericTestData<String, Integer> data;

In this case, we should get [String, Integer]

MultiGenericTestData<String, String> data;

In this case, we should get [String, String]

MultiGenericTestDataSameType<String> data;

In this case, we should get [String]

This becomes even more interesting when type T itself is generic for example

MultiGenericTestDataSameType< MultiGenericTestData< String, Integer> > data;

For this data, we should get MultiGenericTestData and it's generic parameters String and Integer.

Edit:

For further clarification, I would like to get type information without creating any additional class and that should be passed to the serializer i.e I don't want to change my serializer method signature that looks something similar to this []byte serialize(Object payload). We can create as many helper classes we need, also it can be made mandatory to extend payload class from some superclass, (superclass can have logic to extract generic information).


回答1:


This is a rather long answer, but should get you into a good starting position to do what you want.

The "trick" to obtain the generic types at runtime is rather old and the most famous (I guess) modern library that uses that is gson and guava. I guess jackson uses the same trick, because there is simply no other way to do it.

To put it simply, you need a class like this to begin with:

static abstract class MappingRegistrar<IN> {

    private final Type type;

    protected MappingRegistrar() {
        // more will be here shortly
    }
 
    // ... more will come here shortly

} 

If you want to create an instance of it, you are forced to provide a class that would extend it. So you are forced to write something like:

MappingRegistrar<String> one = new MappingRegistrar<>() {};

If you are forced to provide such a superclass, the trick (in the constructor) can take place:

static abstract class MappingRegistrar<IN> {

    private final Type type;

    protected MappingRegistrar() {
        Class<?> cls = getClass();
        Type[] type = ((ParameterizedType) cls.getGenericSuperclass()).getActualTypeArguments();
        this.type = type[0];
    }

}

And now you can find out the generic types. But that's not it. You need to correctly parse them, because a Type can be actually multiple things...

static abstract class MappingRegistrar<IN> {

    private final Type type;

    protected MappingRegistrar() {
        Class<?> cls = getClass();
        Type[] type = ((ParameterizedType) cls.getGenericSuperclass()).getActualTypeArguments();
        this.type = type[0];
    }

    public void seeIt() {
        innerSeeIt(type);
    }

    private void innerSeeIt(Type type) {
        if (type instanceof Class) {
            Class<?> cls = (Class<?>) type;
            boolean isArray = cls.isArray();
            if (isArray) {
                System.out.print(cls.getComponentType().getSimpleName() + "[]");
                return;
            }
            System.out.print(cls.getSimpleName());

        }

        if (type instanceof TypeVariable) {
            Type[] bounds = ((TypeVariable<?>) type).getBounds();
            String s = Arrays.stream(bounds).map(Type::getTypeName).collect(Collectors.joining(", ", "[", "]"));
            System.out.print(s);
        }

        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            String rawType = parameterizedType.getRawType().getTypeName();
            System.out.print(rawType + "<");
            Type[] arguments = parameterizedType.getActualTypeArguments();

            for (int i = 0; i < arguments.length; ++i) {
                innerSeeIt(arguments[i]);
                if (i != arguments.length - 1) {
                    System.out.print(", ");
                }

            }

            System.out.print(">");
            //System.out.println(Arrays.toString(arguments));
        }

        if (type instanceof GenericArrayType) {
            // you need to handle this one too
        }

        if (type instanceof WildcardType) {
            // you need to handle this one too, but it isn't trivial
        }
    }

}

It's not a complete implementation, but for some examples here is what it would print:

 public class Playground2<R extends Number & Serializable> {

    public static void main(String[] args) {
        new Playground2<Integer>().samples();
    }


    public void samples() {

        MappingRegistrar<String> one = new MappingRegistrar<>() {};
        one.seeIt();
        System.out.println("\n-------------");

        MappingRegistrar<String[]> two = new MappingRegistrar<>() {};
        two.seeIt();
        System.out.println("\n-------------");

        MappingRegistrar<R> three = new MappingRegistrar<>() {};
        three.seeIt();
        System.out.println("\n-------------");

        MappingRegistrar<MultiLevelGenericTestData<String, String>> four = new MappingRegistrar<>() {};
        four.seeIt();
        System.out.println("\n-------------");

        MappingRegistrar<MultiGenericTestDataSameType<MultiGenericTestData<String, Integer>>> five = new MappingRegistrar<>() {};
        five.seeIt();
        System.out.println("\n-------------");

    }
}

The results are:

String
-------------
String[]
-------------
[java.lang.Number, java.io.Serializable]
-------------
Playground2$MultiLevelGenericTestData<String, String>
-------------
Playground2$MultiGenericTestDataSameType<Playground2$MultiGenericTestData<String, Integer>>
-------------


来源:https://stackoverflow.com/questions/64873444/generic-class-type-parameter-detail-at-runtime

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!