Most of the time we believe our DI engines should be black boxes, but we try to think otherwise. We encourage developers to know what is going on so they can debug easily and not hit their foreheads against their keyboards. Believe me, I have done so before. That is why WireBox is tightly integrated with LogBox to provide incredible debugging information to ANY appender you desire so you can know what is going on. Another aspect of knowing what the DI engine does is how dependencies are resolved. Here is a typical flow of injection:
Object is requested by name and the Injector tries to check if the mapping exists for that name. If no mapping is found then it tries to locate the object by using the internal scan locations to try to find it. If it cannot find it and there is a parent injector defined, then the request is funneled to the parent injector and we start our process again. If no parent injector is declared and no localization, then we throw a not located exception.
If the object was found via the scan locations, then we register a new mapping according to its location and discover all the metadata out of the object in preparation for construction and DI
We now have a guaranteed mapping so we retrieve it and we verify if the mapping's metadata has been processed or not. If the mapping is marked with no autowiring then we skip to the next step. If not, we process the mapping's metadata and prepare it for DI
We verify that the scope define for the mapping exists, else we throw an invalid scope exception
We ask the scope to produce the mapping object for us. The scope is in charge of persistence, locking, etc.
The scope builds the instance by asking the injector to build a new instance with the correct constructor and constructor arguments and stores it in its scope once the injector builds it. The builder decides what type of construction is needed for the mapping as it can be a CFC, java object, webservice, RSS feed, factory method call, etc. Each constructor argument is processed for dependency resolution.
The scope then sends the instance for DI wiring and process back to the injector
The injector returns the instance
Arrive at the desired injection point and get the injection DSL. If the DSL is empty, then it defaults to the id/model namespace. For this injection DSL Namespace we try to find a valid DSL builder for it. If none is found an exception is thrown. If we have a match, then the DSL builder is called with the DSL string to retrieve.
The DSL builder then tries to parse and process the DSL string for object retrieval. If the DSL is a WireBox mapping then we try to retrieve the instance by name (Refer back to Instance Creation).
If the builder could not produce an instance, it is logged and DI is skipped on it.
Caution Circular dependencies are supported in all injection styles within WireBox. With one caveat, if you choose constructor arguments with circular dependencies, you must use object providers.
Another aspect of our objects is when are they created? Good question!
By default all objects are created ONLY when they are requested, in other words they are lazy created. But what if you are spoiled and you want your stuff NOW NOW NOW! Well, you can, cry if you want to! Just tell WireBox that you want your objects to be eagerly created via the mapping DSL asEagerInit()
function or a eagerInit
annotation on the component.
Dependency injection and instance construction with WireBox is easy. In its most simplest form we can just leverage annotations and be off to dancing Big Willy style! You can use our global injection annotation inject on cfproperties
, setter methods or constructor arguments. This annotation tells WireBox to inject something in place of the property, argument or method; basically it is your code shouting "Hey buddy, I need your help here".
What it injects depends on the contents of this annotation that leverages our injection DSL (Domain Specific Language). The simplest form of the DSL is to just tell WireBox what mapping to bring in for injection. Please note that I say mapping and not object directly, because WireBox works on the concept of an object mapping. This mapping in all reality can be a CFC, a java object, an RSS feed, a webservice, a constant value or pretty much anything you like.
If you don't like annotations because you feel they are too intrusive to your taste, don't worry, we also have a programmatic configuration binder you can use to define all your objects and their dependencies. We will discuss object mappings and our configuration binders later on, so let's look at how cool this is by checking out our Coffee Shop sample class. The CoffeeShop
class below will use our three types of injections to showcase how WireBox works, please note that most likely we would build this class by picking one or the other, which in itself brings in pros and cons for each approach.
So let's break this class down. First, you can see a singleton annotation on the component declaration. This tells WireBox that this class should only be created once and then cached in its internal singleton scope of the injector. In other words, this is called object life scopes. You can refer to the persistence scopes annotations later on in the guide to learn all about how to scope your classes.
Second, we built our coffee shop class with three external dependencies: 1 by cfproperty, 1 by constructor argument and 1 by setter injection. Again, you can see later on in this guide the difference between all these injection styles and choose what you prefer. In this example, we just showcase the different injection styles. Also, as you can see from the source code the three types of injection uses the inject annotation but with different content:
If you just mark a property, argument or method with the inject annotation, WireBox will assume it is a mapping and the ID should be either the property name, the argument name or the method name. However, if you want to specify the id in the DSL string, just use the simple id:{mapping}
dsl notation. That's it! Isn't that cool, you just mark out your dependencies and WireBox will build and inject them for you!
Thirdly, this class has the following method:
The method has a cool little annotation called onDIComplete
that tells WireBox that after all DI dependencies have been injected, then execute the method. That is so cool, WireBox can even open the coffee shop for me so I can get my espresso fix. Not only that but you can have multiple onDIComplete
methods declared and WireBox will call them for you (in discovered order). These are called object post processors that are discovered by annotations or can be configured via our configuration binder and we will learn about them later on. WireBox also fires a series of object life cycle events throughout an object's life span in which you can build listens to and actually perform some cool stuff on them. So now that we got all excited about opening the coffee shop let's get into something even more interesting, unit testing and mocking.
Another important aspect leveraging DI concepts when building our components is that we can immediately write tests for them and leverage mocking to test for actual behaviors. This is a great advantage as it allows you to rapidly test to confirm your component is working without worrying about building or assembling objects in your tests. You have eliminated all kinds of crazy creation and assembler code and just concentrated yourself on the problem at hand. You are now focused to code the greatest piece of software you have ever imagined, thanks to WireBox!
So let's build our unit test (Please note we use our base ColdBox testing classes for ease of use and MockBox integration):
Now we can run our tests and verify that our coffee shop is operational and producing sweet sweet espresso!
We touched briefly on singleton and no scope objects in this section, so let's delve a little into what scoping is. WireBox's default behavior is to create a new instance of an object each time you request it via creation or injection (Transient/Prototype objects), this is the NO SCOPE scope.
Scopes allow you to customize the object's life span and duration. The singleton scope allows for the creation of only one instance of an object that will live for the entire life span of the injector. WireBox ships with several different life span scopes but you can also create your own custom scopes (please see the custom scopes section). You can also tell WireBox in what scope to place the instance into by annotations or via the configuration binder. We have an entire section dedicated to discovering all the WireBox annotations, but let's get a sneak peek at them and also how to do it via our mapping DSL.
You can tag a cfcomponent
tag or component declaration with a scope={named scope}
annotation that tells WireBox what scope to use
You can have nothing on the cfcomponent
tag or component declaration which denotes the NO SCOPE
You can tag a cfcomponent
tag or component declaration with a singleton annotation
Here are the internal scopes that ship with WireBox:
This is cool! We can now have full control of how objects are persisted via the WireBox injector, we are not constricted to one type of persistence anymore.
Caution If you use a persistence scope that expires after time like session, request, cachebox, etc, you will experience a side effect called scope widening injection. WireBox offers a solution to this side effect via WireBox Providers, which we will cover in detail.
In all reality we could be building our objects and its dependencies, object graph, without any configuration just plain location and implicit conventions. This is great but not very flexible for refactoring, so let's do the best practice of defining a mapping or an alias to a real object.
We do this by creating a WireBox configuration binder wirebox.system.ioc.config.Binder
, which is a simple CFC that defines the way WireBox behaves and defines object mappings. This binder is then used to initialize WireBox so it has knowledge of these mappings and our settings.
The Binder is also the way you configure WireBox for operation.
We have now coded our classes and unit tests with some cool annotations in record time, so what do we do next? Well, WireBox works on the idea of three ways to discover and create your classes:
So let's do examples for each where our classes we just built are placed in a directory called model of the root directory.
Implicit Creation
Explicit Binder Configuration
Explicit Creation
Scan Locations Binder Configuration
Set Locations Creation
So our recommendation is to always try to create configuration binders as best practice, but your requirements might dictate something else.
Scope
Description
NOSCOPE
A prototype object that gets created every time it is requested.
PROTOTYPE
A prototype object that gets created every time it is requested.
SINGLETON
Only one instance of the object exists
SESSION
The object will exist in the session scope
APPLICATION
The object will exist in the application scope
REQUEST
The object will exist in the request scope
SERVER
The object will exist in the server scope
CACHEBOX
A object will be time persisted in any CacheBox cache provider
Approach
Motivation
Pros
Cons
Implicit Mappings
To replace createObject()
or new
calls
Very natural as you just request an object by its instantiation path. Very fast prototyping.
Refactoring is very hard as code is plagued with instantiation paths everywhere. Not DRY.
Explicit Mappings
To replace createObject()
calls with named keys
DRY, you can create multiple named mappings that point to the same blueprint of a class. Create multiple iterations of the same class. Very nice decoupling.
Not as fast to prototype as we need to define our mappings before hand in our configuration binder.
Scan Locations
CFC discovery by conventions
A partial instantiation path(s) or folder(s) are mapped so you can retrieve by shorthand names. Very quick to prototype also without using full instantiation paths. Override of implementations can be easily done by discovery.
Harder concept to digest, not as straightforward as implicit and explicit locations.