编程人 cdmana.com

Android avoid pit Guide: gson made another pit!

This is a problem that my former project classmates encountered , The real code is complex , Now I'm going to describe the problem as simply as possible , And the focus will be on Prevention .

One 、 The origin of the problem

Let's start with a very simple one model class Boy:

public class Boy {

    public String boyName;
    public Girl girl;

    public class Girl {
        public String girlName;
    }
}

There are usually a lot of model class , Like every card on the interface , Are all analytical Server Returned data , And then we parse out the cards model Right .

For parsing Server data , Most of the time ,Server The return is json character string , And our client will use Gson To analyze .

Let's take a look at the above example Boy class , adopt Gson Parsed code :

public class Test01 {

    public static void main(String[] args) {
        Gson gson = new Gson();
        String boyJsonStr = "{\"boyName\":\"zhy\",\"girl\":{\"girlName\":\"lmj\"}}";
        Boy boy = gson.fromJson(boyJsonStr, Boy.class);
        System.out.println("boy name is = " + boy.boyName + " , girl name is = " + boy.girl.girlName);
    }

}

The result of the operation is ?

Let's take a look at :

boy name is = zhy , girl name is = lmj

Very normal , In line with our expectations .

Then one day , There is a classmate for girl Class has a new method getBoyName(), Want to get the name of the girl's boy , It's simple :

public class Boy {

    public String boyName;
    public Girl girl;

    public class Girl {
        public String girlName;

        public String getBoyName() {
            return boyName;
        }
    }
}

look , There's nothing wrong with the code , If you let me add on this basis getBoyName(), Maybe that's what the code says .

however , This kind of code buried a deep hole .

What kind of pit ?

Back to our test code , We're now trying to parse to complete json character string , Call girl.getBoyName():

public class Test01 {

    public static void main(String[] args) {
        Gson gson = new Gson();
        String boyJsonStr = "{\"boyName\":\"zhy\",\"girl\":{\"girlName\":\"lmj\"}}";
        Boy boy = gson.fromJson(boyJsonStr, Boy.class);
        System.out.println("boy name is = " + boy.boyName + " , girl name is = " + boy.girl.girlName);
        //  newly added 
        System.out.println(boy.girl.getBoyName());
    }

}

It's simple , Added a line to print .

This time, , What do you think of the running results ?

No problem ? Of course not. , result :

boy name is = zhy , girl name is = lmj
Exception in thread "main" java.lang.NullPointerException
	at com.example.zhanghongyang.blog01.model.Boy$Girl.getBoyName(Boy.java:12)
	at com.example.zhanghongyang.blog01.Test01.main(Test01.java:15)

Boy$Girl.getBoyName It's reported npe, yes girl by null? Obviously not , We printed on it girl.name, It's more unlikely to be boy by null 了 .

That's strange ,getBoyName It's just a line of code :

public String getBoyName() {
    return boyName; // npe
}

Who on earth is responsible for null Well ?

Two 、 The puzzling null pointer

return boyName; You can only guess that it's an object .boyName, This object is null 了 .

Who is this object ?

Let's look at it again getBoyName() The return is boy Object's boyName Field , This method is more detailed. It should be :

public String getBoyName() {
    return Boy.this.boyName;
}

therefore , Now the question is clear , Is, indeed, Boy.this This object is null.

** So here comes the question , Why go through Gson After serialization, you need to , This object is null Well ?**

I want to find out the problem , There's also a front-end problem :

stay Girl Why can we access external classes in a class Boy Properties and methods of ?

3、 ... and 、 Some secrets of non static inner classes

Explore Java The secret of code , The best way is to look at bytecode .

Let's go down and have a look Girl Bytecode , have a look getBodyName() This “ The culprit ” How do you write it ?

javap -v Girl.class

look down getBodyName() Bytecode :

