This article was first published in my personal blog huangmb. Github. IO.

preface

AOP, or faceted programming, is simply a technique for dynamically adding functionality to a program at compile time or run time without modifying the source code.

AOP application scenarios

Typical applications of AOP include logging, performance monitoring, buried point reporting, exception handling and so on. It is possible to write business-neutral additional functionality directly into business code, but this is clearly not the style of a “cleanliness freak” programmer. Moreover, these functions often have variable requirements or can contaminate the implementation of business code and become difficult to maintain when mixed together. Non-intrusive AOP is the best choice for “additional functionality”.

Java AOP implementation

In the Java world, AspectJ is the most well-known AOP framework, seen in both client-side Swing projects (compile-time weaving) and Web platform Spring projects (run-time dynamic proxies).

JavaScript version of AOP implementation?

Is there a framework like AspectJ on JavaScript?

I’m currently working on a React Native project, where the woman asks for performance metrics such as request start and end times, data parsing start and end times, and view rendering start and end times for printed pages.

Faced with such requirements, the first thing to think of is to achieve through AOP. After all, it’s not a product manager requirement, it’s not appropriate to write to the business, and it might even affect the performance of the official version; Write a separate version locally, not in the main warehouse, so that the test girl will come back tomorrow for a version, and have to write again on the new version (think it is not impossible).

Back to the “requirement”, Google “JavaScript” + “AOP” keyword, but did not find a suitable framework chrysene (゚ ~ ゚).

Perhaps such a framework is not needed. Fortunately, JS is a syntactically free, weakly typed language that allows dynamic addition and deletion methods, which is the basis for implementations of various AOP frameworks.

So there is this article, a JS version of the AOP implementation.

The theoretical basis of AOP

Monks chant sutras.

AOP generally has the following concepts:

  • Join Point: A place that can be intercepted, usually a member method or property, which can be called a join point.
  • PointCut: specifically positioned join points. Since every method (or attribute) can be used as a join point, we cannot enhance all methods, so the methods we match for enhancement are pointcuts.
  • Advice: Logic code that we add to a specific pointcut to “enhance” existing functionality.
  • Section (Aspect) :

    Section byPoint of tangencyandTo enhanceComposition is the definition of where and how you are going to do what.

Advice generally comes in the following five types:

  • Before: That is, enforce enhancements before join point execution.
  • An after throw is enhanced after a join point throws an exception, generally allowing access to the exception thrown by the join point.
  • Enhancements are implemented after the join point has been properly executed, generally allowing the return value of the join point to be retrieved.
  • After (final) : Enhancements are implemented after the join point is executed. Whether the join point returns normally or throws an exception, the return value is generally not retrieved because it is not known whether it is an exception or a return.
  • A wrap implements enhancements before and after join point execution, and can even make join point execution optional.

implement

Roll up your sleeves and get to work.

Implement pointcuts and facets

As we know, JavaScript objects have prototype objects, and even properties and methods defined in es6 classes are declared in Prototype.

We can use SomeClass. Prototype. MethodName find SomeClass methodName method of a class, in this way, one of the most simple method of matching tangent point is achieved.

We can modify Prototype to redefine the method, for example:

let target = SomeClass;
let pointCut = 'methodName';
/ / point of tangency
let old = target.prototype[pointCut]
/ / section
target.prototype[pointCut] = function () {
    // Pre-enhanced
    console.log(`method ${pointCut} will be invoke`);
    old();
}
Copy the code

Here we redefined the methodName method for the SomeClass class to precede it with a log statement, which is essentially an enhancement of the before type. This code is the simplest example of a pre-enhanced aspect.

Implement enhancements/notifications

Before implementing specific enhancements, define a method to match pointcuts. The simplest version currently matches directly by method name.

let findPointCut = (target, pointCut) = > {
    if (typeof pointCut === 'string') {
        let func = target.prototype[pointCut];
        // Attribute AOP is not currently supported
        if (typeof func === 'function') {
            returnfunc; }}// Fuzzy matching pointcuts are not currently supported
    return null;
};
Copy the code

Finally, we will provide our AOP tools with the following structure, where target is the class to be enhanced, pointCut is the method name to be enhanced, and CB is the callback, the enhancement code to be injected.

let aop = {
    before(target, pointCut, cb) {
    },
    after(target, pointCut, cb) {
    },
    afterReturn(target, pointCut, cb) {
    },
    afterThrow(target, pointCut, cb) {
    },
    around(target, pointCut, cb) {
    }

};
export default aop;
Copy the code

Taking pre-enhancement as an example, we need to pass the connection point information to the enhancement code as long as the most basic target class, target method and original parameters, so as to facilitate the enhancement code to identify the section information.

The join point information also includes a reference to self, the current object. This information is added because the apply and call methods cannot modify the this reference of the enhanced code when the enhanced code is an arrow function. The attributes of the target object can be accessed through self. Callbacks defined using function can access the target object directly using this.

before(target, pointCut, cb = emptyFunc) {

        let old = findPointCut(target, pointCut);
        if (old) {
            target.prototype[pointCut] = function () {
                let self = this;
                let joinPoint = {
                    target,
                    method: old,
                    args: arguments,
                    self
                };
                cb.apply(self, joinPoint);
                return old.apply(self, arguments); }; }}Copy the code

Because the later enhancements are not too different from this, there can be a lot of duplicate code. All enhancements are now encapsulated and all types of enhancements are merged into the Advice method. The whole AOP complete code is as follows:

