Implementation of AOP from static agent to dynamic agent Emit

Posted by gio2k on Sun, 30 Jan 2022 03:17:46 +0100

[preface]

AOP is the abbreviation of Aspect Oriented Programming, which means Aspect Oriented Programming technology.

What is section?

A code segment without any coupling with the business, such as calling log, sending mail, and even routing distribution. All codes that can be owned by the code and can be fully decoupled from the code can be regarded as the aspect of a business code.

Why do we need AOP?

Let's start with an example:

If we want to collect user operation behavior, we need to master the information of each interface called by the user. What are we going to do at this time?

If AOP technology is not adopted, it is also the simplest. In the first sentence of all method bodies, call a log interface to transfer the method information to the record.

What's the problem?

There is no problem in realizing the business, but the code is bloated and difficult to adjust and maintain.

If we adopt AOP technology, we can inject all classes to collect logs at the start of the system. Before each method call, the AOP framework will automatically call our log code.

Does it save a lot of redundant and useless work? The code will also become very easy to maintain (no need one day, just comment out the relevant code)

Next, let's take a look at the working principle and real process of AOP framework.

[implementation ideas]

AOP framework is generally implemented through static agent and dynamic agent.

  

What is a static proxy?  

Static proxy, also known as compile time proxy, is the way that proxy classes already exist at compile time and are called directly at run time. The popular point is to write code manually to implement the proxy class.

Let's show the implementation process of static agent through an example:

We have a business class with the method Test(). We need to output the log before and after the Test call.

  

Since we want to take Log as a perspective, we must not move the original business code, which also violates the opening and closing principle of object-oriented design.

So what are we going to do? We define a new class BusinessProxy to wrap this class. In order to distinguish and identify multiple methods, the method is also called Test ()

 

 

 

In this way, if we want to add Log to the methods in all Business classes, we will add corresponding methods to wrap in the BusinessProxy proxy class. It does not destroy the original logic, but also realizes the function of front and back logs.

Of course, we can have a more elegant implementation:

  

 

We can define proxy classes that inherit from business classes. Define the methods in the business class as virtual methods. Then we can override the method of the parent class and call the original method of the parent class after adding the log.

Of course, we have more elegant implementation methods:

  

  

We can use reflection technology to write a general Invoke method, through which all methods can be called.

In this way, we implement a static proxy.

Now that we have static agents, why do we have dynamic agents?

We carefully review the implementation process of static proxy. To add facets to all methods, we need to override all business methods in the proxy class. What's more, we have N business classes, so we need to define N agent classes. This is a huge workload.

  

This is the background of the emergence of dynamic agent. You can guess that dynamic agent is to automate this series of cumbersome steps and let the program automatically generate agent classes for us.

What is dynamic agent?

Dynamic agent, also known as runtime agent. During the running of the program, the code of the proxy class is called, and the agent class of the business class is automatically generated. We don't need to write it together, which greatly improves the work efficiency and adjusts the mentality of programmers.

Needless to say, the principle is to dynamically generate the code of static agent. What we need to do is to choose a way to generate code.

Today, I share a simple AOP framework. The code is generated by Emit. Of course, the writing method of Emit code is not the main content to be talked about today. We need to learn it in advance.

Let's start with the effect:

Define an Action attribute class, ActionAttribute, which inherits from ActionBaseAttribute and outputs two logs in the Before and After methods;

  

Define an Action attribute class, InterceptorAttribute, which inherits from InterceptorBaseAttribute, which captures method call exceptions and outputs logs before and after execution;

  

Then define a business class, which implements the IBusinessClass interface and defines various types of methods

  

  

The redundant method is not mapped.

We put the method call aspect label defined above on the business class, indicating that all methods under this class perform exception filtering;

We put the Action feature on the Test method, indicating that the log should be recorded during the Before and After calls of the Test () method;

We define test classes:

  

Try calling:

  

It can be seen that the Interceptor of the full class method tag has printed the corresponding log before and after the Test and GetInt method calls;

The Action method tag is only marked on the Test method, so the log is printed when the Test method Before and After are executed;

[implementation process]

The implementation idea has been explained in detail above. You can refer to the implementation idea of static agent.

We define a dynamic proxy generation class DynamicProxy, which is used to scan the original business code and generate the proxy code;

Define two filter tags, ActionBaseAttribute, and provide "Before" and "After" aspect methods; InterceptorBaseAttribute, which provides the slice method of Invoke "full call" packaging;

Before can get the method and parameter list of the current call, and After can get the result after the current method invocation.

Invoke can get the object and method name and parameter list of the currently called object. Reflection dynamic calls are made here.

