Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.
Reflect your first acquaintance
Reflection contains the word “negative”, so we start with “positive” to understand reflection. Typically, when we use a class, we know exactly what it is, and then we instantiate it directly.
Apple apple = new Apple(); // Direct initialization, "ortho"
apple.setPrice(3);
Copy the code
Reflection doesn’t know what class object I’m initializing at first, so I can’t use the new keyword to create the object. At this point, we use the reflection API provided by the JDK to make reflection calls.
Reflection is when you know what class to operate on at run time, and you can get the complete construct of the class at run time and call the corresponding method.
Class clz = Class.forName("com.test.reflect.Apple");
Method method = clz.getMethod("setPrice".int.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, 3);
Copy the code
The above two pieces of code execute with the same result, but with completely different ideas:
The first piece of code identifies the class to run before it is run: Apple;
The second code knows the class to run from the string value at run time: com.test.reflect.apple.
That’s reflection.
Basic use and common API of reflection
2.1 Basic Usage
public class Apple {
private int price;
public int getPrice(a) {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public static void main(String[] args) throws Exception{
// Normal call
Apple apple = new Apple();
apple.setPrice(3);
System.out.println("Apple Price:" + apple.getPrice());
// Use reflection calls
Class clz = Class.forName("com.test.api.Apple");
Method setPriceMethod = clz.getMethod("setPrice".int.class);
Constructor appleConstructor = clz.getConstructor();
Object appleObj = appleConstructor.newInstance();
setPriceMethod.invoke(appleObj, 3);
Method getPriceMethod = clz.getMethod("getPrice");
System.out.println("Apple Price:"+ getPriceMethod.invoke(appleObj)); }}Copy the code
You can see from the code that we called the setPrice method using reflection and passed the value of 14. The getPrice method is then called using reflection to print its price. The entire output of the above code is:
Apple Price:3
Apple Price:3
Copy the code
2.2 Reflection steps for obtaining an object
As you can see from the simple example above, in general we use reflection to get an object:
1: Gets the Class object instance of the Class
Class clz = Class.forName("com.zhenai.api.Apple");
Copy the code
2: Get the Constructor object from the Class object instance
Constructor appleConstructor = clz.getConstructor();
Copy the code
3: Get the reflection class object using the newInstance method of the Constructor object
Object appleObj = appleConstructor.newInstance();
Copy the code
4: To call a method, you need to go through the following steps:
Get the Method object of the Method:
Method setPriceMethod = clz.getMethod("setPrice", int.class);
Copy the code
Using the invoke method to call a method:
setPriceMethod.invoke(appleObj, 14);
Copy the code
2.3 Common APIS for reflection
2.3.1 Getting the Class object in reflection
In reflection, to get a Class or call a method of a Class, we first need to get the Class object of that Class.
In the Java API, there are three ways to get Class objects:
** First, use the class.forname static method. ** You can use this method to get Class objects when you know the full pathname of the Class.
Class clz = Class.forName("java.lang.String");
Copy the code
Second, use the.class method.
This approach is only suitable for classes where the operation is known before compilation.
Class clz = String.class;
Copy the code
Third, use the getClass() method of the class object.
String str = new String("Hello");
Class clz = str.getClass();
Copy the code
2.3.2 Creating class Objects through Reflection
There are two main ways to create Class objects through reflection: through the newInstance() method of a Class object and through the newInstance() method of a Constructor object.
The first is through newInstance() on Class objects.
Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();
Copy the code
Second: newInstance() via the Constructor object
Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();
Copy the code
Class objects created from a Constructor object can select a specific Constructor, whereas only the default no-argument Constructor can be used from a Class object. The following code calls a constructor with arguments to initialize the class object.
Class clz = Apple.class; Constructor constructor = clz.getConstructor(String.class, int.class); Apple = (Apple)constructor. NewInstance (" red Fuji ", 15);Copy the code
2.3.3 Obtaining Class Attributes, methods and Constructors through Reflection (preliminary)
We can get Class attributes through the getFields() method of the Class object, but we can’t get private attributes.
Class clz = Apple.class;
Field[] fields = clz.getFields();
for (Field field : fields) {
System.out.println(field.getName());
}
Copy the code
The output is:
price
Copy the code
Instead, use the Class object’s getDeclaredFields() method to get all attributes, including private attributes:
Class clz = Apple.class;
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}
Copy the code
The output is:
name
price
Copy the code
Just like getting a class attribute, if we want to get a private method or constructor, we must use methods with the declared keyword.
Three reflections summary
3.1 summary
What is reflection:
From the above example, we can summarize: reflection is the ability to know what class to operate on at run time, and to get the complete construction of the class at run time and call the corresponding method.
Reflection is the key to Java being seen as a dynamic language. Reflection allows programs to use the Reflection API to retrieve the internal information of any class during execution, and to directly manipulate the internal properties and methods of any object.
What does reflection provide:
Objects that construct any class at run time get the member variables and methods that any class has at run time call the methods/properties of any object at run time
Why reflection is slow:
Reflection is not slow, and in some cases can be almost as efficient as direct calls, but there is still a performance overhead for first execution or for no cache, mainly in the following areas
Class.forname () calls the local method, and all the methods and fields we use are loaded at this point. Although this method is cached, the local method has to be converted from JAVA to C++ to JAVA. Native and Java versions of MethodAccessor). Class.getmethod (), iterates through all the common methods of the class, and all the methods of the parent class if there is no match, and getMethods() returns a copy of the result, so this operation consumes both CPU and heap memory and should be avoided in code or cached. The Invoke argument is an Object array, and Object arrays do not support Java base types, and automatic boxing is time-consuming.
3.2 Advanced operations of class loaders, constructors, methods and fields in reflection
The following examples further illustrate how reflection operates on classloaders, constructors, methods, and fields.
First define a custom object Person:
public class Person {
String name;
private int age;
public String getName(a) {
return name;
}
public void setName(String name) {
this.name = name;
System.out.println("this is setName()!");
}
public int getAge(a) {
return age;
}
public void setAge(int age) {
this.age = age;
System.out.println("this is setAge()!");
}
// Contains a constructor with one parameter and a constructor without
public Person(String name, int age) {
super(a);this.name = name;
this.age = age;
}
public Person(a) {
super(a); }// Private methods
private void privateMethod(a){
System.out.println("this is private method!"); }}Copy the code
3.2.1 Operations on the class loader:
public class TestClassLoader {
/* Classloader related */
public static void testClassLoader(a) throws ClassNotFoundException,
FileNotFoundException {
//1. Obtain a system class loader (system class loader)(can obtain, the current class PeflectTest is loaded by it)
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
System.out.println(classLoader);
//2. Get the parent of the system class loader (extension class loader, available).
classLoader = classLoader.getParent();
System.out.println(classLoader);
//3. Get the parent of the extension class loader (boot class loader, not available).
classLoader = classLoader.getParent();
System.out.println(classLoader);
//4. Test which class loader is used to load the current class (system class loader) :
classLoader = Class.forName("cn.enjoyedu.refle.more.ReflectionTest")
.getClassLoader();
System.out.println(classLoader);
//5. Test which class loader is responsible for loading the Object class provided by the JDK.
classLoader = Class.forName("java.lang.Object") .getClassLoader(); System.out.println(classLoader); }}public class ReflectionTest {
public static void main(String[] args) throws Exception { TestClassLoader.testClassLoader(); }}Copy the code
Running results:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@61bbe9ba
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null
Copy the code
3.2.2 Operations on constructors:
public class TestConstructor {
/* Constructor related */
public void testConstructor(a) throws Exception{
String className = "cn.enjoyedu.refle.more.Person";
Class<Person> clazz = (Class<Person>) Class.forName(className);
System.out.println("Get all Constructor objects -----");
Constructor<Person>[] constructors
= (Constructor<Person>[]) clazz.getConstructors();
for(Constructor<Person> constructor: constructors){
System.out.println(constructor);
}
System.out.println("To get a Constructor object, need a list of arguments ----");
Constructor<Person> constructor
= clazz.getConstructor(String.class, int.class);
System.out.println(constructor);
//2. Call the constructor's newInstance() method to create an object
System.out.println("Call the constructor's newInstance() method to create an object -----");
Person obj = constructor.newInstance("Lucas".25); System.out.println(obj.getName()); }}public class ReflectionTest {
public static void main(String[] args) throws Exception {
newTestConstructor().testConstructor(); }}Copy the code
Operation results:
To get all the Constructor object -- -- -- -- - public cn. Enjoyedu. Refle... more. Person (Java. Lang. String, int) public cn. Enjoyedu. Refle. More. The Person () Get a Constructor object, Need parameter list - public cn. Enjoyedu. Refle. More. The Person (Java. Lang. String, int) call the constructor newInstance () method to create objects -- -- -- -- -- LucasCopy the code
3.2.3 Operation of Method and Field:
public class TestMethod {
/* Method related */
public void testMethod(a) throws Exception{
Class clazz = Class.forName("cn.enjoyedu.refle.more.Person");
System.out.println("Get all methods in clazz's corresponding class," +
"Can't get private methods, and gets all methods inherited from their parent class.");
Method[] methods = clazz.getMethods();
for(Method method:methods){
System.out.print(""+method.getName()+"()");
}
System.out.println("");
System.out.println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
System.out.println("Get all methods, including private methods," +
"All declared methods are available, and only methods of the current class are available.");
methods = clazz.getDeclaredMethods();
for(Method method:methods){
System.out.print(""+method.getName()+"()");
}
System.out.println("");
System.out.println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
System.out.println("Gets the specified method," +
"Parameter name and parameter list required, no need to write if no parameter");
Public void setName(String name) {}
Method method = clazz.getDeclaredMethod("setName", String.class);
System.out.println(method);
System.out.println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
Public void setAge(int age) {}
Public void setAge(Integer age) {} Or int. Class */
method = clazz.getDeclaredMethod("setAge".int.class);
System.out.println(method);
System.out.println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
System.out.println("Execute method. The first parameter indicates which object's method to execute." +
", the remaining arguments are the ones passed in when the method is executed.");
Object obje = clazz.newInstance();
method.invoke(obje,18);
/* To execute a private method, invoke must be preceded by method.setaccessible (true); * /
method = clazz.getDeclaredMethod("privateMethod");
System.out.println(method);
System.out.println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
System.out.println("Execute private methods");
method.setAccessible(true); method.invoke(obje); }}public class ReflectionTest {
public static void main(String[] args) throws Exception {
newTestMethod().testMethod(); }}Copy the code
Running results:
Get all methods in clazz's corresponding class, GetName () setName() getAge() setAge() wait() wait() wait() equals() toString() hashCode() getName() setName() getAge() setAge() wait() wait() GetClass () notify notifyAll () () -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- to get all the methods, including private method, all the method statement, can get, And only for the current class method getName () elegantly-named setName () getAge () setAge () privateMethod () -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- get the specified method, need parameter name and argument list, No arguments do not need to write a public void cn. Enjoyedu. Refle. More. Person. The elegantly-named setName (Java. Lang. String) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the public void Cn. Enjoyedu. Refle. More. Person. SetAge (int) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- execution method, the first parameter to the execution of which object method This is setAge()! Private void cn. Enjoyedu. Refle. More. Person. PrivateMethod () -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- to perform a private method this is private method!Copy the code
3.2.4 Operation on Field:
public class TestField {
/* Field related */
public void testField(a) throws Exception{
String className = "cn.enjoyedu.refle.more.Person";
Class clazz = Class.forName(className);
System.out.println("Get all public and private fields, but not superclass fields.");
Field[] fields = clazz.getDeclaredFields();
for(Field field: fields){
System.out.print(""+ field.getName());
}
System.out.println();
System.out.println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
System.out.println("Get specified field");
Field field = clazz.getDeclaredField("name");
System.out.println(field.getName());
System.out.println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
Person person = new Person("ABC".12);
System.out.println("Get the value of the specified field");
Object val = field.get(person);
System.out.println(field.getName()+"="+val);
System.out.println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
System.out.println("Set the value of the specified field of the specified object");
field.set(person,"DEF");
System.out.println(field.getName()+"="+person.getName());
System.out.println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
System.out.println("Fields are private, whether they are read or written," +
"You must first call setAccessible (true).");
For example, in the Person class, the name field is non-private and the age field is private
field = clazz.getDeclaredField("age");
field.setAccessible(true); System.out.println(field.get(person)); }}public class ReflectionTest {
public static void main(String[] args) throws Exception {
newTestField().testField(); }}Copy the code
Running results:
Get all public and private fields, But can't get the superclass field name age -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- for specified field name -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- to get the value of the specified field name = ABC -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- set the value of the specified object specified name = DEF -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- field is private, whether read or write values, Both must first call setAccessible (true) method 12Copy the code
Four reflection advanced in-depth analysis
4.1 Origin and entry of Java reflection: the Class Class
4.1.1 Introduction to the Class
Java is an object-oriented language. Every Class we write can be regarded as an object, which is the object of java.lang.Class. When a Java file is compiled into a class file, the compiler places the corresponding class object at the end of the class file. All contain metadata information about the class. What does metadata information for a class include? Properties, methods, constructors, interfaces implemented, etc., are represented by classes in Java. This is the Class Class!
A Class is a Class that describes a Class, encapsulating information about the Class of the current object.
A Class that has properties, methods, constructors, etc., a Person Class, an Order Class, a Book Class, these are all different classes, and now we need a Class that describes the Class, and that’s Class. The Class Class inherits from the Object Class and is used to obtain all kinds of information related to the Class. It also provides methods to obtain the information related to the Class. The corresponding methods can be used to obtain the corresponding information: the name, attributes, methods, constructors, parent classes, and interfaces of the Class.
For each Class, the JRE reserves an invariant object of type Class for it. A Class object contains information about a particular Class.
Object objects can only be created by the system, which are automatically constructed by the Java virtual machine when the class is loaded and by calling the defineClass method in the class loader.
A Class (rather than an object) will have only one Class instance in the JVM
There are three ways to get a Class object:
1. Obtain the class name from the class name. Class
2. Get the object name from the object.getClass ()
3. Obtain Class. ForName (Class name) from Class name
Common methods of the Class Class
4.1.2 Java class loading process
4.2 Reflection source code analysis
Now that we know how to use reflection, today we’ll take a look at how reflection is implemented in JDK source code. You may not normally use reflection, but you may encounter the following exceptions when developing Web projects:
java.lang.NullPointerException
...
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
Copy the code
You can see that the exception stack indicates the exception in Method 497’s Invoke Method. The invoke Method we refer to here is the invoke Method in our reflection calling Method.
Method method = clz.getMethod("setPrice".int.class);
method.invoke(object, 4); // The invoke method here
Copy the code
For example, in the Spring configuration we often use, we often have the configuration of the related Bean:
<bean class="com.chenshuyi.Apple">
</bean>
Copy the code
After we configure the above configuration in the XML file, Spring uses reflection to load the corresponding Apple class at startup. When an Apple class does not exist or an enlightened exception occurs, the exception stack points the exception to the invoked method.
As you can see, many frameworks use reflection, the last of which is the Invoke Method of the Method class.
Let’s take a look at what the JDK’s Invoke method does.
Entering the Method’s invoke Method, we can see that we start with some permission checks and end with a call to the Invoke Method of the MethodAccessor class for further processing, as shown in the red box below.
So what is a MethodAccessor?
MethodAccessor is an interface that defines the operation of a method call, and it has three concrete implementation classes:
- sun.reflect.DelegatingMethodAccessorImpl
- sun.reflect.MethodAccessorImpl
- sun.reflect.NativeMethodAccessorImpl
To see which class invoke method is invoked by Ma.invoke (), we need to see which class object is returned by the MethodAccessor object, so we need to go into the acquireMethodAccessor() method.
As you can see from the acquireMethodAccessor() method, the code checks to see if a corresponding MethodAccessor object exists. If so, it reuses the previous MethodAccessor object. Otherwise, call the newMethodAccessor method of the ReflectionFactory object to generate a MethodAccessor object.
In the newMethodAccessor method of the ReflectionFactory class, we can see that we first generate a NativeMethodAccessorImpl object, This object again call DelegatingMethodAccessorImpl constructor of a class as a parameter.
The realization of here is to use the proxy pattern, NativeMethodAccessorImpl object to the object DelegatingMethodAccessorImpl agent. We can know view DelegatingMethodAccessorImpl constructor of a class, Is actually NativeMethodAccessorImpl object assigned to delegate DelegatingMethodAccessorImpl class attribute.
So ReflectionFactory newMethodAccessor method returns DelegatingMethodAccessorImpl eventually class object. So we are in front of the ma. The invoke (), it will be into the DelegatingMethodAccessorImpl invoke methods of a class.
After entering DelegatingMethodAccessorImpl invoke methods of a class, this calls the invoke method delegate properties, it has two implementation class, respectively is: DelegatingMethodAccessorImpl and NativeMethodAccessorImpl. As we mentioned earlier, the delegate here is actually a NativeMethodAccessorImpl object, so the Invoke method of NativeMethodAccessorImpl will be entered.
In the invoke method of NativeMethodAccessorImpl, it determines whether the number of invocations has exceeded the threshold. If more than the threshold, then will generate another MethodAccessor object, and the original DelegatingMethodAccessorImpl object attribute points to the latest MethodAccessor delegate.
So here, we can actually see that the MethodAccessor object is actually the entry that generates the reflection class. By looking at the comments in the source code, you can see some design information about the MethodAccessor object.
"Inflation" mechanism. Loading bytecodes to implement Method.invoke() and Constructor.newInstance() currently costs 3-4x more than an invocation via native code for the first invocation (though subsequent invocations have been benchmarked to be over 20x faster).Unfortunately this cost increases startup time for certain applications that use reflection A: (but only once per class) to bootstrap themselves. The first load of bytecode to implement reflection using method.invoke () and constructor.newinstance () takes 3-4 times as long as the load using native code. This leads to longer startup times for applications that frequently use reflection. To avoid this penalty we reuse the existing JVM entry points for the first few invocations of Methods and Constructors and then switch to the bytecode-based implementations. Package-private to be accessible to NativeMethodAccessorImpl and NativeConstructorAccessorImpl. To avoid this painful load time, we reused the JVM entry for the first load, then switched to the implementation of the bytecode implementationCopy the code
As noted in the comments, the actual MethodAccessor implementation comes in two versions, a Native version and a Java version.
The Native version starts fast at first, but slows down over time. The Java version loads slowly at first, but gets faster as the run time side increases. Because of these problems with both, the first time we load we find that we are using an implementation of NativeMethodAccessorImpl, and after 15 reflection calls, The MethodAccessorImpl object generated by MethodAccessorGenerator is used to implement reflection.
The whole process of the Invoke Method of the Method class can be represented as the following sequence diagram:
At this point, we have seen how the Invoke Method of the Method class is implemented. I know that the Invoke method has two internal implementation modes, one is native implementation mode and the other is Java implementation mode, both of which have their own strengths. To maximize performance, the JDK source code uses the proxy design pattern to maximize performance.
The use of reflection methods
5.1 Running configuration File contents through Reflection
Student class:
public class Student {
public void show(a){
System.out.println("is show()"); }}Copy the code
The configuration file TXT is used as an example (pro.txt) :
className = cn.fanshe.Student
methodName = show
Copy the code
The test class:
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Properties;
/* * We use reflection and configuration files so that: when the application is updated, there is no need to modify the source code * * we just need to send the new class to the client and modify the configuration file */
public class Demo {
public static void main(String[] args) throws Exception {
// Get the Class object by reflection
Class stuClass = Class.forName(getValue("className"));//"cn.fanshe.Student"
//2 Get the show() method
Method m = stuClass.getMethod(getValue("methodName"));//show
//3. Call show()
m.invoke(stuClass.getConstructor().newInstance());
}
// This method takes a key and gets the corresponding value in the configuration file
public static String getValue(String key) throws IOException{
Properties pro = new Properties();// Get the object of the configuration file
FileReader in = new FileReader("pro.txt");// Get the input stream
pro.load(in);// Load the stream into the profile object
in.close();
return pro.getProperty(key);// Returns the value obtained by key}}Copy the code
Console output:
is show(a)
Copy the code
Requirements: When we update the system and write a new Student2 class instead of Student, all we need to do is change the pro.txt file. The code doesn’t change at all
Student2 class to replace:
public class Student2 {
public void show2(a){
System.out.println("is show2()"); }}Copy the code
Configuration file changed to:
className = cn.fanshe.Student2
methodName = show2
Copy the code
Console output:
is show2();
Copy the code
5.2 Other uses of the reflection method – bypassing the generic check by reflection
Generics are used at compile time, after which they are erased. So is a test class that can be reflected past the generics check:
import java.lang.reflect.Method;
import java.util.ArrayList;
For example: If I have a set of String generics, how can I add a value of type Integer to the set? * /
public class Demo {
public static void main(String[] args) throws Exception{
ArrayList<String> strList = new ArrayList<>();
strList.add("aaa");
strList.add("bbb");
// strList.add(100);
// Get the Class object of ArrayList and call add() in reverse to add data
Class listClass = strList.getClass(); // Get the bytecode object of the strList object
// Get the add() method
Method m = listClass.getMethod("add", Object.class);
// Call add()
m.invoke(strList, 100);
// iterate over the collection
for(Object obj : strList){ System.out.println(obj); }}}Copy the code
Console output:
aaa
bbb
100
Copy the code
Refer to the article
www.cnblogs.com/chanshuyi/p…
Blog.csdn.net/weixin_4272…
www.cnblogs.com/yougewe/p/1…
Blog.csdn.net/sinat_38259…
Juejin. Im/post / 5 da6be…
www.cnblogs.com/yougewe/p/1…
Juejin. Im/post / 5 c528a…