arrow-left

All pages
gitbookPowered by GitBook
1 of 17

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Overview

In computing, aspect-oriented programming (AOP) is a programming paradigm which aims to increase modularity by allowing the separation of cross-cutting concerns. AOP forms a basis for aspect-oriented software development.

I won't go into incredible software theory but pragmatic examples. What is the value of AOP? What does it solve? Well, how many times have we needed to do things like this:

component name="UserService"{

    function save(){

      log.info("method save() called with arguments: #serializeJSON(arguments)#");

      transaction {
         // do some work here
      }

      log.info("Save completed successfully!");
    }
}

As you can see from the example above, my real business logic is in the //do some work here comment, but our code is littered with logging and transactions. What if I have 10 methods that are very familiarly the same? Do I repeat this same littered code (cross-cutting concernsarrow-up-right)? The answer for most of us has always been "YES! Of Course! How else do I do this?".

Well, you don't have to, AOP to the rescue. What AOP will let you do is abstract all that logging and transaction code to another object usually called an Aspect. Then we need to apply this aspect to something right? Well, in our case it has to apply to a target object, our UserService, and to a specific method, save(), which is usually refered to as the join point.

What does applying an aspect mean? It means that we will take that aspect you write and execute it at different points in time during the execution of the method you want to apply it to. This is usually refer to as an advice, "Hey Buddy! Run it here!!!".

There are multiple types of AOP advices like before, around, after, etc. We have seen in ColdBox event handlers that you can do a preHandler, postHandler and and aroundHandler methods. These are AOP advices localized to event handlers that let you execute code before a handler event, after a handler event, or completely around the handler event. The most powerful form of advice is around, as it allows you to completely surround a method call with your own custom code. Does it ring a bell now? The transaction code for Pete's Sake! You need it to completely surround the method call! Voila! The around advice will allow you to completely take over the execution and you can even determine if you want to continue the execution or not.

So how does this magic happen? Well, our WireBox AOP engine will hijack your method (join point) and replace it with a new one, usually called an AOP proxy. This new method has all the plumbing already to allow you to apply as many aspects you like to that specific method. So as you can see from our diagram below, the save method is now decorated with our two aspects, but for all intent and purposes the outside world does not care about it, they just see the save() method.

Activate The AOP Listener

WireBox has an amazing that can help you modify, listen and do all kinds of magic during object creation, wiring, etc. Our AOP implementation is just a listener (wirebox.system.aop.Mixer) that will transform objects once they are finalized with dependency injection. This means, our AOP engine is completely decoupled from the internals of the DI engine and is incredibly fast and light weight.

So let's activate it in our WireBox binder configuration:

That's it! That tells WireBox to register the AOP engine once it loads. This listener also has some properties that you can tweak:

AOP Vocabulary

  • Aspect: A modularization of a concern that cuts across multiple objects.

  • Target Object : The object that will be applied with Aspects across certain methods or join points.

  • Join Point

Default

Description

generationPath

cf include path

false

/wirebox/system/aop/tmp

The location where UDF stubs will be generated to. This can be to disk or memory.

classMatchReload

boolean

false

false

A cool flag to allow you to reload the class matching dictionary for development purposes only.

Property

Type

event driven architecture

Required