1 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
2     public class ActionBaseAttribute : Attribute
3     {
4         public virtual void Before(string @method, object[] parameters) { }
5 
6         public virtual object After(string @method, object result) { return result; }
7     }
1 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
2     public class InterceptorBaseAttribute : Attribute
3     {
4         public virtual object Invoke(object @object, string @method, object[] parameters)
5         {
6             return @object.GetType().GetMethod(@method).Invoke(@object, parameters);
7         }
8     }

The agent generation class generates runtime IL code in the way of Emit.

Put the code here first:

  1 public class DynamicProxy
  2     {
  3         public static TInterface CreateProxyOfRealize<TInterface, TImp>() where TImp : class, new() where TInterface : class
  4         {
  5             return Invoke<TInterface, TImp>();
  6         }
  7 
  8         public static TProxyClass CreateProxyOfInherit<TProxyClass>() where TProxyClass : class, new()
  9         {
 10             return Invoke<TProxyClass, TProxyClass>(true);
 11         }
 12 
 13         private static TInterface Invoke<TInterface, TImp>(bool inheritMode = false) where TImp : class, new() where TInterface : class
 14         {
 15             var impType = typeof(TImp);
 16 
 17             string nameOfAssembly = impType.Name + "ProxyAssembly";
 18             string nameOfModule = impType.Name + "ProxyModule";
 19             string nameOfType = impType.Name + "Proxy";
 20 
 21             var assemblyName = new AssemblyName(nameOfAssembly);
 22 
 23             var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
 24             var moduleBuilder = assembly.DefineDynamicModule(nameOfModule);
 25 
 26             //var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
 27             //var moduleBuilder = assembly.DefineDynamicModule(nameOfModule, nameOfAssembly + ".dll");
 28 
 29             TypeBuilder typeBuilder;
 30             if (inheritMode)
 31                 typeBuilder = moduleBuilder.DefineType(nameOfType, TypeAttributes.Public, impType);
 32             else
 33                 typeBuilder = moduleBuilder.DefineType(nameOfType, TypeAttributes.Public, null, new[] { typeof(TInterface) });
 34 
 35             InjectInterceptor<TImp>(typeBuilder, impType.GetCustomAttribute(typeof(InterceptorBaseAttribute))?.GetType(), inheritMode);
 36 
 37             var t = typeBuilder.CreateType();
 38 
 39             //assembly.Save(nameOfAssembly + ".dll");
 40 
 41             return Activator.CreateInstance(t) as TInterface;
 42         }
 43 
 44         private static void InjectInterceptor<TImp>(TypeBuilder typeBuilder, Type interceptorAttributeType, bool inheritMode = false)
 45         {
 46             var impType = typeof(TImp);
 47             // ---- define fields ----
 48             FieldBuilder fieldInterceptor = null;
 49             if (interceptorAttributeType != null)
 50             {
 51                 fieldInterceptor = typeBuilder.DefineField("_interceptor", interceptorAttributeType, FieldAttributes.Private);
 52             }
 53             // ---- define costructors ----
 54             if (interceptorAttributeType != null)
 55             {
 56                 var constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, null);
 57                 var ilOfCtor = constructorBuilder.GetILGenerator();
 58 
 59                 ilOfCtor.Emit(OpCodes.Ldarg_0);
 60                 ilOfCtor.Emit(OpCodes.Newobj, interceptorAttributeType.GetConstructor(new Type[0]));
 61                 ilOfCtor.Emit(OpCodes.Stfld, fieldInterceptor);
 62                 ilOfCtor.Emit(OpCodes.Ret);
 63             }
 64 
 65             // ---- define methods ----
 66 
 67             var methodsOfType = impType.GetMethods(BindingFlags.Public | BindingFlags.Instance);
 68 
 69             string[] ignoreMethodName = new[] { "GetType", "ToString", "GetHashCode", "Equals" };
 70 
 71             foreach (var method in methodsOfType)
 72             {
 73                 //ignore method
 74                 if (ignoreMethodName.Contains(method.Name))
 75                     return;
 76 
 77                 var methodParameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
 78 
 79                 MethodAttributes methodAttributes;
 80 
 81                 if (inheritMode)
 82                     methodAttributes = MethodAttributes.Public | MethodAttributes.Virtual;
 83                 else
 84                     methodAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final;
 85 
 86                 var methodBuilder = typeBuilder.DefineMethod(method.Name, methodAttributes, CallingConventions.Standard, method.ReturnType, methodParameterTypes);
 87 
 88                 var ilMethod = methodBuilder.GetILGenerator();
 89 
 90                 // set local field
 91                 var impObj = ilMethod.DeclareLocal(impType);                //instance of imp object
 92                 var methodName = ilMethod.DeclareLocal(typeof(string));     //instance of method name
 93                 var parameters = ilMethod.DeclareLocal(typeof(object[]));   //instance of parameters
 94                 var result = ilMethod.DeclareLocal(typeof(object));         //instance of result
 95                 LocalBuilder actionAttributeObj = null;
 96 
 97                 //attribute init
 98                 Type actionAttributeType = null;
 99                 if (method.GetCustomAttribute(typeof(ActionBaseAttribute)) != null || impType.GetCustomAttribute(typeof(ActionBaseAttribute)) != null)
100                 {
101                     //method can override class attrubute
102                     if (method.GetCustomAttribute(typeof(ActionBaseAttribute)) != null)
103                     {
104                         actionAttributeType = method.GetCustomAttribute(typeof(ActionBaseAttribute)).GetType();
105                     }
106                     else if (impType.GetCustomAttribute(typeof(ActionBaseAttribute)) != null)
107                     {
108                         actionAttributeType = impType.GetCustomAttribute(typeof(ActionBaseAttribute)).GetType();
109                     }
110 
111                     actionAttributeObj = ilMethod.DeclareLocal(actionAttributeType);
112                     ilMethod.Emit(OpCodes.Newobj, actionAttributeType.GetConstructor(new Type[0]));
113                     ilMethod.Emit(OpCodes.Stloc, actionAttributeObj);
114                 }
115 
116                 //instance imp
117                 ilMethod.Emit(OpCodes.Newobj, impType.GetConstructor(new Type[0]));
118                 ilMethod.Emit(OpCodes.Stloc, impObj);
119 
120                 //if no attribute
121                 if (fieldInterceptor != null || actionAttributeObj != null)
122                 {
123                     ilMethod.Emit(OpCodes.Ldstr, method.Name);
124                     ilMethod.Emit(OpCodes.Stloc, methodName);
125 
126                     ilMethod.Emit(OpCodes.Ldc_I4, methodParameterTypes.Length);
127                     ilMethod.Emit(OpCodes.Newarr, typeof(object));
128                     ilMethod.Emit(OpCodes.Stloc, parameters);
129 
130                     // build the method parameters
131                     for (var j = 0; j < methodParameterTypes.Length; j++)
132                     {
133                         ilMethod.Emit(OpCodes.Ldloc, parameters);
134                         ilMethod.Emit(OpCodes.Ldc_I4, j);
135                         ilMethod.Emit(OpCodes.Ldarg, j + 1);
136                         //box
137                         ilMethod.Emit(OpCodes.Box, methodParameterTypes[j]);
138                         ilMethod.Emit(OpCodes.Stelem_Ref);
139                     }
140                 }
141 
142                 //dynamic proxy action before
143                 if (actionAttributeType != null)
144                 {
145                     //load arguments
146                     ilMethod.Emit(OpCodes.Ldloc, actionAttributeObj);
147                     ilMethod.Emit(OpCodes.Ldloc, methodName);
148                     ilMethod.Emit(OpCodes.Ldloc, parameters);
149                     ilMethod.Emit(OpCodes.Call, actionAttributeType.GetMethod("Before"));
150                 }
151 
152                 if (interceptorAttributeType != null)
153                 {
154                     //load arguments
155                     ilMethod.Emit(OpCodes.Ldarg_0);//this
156                     ilMethod.Emit(OpCodes.Ldfld, fieldInterceptor);
157                     ilMethod.Emit(OpCodes.Ldloc, impObj);
158                     ilMethod.Emit(OpCodes.Ldloc, methodName);
159                     ilMethod.Emit(OpCodes.Ldloc, parameters);
160                     // call Invoke() method of Interceptor
161                     ilMethod.Emit(OpCodes.Callvirt, interceptorAttributeType.GetMethod("Invoke"));
162                 }
163                 else
164                 {
165                     //direct call method
166                     if (method.ReturnType == typeof(void) && actionAttributeType == null)
167                     {
168                         ilMethod.Emit(OpCodes.Ldnull);
169                     }
170 
171                     ilMethod.Emit(OpCodes.Ldloc, impObj);
172                     for (var j = 0; j < methodParameterTypes.Length; j++)
173                     {
174                         ilMethod.Emit(OpCodes.Ldarg, j + 1);
175                     }
176                     ilMethod.Emit(OpCodes.Callvirt, impType.GetMethod(method.Name));
177                     //box
178                     if (actionAttributeType != null)
179                     {
180                         if (method.ReturnType != typeof(void))
181                             ilMethod.Emit(OpCodes.Box, method.ReturnType);
182                         else
183                             ilMethod.Emit(OpCodes.Ldnull);
184                     }
185                 }
186 
187                 //dynamic proxy action after
188                 if (actionAttributeType != null)
189                 {
190                     ilMethod.Emit(OpCodes.Stloc, result);
191                     //load arguments
192                     ilMethod.Emit(OpCodes.Ldloc, actionAttributeObj);
193                     ilMethod.Emit(OpCodes.Ldloc, methodName);
194                     ilMethod.Emit(OpCodes.Ldloc, result);
195                     ilMethod.Emit(OpCodes.Call, actionAttributeType.GetMethod("After"));
196                 }
197 
198                 // pop the stack if return void
199                 if (method.ReturnType == typeof(void))
200                 {
201                     ilMethod.Emit(OpCodes.Pop);
202                 }
203                 else
204                 {
205                     //unbox,if direct invoke,no box
206                     if (fieldInterceptor != null || actionAttributeObj != null)
207                     {
208                         if (method.ReturnType.IsValueType)
209                             ilMethod.Emit(OpCodes.Unbox_Any, method.ReturnType);
210                         else
211                             ilMethod.Emit(OpCodes.Castclass, method.ReturnType);
212                     }
213                 }
214                 // complete
215                 ilMethod.Emit(OpCodes.Ret);
216             }
217         }
218     }

