我在基于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
框架解析数组的代码,用到了TypeReference
。TypeReference
的作用就是能够让jackson
获取到泛型List<T>
的参数类型,而不需要传递一个Class<T>
。jackson
最终通过反射拿到T
的实际类型。那为什么需要传个TypeReference
对象呢?
对于类TypeReference<T>
,类型擦除后为TypeReference
,Signature
属性保持的类型签名为<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
解析数组的例子中,调用ObjectMapper
的readValue
时,传递的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
的实际类型,步骤如下:
- 1、调用
this.getClass()
方法获取当前对象的实际类型; - 2、调用
Class
实例的getGenericSuperclass
方法获取泛型父类; - 3、最后调用
Type
的getActualTypeArguments
方法获取泛型父类的参数实际类型;
泛型也叫参数化类型ParameterizedType
,以参数的形式给出,参数可以有多个,因此getActualTypeArguments
方法返回的是一个数组。
如List<String>
返回[”Ljava/lang/String;
”];
如Map<String,Object>
返回[”Ljava/lang/String;
”,”Ljava/lang/Object;
”]。
Type
是java.lang.reflect.Type
,Class
也实现该接口。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
的实现中较为复杂一些,因为需要考虑接口的继承问题,以及动态代理问题。