我在基于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<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的类型。


如何获取泛型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的实际类型,步骤如下:

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

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

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


泛型也叫参数化类型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的实现中较为复杂一些,因为需要考虑接口的继承问题,以及动态代理问题。

https://mp.weixin.qq.com/s/rXbiZl2_98wQC7KrxYdpsg