There are two proxy methods, one is interface oriented implementation, the other is inheritance and rewriting.

However, in the way of inheritance rewriting, all methods of the business class need to be written as virtual virtual methods, and the dynamic class will override this method.

We get the proxy class dll generated at runtime from the Demo in the previous section and decompile it with ILSpy to view the source code:

  

You can see that our proxy class calls the methods in our feature tag respectively.

Core code analysis (the source code has been pasted on the folding part above):

  

Explanation: if the method has an action tag, load the action tag, instantiate the object, load the parameters, and execute the Before method; If the method has an Interceptor tag, use the class field this_ The Interceptor calls the Invoke method of the tag.

  

Explanation: if the Interceptor property label of the face does not exist, the parameters corresponding to the currently scanned method will be loaded and the method will be called directly; If the Action tag exists, wrap the result just called as an object object and pass it to the After method.

Here, if the target parameter is the object type and the actual parameter is the explicit value type returned by the direct call, the boxing operation is required. Otherwise, the call memory error exception will be reported when running.

  

Explanation: if the return value is of void type, it ends directly and returns the result; If the return value is a value type, you need to unpack it manually. If it is a reference type, you need to convert it.

The details of IL implementation are not discussed here.

[system test]

   1. Interface implementation mode, Api test (different types of method calls corresponding to various tag usage modes):

  

