AF-IOCUser Guide

Overview

afIoC is an Inversion of Control (IoC) container and Dependency Injection (DI) framework based on the most excellent Tapestry 5 IoC.

Like Guice? Know Spring? Then you'll love afIoc!

  • Injection - the way you want it!
    • field injection
    • standard ctor injection
    • serialisation ctor injection - new make(|This|in) { in(this) }
  • Distributed service configuration between pods and modules
    • configure any service from any pod / module
    • configure via simple Lists and Maps
  • Override everything
    • override services and configuration, even override your overrides!
    • replace real services with test services
    • set sensible application defaults and let your users override them
  • True lazy loading
    • services are proxied to ensure nothing is created until you actually use it
    • make circular service dependencies a thing of the past!
  • Advise services with aspects
    • intercept method calls to your services
    • apply cross cutting concerns such as authorisation, transactions and logging
  • Extensible
    • inject your own objects, not just services
  • Designed to help YOU the developer!
    • simple API - 1 facet and 4 registry methods and you're away!
    • over 75 bespoke and informative Err messages!
    • Extensively tested: - All tests passed! [34 tests, 205 methods, 498 verifies]

Quick Start

  1. Create services as Plain Old Fantom Objects
  2. Use the @Inject facet to mark fields as dependencies
  3. Define and configure your services in a Module class
  4. Build and start the registry
  5. Go, go, go!
