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

原创 吴就业 78 0 2020-05-18

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/a7ddc8bc093f4d90bc6998f4b0f4dac6

作者:吴就业
链接:https://wujiuye.com/article/a7ddc8bc093f4d90bc6998f4b0f4dac6
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

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

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

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

新版本的OnionShardingJobHandler接口定义如下。

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

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

关于泛型

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

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

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

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

对于泛型方法,如:

public <T> T createT();

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

public Object createT();

如果使用类型界定,如:

public <T extends com.wujiuye.Job> T createT();

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

JVM在执行字节码指令时并不关心参数T的实际类型是什么,只使用擦除后的类型。Signature属性是用于调试和反射以及将class文件反编译为Java代码时使用的。

那么,我们如何通过反射获取一个泛型类的参数化类型T的实际类型呢?

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

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

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

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

而如果是:

public class JobTypeReference extends TypeReference<com.wujiuye.Job>{
}

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

Lcom/wujiuye/TypeReference<Lcom/wujiuye/Job;>;

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

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

public class 匿名 extends TypeReference<List<Job>>{
    
}

因此jackson能够拿到该对象的泛型签名为:Lcom/wujiuye/TypeReference<Ljava/util/List<Lcom/wujiuye/Job;>>

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

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

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

public abstract class TypeReference<T> {
    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的实际类型,步骤如下:

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

List<String>返回[”Ljava/lang/String;”];

Map<String,Object>返回[”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<T>) type;

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

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Spring动态代理奇怪的空指针异常,字段明明不为空,但方法中获取字段的值确是空的

使用`cglib`生成的代理对象 (继承方式),在父类中,通过代理对象调用父类私有方法不会报错,但字段都是空的。

Java反序列化JSON,要避免泛型的类型擦除问题

如图,反序列化JSON数组正常,却在获取数组元素时抛出了类型转换异常。

基准测试框架JMH快速上手

基准测试Benchmark是测量、评估软件性能指标的一种测试,对某个特定目标场景的某项性能指标进行定量的和可对比的测试。

这又是导致事务注解@Transactional不生效的一个原因

事务方法`A`调用事务方法`B`,当方法`B`抛出的异常被方法`A` `catch`后会发生什么?

在同一个线程下数据源多次切换的回溯问题

在某些场景下,我们可能需要多次切换数据源才能处理完同一个请求,也就是在一个线程上多次切换数据源。

深入理解类加载阶段之准备阶段

准备阶段是为类中定义的静态变量分配内存并设置初始化值的阶段,这里的初始值通常情况下指的是对应类型的零值,比如int类型的零值为0。