Conclusion: for the above exhaustive types, various tag use methods have successfully logged;

  2. Inheritance mode, Api test (different types of method calls corresponding to various tag usage modes):

  

Conclusion: the effect of inheritance mode and interface implementation mode is the same, but the method needs different implementation adjustment;

  3. Performance results of directly calling three methods millions of times:

  

Conclusion: it takes 58ms to directly call the three methods one million times

  4. Using the implementation interface mode, the results of three methods are called millions of times

  

Conclusion: the results are shown in the figure above. It should be noted that the three methods have been called millions of times, that is, 300w method calls

  5. Using the inheritance method, the results of three methods are called millions of times

  

Conclusion: the results are shown in the figure above. It should be noted that the three methods have been called millions of times, that is, 300w method calls

Facts have proved that the implementation of IL Emit has high performance.

Comprehensive analysis:

Through various call analysis, we can see where the performance loss is compared with the native method call after using the proxy. The implementation method with the biggest performance gap and the most time-consuming is to add a full class method proxy and use Invoke for full method faceting. This method is time-consuming because it uses the method of reflecting Invoke.

The method of directly adding Action proxy classes to implement Before and After is not far from the original method, and the main loss is on the disassembly box when After is triggered.

To sum up, when we use it, we try to inject AOP into a method as targeted as possible, but try not to inject AOP into all kinds of methods.

[summary]

By implementing an AOP dynamic injection framework, I have a deeper understanding of Emit. Most importantly, I have a certain understanding of the execution process of CLR IL code and benefit a lot.

Problems were also found in the process of using this method. For example, there will be problems when there are ref and out parameters, which need to be improved in the future

The source code of this article has been hosted on GitHub and can be taken by yourself (Star ~): https://github.com/sevenTiny/CodeArts (you can access the link below to get the. Net Standard cross platform version code)

Latest seventiny Bantina. AOP component code (cross platform): https://github.com/sevenTiny/SevenTiny.Bantina

The location of the code is in codearts Under CSharp partition

  

After VS is opened, it can be found under the EmitDynamicProxy partition; All the test items of this blog can be found in the project.

  

Put the source code address again for the reference of friends studying together. I hope it can help you: https://github.com/sevenTiny/CodeArts (you can access the link below to get the. Net Standard cross platform version code)

Latest seventiny Bantina. AOP component code (cross platform): https://github.com/sevenTiny/SevenTiny.Bantina

 

source: https://www.cnblogs.com/7tiny/p/9657451.html

Topics: ioc