Skip to content
logoBack to home screen

Wire in Detail

Wire is an Java extension based on annotations and byte code instrumentation which supports the IOC pattern.

General

The idea of Inversion Of Control (IOC) is to decouple the code that sets up and binds software components (classes) from the implementation code of that components. Built using a small library, containing only one dependency, Wire uses Java as a flexible and direct way of using dependency injection.

Prerequisites for Using Wire

  • import the following artifacts: com.braintribe.ioc.Wire#1.0

Components

Wire consists of the following structural elements:

ManagedInstance

A managed instance is a Java class instance that is managed so that it has a lifecycle within a WireScope. Within that scope, instances can be shared and have cyclic references. Based on the scope, LifecycleListeners can react on the instantiation or disposal of an instance in a cross-cutting way.

A managed instance is an object created by a method within a WireSpace. The method (because a managed instance is effectively a Java method) needs to be annotated with the @Managed annotation:

import com.braintribe.wire.api.annotation.Managed;

@Managed
public FancyThingy fancyOne() {...}

The return type of the method defines the official type of that instance. The concrete type of the instance that the method returns can be any subclass. You can choose to return that concrete type or the super type. You can also choose to make the method public, protected or private. If you choose the private modifier you restrict the access to that bean to only the WireSpace that it is defined in.

If you choose the protected modifier you can still extend the WireSpace but access is still limited to the concrete WireSpace instance. Only with the public modifier you allow other WireSpaces to have access.

The implementation of that method normally creates an instance and assigns it to a variable conventionally named bean. It then parametrizes that instance and finally returns it. The parameterization is often done by calling setters, adders, builders and other code intended to prepare the instance.

The instance being assigned should not be a managed instance from another method.

@Managed
public FancyThingy fancyOne() {
    // instantiation
    FancyThingy bean = new FancyThingy();

    // parameterization
    bean.setFast(true);
    bean.setName("foobar");

    // return the prepared instance
    return bean;
}

The code can be more complex than in this linear example, it can even have conditional parts and loops. Try to keep it simple though if you can and always think if a certain dynamic aspect is really part of wiring or of the implementation.

The code can also access the managed instance in order to add further information on that level. You can read about it in the WireScope section.

Context-sensitive Parametrization

You can also parametrize an instance using context information:

public interface MyContext implements ScopeContext {
String getName();
}

@Managed
public class ExampleWireSpace implements WireSpace {

    @Managed
    public FancyThingy fancyOne(MyContext context) {
        // instantiation
        FancyThingy bean= new FancyThingy();

        // parameterization
        bean.setFast(true);
        // parameterization using runtime information from a ScopeContext
        bean.setName(context.getName());

        // return the prepared instance
        return bean;
    }
}

ManagedInstance Configuration

With a static import of com.braintribe.wire.api.scope.InstanceConfiguration.currentInstance(), you can access your managed instance configuration for further fine tuning, like in the following example:

import static com.braintribe.wire.api.scope.InstanceConfiguration.currentInstance;

@Managed
public class ExampleWireSpace implements WireSpace {

    @Managed
    public FancyThingy fancyOne() {
        // instantiation
        FancyThingy bean = new FancyThingy();

        // parameterization
        bean.setFast(true);
        bean.setName("foobar");

        // accessing the instance configuration to register an individual destroy method
        currentInstance().onDestroy(bean::onDispose);

        // return the prepared instance
        return bean;
    }
}

ManagedInstance Lifecycle

When a managed instance is being instantiated in a WireScope, it may be notified about its parameterization being completed. When the WireScope is closing, its managed instances may be notified about that as well. The actual notification pattern depends on LifeycleListeners configured to the WireContext. The most concise and performant approach is to work with well defined interfaces for lifecycle notification. You can configure the com.braintribe.wire.impl.lifecycle.StandardLifecycleListener class to support the following interfaces on managed instances in the right phase of the lifecycle:

InterfacePost InitializePre Destroy
com.braintribe.cfg.InitializationAwarepostConstruct()
com.braintribe.cfg.DestructionAwarepreDestroy()
java.lang.AutoCloseableclose()
com.braintribe.cfg.LifecycleAwarepostConstruct()preDestroy()

Alternatively, you can configure com.braintribe.wire.impl.lifecycle.StandardLegacyLifecycleListener to additionally support instance notification methods annotated with the JDK annotations:

  • @PostConstruct
  • @PreDestroy

You do this by overriding the onLoaded() method in a WireSpace, for example:

@Managed
public class CustomCartridgeSpace extends CustomCartridgeSpaceFull {


