The background,

The Android project wanted to encapsulate retrofit to provide public methods like GET and POST, for example:

Uniform data format returned:

{
	"errCode":0."errMsg":"ok"."data":...
}
Copy the code

The following may be the case in data:

  • Basic data type data, such as:true,1,stringEtc.
  • Json object, such as{"key1":"value"}
  • Json array, as in[{"key1":"value1"},{"key2":"value2"}]

In this case, the type of data is difficult to determine, and in traditional Retrofit, it can be used in the following ways:

interface ApiService {
  @GET
  Observable<ResponseEntity<String>> getxxx1(...)
  
  @GET
  Observable<ResponseEntity<JsonClass>> getxxx2(...)

  @GET
  Observable<ResponseEntity<List<JsonClass>> getxxx3(...)
}

class JsonClass {}
class ResponseEntity<T>{
  public int errCode;
  public String errMsg;
  public T data;
}
Copy the code

By passing in a generic class in a concrete ResponseEntity, Retrofit can do type conversions via GsonConverterFactory, but this has the following problems: ⚠️

  1. Cannot uniformly handle error codes for returned ResponseEntity

    For example, some fixed error codes are defined in our service. ErrCode =0 indicates that data is returned normally, errCode=1001 indicates that the token has expired and you need to jump to the login page for re-login. If errCode> XXX, you need to pop up a toast prompt. These are not handled uniformly and need to be evaluated in the onSuccess method of each calling interface.

  2. The entire returned data is exposed to the business, but the business only cares about the data represented by T

    For the framework, it should be OK to provide the business with the data it needs, and some low-level judgments should not be exposed to the business

  3. There are multiple ApiService for componentized projects

    In the interview process of these years, I found that some interviewees had an ApiService in each component of the componentized project, and each ApiService assumed the business interface data request of different business modules. Since Java does not support category classification (that is, the same class can be scattered into different files, IOS is a Category, similar to the extension function in Kotlin), which results in multiple Retrofit instances in your project

For these reasons, I wanted to further encapsulate Retrofit in our project to avoid these problems

First I’ll try this:

interface ApiService {
  @GET
  <T> Observable<ResponseEntity<T> get(Map<String,String> params)
    
  @POST
  <T> Observable<ResponseEntity<T>> post(Map<String,String> params)
}
Copy the code

Complains ❌ com. Google. Gson. Internal. LinkedTreeMap always be cast to XXX, investigate its main reason is that exist in the Java generics erase, This results in a GsonConverterFactory conversion where the true type of T is not found, and the ResponseEntity

can be thought of as ResponseEntity
after compilation, which leads to the subject of this article

Gson’s method of deserializing generic data

How do I convert A JSON string into a generic data structure

class ResponseData<T>{
    private int errCode;
    private String errMsg;
    private T data;
		
  	/ /... Omit the get the set
}
class Result{
    private String name;

  	/ /... Omit the get the set
}

public static void main(String[] args) {
  Gson gson = new Gson();
  String r1Str = "{\"errCode\":0,\"errMsg\":\"ok\",\"data\":{\"name\":\"result1\"}}";

  ResponseData<Result> responseData = gson.fromJson(r1Str, ResponseData.class);
  System.out.println(responseData.getData().getName());
}
Copy the code

An error ❌ : Com. Google. Gson. Internal. LinkedTreeMap always be cast to the Result, it is because there are generic erasure problem, can’t write in the code ResponseData < Result >. The class, So we don’t know what the real type of T is during the conversion. Another way:

public static void main(String[] args) {
  Gson gson = new Gson();
  String r1Str = "{\"errCode\":0,\"errMsg\":\"ok\",\"data\":{\"name\":\"result1\"}}";

	ResponseData<Result> responseData = gson.fromJson(r1Str,new TypeToken<ResponseData<Result>>(){}.getType());
  System.out.println(responseData.getData().getName());
}
Copy the code

Use TypeToken to obtain the true type of T, so how to implement TypeToken, let’s look at the source code

protected TypeToken(a) {
  this.type = getSuperclassTypeParameter(this.getClass());
  this.rawType = Types.getRawType(this.type);
  this.hashCode = this.type.hashCode();
}

static Type getSuperclassTypeParameter(Class
        subclass) {
  // getGenericSuperclass returns the Type of the immediate superclass of the entity (Class, interface, primitive Type, or void) represented by this Class.
  Type superclass = subclass.getGenericSuperclass();
  if (superclass instanceof Class) {
    throw new RuntimeException("Missing type parameter.");
  } else {
    // ParameterizedType is used to get generic signature types in metadata.
    ParameterizedType parameterized = (ParameterizedType)superclass;
    // getActualTypeArguments gets the true type arguments
    return Types.canonicalize(parameterized.getActualTypeArguments()[0]); }}public final Type getType(a) {
  return this.type;
}
Copy the code

The core uses getGenericSuperclass and ParameterizedType. In the next section, we’ll take a closer look at these API classes in reflection technology and see how they can be used to retrieve the true type of a generic.

There is one more detail to note:

🏁 new TypeToken