class Main {
  static Void main(Str[] args) {
    registry := IocService([MyModule#]).start.registry

    test1 := (MyService1) registry.serviceById("myservice1")        // return a singleton
    test2 := (MyService1) registry.dependencyByType(MyService1#)    // same instance as test1
    test3 := (MyService1) registry.autobuild(MyService1#)           // build a new service
    test4 := (MyService1) registry.injectIntoFields(MyService1())   // inject into existing Objs

    test1.service2.kick	// --> Ass!
    test2.service2.kick	// --> Ass!
    test3.service2.kick	// --> Ass!
    test4.service2.kick	// --> Ass!

    Service.find(IocService#).uninstall
  }
}

class MyModule {                    // every application needs a module class
  static Void bind(ServiceBinder binder) {
    binder.bindImpl(MyService1#)    // define your singletons here
    binder.bindImpl(MyService2#)
  }
}

class MyService1 {
  @Inject               // you'll use @Inject all the time
  MyService2? service2  // inject services into services!
}

class MyService2 {
  Str kick() { "Ass!" }
}

Terminology

IoC distinguishes between Services and Dependencies.

A service is just a Plain Old Fantom Object where there is only one instance (singleton) or one per thread. Each service is identified by a unique ID (usually the unqualified class name) and may, or may not, be represented by a Mixin. Services are managed by IoC and must be defined by a module. Services may solicit configuration contributed by other modules.

A dependency is any class or object that another class depends on. A dependency may or may not be a service. For example, a class may depend on having a field Int maxNoOfThreads but that Int isn't a service, it's just a number. Non service dependencies are be managed by user defined dependency providers.

A contribution is a means to configure a service.

A module is a class where services and contributions are defined.

Loading Modules

Use the RegistryBuilder or IocService to build and start your Registry of services.

When building a registry, you declare which modules are to loaded. You may also load modules in dependant pods, in which case, each pod should have declared the following meta:

"afIoc.module" : "{qname}"

Where {qname} is a qualified type name of a module. Additional modules can be declared by the @SubModule facet.

Modules can also be loaded from index properties in a similar manner.

Defining Services

Services are defined in Module classes, where each meaningful method is static and annotated with a facet.

The exception is bind() which does not have a facet but is declared with a standard signature. The bind method is the common means to define services, examples below:

static Void bind(ServiceBinder binder) {

  // has service ID of 'MyService'
  binder.bind(MyService#, MyServiceImpl#)

  // has service ID of 'myServiceImpl'
  binder.bindImpl(MyServiceImpl#)

  // has service ID of 'elephant'
  binder.bindImpl(MyServiceImpl#).withId("elephant")
}

Modules may can also define builder methods. These are static methods annotated with the @Build facet. Here you may construct and return the service yourself. Any parameters are taken to be dependencies and are resolved and injected as such when the method is called. For example, to manually build a service with the Id penguin:

@Build { serviceId="penguin" }
static EmailService buildStuff(EmailConfig config) {
  EmailServiceImpl(config)
}

Dependency Injection

IoC performs both ctor and field injection, for normal and const fields.

Note that under the covers, all services are resolved via their unique service ids, injection by type is merely a layer on top, added for convenience.

When IoC autobuilds a service it locates a suitable ctor. This is either the one donned with the @Inject facet or the one with the most parameters. Ctor parameters are taken to be dependencies and are resolved appropriately.

Field injection happens after the object has been created and so fields must be declared as nullable:

@Inject
MyService? myService

The exception to the rule is if you declare a serialisation ctor:

new make(|This|? f) { f?.call(this) }

On calling f all injectable fields will be set, even fields marked as const.

After object construction and field injection, any extra setup may be performed via methods annotated with @PostInjection. These methods may be of any visibility and all parameters are resolved as dependencies.

Service Scope

Services are either created once perApplication (singletons) or once perThread. Application scoped services must be defined as const. If you need mutable state in your const service, try using the ConcurrentState class.

(Using proxies) you can even inject a perThread scoped service into a perApplication scoped service! Think about it... you can inject your http request into any static service you desire!

Service Configuration

Services can solicit configuration from modules simply by declaring a list or a map in their ctor or builder method.

class Example {
  new make(Str[] mimeTypes) { ... }
  ...
}

Modules may then contribute to the Example service:

@Contribute
static Void contributeExample(OrderedConfig conf) {
  conf.add("text/plain")
}

The list and map types are inferred from the ctor definition and all contribution types must fit.

If the service declares a map configuration then contribution methods should take a MappedConfig object. If the map config uses Str as the key, then the created map is caseInsensitive otherwise the map is ordered.

Lazy Loading

Define your service with a mixin and take advantage of true lazy loading!

By fronting your service with a mixin, IoC will generate and compile a service proxy on the fly. The real service is only instantiated when you call a method on the proxy.

This means registry startup times are quicker than ever and circular service dependencies are virtually eliminated!

It also allows you to inject perThread scoped services into perApplication scoped services.

Advise Your Services

Intercept all calls to services defined by a mixin and wrap them in your own code.

See @Advise for details

More!

afIoc comes bundled with utility classes that cover common use cases:

  • RegistryStartup: (Service) Define tasks to execute when the registry starts up.
  • RegistryShutdownHub: (Service) Define tasks to execute when the registry shuts down.
  • ConcurrentState: Lets your const services hold mutable state, accessible from all threads.
  • ConcurrentCache: Shares state across threads providing fast reads and synchronised writes.
  • ThreadStashManager: (Service) Keep tabs on threaded state.
  • TypeCoercer: Coerce Objs of one type to another via Fantom's toX() and fromX() methods.
  • StrategyRegistry: Holds a map of Type:Obj where values may be looked up by closest matching type.
  • PipelineBuilder: (Service) Define a pipeline of filters with a terminator.

Tips

Strive to keep your services const, use ConcurrentState to hold state and delcare a serialisation ctor to keep @Injected fields non-nullable:

new make(|This| injectInto) { injectInto(this) }

Define one main module and declare it in both the pod meta and the pod index props. Use @SubModule to reference additional dependant modules in the same pod.

If you have no say in how your classes are created (say, when you're using flux) then use the following line to inject dependencies when needed:

((IocService) Service.find(IocService#)).injectIntoFields(this)

When creating GUIs (say, with fwt) then use Registry.autobuild to create your panels, commands and other objects. These aren't services and should not be declared as such, but they do often make use of services.

IoC gives detailed error reporting should something go wrong, nevertheless should you need more, use IocHelper.debugOperation to make IoC give trace level contextual information.

Don't be scared of creating const services! Use ConcurrentState to safely store and access mutable state across thread boundaries.

Release Notes

v1.4.2

  • New: Added ConcurrentCache class, an application of ConcurrentState designed for fast reads.
  • New: Added PlasticClassModel.extendClass() for the model may now extend multiple Mixins.
  • New: Added PlasticClassModel.addMethod() for adding new methods.
  • Chg: ConcurrentState state may now be null (and added an instance count).

v1.4.0

  • New: Added OrderedConfig.remove and MappedConfig.remove.
  • New: Added RegistryBuilder.moduleTypes to return a list of modules types held by the builder.
  • New: Added suppressStartupMsg build option.
  • Chg: Rejigged the config override argument order. (Breaking Change.)
  • Chg: Deleted @Deprecated config methods.
  • Chg: Transferred VCS ownership to AlienFactory
  • Chg: Test code is no longer distributed with the afIoc.pod - pod size was nearing 500 Kb!
  • Chg: ThreadStashs have less verbose names.
  • Bug: Could not override ordered config if it was referenced by constraints.

v1.3.10

  • New: Added PipelineBuilder util service.
  • Chg: Registry.autobuild() now looks for a default implementation if passed a mixin.
  • Chg: Made it clear when service creation fails due to Registry Shutdown.
  • Bug: Proxy Types for lazy services are now cached. Would have caused a memory leak when using creating lots of threaded const services.
  • Bug: Ordered contributions with multiple before: constraints could be added to the config list multiple times.
  • Bug: afPlastic would only allow methods to be overridden if they were defined in the immediate parent type.
  • Bug: Stack frames were lost from Errs originating from module builder methods.

v1.3.8

  • New: Added TypeCoercer util class that converts an Obj to a given type using toXXX() and fromXXX() methods.
  • Chg: OrderedConfig contributions are coerced to the contrib type.
  • Chg: MappedConfig key and value contributions are coerced to their required types.
  • Chg: Added shortcut @Operator This add(obj) to OrderedConfig and @Deprecated Void addUnordered(obj).
  • Chg: Added shortcut @Operator This set(key, val) to MappedConfig and @Deprecated Void addMapped(key, val).
  • Chg: Public method on OrderedConfig and MappedConfig now return this and other tweaks.
  • Chg: Exposed @NoDoc PlasticPodCompiler so it may be used outside of afIoc.

v1.3.6

  • Bug: Real impls of proxied const services were not being cached.
  • Bug: The implied order of unordered config in OrderedConfig was not assured.
  • Bug: Could not inject null into const fields via a custom DependencyProvider.

v1.3.4

v1.3.2

  • New: IocErr is thrown on startup if module advisor methods don't match any proxyable serivces.
  • New: Module advisor methods may be marked as optional.
  • New: Add thread clean up handlers to ThreadStashManager.
  • New: Added ThreadStash.contains
  • Chg: Operations Err trace is now part of the Err msg (and no longer logged to sys.err)
  • Chg: ConcurrentState.withState() now returns a Future.
  • Bug: Lifecyle data for threaded Services was not threaded. (Caused problems for threaded proxy services.)

v1.3.0

  • New: Simple Aspect API for advising proxied servies.
  • New: Service proxies for mixins are generated and compiled on the fly to give true lazy loading.
  • New: ThreadStashManager now keeps tabs on your ThreadStashs so they may be cleanup at the end of, um... say a web request!
  • Chg: Revamped LocalStash into ThreadStash
  • Chg: Mapped override keys can always be a Str
  • Chg: Removed Ioc frames from stack traces (no more 150+ line stacktraces!)
  • Chg: Reducded INFO logging.
  • Bug: @Build.serivceId was not overriding the build method name.
  • Bug: Distributed mapped overide could throw an invalid override not found err.
  • Bug: Autobuild now checks if the type is instantiable.

v1.2.2

  • Chg: Registry.autobuild now accepts optional parameters to pass / mix into the ctor.
  • Chg: ConcurrentState now accepts a factory method for creating / initialising state.

v1.2.0

v1.1.0

  • New: Extend IoC by defining your own DependencyProviders.
  • New: @ServiceId lets you disambiguate between different implmentations of the same service mixin.
  • New: @Autobuild injects a fresh service on every injection.
  • New: Ordered configuration contributions are ordered across modules.
  • Bug: Services can be created even if they don't define any ctors.

v1.0.0

  • New: Added addUnorderedAll and addMappedAll to OrderedConfig and MappedConfig.
  • Chg: Multiple instances of ConcurrentState can be created with the same state class.
  • Bug: Made public the withState() and getState() methods on ConcurrentState.
  • Bug: NPE could be thrown if ctor depdendency not found.

v0.0.2

  • New: A fully loaded preview release.