wirebox-aop
Use this skill when working with WireBox Aspect-Oriented Programming (AOP) -- activating the AOP engine (Mixer listener), creating method interceptor aspects, registering aspects via mapAspect(), binding aspects to classes and methods via bindAspect(), using the Matcher DSL (any, regex, mappings, instanceOf, annotatedWith, methods, returns), auto-aspect binding via classMatcher/methodMatcher annotations, the MethodInvocation API (proceed, getArgs, getTarget, getMethod), using built-in aspects (CFTransaction, HibernateTransaction, MethodLogger), and AOP in both standalone WireBox and ColdBox applications.
WireBox AOP โ Aspect-Oriented Programming
When to Use This Skill
Use this skill when:
- Adding cross-cutting concerns (logging, transactions, security, caching, tracing) to methods without modifying them
- Creating reusable method interceptors (aspects)
- Using the built-in
CFTransaction,HibernateTransaction, orMethodLoggeraspects - Binding aspects to classes/methods via DSL or self-binding annotations
Applies to both standalone WireBox and ColdBox applications.
Language Mode Reference
Examples use BoxLang (.bx) / CFML script syntax. Adapt for your target language:
| Concept | BoxLang (.bx) | CFML (.cfc) |
|---|---|---|
| Class declaration | class [extends="..."] { | component [extends="..."] { |
| DI annotation | @inject above property name="svc"; | property name="svc" inject="svc"; |
AOP Vocabulary
| Term | Definition |
|---|---|
| Aspect | A modularized cross-cutting concern (e.g., logging, transactions) |
| Target Object | The object whose methods will be intercepted |
| Join Point | A method execution point where an aspect is applied |
| Advice | The action taken at a join point (before, after, or around) |
| AOP Proxy | The runtime proxy wrapping the original method |
AOP Namespaces
// ColdBox application
coldbox.system.aop
// Standalone WireBox
wirebox.system.aop
WireBox AOP is implemented as a WireBox listener called the Mixer. It transforms objects after DI is complete and is fully decoupled from WireBox internals.
Step 1: Activate the AOP Engine
Register the Mixer listener in your WireBox binder:
Standalone WireBox
// config/WireBox.bx (or .cfc)
class extends="wirebox.system.ioc.config.Binder" {
function configure() {
wirebox = {
listeners : [
{
class : "wirebox.system.aop.Mixer",
properties : {
// Where to write generated UDF stubs (disk or memory)
generationPath : "/wirebox/system/aop/tmp",
// Reload class match dict on each request (dev only)
classMatchReload : false
}
}
]
}
}
}
ColdBox Application
In ColdBox, use the coldbox.system.aop.Mixer namespace:
// config/WireBox.bx
class extends="coldbox.system.ioc.config.ColdBoxBinder" {
function configure() {
wirebox = {
listeners : [
{
class : "coldbox.system.aop.Mixer",
properties : {
generationPath : "/wirebox/system/aop/tmp",
classMatchReload : false
}
}
]
}
// Register and bind aspects here
}
}
Alternatively, register the Mixer as a ColdBox interceptor in config/ColdBox.cfc:
interceptors = [
{ class: "coldbox.system.aop.Mixer", name: "AOPMixer", properties: {} }
]
Step 2: Create an Aspect
An aspect must implement wirebox.system.aop.MethodInterceptor (or coldbox.system.aop.MethodInterceptor in ColdBox):
// models/aspects/MethodLoggerAspect.bx
class implements="wirebox.system.aop.MethodInterceptor" {
// Aspects support full WireBox DI
@inject "logbox:logger:{this}"
property name="log";
function init( boolean logResults = true ) {
variables.logResults = arguments.logResults
return this
}
/**
* The single entry point for all AOP advice.
* Implement before/around/after logic here.
* You MUST call invocation.proceed() to execute the real method.
*/
function invokeMethod( required invocation ) {
var target = arguments.invocation.getTargetName()
var method = arguments.invocation.getMethod()
var args = arguments.invocation.getArgs()
var debug = "target=#target#, method=#method#, args=#serializeJSON(args)#"
// BEFORE advice
log.debug( "BEFORE: #debug#" )
// AROUND โ proceed to the real method (or next aspect in chain)
var result = arguments.invocation.proceed()
// AFTER advice
if ( !isNull( result ) && variables.logResults ) {
log.debug( "AFTER: #debug#, result=#serializeJSON(result)#" )
}
return result ?: javaCast( "null", "" )
}
}
Aspect Rules
- Always call
invocation.proceed()unless you intentionally want to short-circuit the method (e.g., a security aspect that blocks access). - If the method has a return value, return the result of
proceed(). - Aspects are WireBox-managed objects โ full DI is available.
- Multiple aspects can be chained on the same join point; execution follows registration order.
Step 3: Register the Aspect
In the WireBox binder, declare the aspect mapping using mapAspect():
// Register an aspect โ auto-discovers classMatcher/methodMatcher annotations
mapAspect( "MethodLogger" ).to( "models.aspects.MethodLoggerAspect" )
// Disable auto-binding (self-binding annotations ignored)
mapAspect( aspect="MethodLogger", autoBind=false ).to( "models.aspects.MethodLoggerAspect" )
Step 4: Bind the Aspect to Classes and Methods
Use bindAspect() with the Matcher DSL to declare what to intercept:
// Log all save() calls on UserService
bindAspect(
classes : match().mappings( "UserService" ),
methods : match().methods( "save" ),
aspects : "MethodLogger"
)
// Transaction on any method annotated @transactional
bindAspect(
classes : match().any(),
methods : match().annotatedWith( "transactional" ),
aspects : "CFTransaction"
)
// Multiple aspects on regex-matched service classes
bindAspect(
classes : match().regex( ".*Service$" ),
methods : match().any(),
aspects : "MethodLogger,SecurityAspect"
)
Matcher DSL
The match() convenience method in the binder creates a wirebox.system.aop.Matcher instance. Chain these methods:
Class Matchers
| Method | Description |
|---|---|
any() | Matches all classes |
mappings( mappings ) | Match specific mapping names (list or array) |
instanceOf( classPath ) | Match if class isInstanceOf(classPath) |
annotatedWith( annotation [, value] ) | Match classes with an annotation (optionally matching value) |
regex( pattern ) | Match CFC path against a regular expression |
Method Matchers
| Method | Description |
|---|---|
any() | Matches all methods |
methods( names ) | Match specific method names (list or array) |
annotatedWith( annotation [, value] ) | Match methods with an annotation |
returns( type ) | Match methods by return type |
regex( pattern ) | Match method names against a regular expression |
Logical Combinators
| Method | Description |
|---|---|
andMatch( matcher ) | AND two matchers together |
orMatch( matcher ) | OR two matchers together |
// Only public void service methods
bindAspect(
classes : match().regex( ".*Service$" ).andMatch( match().annotatedWith( "monitored" ) ),
methods : match().returns( "void" ),
aspects : "MetricsAspect"
)
Auto-Aspect Binding
Let the aspect declare its own class/method matchers via annotations โ no bindAspect() call needed:
/**
* @classMatcher any
* @methodMatcher annotatedWith:transactional
*/
class name="TransactionAspect"
implements="wirebox.system.aop.MethodInterceptor" {
function invokeMethod( required invocation ) {
transaction {
return arguments.invocation.proceed()
}
}
}
Register it โ WireBox detects the annotations and binds automatically:
mapAspect( "TransactionAspect" ).to( "models.aspects.TransactionAspect" )
Override the self-binding when needed:
mapAspect( aspect="TransactionAspect", autoBind=false ).to( "models.aspects.TransactionAspect" )
bindAspect( classes=match().mappings( "OrderService" ), methods=match().regex( "^save" ), aspects="TransactionAspect" )
classMatcher Annotation DSL Values
| Value | Description |
|---|---|
any | Any class |
annotatedWith:{annotation} | Class has annotation present |
annotatedWith:{annotation}:{value} | Class has annotation with specific value |
mappings:{name,name,...} | Specific named mappings |
instanceOf:{classPath} | Class is an instance of classPath |
regex:{pattern} | Class path matches regex |
methodMatcher Annotation DSL Values
| Value | Description |
|---|---|
any | Any method |
annotatedWith:{annotation} | Method has annotation present |
annotatedWith:{annotation}:{value} | Method has annotation with specific value |
methods:{name,name,...} | Specific method names |
returns:{type} | Methods returning a specific type |
regex:{pattern} | Method name matches regex |
MethodInvocation API
The invocation argument passed to invokeMethod exposes:
| Method | Description |
|---|---|
getMethod() | Name of the intercepted method (join point) |
getMethodMetadata() | Struct of the method's CFC metadata (annotations) |
getArgs() | Current argument collection (struct) |
setArgs( args ) | Override the argument collection before proceeding |
getTarget() | Reference to the target object |
getTargetName() | Name (mapping ID) of the target object |
getTargetMapping() | The WireBox mapping object (has extra attributes etc.) |
proceed() | Execute the real method or the next aspect in chain |
function invokeMethod( required invocation ) {
// Read a method annotation to decide behavior
var meta = arguments.invocation.getMethodMetadata()
if ( structKeyExists( meta, "secured" ) ) {
if ( !variables.securityService.isLoggedIn() ) {
throw( type="SecurityException", message="Not authorized" )
}
}
// Modify arguments before the real call
var args = arguments.invocation.getArgs()
args.auditUser = variables.securityService.getCurrentUser()
arguments.invocation.setArgs( args )
return arguments.invocation.proceed()
}
Built-In Aspects
WireBox ships with three ready-to-use aspects in wirebox.system.aop.aspects (or coldbox.system.aop.aspects):
CFTransaction
Wraps methods in a ColdFusion transaction {} block. Self-binding: matches any class, methods annotated @transactional.
mapAspect( "CFTransaction" ).to( "coldbox.system.aop.aspects.CFTransaction" )
// Now annotate any method with @transactional
class name="OrderService" {
function placeOrder( required order ) transactional {
variables.orderRepository.save( arguments.order )
variables.inventoryService.deduct( arguments.order.getItems() )
}
}
HibernateTransaction
Wraps methods in a native Hibernate transaction block. Same self-binding pattern as CFTransaction.
mapAspect( "HibernateTransaction" ).to( "coldbox.system.aop.aspects.HibernateTransaction" )
MethodLogger
Logs method entries and results via LogBox. Self-binding: any class, any method.
mapAspect( "MethodLogger" ).to( "coldbox.system.aop.aspects.MethodLogger" )
Complete Example โ Security Aspect
// models/aspects/SecuredAspect.bx
/**
* @classMatcher any
* @methodMatcher annotatedWith:secured
*/
class implements="wirebox.system.aop.MethodInterceptor" {
@inject
property name="securityService";
@inject "logbox:logger:{this}"
property name="log";
function invokeMethod( required invocation ) {
var meta = arguments.invocation.getMethodMetadata()
var required = meta.secured ?: "authenticated"
if ( required == "authenticated" && !variables.securityService.isLoggedIn() ) {
log.warn( "Unauthorized access attempt to #arguments.invocation.getMethod()#" )
throw( type="SecurityException", message="Authentication required" )
}
if ( required != "authenticated" && !variables.securityService.hasPermission( required ) ) {
log.warn( "Permission denied [#required#] on #arguments.invocation.getMethod()#" )
throw( type="SecurityException", message="Permission denied: #required#" )
}
return arguments.invocation.proceed()
}
}
// config/WireBox.bx
class extends="coldbox.system.ioc.config.ColdBoxBinder" {
function configure() {
wirebox = {
listeners : [{ class: "coldbox.system.aop.Mixer" }]
}
// Register self-binding aspects
mapAspect( "SecuredAspect" ).to( "models.aspects.SecuredAspect" )
mapAspect( "CFTransaction" ).to( "coldbox.system.aop.aspects.CFTransaction" )
mapAspect( "MethodLogger" ).to( "coldbox.system.aop.aspects.MethodLogger" )
}
}
// models/OrderService.bx
class singleton {
@inject
property name="orderRepository";
// Requires 'place-orders' permission
function checkout( required cart ) secured="place-orders" transactional {
var order = variables.orderRepository.createFromCart( arguments.cart )
variables.orderRepository.save( order )
return order
}
// Requires only login
function getHistory() secured {
return variables.orderRepository.findAll()
}
}
AOP in Standalone WireBox
Same workflow but use wirebox.system.aop.* namespaces:
// Binder listener
wirebox = {
listeners : [{ class: "wirebox.system.aop.Mixer" }]
}
// Aspect implements
class implements="wirebox.system.aop.MethodInterceptor" { ... }
// Map aspect
mapAspect( "MyAspect" ).to( "model.aspects.MyAspect" )
Checklist for Configuring AOP
- Add the Mixer listener to
wirebox.listenersin your binder (standalone:wirebox.system.aop.Mixer; ColdBox:coldbox.system.aop.Mixer) - Create your aspect implementing
MethodInterceptorโ add DI, implementinvokeMethod, callproceed() - Register the aspect with
mapAspect( "Name" ).to( "path.to.Aspect" ) - Bind the aspect with
bindAspect(classes, methods, aspects)โ or use@classMatcher/@methodMatcherannotations for auto-binding - Annotate target methods if using annotation-based matching (e.g.,
transactional,secured)
Anti-Patterns to Avoid
| Anti-pattern | Solution |
|---|---|
Forgetting to call invocation.proceed() | Always call it unless intentionally blocking the method |
Not returning the result of proceed() | return invocation.proceed() โ callers depend on the return value |
Registering the Mixer but not calling mapAspect() | Both steps are required โ Mixer enables AOP; mapAspect registers the aspect |
| Applying AOP to transient objects with volatile state | Prefer singletons or application-scope objects as aspect targets |
| Circular dependencies between aspects | Keep aspects focused; avoid injecting objects that themselves have AOP applied |
Using classMatchReload=true in production | Only enable in development โ it reloads matchers on every request |