5. Tigase Kernel

Tigase Kernel is an implementation of IoC created for Tigase XMPP Server. It is responsible for maintaining object lifecycle and provides mechanisms for dependency resolutions between beans.

Additionally, as and optional feature, Tigase Kernel is capable of configuring beans using a provided bean configurator.

5.1. Basics

5.1.1. What is kernel?

Kernel is an instance of the Kernel class which is responsible for managing scope and visibility of beans. Kernel handles bean:

  • registration of a bean

  • unregistration of a bean

  • initialization of a bean

  • deinitialization of a bean

  • dependency injection to the bean

  • handling of bean lifecycle

  • registration of additional beans based on annotations (optionally using registered class implementing BeanConfigurator as defaultBeanConfigurator)

  • configuration of a bean (optionally thru registered class implementing BeanConfigurator as defaultBeanConfigurator)

Kernel core is responsible for dependency resolution and maintaining lifecycle of beans. Other features, like proper configuration of beans are done by additional beans working inside the Kernel.

Kernel identifies beans by their name, so each kernel may have only one bean named abc. If more than one bean has the same name, then the last one registered will be used as its registration will override previously registered beans. You may use whatever name you want to name a bean inside kernel but it cannot:

  • be service as this name is used by Tigase Kernel internally when RegistrarBean`s are in use (see RegistrarBean)

  • end with #KERNEL as this names are also used by Tigase Kernel internally

Tip

Kernel initializes beans using lazy initialization. This means that if a bean is not required by any other beans, or not retrieved from the kernel manually, an instance will not be created.

During registration of a bean, the kernel checks if there is any beans which requires this newly registered bean and if so, then instance of a newly registered bean will be created and injected to fields which require it.

5.1.2. What is a kernel scope?

Each kernel has its own scope in which it can look for beans. By default kernel while injecting dependencies may look for them only in the same kernel instance in which new instance of a bean is created or in the direct parent kernel. This way it is possible to have separate beans named the same in the different kernel scopes.

Note

If bean is marked as exportable, it is also visible in all descendants kernel scopes.

5.1.3. What is a bean?

A bean is a named instance of the class which has parameterless constructor and which is registered in the kernel.

Warning

Parameterless constructor is a required as it will be used by kernel to create an instance of the bean, see bean lifecycle.

5.2. Lifecycle of a bean

5.2.1. Creating instance of a bean

Instantiation of a bean

During this step, kernel creates instance of the class which was registered for this bean (for more details see Registration of a bean). Instance of a bean is created using paremeterless constructor of a class.

Note

Bean instance is only created for required beans (i.e. beans that were injected somewhere).

Note

It’s possible to create bean instance without the need to inject it anywhere - such bean should be annoted with @Autostart annotation.

Configuring a bean (optional)

In this step kernel passes class instance of a bean to the configurator bean (an instance of BeanConfigurator if available), for configuring it. During this step, BeanConfigurator instance, which is aware of the configuration loaded from the file, injects this configuration to the bean fields annotated with @ConfigField annotation. By default configurator uses reflections to access those fields. However, if a bean has a corresponding public setter/getter methods for a field annotated with @ConfigField (method parameter/return type matches field type), then configurator will use them instead of accessing a field via reflection.

Note

If there is no value for a field specified in the configuration or value is equal to the current value of the field, then configurator will skip setting value for this field (It will also not call setter method even if it exists).

At the end of the configuration step, if bean implements ConfigurationChangedAware interface, then method beanConfigurationChanged(Collection<String> changedFields) is being called, to notify bean about field names which values has changed. This is useful, if you need to update bean configuration, when you have all configuration available inside bean.

Note

Configuration of the bean may be changed at runtime and it will be applied in the same way as initial configuration is passed to the bean. So please keep in mind that getter/setter may be called multiple times - even for already configured and initialized bean.

Injecting dependencies

At this point kernel looks for the bean class fields annotated with @Inject and looks for a value for each of this fields. During this step, kernel checks list of available beans in this kernel, which matches field type and additional constraints specified in the annotation.

When a required value (instance of a bean) is found, then kernel tries to inject it using reflection. However, if there is a matching getter/setter defined for that field it will be called instead of reflection.

Note

If dependency changes, ie. due to reconfiguration, then value of the dependent field will change and setter will be called if it exists. So please keep in mind that getter/setter may be called multiple times - even for already configured and initialized bean.

Initialization of a bean

When bean is configured and dependencies are set, then initialization of a bean is almost finished. At this point, if bean implements Initializable interface, kernel calls initialize() method to allow bean initialize properly if needed.

5.2.2. Destroying instance of a bean

When bean is being unloaded, then reference to its instance is just dropped. However, if bean class implements UnregisterAware interface, then kernel calls beforeUnregister() method. This is very useful in case which bean acquires some resources during initialization and should release them now.

Note

This method will not be called if bean was not initialized fully (bean initialization step was note passed)!

5.2.3. Reconfiguration of a bean (optional)

At any point in time bean may be reconfigured by default bean configurator (instance of BeanConfigurator) registered in the kernel. This will happen in the same way as it described in Configuring a bean in Creating instace of a bean section.

5.2.4. Updating dependencies

It may happen, that due to reconfiguration or registration/unregistration or activation/deactivation of some other beans dependencies of a bean will change. As a result, Tigase Kernel will inject new dependencies as described in Injecting dependencies

5.3. Registration of a bean

There are few ways to register a bean.

5.3.2. Setting parent to class not implementing RegistrarBean interface

If parent is set to the class which is not implementing RegistrarBean interface, then your bean will be registered in the same kernel scope in which parent bean is registered. If you do so, ie. by setting parent to the class of the bean which is registered in the kernel1 and your bean will be also registered in kernel1. As the result it will be exposed to other beans in the same kernel scope. This also means that if you will configure it in the same way as you would set parent to the parent of annotation of the class to which your parent point to.

Example.

@Bean(name="bean1", parent=Kernel.class)
public class Bean1 {
    @ConfigField(desc="Description")
    private int field1 = 0;
    ....
}

@Bean(name="bean2", parent=Bean1.class)
public class Bean2 {
    @ConfigField(desc="Description")
    private int field2 = 0;
    ....
}

In this case it means that bean1 is registered in the root/main kernel instance. At the same time, bean2 is also registered to the root/main kernel as its value of parent property of annotation points to class not implementing RegistrarBean.

To configure value of field1 in instance of bean1 and field2 in instance of bean2 in DSL (for more information about DSL format please check section DSL file format of the Admin Guide) you would need to use following entry in the config file:

bean1 {
    field1 = 1
}
bean2 {
    field2 = 2
}

As you can see, this resulted in the bean2 configuration being on the same level as bean1 configuration.

5.3.3. Calling kernel methods

As a class

To register a bean as a class, you need to have an instance of a Tigase Kernel execute it’s registerBean() method passing your Bean1 class.

kernel.registerBean(Bean1.class).exec();

Note

To be able to use this method you will need to annotate Bean1 class with @Bean annotation and provide a bean name which will be used for registration of the bean.

As a factory

To do this you need to have an instance of a Tigase Kernel execute it’s registerBean() method passing your bean Bean5 class.

kernel.registerBean("bean5").asClass(Bean5.class).withFactory(Bean5Factory.class).exec();

As an instance

For this you need to have an instance of a Tigase Kernel execute it’s registerBean() method passing your bean Bean41 class instance.

Bean41 bean41 = new Bean41();
kernel.registerBean("bean4_1").asInstance(bean41).exec();

Warning

Beans registered as an instance will not inject dependencies. As well this bean instances will not be configured by provided bean configurators.

5.3.4. Using config file (optional)

If there is registered a bean defaultBeanConfigurator which supports registration in the config file, it is possible to do so. By default Tigase XMPP Server uses DSLBeanConfigurator which provides support for that and registration is possible in the config file in DSL. As registration of beans using a config file is part of the admin of the Tigase XMPP Server tasks, it is described in explained in the Admin Guide in subsection Defining bean of DSL file format section.

Tip

This way allows admin to select different class for a bean. This option should be used to provide alternative implementations to the default beans which should be registered using annotations.

Warning

Works only if bean registered as defaultBeanConfigurator supports this feature. By default Tigase XMPP Server uses DSLBeanConfigurator which provides support for that.

5.4. Defining dependencies

All dependencies are defined with annotations:

public class Bean1 {
  @Inject
  private Bean2 bean2;

  @Inject(bean = "bean3")
  private Bean3 bean3;

  @Inject(type = Bean4.class)
  private Bean4 bean4;

  @Inject
  private Special[] tableOfSpecial;

  @Inject(type = Special.class)
  private Set<Special> collectionOfSpecial;

  @Inject(nullAllowed = true)
  private Bean5 bean5;
}

Kernel automatically determines type of a required beans based on field type. As a result, there is no need to specify the type of a bean in case of bean4 field.

When there are more than one bean instances matching required dependency fields, the type needs to be an array or collection. If kernel is unable to resolve dependencies, it will throw an exception unless @Inject annotation has nullAllowed set to true. This is useful to make some dependencies optional. To help kernel select a single bean instance when more that one bean will match field dependency, you may set name of a required bean as shown in annotation to field bean3.

Dependencies are inserted using getters/setters if those methods exist, otherwise they are inserted directly to the fields. Thanks to usage of setters, it is possible to detect a change of dependency instance and react as required, i.e. clear internal cache.

Warning

Kernel is resolving dependencies during injection only using beans visible in its scope. This makes it unable to inject an instance of a class which is not registered in the same kernel as a bean or not visible in this kernel scope (see Scope and visibility).

Warning

If two beans have bidirectional dependencies, then it is required to allow at least one of them be null (make it an optional dependency). In other case it will create circular dependency which cannot be satisfied and kernel will throw exceptions at runtime.

5.5. Nested kernels and exported beans

Tigase Kernel allows the usage of nested kernels. This allows you to create complex applications and maintain proper separation and visibility of beans in scopes as each module (subkernel) may work within its own scope.

Subkernels may be created using one of two ways:

5.5.1. Manual registration of new a new kernel

You can create an instance of a new kernel and register it as a bean within the parent kernel.

Kernel parent = new Kernel("parent");
Kernel child = new Kernel("child");
parent.registerBean(child.getName()).asInstance(child).exec();

5.5.2. Usage of RegistrarBean

You may create a bean which implements the RegistrarBean interfaces. For all beans that implement this interface, subkernels are created. You can access this new kernel within an instance of RegistrarBean class as register(Kernel) and unregister(Kernel) methods are called once the RegistrarBean instance is created or destroyed.

There is also an interface named RegistrarBeanWithDefaultBeanClass. This interface is very useful if you want or need to create a bean which would allow you to configure many subbeans which will have the same class but different names and you do not know names of those beans before configuration will be set. All you need to do is to implement this interface and in method getDefaultBeanClass() return class which should be used for all subbeans defined in configuration for which there will be no class configured.

As an example of such use case is dataSource bean, which allows administrator to easily configure many data sources without passing their class names, ie.

dataSource {
    default () { .... }
    domain1 () { .... }
    domain2 () { .... }
}

With this config we just defined 3 beans named default, domain1 and domain2. All of those beans will be instances of a class returned by a getDefaultBeanClass() method of dataSource bean.

5.5.3. Scope and visibility

Beans that are registered within a parent kernel are visible to beans registered within the first level of child kernels. However, beans registered within child kernels are not available to beans registered in a parent kernel with the exception that they are visible to bean that created the subkernel (an instance of RegistrarBean).

It is possible to export beans so they can be visible outside the first level of child kernels.

To do so, you need to mark the bean as exportable using annotations or by calling the exportable() method.

Using annotation.

@Bean(name = "bean1", exportable = true)
public class Bean1 {
}

Calling exportable().

kernel.registerBean(Bean1.class).exportable().exec();

5.5.4. Dependency graph

Kernel allows the creation of a dependency graph. The following lines will generate it in a format supported by Graphviz.

DependencyGrapher dg = new DependencyGrapher(krnl);
String dot = dg.getDependencyGraph();

5.6. Configuration

The kernel core does not provide any way to configure created beans. Do do that you need to use the DSLBeanConfigurator class by providing its instance within configuration and registration of this instances within kernel.

Example.

Kernel kernel = new Kernel("root");
kernel.registerBean(DefaultTypesConverter.class).exportable().exec();
kernel.registerBean(DSLBeanConfigurator.class).exportable().exec();
DSLBeanConfigurator configurator = kernel.getInstance(DSLBeanConfigurator.class);
Map<String, Object> cfg = new ConfigReader().read(file);
configurator.setProperties(cfg);
// and now register other beans...

5.6.1. DSL and kernel scopes

DSL is a structure based format explained in Tigase XMPP Server Administration Guide: DSL file format section. It is important to know that kernel and beans structure have an impact on what the configuration in DSL will look like.

Example kernel and beans classes.

@Bean(name = "bean1", parent = Kernel.class, active = true )
public class Bean1 implements RegistrarBean {
  @ConfigField(desc = "V1")
  private String v1;

  public void register(Kernel kernel) {
    kernel.registerBean("bean1_1").asClass(Bean11.class).exec();
  }

  public void unregister(Kernel kernel) {}
}

public class Bean11 {
  @ConfigField(desc = "V11")
  private String v11;
}

@Bean(name = "bean1_2", parent = Bean1.class, active = true)
public class Bean12 {
  @ConfigField(desc = "V12")
  private String v12;
}

@Bean(name = "bean2", active = true)
public class Bean2 {
  @ConfigField(desc = "V2")
  private String v2;
}

public class Bean3 {
  @ConfigField(desc = "V3")
  private String v3;
}

public class Main {
  public static void main(String[] args) {
    Kernel kernel = new Kernel("root");
    kernel.registerBean(DefaultTypesConverter.class).exportable().exec();
    kernel.registerBean(DSLBeanConfigurator.class).exportable().exec();
    DSLBeanConfigurator configurator = kernel.getInstance(DSLBeanConfigurator.class);
    Map<String, Object> cfg = new ConfigReader().read(file);
    configurator.setProperties(cfg);

    configurator.registerBeans(null, null, config.getProperties());

    kernel.registerBean("bean4").asClass(Bean2.class).exec();
    kernel.registerBean("bean3").asClass(Bean3.class).exec();
  }
}

Following classes will produce following structure of beans:

  • “bean1” of class Bean1

    • “bean1_1” of class Bean11

    • “bean1_2” of class Bean12

  • “bean4” of class Bean2

  • “bean3” of class Bean3

Note

This is a simplified structure, the actual structure is slightly more complex. However. this version makes it easier to explain structure of beans and impact on configuration file structure.

Warning

Even though Bean2 was annotated with name bean2, it was registered with name bean4 as this name was passed during registration of a bean in main() method.

Tip

Bean12 was registered under name bean1_2 as subbean of Bean1 as a result of annotation of Bean12

As mentioned DSL file structure depends on structure of beans, a file to set a config field in each bean to bean name should look like that:

'bean1' () {
    'v1' = 'bean1'

    'bean1_1' () {
        'v11' = 'bean1_1'
    }
    'bean1_2' () {
        'v12' = 'bean1_2'
    }
}
'bean4' () {
    'v2' = 'bean4'
}
'bean3' () {
    'v3' = 'bean3'
}