如何获取泛型类的参数化类型?

关注“Java艺术”一起来充电吧!

我在基于XXL-JOB进行二次开发的XXL-JOB-ONION分布式定时任务调度系统项目中,添加了一个ONION_BEAN的运行模式,约定定时任务必须通过实现OnionShardingJobHandler接口开发。

@FunctionalInterface
public interface OnionShardingJobHandler {
    void doExecute(int shardingTotal, int currentShardingIndex, String param) throws Exception;
}

参数使用String传递,因此在编写每个Job时,都需要写一行将String解析为Java对象的代码,因此我想把这个重复的步骤去掉,让接口支持泛型,参数支持泛型,让框架自动解析。新版本的OnionShardingJobHandler接口如下。

@FunctionalInterface
public interface OnionShardingJobHandler {
    void doExecute(int shardingTotal, int currentShardingIndex, T param) throws Exception;
}

那么问题来了,框架怎么知道这个T到底是什么类型呢?


关于泛型

熟悉class文件结构以及字节码的朋友应该都知道,Java泛型是通过"类型擦除"实现的,在编译期由编译器将泛型擦除,泛型类擦除后就是对应类型的裸类型。如List,类型擦除后为裸类型List

泛型支持类型界定,即限定T是某个类的子类,使用extends关键字实现。如List,那么就是限定T只能是Job类或其子类,List只能存储Job类或子类的实例。

编译后,泛型信息存储在class文件结构对应项的属性表中,使用Signature属性存储。每个类、字段、方法至多可以有一个Signature属性。

如泛型类的类型签名,编译后存储在该类的class文件结构的属性表的Signature属性中;泛型字段的类型签名,编译后存储在该字段结构的属性表的Signature属性中;泛型方法的方法签名,编译后存储在该方法结构的属性表的Signature属性中。

对于泛型方法,如

public  T createT();

编译后该方法的方法描述符为()Ljava/lang/Object;,方法变为

public Object createT();

如果使用类型界定,如

public  T createT();

那么编译后该方法的方法描述为()Lcom/wujiuye/Job;

JVM在执行字节码指令时并不关心参数T的实际类型是什么,只使用擦除后的类型。Signature属性是用于调试和反射以及将class文件反编译为Java代码时使用的。那么,我们如何通过反射获取一个泛型类的参数化类型T的实际类型呢?

为什么通过反射能够获取到泛型T的实际类型

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.readValue(jsonStr, new TypeReference>() {});

这段代码熟悉吗?这是使用jackson框架解析数组的代码,用到了TypeReferenceTypeReference的作用就是能够让jackson获取到泛型List的参数类型,而不需要传递一个Classjackson最终通过反射拿到T的实际类型。那为什么需要传一个TypeReference对象呢?

对于类TypeReference,类型擦除后为TypeReferenceSignature属性保持的类型签名为Ljava/lang/Object;,因此我们无法通过反射获取到T代表的是什么。

而如果是:

public class JobTypeReference extends TypeReference{
}

编译后JobTypeReference类的泛型签名为:

Lcom/wujiuye/TypeReference;

这样我们就可以从类型签名中拿到参数T的实际类型为Job

在使用jackson解析数组的例子中,调用ObjectMapperreadValue时,传递的new TypeReference>() {}对象是一个匿名内部类,编译器会为这句代码生成一个内部类,相当于生成了一个这样的类:

public class 匿名 extends TypeReference>{
    
}

因此jackson能够能到该对象的泛型签名为:Lcom/wujiuye/TypeReference>

也就能获取到泛型List的参数T的类型。


如何获取泛型T的实际类型

jackson框架的TypeReference类为例,TypeReference的源码如下(为了便于读者理解,我简化了):

public abstract class TypeReference {
    protected final Type _type;

    protected TypeReference() {
        Type superClass = this.getClass().getGenericSuperclass();
        if (superClass instanceof Class) {
            throw new IllegalArgumentException("Internal error: TypeReference constructed without actual type information");
        } else {
            this._type = ((ParameterizedType)superClass).getActualTypeArguments()[0];
        }
    }

    public Type getType() {
        return this._type;
    }
    
}

TypeReference的构造方法中使用了反射获取T的实际类型,步骤如下:

  • 1、调用this.getClass()方法获取当前对象的实际类型;

  • 2、调用Class实例的getGenericSuperclass方法获取泛型父类;

  • 3、最后调用TypegetActualTypeArguments方法获取泛型父类的参数实际类型;

泛型也叫参数化类型ParameterizedType,以参数的形式给出,参数可以有多个,因此getActualTypeArguments方法返回的是一个数组。

List返回["Ljava/lang/String;"];

Map返回["Ljava/lang/String;","Ljava/lang/Object;"]。

Typejava.lang.reflect.TypeClass也实现该接口。Type接口的定义如下:

public interface Type {

    default String getTypeName() {
        return toString();
    }
    
}

因此拿到Type我们只能调用getTypeName获取到类型的名称。除非知道Type的具体类型,或者Type就是Class。想要了解的朋友可以查看jackson的源码。其实拿到类型名称之后,我们也可以通过调用Class.forName方法获取Class对象。


扩展

如果TypeReference是一个接口呢?

Type[] implInterfaces = this.onionShardingJobHandler.getClass().getGenericInterfaces();

因为一个类可以实现多个接口,所以getGenericInterfaces返回的是一个数组。

demo如下:

    Type[] implInterfaces = this.getClass().getGenericInterfaces();
    // 解决cglib动态代理问题
    if (this.getClass().getName().contains("CGLIB")) {
        implInterfaces = this.getClass().getSuperclass().getGenericInterfaces();
    }
    Type jobType = implInterfaces[0]
    // 获取泛型接口的泛型参数
    Type type = ((ParameterizedType) jobType).getActualTypeArguments()[0];
    if (!(type instanceof Class)) {
        throw new RuntimeException("Missing type parameter.");
    }
    jobParamClass = (Class) type;

这个demo相对简单,在XXL-JOB-ONION的实现中较为复杂一些,因为需要考虑接口的继承问题,以及动态代理问题。

公众号:Java艺术

扫码关注最新动态

你可能感兴趣的:(如何获取泛型类的参数化类型?)