|
| 1 | +--- |
| 2 | +layout: page |
| 3 | +title: "Using AOP/1" |
| 4 | +date: 2016-09-16 20:00 |
| 5 | +comments: false |
| 6 | +sharing: false |
| 7 | +footer: true |
| 8 | +--- |
| 9 | +_This is documentation for the upcoming 4.1 release. For the current release, see [this documentation](/documentation/)._ |
| 10 | + |
| 11 | +AOP/1 is a simple Aspect Oriented Programming extension for [DI/1 (a.k.a Inject One)](using-di-one.html) which allows you to define interceptors for your beans. |
| 12 | + |
| 13 | +These interceptors can run code before or after a method is called on a bean without the need for you to alter the code in your bean. This allows you to create generic services (such as a logger service) that is coded and configured to operate completely separate from your other services and beans. What this means is you no longer need to mix unrelated service code together by using dedicated interceptors. |
| 14 | + |
| 15 | +_The information below assumes that you already have a good working knowledge of DI/1._ |
| 16 | + |
| 17 | +* TOC |
| 18 | +{:toc} |
| 19 | + |
| 20 | +# Getting Started with AOP/1 |
| 21 | + |
| 22 | +Create an instance of the AOP/1 extended DI/1 bean factory and specify the folder(s) you want it to search for beans. |
| 23 | + |
| 24 | + var beanFactory = new framework.aop("/model"); |
| 25 | + |
| 26 | +So far nothing difficult since this is what we would typically see from [DI/1](using-di-one.html). Now, if we want to intercept method calls to an object, we need to declare the interceptors and the objects that should be intercepted. |
| 27 | + |
| 28 | + var beanFactory = new framework.aop("/model"); |
| 29 | + |
| 30 | + beanFactory.intercept("pdfService", "beforeInterceptor"); |
| 31 | + beanFactory.intercept("pdfService", "afterInterceptor", "createDocument"); |
| 32 | + |
| 33 | + var ps = beanFactory.getBean("pdfService"); |
| 34 | + |
| 35 | + var document = ps.createDocument("http://seancorfield.github.io"); |
| 36 | + var pages = ps.splitPages(document); |
| 37 | + |
| 38 | +In this example the `beforeInterceptor` will intercept every call to the `pdfService()`, but the `afterInterceptor` will only intercept calls to the `createDocument()` method. Due to AOP/1 creating intercept points on the bean being intercepted, it is generally recommended to list the methods to be intercepted when declaring the interceptors so there are not unnecessary calls made on other methods. |
| 39 | + |
| 40 | +## Creating Interceptors |
| 41 | + |
| 42 | +A common practice for DI/1 is to place beans and services within a model folder like so: |
| 43 | + |
| 44 | +* `/model/beans/` |
| 45 | +* `/model/services/` |
| 46 | + |
| 47 | +Interceptors can follow this pattern in order to make it simple for the factory to locate the interceptors with the rest of the model. |
| 48 | + |
| 49 | +* `/model/interceptors/` |
| 50 | + |
| 51 | +### Before Interceptors |
| 52 | + |
| 53 | +Before interceptors will intercept method calls _before_ they are executed. They cannot affect the result of a method call, but they can be used to alter the arguments going to the method call and they can perform operation that you wish to be performed before the method call. In order for an interceptor to operate as a before interceptor, it only needs the `before()` method to be defined. |
| 54 | + |
| 55 | + component { |
| 56 | + function before(targetBean, methodName, args) { |
| 57 | + arguments.args.input = "before" & arguments.args.input; |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | +Because the interceptor is like any other bean handled by DI/1, dependencies can be intjected into the interceptor and used by the interceptor. |
| 62 | + |
| 63 | + component { |
| 64 | + property logService; |
| 65 | + |
| 66 | + function before(targetBean, methodName, args) { |
| 67 | + getLogService().logMethodCall(arguments.methodName, arguments.args); |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | +### After Interceptors |
| 72 | + |
| 73 | +Just like the name implies, after interceptors will intercept method calls _after_ they are executed. They cannot affect the arguments going to the method call, but they can monitor or alter the result of the method call. In order for an interceptor to operate as an after interceptor, it only needs the `after()` method to be defined. |
| 74 | + |
| 75 | + component { |
| 76 | + property logService; |
| 77 | + |
| 78 | + function after(targetBean, methodName, args, result) { |
| 79 | + if (structKeyExists(arguments, "result) && !isNull(arguments.result) { |
| 80 | + getLogService().logMethodCallResult(arguments.methodName, arguments.args, arguments.result); |
| 81 | + } |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | +Should you wish to alter the result being returned, all that is needed is to return something from the `after()` method. |
| 86 | + |
| 87 | + component { |
| 88 | + function after(targetBean, methodName, args, result) { |
| 89 | + if (structKeyExists(arguments, "result) && !isNull(arguments.result) { |
| 90 | + return arguments.result & "After"; |
| 91 | + } |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | +### onError Interceptors |
| 96 | + |
| 97 | +onError interceptors allow errors that occur during the execution of intercepted method calls to be handled outside the normal flow of model execution. This can be used for situations where the normal error handling of your application will not produce the desired result. |
| 98 | + |
| 99 | + component { |
| 100 | + property logService; |
| 101 | + |
| 102 | + function onError(targetBean, methodName, args, exception) { |
| 103 | + getLogService().logException(arguments.methodName, arguments.args, arguments.exception); |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | +### Around Interceptors |
| 108 | + |
| 109 | +Around interceptors are an interesting interceptor, because unlike other interceptor types, an around interceptor can actually stop execution of an intercepted method. This accomplished because, unlike before, after, and onError interceptors which are called externally in the order they are defined in their stacks, the around interceptors always call the next interceptor in their stack. |
| 110 | + |
| 111 | +Calling the next interceptor in the stack for around interceptors is accomplished by calling the `proceed()` method. The `proceed()` method is automatically added to any interceptor that has an `around()` method. An around interceptor can stop the execution chain by simply not calling the `proceed()` method. The around interceptors can preform the actions of both before and after interceptors as well. |
| 112 | + |
| 113 | + component { |
| 114 | + property logService; |
| 115 | + property userService; |
| 116 | + |
| 117 | + function around(targetBean, methodName, args) { |
| 118 | + // Perform 'before' arguments manipulation. |
| 119 | + arguments.args.name = getUserService().getCurrentUser().getName(); |
| 120 | + |
| 121 | + if (getUserService().getCurrentUser().hasPermission("administrator")) { |
| 122 | + var result = proceed(arguments.targetBean, arguments.methodName, arguments.args); |
| 123 | + |
| 124 | + if (!isNull(result)) |
| 125 | + { |
| 126 | + getLogService().logMethodCallResult(arguments.methodName, arguments.args, result); |
| 127 | + |
| 128 | + return result; |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | +Since an around interceptor may intercept multiple methods, the method must be able to handle any type of result being returned (including void/null). The example above demonstrates handling when a result is present and demonstrates how the execution chain can be stopped by not calling the `proceed()` method if the current user is not an 'administrator'. |
| 135 | + |
| 136 | +# Advanced Usage & Understanding |
| 137 | + |
| 138 | +The following section explains additional features and concepts that may prove useful when implementing AOP/1. |
| 139 | + |
| 140 | +## Loading Interceptors Via Configuration |
| 141 | + |
| 142 | +AOP/1 extends DI/1 so it has access to the `config` parameter of the constructor. |
| 143 | + |
| 144 | + var interceptors = [{beanName = "stringUtilityService", interceptorName = "afterInterceptor"}]; |
| 145 | + var factory = new framework.aop(folders, {interceptors = interceptors}); |
| 146 | + |
| 147 | +The `interceptors` configuration is just an array of structures that define the interceptors to be loaded like so: |
| 148 | + |
| 149 | + var interceptors = |
| 150 | + [ |
| 151 | + {beanName = "stringUtilityService", interceptorName = "beforeInterceptor", methods = "forward,reverse,split"}, |
| 152 | + {beanName = "stringUtilityService", interceptorName = "afterInterceptor"}, |
| 153 | + {beanName = "stringUtilityService", interceptorName = "afterInterceptor2", methods = ""}, |
| 154 | + {beanName = "stringUtilityService", interceptorName = "afterInterceptor3", methods = "*"}, |
| 155 | + {beanName = "stringUtilityService", interceptorName = "aroundInterceptor", methods = "reverse"} |
| 156 | + ]; |
| 157 | + |
| 158 | +When the `methods` key is missing from the interceptor definition or it contains an empty value or asterisk, then AOP/1 assumes that all methods on the bean should be intercepted. |
| 159 | + |
| 160 | +## Helper Methods |
| 161 | + |
| 162 | +`isLast()` |
| 163 | +This method is automatically added to any **around** interceptor and will tell you if the interceptor is the last in the execution chain. |
| 164 | + |
| 165 | +`translateArgs(any targetBean, string methodName, struct args, boolean replace)` |
| 166 | +This method is automatically added to any interceptor and will attempt translate position based arguments into name based arguments. This method has a `replace` argument that when set to `true` will replace the `args` with a copy of named arguments. |
| 167 | + |
| 168 | +## Intercepting Cross Object Calls & Private Methods |
| 169 | + |
| 170 | +Unlike some other AOP frameworks, AOP/1 has the ability to intercept cross object calls. What this means is, if you are intercepting methods (`method1()`, `method2()`, `method3()`) on `myService` and `method2()` actually makes a call to `method3()`, then AOP/1 will intercept the call from `method2()` to `method3()` in addition to original call to `method2`. |
| 171 | + |
| 172 | +In addition to intercepting cross object method calls, AOP/1 can also intercept calls to private methods. |
| 173 | + |
| 174 | +## Multiple Interceptor |
| 175 | + |
| 176 | +You may find yourself creating an interceptor that performs multiple similar tasks and it is logical to group multiple different interceptor types together. This can be accomplished by simply creating the correct methods in the same component. For instance, if you have an interceptor that you want to perform both before and after interceptions, then you simply add both the `before()` and `after()` methods to the component. AOP/1 will place an interceptor in multiple execution stacks if it has more than one interceptor type method present. |
| 177 | + |
| 178 | +## Stack Execution |
| 179 | + |
| 180 | +Stacks are executed in the following order. |
| 181 | + |
| 182 | +* **before** |
| 183 | +* **around** |
| 184 | +* **after** |
| 185 | +* **onError** |
| 186 | + |
| 187 | +All the stacks will only execute if there is an interceptor of their type present. If the stack is emtpy, nothing is executed. The **onError** stack only executes if there is an error in the execution of the other stacks. The **before** and **after** stacks execute like a queue and will execute from start to finish regardless of changes to the arguments or result, skipping any interceptors that do not match the currently intercepted bean method. The **around** stack executes more like a chain. The chain execution can be stopped by not calling the `proceed()` method. |
0 commit comments