public java.lang.String getBoyName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #1                  // Field this$0:Lcom/example/zhanghongyang/blog01/model/Boy;
         4: getfield      #3                  // Field com/example/zhanghongyang/blog01/model/Boy.boyName:Ljava/lang/String;
         7: areturn

You can see aload_0, Must be this Object , And then there was getfield obtain t h i s 0 word paragraph , Again through too this0 Field , Re pass this0 word paragraph , Again through too this0 Go again getfield obtain boyName Field , in other words :

public String getBoyName() {
    return boyName;
}

amount to :

public String getBoyName(){
	return $this0.boyName;
}

So this $this0 Where did it come from ?

Let's take another look Girl Member variables of bytecode of :

final com.example.zhanghongyang.blog01.model.Boy this$0;
    descriptor: Lcom/example/zhanghongyang/blog01/model/Boy;
    flags: ACC_FINAL, ACC_SYNTHETIC

One of them is this$0 Field , This is when you get confused , I don't have it in my code ?

We'll explain later .

Look at this again this$0 Where can I assign values ?

Turn over the bytecode , Find out Girl It's written like this :

public com.example.zhanghongyang.blog01.model.Boy$Girl(com.example.zhanghongyang.blog01.model.Boy);
    descriptor: (Lcom/example/zhanghongyang/blog01/model/Boy;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Lcom/example/zhanghongyang/blog01/model/Boy;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/example/zhanghongyang/blog01/model/Boy$Girl;
            0      10     1 this$0   Lcom/example/zhanghongyang/blog01/model/Boy;

You can see that this constructor contains a formal parameter , namely Boy object , Eventually this will be assigned to us $this0.

And we've got one more thing , Let's look at it as a whole Girl Bytecode :

public class com.example.zhanghongyang.blog01.model.Boy$Girl {
  public java.lang.String girlName;
  final com.example.zhanghongyang.blog01.model.Boy this$0;
  public com.example.zhanghongyang.blog01.model.Boy$Girl(com.example.zhanghongyang.blog01.model.Boy);
  public java.lang.String getBoyName();
}

It has only one construction method , That's what we said just now Boy Object constructor .

Here's a little bit of knowledge , Not all objects that don't write constructors , There will be a default parameterless construction .

in other words :

If you want to construct a normal Girl object , In theory, one has to be introduced Boy Object's .

So normally you want to build a Girl object ,Java You have to write the code like this :

public static void testGenerateGirl() {
    Boy.Girl girl = new Boy().new Girl();
}

To have a first body To have girl.

here , We've got the secret of nonstatic inner classes calling outer classes , Let's think about it Java Why do you design it like this ?

because Java Support for non static inner classes , And the inner class can access the properties and variables of the outer class , But after compiling , In fact, inner classes become independent class objects , Such as below :

01_01.png Let another class have access to members in another class , Then you have to transfer the visited object into , I think it will be able to pass in , So the only way to construct it is the most suitable one .

You can see Java Compiler to support some features , Support in silence , In fact, this kind of support is more than that , You can see it in a lot of places , And some of these variables and methods added during compilation , There will be a modifier to decorate :ACC_SYNTHETIC.

Don't believe it , Take a closer look at $this0 Statement of .

final com.example.zhanghongyang.blog01.model.Boy this$0;
descriptor: Lcom/example/zhanghongyang/blog01/model/Boy;
flags: ACC_FINAL, ACC_SYNTHETIC

Come here , We have a complete understanding of the process , Must be Gson When deserializing a string as an object, no body object , And then cause $this0 It's always been null, When we call member methods of any external class 、 Member variable is , I'll throw you a NullPointerException.

Four 、Gson How to construct a non static anonymous inner class object ?

Now I'm just curious , Because we have seen Girl There is no parameterless construction , Only one contains Boy Parameter construction method , that Girl object Gson How was it created ?

It's finding the belt Body Parameter construction method , And then the reflection newInstance, It's just Body Object passes in null?

It seems to make sense , Let's look at the code to see if it's like this :

This is actually the same as the other one I wrote before Gson The pit of the source code analysis is similar to :

Android A pit guide ,Gson And Kotlin It's an unsafe operation

I'll make a long story short :

Gson Build objects inside , One is to find the type of the object , And then find the corresponding TypeAdapter To deal with , In this case, our Girl object , Eventually it will come to ReflectiveTypeAdapterFactory.create And then return a TypeAdapter.

I can only carry it one more time :

# ReflectiveTypeAdapterFactory.create
@Override 
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
	Class<? super T> raw = type.getRawType();
	
	if (!Object.class.isAssignableFrom(raw)) {
	  return null; // it's a primitive!
	}
	
	ObjectConstructor<T> constructor = constructorConstructor.get(type);
	return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
}

