The commonplace series AOP -- the underlying implementation principle of cglib dynamic agent
preface
Last AOP -- the underlying implementation principle of JDK dynamic agent This paper briefly explains the implementation of JDK dynamic agent. There is another Gemini in the common implementation of dynamic agent - CGLIB, so this article will introduce CGLIB dynamic agent. This article is before reuse AOP -- Analysis of the principle of spring AOP In the code example of the CGLIB part of the article, the use of CGLIB is very simple. You only need to implement a MethodInterceptor by yourself, and then use the Enhancer#create() method to create a dynamic proxy processing, and then call the method through the generated proxy class to realize the effect of Aop. Are you curious about why it can be so simple? Next, let's analyze what CGLIB has done for us.
Then the next article is mainly divided into two parts to analyze
- Generation process of dynamic agent
- Calling procedure of dynamic proxy
Generation of dynamic agent
This part answers how the dynamic agent is generated and what the CGLIB bottom layer does for us. You can see that creating proxy objects is inseparable from the Enhancer class. What is the role of this class? The notes on the extracted class are as follows:
Generates dynamic subclasses to enable method interception. This class started as a substitute for the standard Dynamic Proxy support included with JDK 1.3, but one that allowed the proxies to extend a concrete base class, in addition to implementing interfaces. The dynamically generated subclasses override the non-final methods of the superclass and have hooks which callback to user-defined interceptor implementations.
Make method interception effective by generating dynamic subclasses. This class started as an alternative to the standard dynamic proxy support included in JDK 1.3, but it allows the proxy to extend specific base classes in addition to implementing interfaces. The dynamically generated subclasses cover the non final methods of the superclass and have hooks that call back to the implementation of user-defined interceptors.
In short, it is to generate a proxy subclass and call back to the interceptor of the custom implementation when calling the method.
First, let's look at the simple sample code
@Test public void cglibProxyTest(){ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(CalculateServiceImpl.class); enhancer.setCallback(new MyMethodInterceptor()); CalculateService calculateService = (CalculateService) enhancer.create(); calculateService.calculate(); }
As you can see, we only need to set up superClass and callback to call the create() method to generate a desired object. Next, let's look at what happened to create().
/** * Generate a new class if necessary and uses the specified * callbacks (if any) to create a new object instance. * Uses the no-arg constructor of the superclass. * @return a new instance */ public Object create() { classOnly = false; argumentTypes = null; return createHelper(); }
You can see that key is used here_ Factory generates a key, which encapsulates multiple values and belongs to the implementation of multi valued keys. Let's take a look at the usage of KeyFactory.
private Object createHelper() { preValidate(); Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null, ReflectUtils.getNames(interfaces), filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter), callbackTypes, useFactory, interceptDuringConstruction, serialVersionUID); this.currentKey = key; Object result = super.create(key); return result; }
KeyFactory is an important unique identifier generator in the class library. It is used for the key when CGLIB implements caching and compares the underlying basic classes.
The notes on the extracted class are as follows:
Generates classes to handle multi-valued keys, for use in things such as Maps and Sets. To generate a <code>KeyFactory</code>, you need to supply an interface which describes the structure of the key. The interface should have a single method named <code>newInstance</code>, which returns an <code>Object</code>. The arguments array can be <i>anything</i>--Objects, primitive values, or single or multi-dimension arrays of either. For example: <p><pre> private interface IntStringKey { public Object newInstance(int i, String s); } </pre><p> Once you have made a <code>KeyFactory</code>, you generate a new key by calling the <code>newInstance</code> method defined by your interface. <p><pre> IntStringKey factory = (IntStringKey)KeyFactory.create(IntStringKey.class); Object key1 = factory.newInstance(4, "Hello"); Object key2 = factory.newInstance(4, "World"); </pre><p>
KeyFactory can generate classes that handle multivalued keys, which can be used for things like Maps and Sets. The use of KeyFactory is also very simple. You only need to provide an interface, define a newInstance() method, and call (intstringkey) KeyFactory Create (intstringkey. Class) can generate a key class.
Next, through super create(key) calls the create(key) method of the parent class.
protected Object create(Object key) { try { ClassLoader loader = getClassLoader(); Map<ClassLoader, ClassLoaderData> cache = CACHE; // First try to get ClassLoaderData through cache ClassLoaderData data = cache.get(loader); if (data == null) { synchronized (AbstractClassGenerator.class) { cache = CACHE; data = cache.get(loader); if (data == null) { Map<ClassLoader, ClassLoaderData> newCache = new WeakHashMap<ClassLoader, ClassLoaderData>(cache); data = new ClassLoaderData(loader); newCache.put(loader, data); CACHE = newCache; } } } this.key = key; // The proxy class is really generated here Object obj = data.get(this, getUseCache()); if (obj instanceof Class) { return firstInstance((Class) obj); } return nextInstance(obj); } // Omission exception }
Enter ClassLoaderData#get() method
public Object get(AbstractClassGenerator gen, boolean useCache) { if (!useCache) { return gen.generate(ClassLoaderData.this); } else { Object cachedValue = generatedClasses.get(gen); return gen.unwrapCachedValue(cachedValue); } }
Follow up on generatedclasses Get (Gen) method. The first time you come in here, the front will be empty, so you will create a node createEntry().
public V get(K key) { final KK cacheKey = keyMapper.apply(key); Object v = map.get(cacheKey); if (v != null && !(v instanceof FutureTask)) { return (V) v; } return createEntry(key, cacheKey, v); }
You can see that FutureTask is used to perform creation asynchronously, which is used to improve the performance during creation.
protected V createEntry(final K key, KK cacheKey, Object v) { FutureTask<V> task; boolean creator = false; if (v != null) { // Another thread is already loading an instance task = (FutureTask<V>) v; } else { // Create a FutureTask task = new FutureTask<V>(new Callable<V>() { public V call() throws Exception { return loader.apply(key); } }); // Verify whether this task already exists Object prevTask = map.putIfAbsent(cacheKey, task); if (prevTask == null) { // creator does the load // Execute FutureTask creator = true; task.run(); } else if (prevTask instanceof FutureTask) { task = (FutureTask<V>) prevTask; } else { return (V) prevTask; } } V result; try { // If you go here, it means that there is a FutureTask executed normally. Try to obtain the results of FutureTask result = task.get(); } // Omit some exceptions if (creator) { map.put(cacheKey, result); } return result; }
Take a look at the above code along with the comments. The key point is loader Apply (key). This loader is passed in when new LoadingCache().
task = new FutureTask<V>(new Callable<V>() { public V call() throws Exception { return loader.apply(key); } });
The logic of loader is as follows:
Function<AbstractClassGenerator, Object> load = new Function<AbstractClassGenerator, Object>() { public Object apply(AbstractClassGenerator gen) { // The proxy class is generated here Class klass = gen.generate(ClassLoaderData.this); return gen.wrapCachedClass(klass); } }; generatedClasses = new LoadingCache<AbstractClassGenerator, Object, Object>(GET_KEY, load);
Follow up the gen.generate(ClassLoaderData.this) method
protected Class generate(ClassLoaderData data) { Class gen; Object save = CURRENT.get(); CURRENT.set(this); try { ClassLoader classLoader = data.getClassLoader(); //Omit some logic and logs. The key point is here. The bytecode of the agent class will be generated here byte[] b = strategy.generate(this); String className = ClassNameReader.getClassName(new ClassReader(b)); ProtectionDomain protectionDomain = getProtectionDomain(); synchronized (classLoader) { // just in case if (protectionDomain == null) { gen = ReflectUtils.defineClass(className, b, classLoader); } else { gen = ReflectUtils.defineClass(className, b, classLoader, protectionDomain); } } return gen; } // Omission exception }
The default implementation is DefaultGeneratorStrategy. You can see that a DebuggingClassWriter is obtained here. CGLIB encapsulates the processing class of ASM, which is used to generate the byte stream of class, and callback the classgenerator through GeneratorStrategy Generateclass (DebuggingClassWriter) calls back the byte processing of the custom class object to the specific CGLIB upper operation class. For example, the specific BeanCopier controls the generation of bytecode.
public byte[] generate(ClassGenerator cg) throws Exception { DebuggingClassWriter cw = getClassVisitor(); transform(cg).generateClass(cw); return transform(cw.toByteArray()); }
In the constructor of FastClassEmitter class, the related operations of ASM are encapsulated through the DebuggingClassWriter passed in above, which is used to dynamically generate the bytecode of proxy class. Here, we will not go deep into the principle of ASM. If you are interested, you can see it Implementation principle of bytecode operation framework ASM.
public void generateClass(ClassVisitor v) throws Exception { new FastClassEmitter(v, getClassName(), type); }
Finally, a byte [] array is obtained through transform(cw.toByteArray()). Well, you can get the bytecode of an agent here. Next, go back to the AbstractClassGenerator#create() method.
// obj is the obtained proxy Class. Here is a Class object Object obj = data.get(this, getUseCache()); if (obj instanceof Class) { return firstInstance((Class) obj); } return nextInstance(obj);
Just instantiate the object and return it. So far, a proxy class has been obtained.
protected Object firstInstance(Class type) { return ReflectUtils.newInstance(type, new Class[]{ Class.class }, new Object[]{ this.type }); }
Call of dynamic proxy
Code sample
Here is an example of HelloService, which is enhanced by HelloMethodInterceptor.
public class HelloService { public void sayHello(){ System.out.println("hello"); } } public class HelloMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("before say hello..."); return methodProxy.invokeSuper(object,objects); } }
Test the method and save the generated proxy class.
public class HelloTest { @Test public void cglibProxyTest(){ System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"C:\\my_study_project"); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(HelloService.class); enhancer.setCallback(new HelloMethodInterceptor()); HelloService helloService = (HelloService) enhancer.create(); helloService.sayHello(); } }
So what did the above steps produce? Let's take a look at the generated classes.
HelloService$$EnhancerByCGLIB$933e33 is the generated proxy class, HelloService$$FastClassByCGLIB$$a685f36d is the FastClass of the target class, and HelloService$$EnhancerByCGLIB$933e33 $$fastclassbycglib $$33d595dd is the FastClass of the proxy class.
Decompiled code
Let's take a look at the decompiled code of each class
proxy class
The code of each class is very long, so not all of them will be posted here. For ease of reading, only sayHello() and hashCode() methods are retained here for comparison and elaboration. If you want to view all the code, run the test code yourself, and you can find these three class files in the corresponding path.
package io.codegitz.service; public class HelloService$$EnhancerByCGLIB$$91933e33 extends HelloService implements Factory { private boolean CGLIB$BOUND; public static Object CGLIB$FACTORY_DATA; private static final ThreadLocal CGLIB$THREAD_CALLBACKS; private static final Callback[] CGLIB$STATIC_CALLBACKS; private MethodInterceptor CGLIB$CALLBACK_0; private static Object CGLIB$CALLBACK_FILTER; private static final Method CGLIB$sayHello$0$Method; private static final MethodProxy CGLIB$sayHello$0$Proxy; private static final Method CGLIB$hashCode$3$Method; private static final MethodProxy CGLIB$hashCode$3$Proxy; // Initialize all methods of this class static void CGLIB$STATICHOOK1() { CGLIB$THREAD_CALLBACKS = new ThreadLocal(); CGLIB$emptyArgs = new Object[0]; Class var0 = Class.forName("io.codegitz.service.HelloService$$EnhancerByCGLIB$$91933e33"); Class var1; Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods()); CGLIB$hashCode$3$Method = var10000[2]; CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3"); CGLIB$sayHello$0$Method = ReflectUtils.findMethods(new String[]{"sayHello", "()V"}, (var1 = Class.forName("io.codegitz.service.HelloService")).getDeclaredMethods())[0]; CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "()V", "sayHello", "CGLIB$sayHello$0"); } // Call the target class method final void CGLIB$sayHello$0() { super.sayHello(); } // Method of agent logic public final void sayHello() { MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; if (var10000 == null) { CGLIB$BIND_CALLBACKS(this); var10000 = this.CGLIB$CALLBACK_0; } if (var10000 != null) { var10000.intercept(this, CGLIB$sayHello$0$Method, CGLIB$emptyArgs, CGLIB$sayHello$0$Proxy); } else { super.sayHello(); } } //The hashCode method is similar final int CGLIB$hashCode$3() { return super.hashCode(); } public final int hashCode() { MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; if (var10000 == null) { CGLIB$BIND_CALLBACKS(this); var10000 = this.CGLIB$CALLBACK_0; } if (var10000 != null) { Object var1 = var10000.intercept(this, CGLIB$hashCode$3$Method, CGLIB$emptyArgs, CGLIB$hashCode$3$Proxy); return var1 == null ? 0 : ((Number)var1).intValue(); } else { return super.hashCode(); } } // Generate MethodProxy. fastClass will be generated by calling MethodProxy, which is the key to high-performance calling public static MethodProxy CGLIB$findMethodProxy(Signature var0) { String var10000 = var0.toString(); switch(var10000.hashCode()) { case 1535311470: if (var10000.equals("sayHello()V")) { return CGLIB$sayHello$0$Proxy; } break; case 1984935277: if (var10000.equals("hashCode()I")) { return CGLIB$hashCode$3$Proxy; } } return null; } // Initialization method static { CGLIB$STATICHOOK1(); } }
Proxy class fastClass
fastClass establishes an index for all methods. When calling, it searches for methods by passing in an index, so as to avoid the performance overhead of reflection. This is a typical space for time implementation.
package io.codegitz.service; import io.codegitz.service.HelloService..EnhancerByCGLIB..91933e33; import java.lang.reflect.InvocationTargetException; import net.sf.cglib.core.Signature; import net.sf.cglib.proxy.Callback; import net.sf.cglib.reflect.FastClass; public class HelloService$$EnhancerByCGLIB$$91933e33$$FastClassByCGLIB$$33d595dd extends FastClass { public HelloService$$EnhancerByCGLIB$$91933e33$$FastClassByCGLIB$$33d595dd(Class var1) { super(var1); } // Get method index through method signature public int getIndex(Signature var1) { String var10000 = var1.toString(); switch(var10000.hashCode()) { case -1411842725: if (var10000.equals("CGLIB$hashCode$3()I")) { return 16; } break; case 291273791: if (var10000.equals("CGLIB$sayHello$0()V")) { return 14; } break; case 1535311470: if (var10000.equals("sayHello()V")) { return 7; } break; case 1984935277: if (var10000.equals("hashCode()I")) { return 2; } } return -1; } // Get index by method name public int getIndex(String var1, Class[] var2) { switch(var1.hashCode()) { case -2012993625: if (var1.equals("sayHello")) { switch(var2.length) { case 0: return 7; } } break; case -1983192202: if (var1.equals("CGLIB$sayHello$0")) { switch(var2.length) { case 0: return 14; } } break; case -29025555: if (var1.equals("CGLIB$hashCode$3")) { switch(var2.length) { case 0: return 16; } } break; case 147696667: if (var1.equals("hashCode")) { switch(var2.length) { case 0: return 2; } } break; } return -1; } public int getIndex(Class[] var1) { switch(var1.length) { case 0: return 0; default: return -1; } } // Get method execution by passing in the method's index var1 public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException { 91933e33 var10000 = (91933e33)var2; int var10001 = var1; try { switch(var10001) { case 2: return new Integer(var10000.hashCode()); case 7: var10000.sayHello(); return null; case 14: var10000.CGLIB$sayHello$0(); return null; case 16: return new Integer(var10000.CGLIB$hashCode$3()); } } catch (Throwable var4) { throw new InvocationTargetException(var4); } throw new IllegalArgumentException("Cannot find matching method/constructor"); } public Object newInstance(int var1, Object[] var2) throws InvocationTargetException { 91933e33 var10000 = new 91933e33; 91933e33 var10001 = var10000; int var10002 = var1; try { switch(var10002) { case 0: var10001.<init>(); return var10000; } } catch (Throwable var3) { throw new InvocationTargetException(var3); } throw new IllegalArgumentException("Cannot find matching method/constructor"); } public int getMaxIndex() { return 20; } }
Target class fastClass
CGLIB not only generates a fastClass for the proxy class, but also generates a fastClass for the original target class. The principle is similar. Both of them establish the index of the method, find the method through passing in the index, and execute the method, avoiding the performance overhead of reflection acquisition method.
package io.codegitz.service; import java.lang.reflect.InvocationTargetException; import net.sf.cglib.core.Signature; import net.sf.cglib.reflect.FastClass; public class HelloService$$FastClassByCGLIB$$a685f36d extends FastClass { public HelloService$$FastClassByCGLIB$$a685f36d(Class var1) { super(var1); } // Get index by method signature public int getIndex(Signature var1) { String var10000 = var1.toString(); switch(var10000.hashCode()) { case 1535311470: if (var10000.equals("sayHello()V")) { return 0; } break; case 1984935277: if (var10000.equals("hashCode()I")) { return 3; } } return -1; } // Get method index by method name public int getIndex(String var1, Class[] var2) { switch(var1.hashCode()) { case -2012993625: if (var1.equals("sayHello")) { switch(var2.length) { case 0: return 0; } } break; case 147696667: if (var1.equals("hashCode")) { switch(var2.length) { case 0: return 3; } } } return -1; } public int getIndex(Class[] var1) { switch(var1.length) { case 0: return 0; default: return -1; } } // Get the corresponding method execution according to the passed in var1 public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException { HelloService var10000 = (HelloService)var2; int var10001 = var1; try { switch(var10001) { case 0: var10000.sayHello(); return null; case 3: return new Integer(var10000.hashCode()); } } catch (Throwable var4) { throw new InvocationTargetException(var4); } throw new IllegalArgumentException("Cannot find matching method/constructor"); } public Object newInstance(int var1, Object[] var2) throws InvocationTargetException { HelloService var10000 = new HelloService; HelloService var10001 = var10000; int var10002 = var1; try { switch(var10002) { case 0: var10001.<init>(); return var10000; } } catch (Throwable var3) { throw new InvocationTargetException(var3); } throw new IllegalArgumentException("Cannot find matching method/constructor"); } public int getMaxIndex() { return 3; } }
Call procedure analysis
Do these codes look messy? Have no idea where to start?
Before starting code analysis, take a look at the execution flow chart. The steps are still relatively simple and clear
Next, here is the logic of the dynamic proxy class. You can see the HelloService$$EnhancerByCGLIB$933e33#sayHello() method.
public final void sayHello() { // Get the interceptor. This is the HelloMethodInterceptor MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; if (var10000 == null) { CGLIB$BIND_CALLBACKS(this); var10000 = this.CGLIB$CALLBACK_0; } // If it is not empty, the interceptor will be executed if (var10000 != null) { // Note the parameters passed in here. A method and a MethodProxy are passed in here, and the logic in the custom HelloMethodInterceptor will be entered here var10000.intercept(this, CGLIB$sayHello$0$Method, CGLIB$emptyArgs, CGLIB$sayHello$0$Proxy); } else { super.sayHello(); } }
Here is through methodProxy Invokesuper (object, objects). Call the invokeSuper() method. Note that there is another invoke() method in methodProxy. What's the difference between the two? Obviously, invokeSuper() is the method of calling the parent class, and invoke() is the method of calling the proxy passing through the interceptor. If you call invoke(), you will go to the interceptor every time, which will cause an endless loop.
Follow up methodproxy Invokesuper() method. According to the comments, the original method without proxy is called here.
/** * Invoke the original (super) method on the specified object. * @param obj the enhanced object, must be the object passed as the first * argument to the MethodInterceptor * @param args the arguments passed to the intercepted method; you may substitute a different * argument array as long as the types are compatible * @see MethodInterceptor#intercept * @throws Throwable the bare exceptions thrown by the called method are passed through * without wrapping in an <code>InvocationTargetException</code> */ public Object invokeSuper(Object obj, Object[] args) throws Throwable { try { init(); FastClassInfo fci = fastClassInfo; return fci.f2.invoke(fci.i2, obj, args); } catch (InvocationTargetException e) { throw e.getTargetException(); } }
As you can see, the call here is different from the reflection call. Reflection calls usually pass in methods directly, and then invoke() directly. Here, init() will be performed to initialize a FastClassInfo, and then FCI f2. Invoke (fci.i2, obj, args) to call the method. Here is the key to high-performance call. Here, the FastClass of proxy class method and implementation class will be used, and then the method execution will be directly obtained through the subscript index of the incoming method during the call, so as to realize the space for time operation.
Let's look at the init() method. This method is used to initialize the fastClassInfo class. The detailed initialization process will not be resolved. Here is just what is finally generated.
private void init() { /* * Using volatile invariants allows us to initialize FastClass and method index pairs atomically * Using a volatile invariant allows us to initialize the FastClass and * method index pairs atomically. * * Double check locking is safe to use volatile in Java 5. Before 1.5, this code might allow multiple instantiations of fastClassInfo, which seems benign. * Double-checked locking is safe with volatile in Java 5. Before 1.5 this * code could allow fastClassInfo to be instantiated more than once, which * appears to be benign. */ if (fastClassInfo == null) { synchronized (initLock) { if (fastClassInfo == null) { CreateInfo ci = createInfo; FastClassInfo fci = new FastClassInfo(); // Generate target class fastClass fci.f1 = helper(ci, ci.c1); // Generate proxy class fastClass fci.f2 = helper(ci, ci.c2); // Generate target class index fci.i1 = fci.f1.getIndex(sig1); // Generate proxy class index fci.i2 = fci.f2.getIndex(sig2); fastClassInfo = fci; createInfo = null; } } } }
The following is the assignment of each attribute during initialization
After initialization, you can return to FCI f2. Invoke (fci.i2, obj, args) is called, and you can see FCI The type of F2 is HelloService$$EnhancerByCGLIB$933e33$$FastClassByCGLIB$d595dd, which is the fastClass of the agent class posted above. Take a look at the invoke() method of this class
View the decompiled invoke() method code
public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException { 91933e33 var10000 = (91933e33)var2; int var10001 = var1; try { switch(var10001) { case 2: return new Integer(var10000.hashCode()); case 7: var10000.sayHello(); return null; case 16: var10000.CGLIB$sayHello$0(); return null; case 17: return new Integer(var10000.CGLIB$hashCode$3()); } } catch (Throwable var4) { throw new InvocationTargetException(var4); } throw new IllegalArgumentException("Cannot find matching method/constructor"); }
Here, the switch will match to 16, and then execute var10000 Cglib $sayhello $0(), the type of this var10000 is
Find the HelloService$$EnhancerByCGLIB$933e33#CGLIB$sayHello helloservice $$enhancerbycglib $$91933e33#cglib $sayhello $0() () method. You can see that the HelloService#sayHello() method is called directly here. Similarly, the logic of the methodProxy#invoke() method is similar. Just be careful not to loop when calling.
final void CGLIB$sayHello$0() { super.sayHello(); }
Here, you can see that when the proxy method generated by CGLIB is called, it first passes through the call interceptor, and then calls the target method. When methodProxy calls the target method, it will generate fastClass. There are all methods of proxy class and target class and matching subscripts in fastClass. The corresponding methods can be found through the passed in subscripts, The method call here only needs to initialize fastClass for the first time, and can be called directly later, so as to improve the execution performance. This is also the key to the higher execution efficiency of CGLIB than JDK dynamic agent. Here, the idea of space for time is worth learning from, and it is completely worth consuming memory appropriately to improve execution efficiency.
summary
Looking back on this article, the first half of this article roughly explains the steps of CGLIB generating a proxy class through an example, but the bytecode operation of integrating ASM is skipped, which is effective and dare not make a mistake. Dig a hole and be able to fill it later. The second half explains the calling process in combination with the decompiled class file. This part is very simple. You should be able to sort it out quickly after debugging yourself.
If anyone sees here, the old saying will be repeated here. It's a long way to encourage you. I'll seek it from top to bottom.