如何獲取泛型類的參數化類型?

關注“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框架解析數組的代碼,用到了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的實現中較爲複雜一些,因爲需要考慮接口的繼承問題,以及動態代理問題。

公衆號:Java藝術

掃碼關注最新動態

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章