The key to see constructor The assignment of this object , It's about constructing objects at a glance .

# ConstructorConstructor.get
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
    final Type type = typeToken.getType();
    final Class<? super T> rawType = typeToken.getRawType();
	
	// ... Omit some cache container related code 

    ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);
    if (defaultConstructor != null) {
      return defaultConstructor;
    }

    ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
    if (defaultImplementation != null) {
      return defaultImplementation;
    }

    // finally try unsafe
    return newUnsafeAllocator(type, rawType);
  }

You can see that the return value of this method is 3 A process :

newDefaultConstructor
newDefaultImplementationConstructor
newUnsafeAllocator

Let's look at the first one newDefaultConstructor

private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) {
    try {
      final Constructor<? super T> constructor = rawType.getDeclaredConstructor();
      if (!constructor.isAccessible()) {
        constructor.setAccessible(true);
      }
      return new ObjectConstructor<T>() {
        @SuppressWarnings("unchecked") // T is the same raw type as is requested
        @Override public T construct() {
            Object[] args = null;
            return (T) constructor.newInstance(args);
            
            //  Omitted some exception handling 
      };
    } catch (NoSuchMethodException e) {
      return null;
    }
  }

You can see , It's simple , Tried to get a parameterless constructor , If you can find , Through newInstance Building objects in a reflective way .

Follow our Girl Code for , There is no parameterless construction , And it will hit NoSuchMethodException, return null.

return null Will go newDefaultImplementationConstructor, This method contains the logic of some collection class related objects , Just skip .

that , In the end, we have to go :newUnsafeAllocator The method .

You can see from the naming , This is an unsafe operation .

newUnsafeAllocator In the end, how to build an object insecure ?

To look down , The final implementation is :

public static UnsafeAllocator create() {
// try JVM
// public class Unsafe {
//   public Object allocateInstance(Class<?> type);
// }
try {
  Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
  Field f = unsafeClass.getDeclaredField("theUnsafe");
  f.setAccessible(true);
  final Object unsafe = f.get(null);
  final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
  return new UnsafeAllocator() {
    @Override
    @SuppressWarnings("unchecked")
    public <T> T newInstance(Class<T> c) throws Exception {
      assertInstantiable(c);
      return (T) allocateInstance.invoke(unsafe, c);
    }
  };
} catch (Exception ignored) {
}
  
// try dalvikvm, post-gingerbread use ObjectStreamClass
// try dalvikvm, pre-gingerbread , ObjectInputStream

}


Um. … We're wrong about that ,Gson In fact, the interior has not found a suitable construction method , Building an object in a very insecure way .

About more UnSafe Knowledge , You can refer to :

Ask every day | Java You can still create objects like this ?

5、 ... and 、 How to avoid this problem ?

In fact, the best way , Will be Gson To do the deserialization of this model object , Try not to write nonstatic inner classes as much as possible .

stay Gson In the user's Guide for , In fact, it has been written that :

https://github.com/google/gson/blob/master/UserGuide.md#TOC-Nested-Classes-including-Inner-Classes-

01_02.png

If you want to write a non static inner class case, You have two choices to make sure it's right :

  1. Inner classes are written as static inner classes ;
  2. Customize InstanceCreator

2 The sample code for is here , But we don't recommend that you use .

Um. … therefore , I'll simplify the translation , Namely :

Don't ask , To ask is to add static

Don't use this verbal request , How can we make the students in the team consciously abide by it , Anyone who doesn't pay attention will make mistakes , So we usually meet this kind of conventional writing , The best way is to add monitoring and error correction , Not so , Compiler error .

6、 ... and 、 Let's monitor it ?

I thought about it in my head , Yes 4 One way might work .

Um. … You can also choose to think about it yourself , Then look down .

  1. The most simple 、 Most violent , When compiling , scanning model In the directory , Direct reading java Source file , Do regular matching to find non static inner classes , And then just pick a compiler task, Tied in front of it , You can run it every time you compile .
  2. Gradle Transform, Don't talk about it , scanning model Under the bag class class , Then look at the class name if it contains A B Of shape type , And structure build Fang Law in only Yes One individual Need to be want A Of structure build And become member change The amount package contain B In the form of , And only one of the construction methods needs A And the member variable contains B Of shape type , And structure build Fang Law in only Yes One individual Need to be want A Of structure build And become member change The amount package contain this0 Take down .
  3. AST perhaps lint Do syntax tree analysis ;
  4. Run time to match , It's the same , Run time to get model Object package path class object , And then do rule matching .

Okay , The above four plans are my tentative ideas , In theory, it should all work , Actually, it doesn't have to work , Welcome to try , Or come up with a new plan .

There's a new plan , Please leave a message to add some knowledge

In view of the length …

No , Actually, I haven't written any of them , I don't want to write all of them , This blog is too long .

  • programme 1, You can write it when you clap your thighs , too , But I feel 1 The most real , And it's very fast to trigger , It doesn't affect the R & D experience very much ;
  • programme 2, Let's check Transform The basic way of writing , utilize javassist, perhaps ASM, It's not a big problem , too ;
  • programme 3,AST I'm going to check my grammar too , It's hard for me to write , too ;
  • programme 4, It's the last one I came up with , Write it down .

In fact, the plan 4, If you see ARouter Initialization of earlier versions of , You'll see .

It's actually traversal dex All classes in , According to the package + Class name rules to match , And then there's the launch API 了 .

Let's write down .

Runtime , We're going to traverse the class , Is to get dex, How to get dex Well ?

Can pass apk obtain ,apk How to take it ? Actually, through cotext Can get apk route .

public class PureInnerClassDetector {
    private static final String sPackageNeedDetect = "com.example.zhanghongyang.blog01.model";

    public static void startDetect(Application context) {

        try {
            final Set<String> classNames = new HashSet<>();
            ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
            File sourceApk = new File(applicationInfo.sourceDir);
            DexFile dexfile = new DexFile(sourceApk);
            Enumeration<String> dexEntries = dexfile.entries();
            while (dexEntries.hasMoreElements()) {
                String className = dexEntries.nextElement();
                Log.d("zhy-blog", "detect " + className);
                if (className.startsWith(sPackageNeedDetect)) {
                    if (isPureInnerClass(className)) {
                        classNames.add(className);
                    }
                }
            }
            if (!classNames.isEmpty()) {
                for (String className : classNames) {
                    // crash ?
                    Log.e("zhy-blog", " Writing nonstatic inner classes was found :" + className);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static boolean isPureInnerClass(String className) {
        if (!className.contains("$")) {
            return false;
        }
        try {
            Class<?> aClass = Class.forName(className);
            Field $this0 = aClass.getDeclaredField("this$0");
            if (!$this0.isSynthetic()) {
                return false;
            }
            //  Other matching conditions 
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

}

start-up app:

01_03.png

The above is just demo Code , Not serious , We need to improve ourselves .

Just a few dozen lines of code , First, through cotext take ApplicationInfo, that apk Of path, And then build DexFile object , Just traverse the classes , Find the class , You can do the matching .

over~~

Scroll to Top