wirebox.listeners = [
    { class="wirebox.system.aop.Mixer", properties={} }
];
: A point of execution in a target object that will be applied a specific aspect to it. This is usually the execution of a method.
  • Advice : An action taken at a particular join point. Usually, before, after or around it.

  • AOP Proxy : An object or method representation for the original join point or method.

  • WireBox has an amazing event driven architecture that can help you modify, listen and do all kinds of magic during object creation, wiring, etc. Our AOP implementation is just a listener that will transform objects once they are finalized with dependency injection. This means, our AOP engine is completely decoupled from the interals of the DI engine and is incredibly fast and light weight. So let's activate it in our WireBox binder configuration:

    That's it! That tells WireBox to register the AOP engine once it loads. This listener also has some properties that you can tweak:

    Property

    Type

    Required

    Default Value

    Description

    generationPath

    cf include path

    false

    /wirebox/system/aop/tmp

    The location where UDF stubs will be generated to. This can be to disk or memory.

    classMatchReload

    boolean

    false

    false

    wirebox.listeners = [
        { class="coldbox.system.aop.Mixer",properties={} }
    ];

    A cool flag to allow you to reload the class matching dictionary for development purposes only.

    Summary

    So now that we mapped our self binding aspect, let's clean up our code further:

    component name="UserService"{
    
        function save() transactional{
            // do some work here
        }
    }

    Now isn't this amazing! We are back to our original business logic and code with no extra fluff around logging, transactions and pretty much just code noise. Now we are talking and cooking with AOP. So hopefully by now you nave a good grasp of what AOP does for you and how to implement it in WireBox. So here are the steps to remind you:

    1. Create an aspect that implements: wirebox.system.aop.MethodInterceptor

    2. Map the aspect in your WireBox binder via the mapAspect() method

    3. Bind the aspect to classes and methods via annotations or the bindAspect() method

    So, easy as 1-2-3! AOP is powerful and extermely flexible. Hopefully, your imagination is now going into overdrive and coming up with ways to not only clean up your code from cross cutting concerns, but also help you do things like:

    • threaded methods

    • transform method results

    • method auditing

    MethodMatcher Annotation DSL

    Create a methodMatcher annotation on the component with the following DSL values:

    HibernateTransaction

    This aspect is a self-binding aspect that will surround methods with native Hibernate transaction blocks.

    To use, just declare in your binder, overriding the self-binding is totally optional.

    method security
  • cache method results

  • etc.

  • returns:{type}

    Matches to ONLY the methods that return the {type}

    methods:{methods}

    Matches to ONLY the named methods(s) you pass to this method as a list or array.

    regex:{regex}

    Matches against a CFC instantiation path or function name using regular expressions

    DSL

    Description

    any

    Matches against any class path or method name

    annotatedWith:{annotation}

    Matches against the finding of an annotation in a cfcomponent

    annotatedWith:{annotation}:{value}

    Matches against the finding of an annotation value in a cfcomponent

    ClassMatcher

    MethodMatcher

    any

    annotatedWith:transactional

    mapAspect("CFTransaction").to("coldbox.system.aop.aspects.HibernateTransaction");

    CFTransaction

    This aspect is a self-binding aspect that will surround methods with simple ColdFusion transaction blocks.

    ClassMatcher

    MethodMatcher

    any

    annotatedWith:transactional

    To use, just declare in your binder, overriding the self-binding is totally optional.

    mapAspect("CFTransaction").to("coldbox.system.aop.aspects.CFTransaction");

    Included Aspects

    WireBox comes bundled with three aspects that you can use in your applications and can be found in wirebox.system.aop.aspects:

    Aspect

    Description

    CFTransaction

    A simple ColdFusion transaction Aspect for WireBox

    HibernateTransaction

    A cool annotation based Transaction Aspect for WireBox

    MethodLogger

    A simple interceptor that logs method calls and their results

    MethodLogger

    This aspect enables you to log method calls and their results. You will have to bind it to the methods you want it to listen to. It has one constructor argument:

    Name

    Type

    Required

    Default

    Description

    logResults

    boolean

    false

    true

    Whether to log the results of method calls or not.

    // Map the Aspect
    mapAspect("MethodLogger")
        .to("coldbox.system.aop.aspects.MethodLogger")
        .initArg(name="logResults",value="true or false");
    
    // Bind the Aspect to all service class methods
    bindAspect(classes=matcher().regex('.*Service'), methods=matcher().any(), aspects="MethodLogger");

    MethodInvocation Useful Methods

    Here are a list of the most useful methods in this CFC

    • getMethod() : Get the name of the method (join point) that we are proxying and is being executed

    • getMethodMetadata() : Get the metadata structure of the current executing method. A great way to check for annotations on the method (join point)

    • getArgs() : Get the argument collection of the method call

    • setArgs() : Override the argument collection of the method call

    • getTarget() : Get the object reference to the target object

    • getTargetName() : Get the name of the target object

    • getTargetMapping() : Get the object mapping of the proxied object. Great for getting metadata information about the object, extra attributes, etc.

    • proceed() : This is where the magic happens, this method tells WireBox AOP to continue to execute the target method. If you do not call this in your aspect, then the proxyied method will NOT be executed.

    The last method is the most important one as it tells WireBox AOP to continue executing either more aspects, the proxyed method or nothing at all.

    ClassMatcher Annotation DSL

    Create a classMatcher annotation on the component with the following DSL values:

    DSL

    Description

    any

    Matches against any class path or method name

    annotatedWith:{annotation}

    Matches against the finding of an annotation in a cfcomponent

    annotatedWith:{annotation}:{value}

    Matches against the finding of an annotation value in a cfcomponent

    MethodLogger Aspect

    Here is my MethodLogger aspect that I will create:

    You can see that I do some DI via annotations:

    A normal constructor with one optional argument for logging results:

    And our invokeMethod implementation:

    As you can see, the before advice part is what happens before the execution of the real method (or more aspects) occurrs. So everything before the call to arguments.invocation.proceed():

    Then we execute the real method or more aspects (we do not do anything around the method call):

    Auto Aspect Binding

    WireBox AOP supports the concept of self aspect bindings. This means that we can make things even simpler by letting the Aspect you build control what class and methods it will match against. This is great, one less thing to remember when developing the code. So let's start with the code first:

    That's it! Our transaction aspect is done and it will also bind itself to ANY class and ANY method that has the transactional annotation. Then you just map it:

    We are done now, by mapping the aspect WireBox detects the two annotations classMatcher and methodMatcher and binds it for you. WOW, but where did you get that from? Well, the component has two annotations:

    How cool is that! My aspect can determine the matching for me already.

    Aspect Registration

    Now that we have built our aspect we need to register it with WireBox so it knows about it and all DI can be performed in it. Let's open our WireBox binder and use the following DSL method:

    • mapAspect(aspect,autoBinding=[true])

    This tells WireBox to register a new aspect called MethodLogger that points to the CFC model.aspects.MethodLogger that I have just built. WireBox will then mark that object as an aspect, create it once the injector loads and have it ready for building.

    mappings:{mappings}

    Matches to ONLY the named mapping(s) you pass to this method as a list or array.

    instanceOf:{classPath}

    Matches if the target object is an instance of the classPath. This internally uses the ColdFusion isInstanceOf() method.

    regex:{regex}

    Matches against a CFC instantiation path or function name using regular expressions

    mapAspect("MethodLogger").to("model.aspects.MethodLogger");
    Finally, we do the after advice part which happens after the method or other aspects fire and results are returned:

    That's it. I have succesfully created an aspect. What's next!

    <cfcomponent output="false" implements="wirebox.system.aop.MethodInterceptor" hint="A simple interceptor that logs method calls and their results">
    
        <---  Dependencies --->
        <cfproperty name="log" inject="logbox:logger:{this}">
    
        <---  init --->
        <cffunction name="init" output="false" access="public" returntype="any" hint="Constructor">
            <cfargument name="logResults" type="boolean" required="false" default="true" hint="Do we log results or not?"/>
            <cfscript>
                instance = {
                    logResults = arguments.logResults
                };
    
                return this;
            </cfscript>
        </cffunction>
    
        <---  invokeMethod --->
        <cffunction name="invokeMethod" output="false" access="public" returntype="any" hint="Invoke an AOP method invocation">
            <cfargument name="invocation" required="true" hint="The method invocation object: wirebox.system.aop.MethodInvocation">
            <cfscript>
                var refLocal = {};
                var debugString = "target: #arguments.invocation.getTargetName()#,method: #arguments.invocation.getMethod()#,arguments:#serializeJSON(arguments.invocation.getArgs())#";
    
                // log incoming call
                log.debug(debugString);
    
                // proceed execution
                refLocal.results = arguments.invocation.proceed();
    
                // result logging and returns
                if( structKeyExists(refLocal,"results") ){
                    if( instance.logResults ){
                        log.debug("#debugString#, results:", refLocal.results);
                    }
                    return refLocal.results;
                }
            </cfscript>
        </cffunction>
    
    </cfcomponent>
    hashtag
    Overiding Bindings

    One thing to note about self binding aspects is that you can also override their matching by using the autoBind argument in the mapAspect() method call. So if you wanted to override the class and method matching on this aspect you would do this:

    /**
    * @classMatcher any
    * @methodMatcher annotatedWith:transactional
    */
    component name="TransactionAspect" implements="coldbox.system.aop.MethodInterceptor"{
    
        function init(){ return this; }
    
        function invokeMethod(required invocation) output=false{
    
            // Let's do the around advice now:
            transaction {
                // execute the method or other aspects in a transaction
                return arguments.invocation.proceed();
            }
    
        }
    }
    mapAspect("TransactionAspect").to("model.aspects.MyTransactionAspect");
    <---  Dependencies --->
    <cfproperty name="log" inject="logbox:logger:{this}">
    <---  init --->
    <cffunction name="init" output="false" access="public" returntype="any" hint="Constructor">
        <cfargument name="logResults" type="boolean" required="false" default="true" hint="Do we log results or not?"/>
        <cfscript>
            instance = {
                logResults = arguments.logResults
            };
    
            return this;
        </cfscript>
    </cffunction>
    <---  invokeMethod --->
    <cffunction name="invokeMethod" output="false" access="public" returntype="any" hint="Invoke an AOP method invocation">
    <cfargument name="invocation" required="true" hint="The method invocation object: wirebox.system.aop.MethodInvocation">
    <cfscript>
        var refLocal = {};
        var debugString = "target: #arguments.invocation.getTargetName()#,method: #arguments.invocation.getMethod()#,arguments:#serializeJSON(arguments.invocation.getArgs())#";
    
        // log incoming call
        log.debug(debugString);
    
        // proceed execution
        refLocal.results = arguments.invocation.proceed();
    
        // result logging and returns
        if( structKeyExists(refLocal,"results") ){
            if( instance.logResults ){
                log.debug("#debugString#, results:", refLocal.results);
            }
            return refLocal.results;
        }
    </cfscript>
    </cffunction>
    var refLocal = {};
    var debugString = "target: #arguments.invocation.getTargetName()#,method: #arguments.invocation.getMethod()#,arguments:#serializeJSON(arguments.invocation.getArgs())#";
    
    // log incoming call
    log.debug(debugString);
    // proceed execution
    refLocal.results = arguments.invocation.proceed();
    // result logging and returns
    if( structKeyExists(refLocal,"results") ){
        if( instance.logResults ){
            log.debug("#debugString#, results:", refLocal.results);
        }
        return refLocal.results;
    }
    /**
    * @classMatcher any
    * @methodMatcher annotatedWith:transactional
    */
    OR
    component classMatcher="any" methodMatcher="annotatedWith:transactional"{}
    mapAspect(aspect="TransactionAspect",autoBind=false).to("model.aspects.MyTransactionAspect");
    
    // match only methods that start with the regex ^save
    bindAspect(classes=match().any(),methods=match().regex("^save"),aspects="TransactionAspect");

    Aspect Binding

    Since we have now mapped our aspect in WireBox, we now need to tell it the most important things:

    1. To what classes or CFCs should we apply this aspect to?

    2. To what methods or join points should we apply this aspect to?

    We do this with another binder DSL method:

    • bindAspect(classes,methods,aspects)

    What is up with that funky match().... stuff? Well, the classes and methods arguments of the bindAspect() call uses our AOP funky matcher methods that exist in an object called: wirebox.system.aop.Matcher. This matcher object is used to match classes and methods to whatever criteria you like. The binder has a convenience method called match() that creates a new matcher object for you and returns it so you can configure it for classes and method matching. Here are the most common matching methods below:

    WOW! We can really pin point anything in our system! So now that we have binded our aspect to our UserService I can rewrite it to this:

    Now isn't that pretty? Much nicer and compact hugh! Plus I can reuse the method logger for ANY method in ANY class I desire (Muaaaahaaaa), that's an evil laugh by the way! But we are not done, let's keep going and do the Transactional aspect.

    true

    Matches against the finding of an annotation in a cfcomponent or cffuntion or matches against the finding AND value of the annotation.

    mappings(mappings)

    true

    false

    Matches to ONLY the named mapping(s) you pass to this method as a list or array.

    instanceOf(classPath)

    true

    false

    Matches if the target object is an instance of the classPath. This internally uses the ColdFusion isInstanceOf() method

    regex(regex)

    true

    true

    Matches against a CFC instantiation path or function name using regular expressions

    methods(methods)

    false

    true

    Matches against a list of explicit method names as a list or array

    andMatch(matcher)

    true

    true

    Does an AND evaluation with the current matcher and the one you pass in the method.

    orMatch(matcher)

    true

    true

    Does an OR evaluation with the current matcher and the one you pass in the method.

    Method

    Class Matching

    Method Matching

    Description

    any()

    true

    true

    Matches against any class path or method name

    returns(type)

    false

    true

    Matches against the return type of methods

    annotatedWith(annotation,[value])

    true

    bindAspect(classes=match().mappings('UserService'),methods=match().methods('save'),aspects="MethodLogger");
    component name="UserService"{
    
        function save(){
    
          transaction {
             // do some work here
          }
    
        }
    }

    AOP Intro

    WireBox fully supports aspect-oriented programmingarrow-up-right (AOP) for ColdFusion (CFML) and any ColdFusion framework. Just note the different namespaces if using within the ColdBox Platform and standalone WireBox.

    hashtag
    WireBox AOP RefCard

    https://github.com/ColdBox/cbox-refcards/raw/master/WireBox%20AOP/WireBox-AOP-Refcard.pdf

    Our WireBox AOP RefCardarrow-up-right will get you up and running in no time.

    hashtag
    Code Namespaces

    hashtag
    Requirements

    • ColdFusion 11+

    • Lucee 4.5+

    AND

    • Disk/Memory Generation

    Create Your Aspect

    Now that we have activated the AOP engine, let's build a simple method logger aspect that will intercept before our method is called and after our method is called. So if you remember your AOP dictionary terms, we will create an aspect that does a before and after advice on the method. Phew! To do this we must implement a CFC that WireBox AOP gives you as a template: wirebox.system.aop.MethodInterceptor. This CFC interface looks like this:

    This means, that we must create a CFC that implements the invokeMethod method with our own custom code. It also receives 1 argument called invocation that maps to a CFC called wirebox.system.aop.MethodInvocation that you can learn from our cool .

    Our approach to AOP is simplicity, therefore this invokeMethod

    implements the most powerful advice called around advice, so you will always do an around advice, but it will be up to your custom code to decide what it does before (
    beforeAdvice
    ), around (
    aroundAdvice
    ) and after (
    afterAdvice
    ) the method call.

    The other advantage of WireBox AOP aspects is that once they are registered with WireBox they act just like normal DI objects in WireBox, therefore you can apply any type of dependency injection to them.

    <cfinterface hint="Our AOP Method Interceptor Interface">
    
        <---  invokeMethod --->
        <cffunction name="invokeMethod" output="false" access="public" returntype="any" hint="Invoke an AOP method invocation">
            <cfargument name="invocation" required="true" hint="The method invocation object: wirebox.system.ioc.aop.MethodInvocation">
        </cffunction>
    
    </cfinterface>
    APIarrow-up-right
    // ColdBox
    coldbox.system.aop
    
    // WireBox Standalone
    wirebox.system.aop