Aspect Oriented Programming (AOP) and Implementation of Proxy Mechanism
Author/Huang Hanming [Issue Date: 2016/4/6]
Imagine a situation: you write a program to control an aircraft and perform related checks before take-off, such as whether conditions and runway status. The aircraft is ready to take off at the designated runway. This program may look like this:
In requestTakeOff method, the code in line 14 first records "received the request of a takeoff clearance" then the code in line 15 checks if the aircraft is on runway 23. The code in the seventeenth line re-records "Takeoff clearance checked" before a result is returned. This design may seem perfect, but mix the "record" action which doesn't belong to business logic into real business logic, especially that the "record" action occurs during takeoff but also landing. In this way, as the scale of the program gradually expands, it will become incredibly difficult to maintain or modify the true business logic. In addition, it will be very troublesome to modify the mode of this "record" in the future (for example, alter System.out.println to Logger). Further, any modification to the original program code violates Principle 1 of the OCP (Open-Closed Principle).
To avoid such a situation, we must detach the "record" action from the real business logic. After extracting common features that are scattered in various business logic, we then modularize them giving the program low coupling force, high cohesion, and improving its readability and recyclability. This demand promotes the production of AOP (Aspect Oriented Programming).
Aspect Oriented Programming (AOP)
AOP can be considered as "problem-oriented programming," "view-oriented programming" or "aspect-oriented programming," and is mainly used in the development of functions, such as: log record, performance statistics, safety control, transaction processing, and exception handling. In order to extract these functions from the business logic, for example, the concept of AOP can be applied to the "record" function mentioned in the above codes of took-off to extract it from the business logic - runway check.
In AOP, those system-level functions which are inserted into the business logic but are otherwise unrelated to it are called Cross-cutting concerns. The Cross-cutting concerns cross-cut main business logic, resulting in more complex business logic modules. When these Cross-cutting concerns are extracted by AOP, the business logic modules are simplified. Additionally, Cross-cutting concerns will be modularized making them easily applied to a variety of commercial logic and available to modify the functions of these Cross-cutting concerns without modifying the existing business logic. The application of AOP makes our program "open to extension, but closed to modification," which is in line with the OCP principle. The following figure describes the concept of Cross-cutting concerns.
Implement AOP by JAVA
1. Static Proxy
In JAVA, AOP 's implementation method is to use the proxy pattern of the design patterns. The proxy pattern provides a class as an intermediary which is used to work for the original object. In other words, we do not directly access the original object, rather copy through the pipe of the proxy. Considering codes of an aircraft's takeoff program mentioned earlier, the program with UML is as follows after being converted into the proxy mode:
After the take-off program is converted into the proxy mode, a proxy class named TakeOFFproxy is added. As with the original TakeOff class, TakeOffProxy is used to implement the interface ITakeOff, but the proxy class requestTakeOff method is actually used to call the proxied class TakeOff that will perform the actual business logic. In this way, we add needed cross-cutting concerns in the requestTakeOff method of TakeOffProxy. AirportDemo operates on the interface ITakeOff and its operating entity is TakeOffProxy. This may be very abstract, and let's look at the code.
Both the proxy class (TakeOffProxy) and the original class (TakeOff) must be implemented on a common interface defining a method for the two classes' implementation, and here we name this interface ITakeOff. Because ITakeOff's interface has been defined, when want to call requestTakeOff. We can operate on this interface without knowing the exact class of the implementation interface or understanding how to implement it by this method. Even when needing to modify this method's code, we do not need to change the call mode. Return to this section after reading all code if this is unclean now. Next we will look at the code of the proxy class TakeOffProxy.
In the structure, TakeOffProxy binds the data object ItakeOff, which really performs requestTakeOff, and allows this object to perform requestTakeOff in requestTakeOff. Because this is an operation on the interface, TakeOffProxy doesn't need to know the actual implementation and low coupling allows us to freely modify the actual work items of requestTakeOff without considering how other modules might be affected. Finally, TakeOffProxy lets TakeOff handle the real business logic and is only responsible for journal record. In the future we are able to change the implementation of journal record as long as we modify the contents of TakeOffProxy. For example, here we regard log record as a method and replace System.out.println with Logger.
Concerning the proxied class TakeOff — because the log record has been put into TakeOffProxy, TakeOff only needs to handle the real work items of requestTakeOff, which simplifies the code and makes it purer. We also can modify the implementation without worrying how that modification may affect other modules. After talking about the two classes mentioned above, we can see that we have abstracted these cross-cutting concerns (namely journal records), and TakeOffProxy is only responsible for processing journals and TakeOff has only real business logic. Finally, let us see how to use these classes.
Using these classes is very simple. We first operate on the interface ITakeOff In line 15, and then specify the proxy class and the proxied class in line 14. In other words, we specify cross-cutting concerns as TakeOffProxy (to handle journal record) in line 14, while the business logic is handled by TakeOff. Such implementation of proxy mode is called static proxy. It has drawbacks: when there's a large amount of commercial logic, we have to write a lot of proxy classes because different commercial logic needs different call methods. For example, a plane's took-off will call requestTakeOff, but its landing will call the requestLanding and the passed parameters may also be different. This time we may need to create a number of different classes to handle the same transaction (journal record). To solve this kind of problem, we must use the reflection mechanism provided by JAVA.
2. Dynamic Proxy
The proxy implemented by using the JAVA reflection mechanism is called dynamic proxy. Dynamic proxy has the same concept but a different implementation mode from that of a static proxy. Here is the program code.
We must define an interface to implement the proxied class and in this interface specify methods that require implementation. Dynamic proxy is somewhat different from static proxy: the proxy class do not need to implement this interface. Further, they do not need to understand which interfaces should be implemented. Each is in charge of its own affairs (e.g., journal records in this case) when implement as a dynamic proxy. Next we will describe how to write proxy classes when implementing a dynamic proxy.
To implement dynamic agency, we must establish a Handler, and this Handler must implement the interface java.lang.reflect.InvocationHandler and the method invoke defined in this interface. We can see that, apart from the method invoke, other parts are almost identical with a static proxy except that the object in line 19 changes from original ITakeOff type to Object type. Object type is the parent class of all objects and therefore is able to receive entities of any other types. For this very reason, LogHandler only needs to concentrate on dealing with journals without caring which kind of business logic it has crossed into.
The LogHandler in the invoke method still delegates real business logic to the proxied class, but this time it doesn't know who the proxied class (because the proxied class has been bound to Object type) or the name of the real business logic. LogHandler calls real business logic only through method.invoke, and in this way it is completely decoupled from the business logic.
The proxied class in the dynamic proxy is only in charge with processing business logic, which is the same as with that in the static proxy. Next, let us see the usage of the dynamic proxy.
From line 16 we can see that the parameter passed into the structure LogHandler is the entity TakeOff, which indicates it is the class TakeOff that actually performs business logic. In line 17 a proxy class is dynamically established by using the JAVA reflection mechanism and then in line 19 business logic will be executed through this proxy class. The benefit of the dynamic proxy is that we needn't write new proxy classes when encountering different business logic, such as the preceding example in which if want to "land" instead of "take off", we only change the way of calling in AirportDemo by replacing new TakeOff(), TakeOff.class with new Landing() and Landing.class separately to change the business logic (such as landing.requestLanding()), completely without modifying LogHandler.
Of course, the dynamic proxy has disadvantages, which are the same as with the static proxy. Because the proxy class in the dynamic proxy is dynamically generated by JAVA during runtime, the program's execution performance will be affected indirectly, which is the one fly in the ointment of JAVA dynamic proxy.
3. Dynamic Proxy Binds Multiple Cross-cutting Concerns
Sharp-eyed programmers may find that both static and dynamic proxies in the previous examples only have a cross-cutting concern (journal processing) and both the proxy class and Handler's structure in the two implementations only receive one kind of proxied class. In other words, all of the existing examples only have a cross-cutting concern, however, the general system usually has more than one cross-cutting concern including privilege check, exception handling apart from journal record. In this example, apart from runway check before the airplane's take-off, you may also need to check whether this aircraft has approved the flight, as a result, more than one proxied classes or Handlers will be created and bound. How shall we solve this problem?
In fact, to solve this problem, we just need to make a few changes to the dynamic proxy: replace LogHandler with a generic Handler and bind multiple Handles by calling other Handlers in it. Let us see the code.
Since our aim is to bind multiple cross-cutting concerns instead of modifying the business logic, business logic and corresponding implementation interface need not to be modified. What needs more modification is GneHandler, the general Handler. We need to call other Handlers in GneHandler and to facilitate calling, we must define a common interface –AOPHandler.
This interface defines two methods, beforeInvoke and afterInvoke. These two methods control the Handler that implements AOPHandler, respectively defining what should be done before and after the business logic is executed. Let's see the GneHandler that calls these Handlers to better understand the need for defining the AOPHandler in this way.
GneHandler is rewritten from the LogHandler in the previous examples. In fact, its role is exactly the same with LogHandler, but here we modify "journal record" to "calling other Handlers". We can see that it calls each Handler's beforeInvoke sequentially before real business logic is executed and afterInvoke after real business logic is executed. In this way, we can achieve the objective of binding multiple Handlers. Next we talk about the usage of GneHandler.
Rewritten from the dynamic proxy, it has basically the same architecture as with the dynamic proxy. What we see here is code from line 20 to line 24 and we find that different Handlers are added into handlers ArrayList and passed in GneHandler's structure to let GneHandler sequentially trigger these handlers. Of course, these Handlers handle different transactions and modification of these Handlers will not affect other Handlers.
AOP is just a concept, and both the static proxy and the dynamic proxy are merely AOP implementations that only need to meet AOP's concept. In addition to the static and dynamic proxies, there are other frameworks used to implement AOP among which the most well-known AOP framework in the field of JAVA is currently AspectJ. If you are interested, go to eclipse.org/aspectj to learn more. Although AOP brings many benefits, it cannot be denied that the implementation of AOP slightly complicates programming and therefore we best not overuse AOP. When using AOP, a detailed description of the file should be included to help new employees quickly understand the application architecture.
This article is from RUN!PC.
1. The OCP principle is proposed by Bertrand Meyer in his talk, "Software entities should be open for extension but closed for modification." If a module's functionality is extensible (newly added), it is open for extension, in addition, if a module is called by other modules and cannot be modified, it is closed for modification. In other words, "Open for extension but closed for modification" means that the software should deal with changes through functionality extension rather than modifying the original code to adapt to change. (The author serves the SYSCOM GROUP and contributes to RUN!PC.)
21. Guidelines for Aspect-Oriented Design, Christina von Flach G. Chavez, Carlos J.P