let emptyFunc = (a)= >{};let findPointCut = (target, pointCut) = > {
    if (typeof pointCut === 'string') {
        let func = target.prototype[pointCut];
        // Attribute AOP is not currently supported
        if (typeof func === 'function') {
            returnfunc; }}// Fuzzy matching pointcuts are not currently supported
    return null;
};
let advice = (target, pointCut, advice = {}) = > {
    let old = findPointCut(target, pointCut);
    if (old) {
        target.prototype[pointCut] = function () {
            let self = this;
            let args = arguments;
            let joinPoint = {
                target,
                method: old,
                args,
                self
            };
            let {before, round, after, afterReturn, afterThrow} = advice;
            // Pre-enhanced
            before && before.apply(self, joinPoint);
            // Surround enhancement
            let roundJoinPoint = joinPoint;
            if (round) {
                roundJoinPoint = Object.assign(joinPoint, {
                    handle: (a)= > {
                        return old.apply(self, arguments|| args); }}); }else {
                // The round enhancement is not declared
                round = (a)= > {
                    old.apply(self, args);
                };
            }


            if (after || afterReturn || afterThrow) {
                let result = null;
                let error = null;
                try {
                    result = round.apply(self, roundJoinPoint);
                    // Return enhancement
                    return afterReturn && afterReturn.call(self, joinPoint, result) || result;
                } catch (e) {
                    error = e;
                    // Exception enhancement
                    let shouldIntercept = afterThrow && afterThrow.call(self, joinPoint, e);
                    if(! shouldIntercept) {throwe; }}finally {
                    // post-enhanceafter && after.call(self, joinPoint, result, error); }}else {
                // Execute the original method without defining any post-enhancement
                returnround.call(self, roundJoinPoint); }}; }};letaop = { before(target, pointCut, before = emptyFunc) { advice(target, pointCut, {before}); }, after(target, pointCut, after = emptyFunc) { advice(target, pointCut, {after}); }, afterReturn(target, pointCut, afterReturn = emptyFunc) { advice(target, pointCut, {afterReturn}); }, afterThrow(target, pointCut, afterThrow = emptyFunc) { advice(target, pointCut, {afterThrow}); }, round(target, pointCut, round = emptyFunc) { advice(target, pointCut, {round}); }};export default aop;
Copy the code

Now our before can be simplified as:

 before(target, pointCut, before = emptyFunc) {
    advice(target, pointCut, {before});
 }
Copy the code

Method of use

Lead before

The pre-enhancement does not interfere with the execution of the original method, with only one parameter, the join point information, accessing the class and method of the pointcut as well as the current parameter and this reference.

import Test from './test';
aop.before(Test, 'test', (joinPoint) => {
    let {target, method, args, self} = joinPoint;
    console.log('Test method will be executed');
});
Copy the code

The rear after

Post-enhancement is executed after the original method completes execution, with parameters that include return results and exceptions in addition to join point information. Because the original method can either return normally or throw an exception, either result or error is null (AspectJ does not do this).

import Test from './test';
aop.after(Test, 'test', (joinPoint, result, error) => {
    let {target, method, args, self} = joinPoint;
    console.log('Test method completed');
});
Copy the code

Return afterReturn

Return enhancement retrieves the return value of the original method, the second argument to the callback. If the return value needs to be modified, return can be enhanced, otherwise use the original return value.

import Test from './test';
aop.afterReturn(Test, 'test', (joinPoint, result) => {
    let {target, method, args, self} = joinPoint;
    console.log('Test method successfully executed');
    // You can modify the return value
    return newResult;
});
Copy the code

Abnormal afterThrow

Exception enhancement is performed when an exception occurs in the original method, and the second parameter to the callback is an exception.

And the callback can use a Boolean value to indicate whether or not an exception is truncated. The exception will not continue to be thrown when return true (AspectJ does not do this).

import Test from './test';
aop.afterThrow(Test, 'test', (joinPoint, error) => {
    let {target, method, args, self} = joinPoint;
    console.log('Test method throws an exception');
});
Copy the code

Orbiting around

The wrap enhancement is the most flexible method, which gives the execution of the original method to the enhancement code to call. A handle method is added to the join point, and the handle method is called manually in the enhancement code. Therefore, the first four enhancement types can be implemented according to the call timing, and the parameters and return values of the original method can be customized. The arround enhancement requires a return result to the caller of the original method

import Test from './test';
aop.around(Test, 'test', (joinPoint, error) => {
    let {target, method, args, self, handle} = joinPoint;
    console.log('Test method about to execute');
    let result = handle(); // Call without arguments is to call the original method with the original arguments
    // let result = handle(args) // Call the original method with the specified argument
    // Result can be processed
    console.log('Test method completed');
    // a result must be returned
    return result;
});
Copy the code

At the end

Thanks to the dynamic nature of the JavaScript language, it is very easy to implement a basic version of AOP with decent functionality, which can be used for common NodeJs, React Native and other projects.

Of course, there are still many shortcomings, such as more flexible aspects, etc. If you have used AspectJ, you probably know that aspects can declare pointcuts through the whole class name, specific annotations, inheritance relationships, fuzzy matching, and many other ways, which undoubtedly makes aop more flexible to use. There are also improvements to the React Component class AOP, which can be implemented as a react-proxy.

Aop may be gradually optimized and improved later, depending on the application scenario.