關注“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<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的類型。
如何獲取泛型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
的實現中較爲複雜一些,因爲需要考慮接口的繼承問題,以及動態代理問題。
公衆號:Java藝術
掃碼關注最新動態