(){}

The TypeToken constructor is protected, indicating that the package and subclasses are available. Adding a {} extends the class, which is equivalent to:

class TypeToken$0 extends TypeToken<ResponseData<Result>>{}
TypeToken<ResponseData<Result>> sToken = new TypeToken$0(a);Copy the code

How do I get the real type of a generic by reflection?

When we reflect a generic class, we need the real data types in the generic to perform operations such as JSON deserialization. This needs to be done through the Type system. The Type interface contains an implementation Class and four implementation interfaces:

  • TypeVariable

    Generic type variables. The upper and lower limits of generics can be obtained.

  • ParameterizedType

    Generic signature type (generic real type) in metadata

  • GenericArrayType

    When you need to describe an array of generic classes, such as List[],Map[], this interface is implemented as Type.

  • WildcardType

    Wildcard generics to obtain upper and lower limits of information;

3.1, TypeVariable
 public class TestType <K extends Comparable & Serializable.V> {
    K key;
    V value;
    public static void main(String[] args) throws Exception {
			// Get the field type
			Field fk = TestType.class.getDeclaredField("key"); 
      Field fv = TestType.class.getDeclaredField("value");
      // getGenericType
			TypeVariable keyType = (TypeVariable)fk.getGenericType(); 
      TypeVariable valueType = (TypeVariable)fv.getGenericType(); 
      / / getName method
			System.out.println(keyType.getName()); 
      System.out.println(valueType.getName());
			/ / getGenericDeclaration method
      System.out.println(keyType.getGenericDeclaration());
      System.out.println(valueType.getGenericDeclaration()); 
      / / getBounds method
      System.out.println("Upper bound on K :");
			for (Type type : keyType.getBounds()) {
				System.out.println(type);
			}
			System.out.println("Upper bound of V :");
			for(Type type : valueType.getBounds()) { System.out.println(type); }}}Copy the code
3.2, ParameterizedType
 
public class TestType {
    Map<String, String> map;
		public static void main(String[] args) throws Exception {
			Field f = TestType.class.getDeclaredField("map"); 
      System.out.println(f.getGenericType()); // java.util.Map<java.lang.String,java.lang.String>
			ParameterizedType pType = (ParameterizedType) f.getGenericType(); 
      System.out.println(pType.getRawType()); // interface java.util.Map 
      for (Type type : pType.getActualTypeArguments()) {
				System.out.println(type); // Print it twice: class java.lang.string}}}Copy the code
3.3, GenericArrayType
 public class TestType<T> {
    List<String>[] lists;
		public static void main(String[] args) throws Exception {
			Field f = TestType.class.getDeclaredField("lists"); GenericArrayType genericType = (GenericArrayType) f.getGenericType(); System.out.println(genericType.getGenericComponentType()); }}Copy the code
3.4, WildcardType
public class TestType {
	private List<? extends Number> a; / / ceiling
  private List<? super String> b; / / lower limit
	public static void main(String[] args) throws Exception { 
    Field fieldA = TestType.class.getDeclaredField("a"); 
    Field fieldB = TestType.class.getDeclaredField("b"); 
    // Get the template type first
		ParameterizedType pTypeA = (ParameterizedType) fieldA.getGenericType(); 
    ParameterizedType pTypeB = (ParameterizedType) fieldB.getGenericType(); 
    // Get the wildcard type from the template
		WildcardType wTypeA = (WildcardType) pTypeA.getActualTypeArguments()[0];
    WildcardType wTypeB = (WildcardType) pTypeB.getActualTypeArguments()[0]; 
    // Method test
		System.out.println(wTypeA.getUpperBounds()[0]); // class java.lang.Number
    System.out.println(wTypeB.getLowerBounds()[0]); // class java.lang.String 
    // What is the wildcard type? extends java.lang.NumberSystem.out.println(wTypeA); }}Copy the code

As you can see from the above four classes, to get the actual type of a generic type, ParameterizedType is required, which is obtained from getActualTypeArguments.

Now that we know about these apis, how do we implement a method that gets generic types ourselves, as we’ll see in the next section

4. Customize TypeToken

public class MyTypeToken<T> {
  final Type type;
  protected MyTypeToken(a) {
    this.type = getSuperclassTypeParameter(this.getClass());
  }

  static Type getSuperclassTypeParameter(Class
        subclass) {
    Type superclass = subclass.getGenericSuperclass();
    if (superclass instanceof Class) {
      throw new RuntimeException("Missing type parameter.");
    } else {
      ParameterizedType parameterized = (ParameterizedType)superclass;
      return parameterized.getActualTypeArguments()[0]; }}public Type getType(a) {
    returntype; }}Copy the code

Usage:

String r1Str = "{\"errCode\":0,\"errMsg\":\"ok\",\"data\":{\"name\":\"result1\"}}";
System.out.println(r1Str);
ResponseData<Result> responseData = gson.fromJson(r1Str,new MyTypeToken<ResponseData<Result>>(){}.getType());
System.out.println(responseData.getData().getName());
Copy the code

⚠️new MyTypeToken

(){} *{}**.