This is a problem that my former classmates encountered. Real code is quite complex. Now I will describe this problem as simply as possible, and focus on exploring why this situation occurs and how to monitor it.

First, the origin of the problem

Let’s start with a very simple model class Boy:

public class Boy { public String boyName; public Girl girl; public class Girl { public String girlName; }}Copy the code

There are a lot of model classes in a project, so for example, every card on the interface, it parses the data returned by the Server, and then parses the card model.

For parsing Server data, in most cases, the Server returns a JSON string, which we clients parse using Gson.

Let’s look at the above example Boy class, which is parsed by Gson:

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); }}Copy the code

What’s the result?

Let’s take a look:

boy name is = zhy , girl name is = lmj
Copy the code

It’s perfectly normal. It’s what we expected.

One day, a student added a method getBoyName() to the girl class. To get the name of the boy the girl wants, it was simple:

public class Boy { public String boyName; public Girl girl; public class Girl { public String girlName; public String getBoyName() { return boyName; }}}Copy the code

It looks like the code is fine. If you ask me to add getBoyName() on top of this, the code probably does the same thing.

However, such code buries deep holes.

What kind of pit?

Going back to our test code, let’s now try to parse the finished JSON string by calling 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); / / the new System. Out. Println (boy. Girl. GetBoyName ()); }}Copy the code

It’s easy. I added a line to print.

This time, what do you think it’s going to look like?

Or no problem? Of course not. As a 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)

Copy the code

Boy$girl. getBoyName = nPE; Obviously not, we printed the girl.name above, so it is not possible that boy is null.

That’s weird. GetBoyName has just one line of code:

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

Who is null?

Confusing null Pointers

return boyName; We can only guess that it is an object. BoyName, this object is null.

Who is this object?

GetBoyName () returns the boyName field of the boy object.

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

So, now it’s clear that Boy. This is null.

** Why should this object be null after the Gson serialization? 民运分子

To understand this question, there is a preemptive question:

Why do we have access to the properties and methods of the external class Boy in the Girl class?

Some secrets of non-static inner classes

The best way to explore the secrets of Java code is to look at bytecode.

Let’s take a look at the bytecode for Girl and see what the culprit getBodyName() is.

javap -v Girl.class
Copy the code

Take a look at the bytecode for getBodyName() :

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

Copy the code

Can see aload_0, affirmation is to this object, and then the getfield get this0 fields, through this0 fields, through this0 fields, then go through this0 getfield get boyName fields, that is to say:

public String getBoyName() {
    return boyName;
}
Copy the code

Is equivalent to:

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

So where did this $this0 come from?

Let’s take a look at the Girl bytecode member variable:

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

“This $0” = “this$0”;

We’ll explain later.

Now, where can I assign this$0?

Flipping through the bytecode, the Girl constructor reads:

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;Copy the code

You can see that this constructor contains a parameter, the Boy object, which will eventually be assigned to our $this0.

And here’s the next thing we’ve posted, let’s take a look at the Girl’s bytecode in general:

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();
}
Copy the code

It has only one constructor, which is the one we just said we need to pass in to the Boy object.

A little bit of knowledge here is that not all objects that have no constructors will have a default constructor with no arguments.

In other words:

If you want to construct a normal Girl object, you theoretically have to pass in a Boy object.

So normally if you want to build a Girl object, you would write Java code like this:

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

You can’t have a girl without a body.

Now that we know the secret of a non-static inner class calling an outer class, let’s think about why Java is designed this way.

Because Java supports non-static inner classes, and the inner class can access the properties and variables of the outer class, after compilation, the inner class actually becomes a separate class object, such as the following:

As you can see, the Java compiler provides implicit support for some of these features, which can be seen in many places, and some of the new variables and methods added during compilation have a modifier to modify them: ACC_SYNTHETIC.

Take a closer look at $this0’s statement.

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

At this point, we are fully aware of the process. It must be that Gson did not pass in the body object when deserializing the string as an object, thus causing $this0 to always be null. When we call any external class member method, member variable is, I’m gonna throw you a NullPointerException.

How does Gson construct non-static anonymous inner class objects?

Now I’m just curious, since we’ve already seen that there is no no-argument constructor for Girl, only a constructor that takes a parameter Boy, so how is the Girl object Gson created?

Is it to find the constructor with the Body argument and reflect newInstance, except that the Body object is passed null?

Let’s take a look at the code and see if this is true:

This is actually similar to another Gson pit source analysis I wrote earlier:

In the Android Pit Avoidance guide, Gson and Kotlin collided into an unsafe operation

I’ll make this short:

Gson to build objects, one is by finding the type of the object, and then find the corresponding TypeAdapter to deal with, in this case our Girl objects, will walk to ReflectiveTypeAdapterFactory. Create and return a TypeAdapter.

I can only carry 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)); }Copy the code

Focus on the assignment of an object called constructor, which is immediately associated with constructing an object.

# ConstructorConstructor.get public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) { final Type type = typeToken.getType(); final Class<? super T> rawType = typeToken.getRawType(); / /... 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); }Copy the code

You can see that the return value of this method has three processes:

newDefaultConstructor
newDefaultImplementationConstructor
newUnsafeAllocator
Copy the code

Let’s look at our first 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); // Omit some exception handling}; } catch (NoSuchMethodException e) { return null; }}Copy the code

As you can see, it’s easy to try to get a constructor with no arguments, and if you can find one, build the object by newInstance reflection.

The code that follows our Girl does not have a no-argument construct, thus hitting NoSuchMethodException and returning null.

Returns null newDefaultImplementationConstructor will go, this method there are some collections of related objects logic, skip.

Then, we have to go to the newUnsafeAllocator method.

As you can see from the name above, this is an unsafe operation.

How does newUnsafeAllocator end up building an object unsafely?

If you look below, 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 }Copy the code

B: well… We guessed wrong. Gson actually builds an object internally in a very unsafe way after failing to find the constructor it thinks is appropriate.

For more information on UnSafe, see:

Daily asking | in Java can create objects so much?

How to avoid this problem?

In fact, the best approach, which is to deserialize the Model object by Gson, is to try not to write non-static inner classes.

In the Gson user guide, it actually says:

Github.com/google/gson…

If you have a case to write a non-static inner class, you have two choices to make sure it is correct:

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

Sample code for 2 is here, but we don’t recommend you use it.

B: well… So, my simplified translation is:

Don’t ask, ask is static

Do not use this verbal requirements, how can we make the team of students consciously comply with it, who do not pay attention to will write wrong, so generally encounter this kind of contractual writing, the best way is to add monitoring error correction, do not write so, compiling error.

Six. Why don’t we monitor it?

I thought in my head, there are four ways this might work.

B: well… You can also choose to think about it and look down.

  1. At compile time, scan the directory where the Model is located, read the Java source file directly, do regular matching to find the non-static inner class, and then pick any compile time task, tie it in front of it, and run it every compile time.
  2. Gradle Transform, scan the class under the model package, and look at the class name if it contains the form A and B, if there is only one constructor that requires the form A and the member variable contains the form B, if there is only one constructor that requires the form A and the member variable contains the form B, if there is only one constructor that requires the form A and the member variable contains the form B, And only one of the constructors requires the construction of A and the member variable contains this0.
  3. AST or Lint do syntax tree analysis;
  4. The runtime matches, same thing, the runtime gets all the class objects in the package path of the Model object, and then does the rule match.

All right, the above four schemes are my temporary thoughts. They should all be feasible in theory, but not necessarily feasible in practice. Welcome to try, or put forward new schemes.

There is a new scheme, please leave a message to supplement the next aspect of knowledge

Given the space…

No, actually I haven’t written any of them. I don’t want to write all of them. It would be too long for my blog.

  • Plan 1, we clap thighs can write out, too, but I feel 1 the most real, and trigger speed is very fast, do not affect the development experience;
  • Solution 2, you look up the basic writing method of Transform, using Javassist, or ASM, it is estimated that the problem is not too big, pass;
  • Plan 3, I also have to look up the AST grammar, I also have trouble writing, too;
  • Plan four, which was the last one I came up with, let me write it down.

In fact, scheme 4, if you see the initialization of an earlier version of ARouter, you can see it.

In fact, it is to iterate over all the classes in dex, according to the package + class name rule to match, and then it is to emit the API.

Let’s write it down.

At run time, we’re going to go through the class, we’re going to get dex, how do we get dex?

You can get it through APK. How do you get APK? You can actually get the APK path by cotext.

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 a non-static inner class 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; }}}Copy the code

Start the app:

The above is only demo code, not rigorous, need to improve.

A few dozen lines of code, first through cotext take ApplicationInfo, then apK path, and then build a DexFile object, iterate through the class, find the class, you can do the match.

over~~