[Introduction]
Aspect-oriented programming (AOP) is a programming paradigm invented by Xerox PARC in the 1990s s of the Xerox Corporation, it allows developers to better separate tasks that shouldn't be entangled with each other (such as mathematical operations and Exception Handling. The AOP method has many advantages. First, because the operation is more concise, the performance is improved. Second, it makes Program Members can spend less time rewriting the same Code . In short, AOP can provide better encapsulation for different processes and improve future interoperability.
What makes software engineers want to become hardware engineers? Since the invention of the function, programmers have spent a lot of time (and most of their boss's money) trying to design such a system: they are just a combination of models, made up of parts created by others, it is arranged in a unique shape and then covered with some pleasing colors. Functions, templates, classes, components, and so on are all attempts by software engineers to create "software integrated circuits" (simulating the electronic devices of hardware designers.
All of these are attributed to Lego ). The cool click made when two pieces of toys (that is, components) are combined is addictive, which will prompt many programmers to invent a new encapsulation and reuse mechanism. The latest developments in this area are called Aspect-oriented programming (AOP: Aspect-Oriented Programming ). The core of AOP is to arrange components (one stacked on the other), so that you can obtain the level of reuse that cannot be obtained by other component-based development methods. This arrangement is performed in the call stack between the client and the object. The result is that a specific environment is created for the object. This environment is exactly what AOP programmers are pursuing. Continue to read this article and you will understand this.
The sample code provided in this article is divided into two parts: COM and Microsoft. NET. A basic structure is created for the com part. You can add an aspect to the COM Object (Editor's note: the original English text is aspect, and the word is translated as "aspect" after this article "), the user interface is provided to configure the class, and an example is provided for implementation in the basic structure we provide. The. NET section describes how to use the built-in. net infrastructure to complete the same job for the com version, but the code is less and more options are used. It also provides examples that suit this infrastructure. This article will describe all these codes later.
What is AOP?
Generally, an object is "bonded" by a line of code. Create this object. Create the object. Set attributes for the object (whose value is this object. Some user data is also dotted. Stir everything together. It can be executed when the running time reaches 450 degrees Celsius. Connecting multiple components in this way can lead to the following problem: to implement different methods, it takes a lot of time to write the same code. These lines of code usually have the following operations: log the activity of this method into a file for debugging, run the security check, start a transaction, open a database connection, remember to capture C ++ exceptions or Win32 structured exceptions to convert to com exceptions, and verify the parameters. Also, remember the settings when the destruction method starts after the method is executed.
This type of repetitive code often occurs because developers are trained to design the system based on the terms in the software press release. If the banking system is designed, account and customer categories are essential, they collect their unique details in one place, however, each of these methods also requires operations such as logs, security checks, and transaction management. The difference is that operations such as logs are system aspects unrelated to specific applications. Everyone needs them. Everyone writes this code. Everyone hates this.
Oh, not everyone ...... Everyone needs to use these services, and everyone hates writing repetitive code, but not everyone needs to write them. For example, COM + and. Net programmers can perform so-called attribute-based programming, also known as Declarative Programming. This technology allows programmers to use attributes to modify types or methods and declare that certain services need to be provided at runtime. For example, some services provided by COM +, including role-based security, real-time activation, distributed transaction management, and packet aling processing. When calling a method, Runtime (Editor's note: runtime, refers. the software running environment provided by the. NET Framework is placed into a group of objects obtained between the client and the server (for COM + programmers, it is called a "listener",. net programmers, called "message receiving"), provides services for each method, without the need for component developers to write any code. This is the simplest form of Aspect-Oriented Programming.
In the field of AOP, the COM + listener is related to components through metadata. Metadata is used to construct this set of items during runtime, which is usually performed when an object is created. When the client calls a method, the special aspects in turn obtain the opportunity to process the call, execute the service, and finally call the method of the object. Each aspect has the opportunity to expand. In this way, you can extract the same lines of code to be written in each method of each component and put them in various aspects for the runtime to place them. This group provides context for the execution of component methods. Context provides the implementation of methods in the environment, and the operations have special significance.
For example, figure 1 shows an object securely stored in the context, which provides error propagation, transaction management, and synchronization. The programs in the Win32 console application can assume that the console exists in the context and the result of calling printf is displayed on the console. Similarly, the AOP object can assume that the transaction has been established, the transaction contains the part that calls the database. If a problem occurs when you set these services (for example, there are not enough resources to establish a transaction), the object will not be called, so you don't have to worry about it.
General use AOP
Although COM + provides most of the services required by AOP, to use it as a general-purpose AOP environment, it lacks the necessary important details: the ability to define custom aspects. For example, if role-based security is not suitable, role-based security cannot be implemented (as if the most dangerous person is allowed to protect their own objects ). If programmers have this capability, many com usages can be implemented using the AOP framework. Figure 2 provides a brief list of examples.
Exception and error handling
Transaction Management
LOG method call
Asynchronous Method call
Security check
Extended Binary Component Automation to enable expando objects
Contractual design similar to Eifel
Parameter Verification
Figure 2 com usage for AOP
Design Framework
Of course, with such a framework concept, we must build it. We hope this framework has the following features:
Q is the runtime that Concatenates the client and the object.
Q user-defined aspects are implemented using COM components.
Q: Which of the following is the metadata description associated with each COM component, like the COM + directory.
Q can be used by the client to activate the component when the component is ready.
The concept of the AOP framework is very simple. The key is listening and delegation. The method of listening is to convince the caller that the interface pointer it holds points to the object it requested. In fact, this is a pointer to the listener, you can use one of the activation techniques described later in this article to obtain the listener. The listener needs to implement the same interface as the target component, but it needs to delegate all calls through the aspect stack associated with the component. When calling a method, the listener will provide the opportunity for preprocessing and post-processing calls for each aspect, as well as the ability to spread or cancel the current call.
The AOP framework performs two different steps: component activation and method calling. In component activation, the AOP framework implements the object stack and returns a reference to the listener instead of a reference to the actual object. In a method call, when the caller calls a method call to the listener, the listener delegates the call to all registered aspects so that the [in] and [in, out] the parameter is preprocessed and the actual call is provided to the object. Then, the call return values returned by the component and the [In, out] and [out] parameters on the call Stack are passed to the party for post-processing.
As a COM Object
In our AOP framework, we implement the com class of the iaspect interface, as shown in 3.
Interface iaspect: iunknown {
Hresult preprocess ([in] iunknown * punkdelegatee,
[In] BSTR riid,
[In] BSTR strmethodname,
[In] Long nvtblslot,
[In] iunknown * penum );
Hresult postprocess ([in] hresult hroriginal,
[In] iunknown * punkdelegatee,
[In] BSTR riid,
[In] BSTR strmethodname,
[In] Long nvtblslot,
[In] iunknown * penum );
}
The Framework calls the iaspect: preprocess method in all specified aspects before passing the method call to the actual underlying component instance (hereinafter referred to as the principal. It passes the identity of the principal, the IID of the interface, the method name, The vtbl slot of the method, and the enumerator of the [in] and [In, out] parameters to the corresponding aspect. If a failed hresult is returned from preprocess, the framework does not provide the call to the delegated user. In fact, the call is canceled.
After preprocessing is successful, the Framework provides the actual call to the delegate. The framework will call the iaspect: postprocess method to pass all the parameters required by the hresult and postprocess Methods returned by the delegate, whether or not the delegate returns the hresult, however, this time the enumerator is built on the [out], [In, out], and [out, retval] parameters.
Figure 4 shows how to write call tracing. It can trace the parameters provided by all callers passing to the delegate method.
Figure 4 call-tracing aspect
Class ccalltracingaspect: Public iaspect ,...{
Public:
Begin_category_map (ccalltracingaspect)
Implemented_category (catid_aspects)
End_category_map ()
Stdmethodimp preprocess (...)
{Return dumpstack (true, riid, strmethodname, penum );}
Stdmethodimp postprocess (...)
{Return dumpstack (false, riid, strmethodname, penum );}
Hresult dumpstack (bool preprocess, BSTR riid,
BSTR strmethodname, iunknown * penum ){
If (preprocess) atltrace ("preprocessing: % s (", strmethodname );
Else atltrace ("postprocessing: % s (", strmethodname );
Ccomptr <ienumvariant> spenumvar;
Penum-> QueryInterface (& spenumvar );
Ccomvariant V;
Bool bneedcomma = false;
While (spenumvar-> next (1, & V, 0) = s_ OK ){
If (bneedcomma) atltrace (",");
Else bneedcomma = true;
Atltrace ("% s", tostring (V ));
}
Atltrace (") \ n ");
Return s_ OK;
}
...
};
Now that we have a framework for calling and a usable aspect, we need a mechanism to concatenate them. This operation is performed when the object is activated.
Object activation
Although we need to stack up any number of aspects between the client and the object, the client should be able to create an object and call its method, just as in the case of no listening. Unfortunately, if COM does not adopt some fancy technical means (this is exactly what Microsoft Transaction Service must implement before it is integrated into the com infrastructure and renamed as COM + ), it does not support any extended code injected into its master activation API cocreateinstance. However, COM does provide a fully extended activation API: GetObject in Visual Basic (cogetobject for C ++ programmers ). We use a custom name object to construct the AOP activation code based on this API.
A com name object is a piece of code that converts any string (called the display name) to a COM Object. This means you can create a new one or find one from the file, even download from the moon. Our AOP name object obtains metadata (describes the aspects associated with the classes discussed here), creates instances of this class, constructs the aspect stack, and concatenates them through the AOP listener, then, return the listener to the client. The following is an example:
Private sub form_load ()
Set myfoo = GetObject ("aoactivator: C: \ aopfoo. xml ")
Myfoo. dosomethingfooish
End sub
Note that the client does not need any special operations to use components except to obtain the foo instance. Although the aopfoo. xml file associates any number of aspects with the specific instance of Foo, it implements the same interface, and more importantly, it has the same semantics.
Implementing a custom com name object is a magical technique in a sense, mainly involving the internal knowledge of previous Ole details. Fortunately, most of the implementation content is lazy, and COMCommunityA long time ago, the basic implementation of the name object was written into an ATL class called ccommoniker. (Visit http://www.sellsbrothers.com/toolsto obtain the framework of the comname .) To use this framework, we really need to care about parsedisplayname (this is a boring method for analyzing custom display name syntax) and bindtoobject (part of the name object, this name object is used to activate the COM Object indicated by the display name provided by the client) (see figure 5 ).
Figure 5 creating the object in bindtoobject
Stdmethodimp caopfactory: bindtoobject (
Ibindctx * PBC, imoniker * pmktoleft, refiid riidresult,
Void ** ppvresult ){
// Parsedisplayname has already pulled in the metadata from
// The XML file supplied by the client
// Create the object to be hosted in our AOP Environment
Ccomptr <iunknown> spcomp;
Hresult hR = spcomp. cocreateinstance (m_clsid );
If (failed (HR) return hr;
// Create our interceptor and return it the client
... // Magic happens...
}
Note that the code in Figure 5 does not show the most difficult part-creating and initializing the listener. The difficulty lies not in the listener itself, but in what the listener must do. Keep in mind that our general AOP framework must be able to respond to the QueryInterface method with a set of interfaces that are exactly the same as any encapsulated component. The returned interface must be able to obtain the call stack provided by the client of each method, pass it to all aspects, and always pass it to the component itself to keep the parameters complete-no matter how many, and the type. This is a very difficult task, involving a large number of _ declspec (naked) and ASM thunk.
Fortunately, because the com community is very mature, we were able to stand on the shoulders of giants using the universal delegate (UD), a COM component created by Keith Brown to execute this task. Keith has written in two parts in MSJ to describe his UD,ArticleThe name is "Building a lightweight com interception framework, Part I: the universal delegator" (partition), and Part II: "the guts of the UD" (partition). We can use the UD of Keith to implement the AOP framework, which reduces the "magic" part of the bindtoobject implementation, as shown in 6.
Figure 6 rest of bindtoobject implementation
Stdmethodimp caopfactory: bindtoobject (
Ibindctx * PBC, imoniker * pmktoleft, refiid riidresult,
Void ** ppvresult ){
...
// Create our interceptor and return it the client
// Create and initialize our hook
Ccomobject <Chook> * phook;
HR = phook-> createinstance (& phook );
If (failed (HR) return hr;
Ccomptr <idelegatorhookqi> sphook;
HR = phook-> QueryInterface (& sphook );
If (failed (HR) return hr;
HR = phook-> setaspects (m_displayname, m_aspects.size (),
& M_aspects [0]);
If (failed (HR) return hr;
// Create the udfactory
Ccomptr <idelegatorfactory> spdel;
HR = cogetclassobject (_ uuidof (codelegator21), clsctx_inproc, 0,
_ Uuidof (idelegatorfactory), (void **) & spdel );
If (failed (HR) return hr;
// Create the interceptor
HR = spdel-> createdelegator (0, spcomp, 0, sphook, 0,
Riidresult, ppvresult );
}
To wrap the target component for the client, perform the following four steps:
1. Use the CLSID of the component to create the actual component. The CLSID is passed to the name object originally in the metadata XML file.
2. A delegatorhook object is created to listen for the QueryInterface call to the object. The hook is responsible for routing method calls to every aspect.
3. Create a UD object to retrieve the idelegatorfactory interface.
4. Call createdelegator using idelegatorfactory to pass the interface of the actual object, the delegate Hook, the IID (riidresult) of the interface requested by the source caller, And the pointer to the interface pointer (ppvresult ). The delegate returns a pointer to the listener, which can call the delegate hook of each call.
Result 7 is displayed. The client can use the listener as the actual interface pointer of the target component. After the call, they are routed along the path to the target component.
Aspect Builder
To activate components and link all aspects correctly, our AOP name object relies on an XML file to describe components and associated aspects. The format is very simple. It only contains the CLSID of the component and the CLSID of the aspect component. The example in Figure 8 encapsulates Microsoft FlexGrid Control in two aspects.
Figure 8 wrapping the Microsoft FlexGrid Control
<Aopframework>
<Component name = "Microsoft FlexGrid Control, version 6.0">
<CLSID> {6262d3a0-531b-11cf-91f6-c2863c0000e30} </CLSID>
</Component>
<Aspects>
<Aspect name = "Call-tracing aspect">
<CLSID> {49efa33a-fdb2-4aed-807e-4d447d096642}
</CLSID>
</Aspect>
<Aspect name = "synchronization aspect">
<CLSID> {6dba0579-8846-46a2-beff-382725a1022c}
</CLSID>
</Aspect>
</Aspects>
</Aopframework>
To simplify the task of creating an AOP metadata instance, we created aspect Builder (9 ).
The aspect builder will enumerate all aspects registered on the machine and display them in the List View on the right using a cloud map. The client area of aspect builder contains the graphical representation of the component. You can double-click it (or use the corresponding menu item) and specify the progid of the component. After selecting a component, you can drag and drop the aspect to the client area and add the aspect to the AOP metadata of the component.
To generate the XML format required for the AOP name object, select the "compile" menu item in the "Tools" menu, and the metadata will be displayed in the bottom pane. You can write scripts in the verify aspects pane to verify that the metadata is correct. You can save compiled XML instances on disks or use aspect builder to reload them.
. Net
Although the aspect builder greatly simplifies the work, the aspect metadata is stored separately from the component, which makes it inconvenient to program AOP in COM. Unfortunately, the metadata of COM lacks many necessary functions in terms of scalability, which is why we feel that metadata and classes need to be stored separately first. However, as a clear successor to com,. Net does not have this problem .. Net metadata is completely scalable, so it has all the necessary foundations and can be directly associated with the class itself through attributes. For example, given a custom. Net attribute, we can easily associate the Call trace attribute with the. NET method:
Public class bar {
[Calltracingattribute ("in bar ctor")]
Public Bar (){}
[Calltracingattribute ("in bar. Calculate Method")]
Public int calculate (int x, int y) {return X + Y ;}
}
Note that the square brackets contain the calltracingattribute and the string output when accessing the method. This is the property syntax that associates the custom metadata with the two bar methods.
Like the AOP framework in COM, attributes in. NET are classified based on components in. net .. The Custom Attributes in. NET are implemented using classes derived from attributes, as shown below:
Using system;
Using system. reflection;
[Attributeusage (attributetargets. classmembers,
Allowmultiple = false)]
Public class calltracingattribute: attribute {
Public calltracingattribute (string s ){
Console. writeline (s );
}
}
Our attribute classes also have attributes that modify their behavior. In this case, we require that this attribute be associated only with the method, instead of the assembly, class, or field, and each method can have only one trace attribute associated with it.
After we associate attributes with methods, we are half done. To provide the AOP function, you also need to access the call stack before and after the environment necessary for each method to establish the execution component. This requires a listener and the context on which the component depends. In COM, we require the client to activate the component using the AOP name object to implement this task. Fortunately,. Net has built-in hooks, so the client does not have to do any special work.
Context bound object
The key to listening in. Net (the same as in COM) is to provide context for COM components. In the COM + and custom AOP frameworks, by stacking between the client and the object, context is provided for the component before the method is executed. In. net, context is provided for any class derived from system. contextboundobject:
Public class liketolivealone: contextboundobject {...}
When the liketolivealone class instance is activated, a separate context is automatically created during. Net runtime for its survival, and a listener is created, from which we can suspend our own aspects .. Net Listener is a combination of two objects-transparent proxy and real proxy. The behavior of the transparent proxy is the same as that of the target object, and the same as that of the com aop listener. It serializes the call stack into an object called a message and then delivers the message to the real proxy. The real proxy receives the message and sends it to the first message for processing. The first message receiving processes the message, sends it to the next message receiving in the message receiving stack between the client and the object, and then processes the message. The next message is also received, and so on, until it reaches the receiving of the stack builder, which deserializes the message back to the call stack, calls the object, serializes the outbound parameters and return values, and return to the previous message reception. This call chain 10 is shown.
To participate in this message receiving chain, we first need to derive attributes from contextattribute (not just attribute), and provide the so-called context attributes to update the attributes to participate in the context binding object:
[Attributeusage (attributetargets. Class)]
Public class calltracingattribute: contextattribute {
Public calltracingattribute ():
Base ("calltrace "){}
Public override void
Getpropertiesfornewcontext
(Iconstructioncallmessage CCM ){
CCM. contextproperties. Add (New
Calltracingproperty ());
}
...
}
When this object is activated, the getpropertiesfornewcontext method is called for each context attribute. In this way, we can add our context attributes to the attribute list associated with the new context created for the object. Context attribute allows us to associate a message receipt with an object in the message receipt chain. The property class implements icontextobject and icontextobjectsink as the factory for receiving messages:
Public class calltracingproperty: icontextproperty,
Icontributeobjectsink {
Public imessagesink getobjectsink (externalbyrefobject O,
Imessagesink next ){
Return new calltracingaspect (next );
}
...
}
The process of creating attributes for the proxy is shown in 11, where context attributes are created first, and then message receipt is created.
. Net
When everything is correctly appended, each call will go to the imessagesink implementation. Syncprocessmessage allows us to pre-process and post-process messages, as shown in Figure 12.
Figure 12. Net call-tracing aspect
Internal class calltracingaspect: imessagesink {
Private imessagesink m_next;
Private string m_typeandname;
Internal calltracingaspect (imessagesink next ){
// Cache the next sink in the chain
M_next = next;
}
Public iMessage syncprocessmessage (iMessage MSG ){
Preprocess (MSG );
IMessage returnmethod = m_next.syncprocessmessage (MSG );
Postprocess (MSG, returnmethod );
Return returnmethod;
}
Private void preprocess (iMessage MSG ){
// We only want to process method CILS
If (! (MSG is imethodmessage) return;
Imethodmessage call = MSG as imethodmessage;
Type T = type. GetType (call. typename );
M_typeandname = T. Name + "." + call. methodname;
Console. Write ("preprocessing:" + m_typeandname + "(");
// Loop through the [in] Parameters
For (INT I = 0; I <call. argcount; ++ I ){
If (I> 0) console. Write (",");
Console. Write (call. getargname (I) + "=" + call. getarg (I ));
}
Console. writeline (")");
// Set us up in the callcontext
Call. logicalcallcontext. setdata (contextname, this );
}
Private void postprocess (iMessage MSG, iMessage msgreturn)
{
// We only want to process method return CILS
If (! (MSG is imethodmessage) |
! (Msgreturn is imethodreturnmessage) return;
Imethodreturnmessage retmsg = (imethodreturnmessage) msgreturn;
Console. Write ("postprocessing :");
Exception E = retmsg. exception;
If (E! = NULL ){
Console. writeline ("exception was thrown:" + E );
Return;
}
// Loop through all the [out] Parameters
Console. Write (m_typeandname + "(");
If (retmsg. outargcount> 0 ){
Console. Write ("out parameters [");
For (INT I = 0; I <retmsg. outargcount; ++ I ){
If (I> 0) console. Write (",");
Console. Write (retmsg. getoutargname (I) + "=" +
Retmsg. getoutarg (I ));
}
Console. Write ("]");
}
If (retmsg. returnvalue. GetType ()! = Typeof (void ))
Console. Write ("returned [" + retmsg. returnvalue + "]");
Console. writeline (")");
}
...
}
Finally, you want to use calltracingattribute to declare your preference for the context binding class associated with call tracing:
[AOP. Experiments. calltracingattribute ()]
Public class traceme: contextboundobject {
Public int returnfive (string s ){
Return 5;
}
}
Note that context attributes are associated with classes rather than each method .. The net context architecture will automatically notify us of each method, so our call trace attributes have all the required information, which avoids the previous processing of common attributes, it is difficult to manually associate attributes with each method. When the client class instantiates the class and calls a method, the aspect is activated:
Public class client {
Public static void main (){
Traceme = new traceme ();
Traceme. returnfive ("stringarg ");
}
}
During runtime, the client and the object-oriented objects output the following content:
Preprocessing: traceme. returnfive (S = stringarg)
Postprocessing: traceme. returnfive (returned [5])
Aspect and context
So far, we have not yet achieved the expected AOP ideal in this simple aspect. Although it can indeed be used for separate preprocessing and post-processing of method calls, what is really interesting is the impact of the aspect on method execution itself. For example, in the aspect of COM + transactions, all resource providers used in the object method are involved in the same transaction. In this way, only the transactions provided by COM + transactions can be aborted. Therefore, COM + adds the com call context, which provides a gathering point for all components that are interested in accessing the current transaction. Similarly,. NET provides an extensible call context that allows methods to participate. For example, you can place the object in the. NET context so that the object (which is encapsulated in call tracing) can add information to the trace message stream, as shown below:
Internal class calltracingaspect: imessagesink {
Public static string contextname {
Get {return "calltrace ";}
}
private void preprocess (iMessage MSG) {
...
// set us up in the call context
call. logicalcallcontext. setdata (contextname, this);
}< br>...
}< br> once an aspect is added to the call context, the method can be extracted again and used in the trail:
[calltracingattribute ()]
public class traceme: contextboundobject
Public int returnfive (string s)
Object OBJ =
callcontext. getdata (calltracingaspect. contextname);
calltracin Gaspect aspect = (calltracingaspect) OBJ;
aspect. trace ("Inside methodcall");
return 5;
}< br> adds the call context by providing a method ,. net allows you to set a real environment for the object. In our example, it is similar to the transactional aspect of COM + that allows an object to add a trace statement to a stream without having to know the stream destination, how to create a stream, and when to destroy the stream:
preprocessing: traceme. returnfive (S = stringarg)
During: traceme. returnfive: Inside methodcall
postprocessing: traceme. returnfive (returned [5])
conclusion
with Aspect-Oriented Programming, developers can encapsulate the use of public services across components in the same way as encapsulated components themselves. By using metadata and listeners, you can place any service between the client and the object. Such operations are semi-seamless in COM and seamless in. net. This article describes how to access the call stack when calling methods. They provide an added context for objects to survive. Although it is not mature compared with structured or object-oriented programming, local support for AOP in. NET provides a valuable tool for us to pursue software dreams like Lego toys.