    @Override
    public void onLoaded(WireContextConfiguration configuration) {

        configuration.addLifecycleListener(new LifecycleListener() {

            @Override
            public void onPreDestroy(InstanceHolder beanHolder, Object bean) {
                // nothing to do
            }

            @Override
            public void onPostConstruct(InstanceHolder beanHolder, Object bean) {
                if (beanHolder.space() instanceof EnsureGlobalIds && bean instanceof GenericEntity) {
                    GenericEntity entity = (GenericEntity) bean;
                    if (entity.getGlobalId() == null) {
                        String globalId = "bean:"+externalId+"/"+beanHolder.space().getClass().getSimpleName()+"/"+beanHolder.name();
                        entity.setGlobalId(globalId);

                    }
                }

            }
        });


        super.onLoaded(configuration);
    }
}

As you can do this on any space implementation you have to consider when a space has the onLoaded() method called.

Generally, the spaces are initialized as per the @import statments starting the traversing at the space that is associated to the top level contract. This means that in the following example the MyMainSpace space will be loaded last:

MyMainSpace implements MyMainContract {
  @Import
  private MetaSpace meta;
  @Import
  private SomeOtherSpace someOther;
}

That means if the main space has child spaces it can never be the first one to load.

If you only want to support the methods without the interfaces, you can configure com.braintribe.wire.impl.lifecycle.AnnotationBasedLifecycleListener.

WireSpace

A WireSpace is a bean namespace class where managed instance methods are placed.

Within a WireSpace, methods can reference other managed instance methods directly via this and can therefore also access private and protected managed instance methods. All managed instances within a WireSpace are lazily instantiated. So at least one access to such a method must have occurred that an instance is being created.

A WireSpace class implements the interface com.braintribe.wire.api.space.WireSpace and is annotated with @Managed.

A WireSpace can inherit from another WireSpace. But if you use a common super WireSpace, two deriving WireSpaces will only share the same code but won't share managed instances. If you want to share managed instances then use the import feature.

The following example shows a WireSpace with two bean defining methods that mutually reference each other conveniently with a direct method call in their parameterization part. That cyclic call structure would normally result in a stack overflow, but as Wire is an extension of Java, the logic works otherwise and ensures the cyclic referentiality within a WireScope.

@Managed
public class ExampleWireSpace implements WireSpace {

    @Managed
    public FancyThingy fancyOne() {
        // instantiation
        FancyThingy bean= new FancyThingy();

        // parameterization
        bean.setFast(true);
        bean.setName("foobar");
        bean.setOther(fancyTwo());

        // return the prepared instance
        return bean;
    }

    @Managed
    public FancyThingy fancyTwo() {
        // instantiation
        FancyThingy bean = new FancyThingy();

        // parameterization
        bean.setFast(false);
        bean.setName("barfoo");
        bean.setOther(fancyOne());

        // return the prepared instance
        return bean;
    }
}

Loading WireSpaces

When a WireSpace is loaded, the onLoaded(WireContextConfiguration) method is called.

This method can be used to register meta functionality for the WireContext itself on the WireContextConfiguration but also to instantiate eager managed instances by simply calling their methods as shown in the example:

@Managed
public class ExampleWireSpace implements WireSpace {

void onLoaded(WireContextConfiguration configuration) {
    // eager instantiations
    fancyOne();
    fancyTwo();
    }
}
// here the bean definitions would follow ….

Importing WireSpaces

If a managed instance method in a WireSpace wants to reference one from another WireSpace, it has to import the other WireSpace in order to do so. The referenced method must have been declared as public. The import is done with an uninitialized member variable that is annotated with the @Import annotation:

import com.braintribe.wire.api.annotation.Import;

@Import
private OtherSpace otherSpace; {...}

Instances of WireSpace are managed by the WireContext which keeps them as singletons per class so that every import will bring the same WireSpace and therefore the same managed instances.

The following example shows the import of the other WireSpace and the access to one of its managed instances:

@Managed
public class ExampleWireSpace implements WireSpace {

    @Import
    private OtherSpace otherSpace;

    @Managed
    public FancyThingy fancyOne() {
        // instantiation
        FancyThingy bean = new FancyThingy();

        // parameterization
        bean.setFast(true);
        bean.setName("foobar");
        bean.setOther(otherSpace.fancyOther());

        // return the prepared instance
        return bean;
    }
}

Imports can also be used to access WireScopes.

Methods Without @Managed

WireSpace classes can also contain unmanaged methods that are not annotated. These methods can be helpers, switches and other constructions, but you should be aware of the concern of WireSpaces. You can still access code from other classes (and even artifacts) that are not WireSpace-related.

Contract

Contracts are interfaces that extend com.braintribe.wire.api.space.WireSpace.

In these interfaces, you can have abstract methods that must be implemented by an associated WireSpace class. The methods should not be annotated with @Managed as this is not inherited by implementation classes. The managed instance nature of a method is always only given by the implementation class and never by a contract.

Contract WireSpaces serve different purposes. One is the Classloader separation and the other is to support abstraction that could help in plugin systems and also for a more decoupled way to describe what is available for other WireSpaces.

WireContext

A WireContext manages a number of WireSpaces which are loaded through a specific classloader that perform instrumentation of the managed instance methods in each WireSpace to add the cross cutting code of scoping.

WireSpaces are loaded top down the @Import structure in a depth-first order and initialized in the bottom up order. During the initialization, the WireContext is responsible for injecting values for the member fields annotated with @Import. The loading starts with the WireSpace implementation that is associated to the main contract.

The main contract is mandatory and defines what is accessible to the outside of a WireContext. As the main contract has to be like any other contract, the WireContext interface ensures that only instrumented classes are used as implementations for the contract.

Setting Up a WireContext

You can setup a WireContext with a com.braintribe.wire.api.context.WireContextBuilder that you obtain from the static entry point method com.braintribe.wire.api.Wire.context(Class<S> wireSpace).

WireContextBuilder allows you to:

  • bind contracts to implementation WireSpaces in various ways:
    • explicit mapping
    • pattern based mapping working on package and simple class name
  • define a default WireScope
  • configure the internal classloader if needed
  • set a parent WireContext from which contracts could be resolved

Inspect the code below to see how a context is normally built:

// build the WireContext for a given root contract
WireContext<MainContract> context = Wire
.context(MainContract.class)
.bindContracts("com.braintribe.wire.test.basic")
.build();

// from the built context access the root contract to get beans from it
MainContract mainContract = context.contract();
Something someThing = mainContract.someThing();

WireScope

A WireScope is the central point of control managed instances are managed with. A WireScope controls the lifecycle of the instances, notifies LifecycleListeners within that lifecycle and also takes care about locking to support multithread-safety.

Scopes are enumeration constants defined in com.braintribe.wire.api.annotation.Scope. The supported scopes are:

Enum ConstantDescription
singletonThe most commonly used scope that instantiates only once. This is the default scope of a WireContext.
inheritNot an actual scope but inheriting the scope from a higher context (e.g. WireSpace, WireContext). This is the default scope on a WireSpace, and a managed instance method. The inheritance is as follows:
- WireContext
- WireSpace
- Managed Instance Method
prototypeThis scope will always create a new instance when a managed instance method is being called. The managed instances are only managed during construction and not hold afterwards. This means such instances are never destructed by Wire.
refereeInstances in that scope will be destroyed when the caller that called a managed instance method of that scope is destroyed.

Applying a Scope

You can apply the scope either when building a WireContext:

// build the WireContext for a given root contract
WireContext<MainContract> context = Wire
.context(MainContract.class)
.bindContracts("com.braintribe.wire.test.basic")
.defaultScope(Scope.singleton)
.build();

or in the @Managed annotation on WireSpace or managed instance methods:

@Managed(Scope.prototype)
public class ExampleWireSpace implements WireSpace {

@Managed(Scope.singleton)
public FancyThingy fancyOne() {
    // instantiation
    FancyThingy bean = new FancyThingy();

    // parameterization
    bean.setFast(true);
    bean.setName("foobar");

    // return the prepared instance
    return bean;
    }
}

WireModule

A WireModule is used to group artifact WireSpaces so they can be bundled together and used in another artifact by allowing you to do their Wire context configuration internally. Each WireModule can depend on a number of other WireModules, which in turn allows you to use their WireSpaces without facing any class loading related issues.

Each WireModule instance hosts:

  • WireModule dependencies
  • A WireContextBuilder configuration

If a WireModule also hosts a WireContract to be used as a main contract of WireContext, we call it a WireTerminalModule.

By default, WireContextBuilder is configured to load space classes based on package name convention i.e. from WireModule class package sub-packages named contract where WireContracts reside and space where their implementing WireSpaces reside. You can always override this default behavior.

Implementing a WireModule

A WireModule is meant to be singleton by nature and therefore is implemented as an enum. Examples of a WireModule implementation (terminal and non terminal):

// wire terminal module
public enum OurWireModule implements WireTerminalModule<MainWireContract> {

	INSTANCE;
	
	// this is where we specify our wire terminal module dependencies
	@Override
	public List<WireModule> dependencies() {
		return list(SomeWireModule1.INSTANCE, SomeWireModule2.INSTANCE);
	}
	
	// this is where we configure a wire context builder
	@Override
	public void configureContext(WireContextBuilder<?> contextBuilder) {
		WireTerminalModule.super.configureContext(contextBuilder);
	}
	
	// this is where we specify a main contract
	@Override
	public Class<MainWireContract> contract() {
		return MainWireContract.class;
	}
}

// wire module
public enum OurWireModule implements WireModule {

	INSTANCE;
	
	// this is where we specify our wire module dependencies
	@Override
	public List<WireModule> dependencies() {
		return list(SomeWireModule1.INSTANCE, SomeWireModule2.INSTANCE);
	}
	
	// this is where we configure a wire context builder
	@Override
	public void configureContext(WireContextBuilder<?> contextBuilder) {
		WireTerminalModule.super.configureContext(contextBuilder);
	}
}