Things to consider when writing WSO2 Carbon Components and Features

In this post, I’m looking at some important things to consider when writing WSO2 Carbon based components and features. But most of them are common and can be used for other development related activities as-well.

Contents focused in this post are : 

OSGi level best practices.

  • Proper bundle creation using bundle plugin
  • Proper package imports and exports with defined versions (or range)
  • Avoid using Dynamicimport-Package *, why?
  • Avoid using *;resolution:=optional, why?
  • Avoid using Require-Bundle, why?
  • Proper usage if bundle concepts (eg : internal package)

Maven dependency definitions related best practices.

  • Use parent pom for dependencyManagement, pluginManagement.
  • Use property based approach with defining dependency versions
  • Use only required dependencies (remove unused)

Carbon components and features related best practices

  • Front-End/Back-End separated components/features.
  • Properly defining features (including vs importing)

Business functionality level best practices.

  • Follow API and Implementation separation when needed.
  • Use OSGi Declarative Service approach for exposing/reusing other component functionalities when needed.

Introduction

A carbon component is the base unit for all features supported by WSO2 platform such as security, clustering, logging, statistics, and management. This is called the compotenized middleware, which is build using the OSGi – The Dynamic Modular System for Java.

There are quite a number of resources and articles on how to write a new carbon component to fit the requirement at hand. But many users and developers tend to forget the best practices that should be followed when writing a carbon component and then the related carbon feature.

Maintaining a carbon component is easy when it was followed with the best practices during the time it was written. This allows the developers of the carbon platform to troubleshoot errors easily with components. If not it becomes a nightmare to find a fix some issues with components. (Eg : OSGi level package imports/exports with class loading issues).

OSGi level best practices

The OSGi plays a major role with carbon components. Since WSO2 Carbon Platform uses OSGi for the underlying modular layer, it is important to follow these best practices.

Proper bundle manifest headers using bundle plugin

The most important task in building OSGi bundles is to write the MANIFEST.MF file correctly. This is where the package information is defined for bundles. The tool used for this task is the bundle plugin, because manually writing this file is not easy.

The below is a sample bundle plugin configuration for a bundle

<plugins>
 <plugin>
   <groupId>org.apache.felix</groupId>
   <artifactId>maven-bundle-plugin</artifactId>
   <configuration>
     <instructions>
       <Bundle-Vendor>WSO2 Inc</Bundle-Vendor>
       <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
     </instructions>
   </configuration>
 </plugin>
</plugins>

The WSO2 way is to use project.artifactId as the bundle symbolic name and bundle vendor as “WSO2 Inc”. More info on using bundle plugin can be found from this article.

Package import and export with defined versions (or range)

The unit of isolation for bundles(or carbon components) at OSGi level is the packages. Bundles will import packages from other bundles for its usage and export packages to the OSGi environment for other bundles to use. They will also have some private packages, which are not exposed outside, but used within the bundle only.

Here is an example bundle plugin configuration

<plugin>
    <groupId>org.apache.felix</groupId>
    <artifactId>maven-bundle-plugin</artifactId>
    <extensions>true</extensions>
    <configuration>
        <instructions>
            <Bundle-Vendor>WSO2 Inc</Bundle-Vendor>
            <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
            <Bundle-Activator>
                 org.wso2.carbon.clustering.internal.CarbonClusterBundleActivator
            </Bundle-Activator>
            <Private-Package>
                org.wso2.carbon.clustering.internal
            </Private-Package>
            <Export-Package>
                !org.wso2.carbon.clustering.internal,
                org.wso2.carbon.clustering.*;version="5.0.0"
            </Export-Package>
            <Import-Package>
                org.slf4j.*;version="${slf4j.logging.import.version.range}",
                org.osgi.framework.*;version="${osgi.framework.import.version.range}",
                org.osgi.service.component.*;version="[1.0.0, 1.1.0)",
                com.hazelcast.core.*;version="[3.0.1, 3.1.0)",
            </Import-Package>
        </instructions>
    </configuration>
</plugin>

Best practices.

  1. Export packages with a version.

This will ensure that the packages you export will have a version at run-time. So that interested parties can import packages using that version. If a version is not specified, then it will be considered as 0.0.0 by the framework. Also if you want to exclude some packages, then you can use the “!”.

  1. Import packages with the version range.

This is needed in order to ensure that you bundle gets wired with the correct bundle at runtime. There can be multiple bundles exporting the same package, but with different versions. If a version range is not specified, your bundle gets wired with the bundle which exports the higher version of the package. This may lead to some issues, unexpected behaviour at runtime, where the packages imported may have different classes, methods or business logic.

When importing packages the best practice is to use the semantic version approach with pattern like com.foo.bar.*;version=”[1.0.0, 1.1.0)”,

This informs that my bundle requires com.foo.bar.* package that is exported in the version range of >=1.0.0 and <1.1.0. This is normal way to ensure that there will be no API changes of the package from a minor version.

The version range for non specified packages are considered like (0.0.0, ). This means that any version that is between 0.0.0 to can get wired with the bundle.

Avoid using Dynamicimport-Package “*”

The <DynamicImport-Package>*</DynamicImport-Package> manifest header is used to allow a bundle to be wired up to packages that may not known in advance. This may look like a easy way to handle package imports, but it tends to break the modular nature of OSGi. The reason is that this will make the class-path search of OSGi Framework a very expensive operation for the packages involved and breaks the concept of versions in OSGi.

This header is only required in a situation where the Class.forName() is used within your bundle. Other than that, it is not recommended to use this header.

Avoid using *;resolution:=optional

The resolution:=optional directive is used in a situation where some packages or rather dependencies are not required for the bundle to resolve. This is ok if some packages are not found in run-time but your bundle needs to resolve. But the directive with “*” will become an issue where the bundle plugin will find all package imports for you and place it in the manifest file, when you have not explicitly mentioned them in import packages section. It will also put some version range for those missing imports based on the dependency version. Sometimes, this will become hard when resolving issues where you bundle gets wired to a different package (bundle) but not the expected one at run-time. You can use this directive in a situation where some packages are actually not needed for your bundle to resolve at run-time. But the same directive with “*” is not recommended, as it may cause bundle wiring issues.

Avoid using Require-Bundle

This is actually an anti pattern in OSGi world, where a complete bundle is added as a dependency to a bundle. The recommended approach is to use package imports/exports from/to bundles.

Proper usage of bundle concepts (eg : internal package)

The internal package (conceptually package name consisting “internal” in any WSO2 carbon components) of a bundle is considered as private to that bundle and used within the bundle only. This is where the BundleActivator class and OSGi Service Holder classes reside. These are private to that bundle and should not be exposed to outside.

For exposing/reusing other component functionalities the recommended approach is to use OSGi Services. The internal framework which supports the dynamic nature for services is the Declarative Services Framework.

Maven dependency definitions related best practices

Since maven is the mostly used tool for the project build management, it is also important to use the best practices with defining pom files and project.

Use parent pom for dependencyManagement, pluginManagement, repository management.

The recommended approach for defining maven build management sections such as dependencyManagement, pluginManagement, repository (both dependency and plugins) is to use the root parent pom. This the parent for all the modules in maven project. Each sub-modules (or child) will refer these. They will not have to define the versions for these when referring them in their poms. The parent pom is the only place to define all the dependency , plugin, versions, and other common properties. This will enable the easy management of the project, where you will need to do minimal change when you’re updating a version. An example pom which follow the above best practices is found here

Use property based approach with defining dependency versions

When a dependency, or a plugin is defined in the management section in the parent pom, it is recommended to use property based approach to define the versions.

Consider the following example

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-server</artifactId>
    <version>${version.jetty.server}</version>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-http</artifactId>
    <version>${version.jetty.server}</version>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-util</artifactId>
    <version>${version.jetty.server}</version>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-io</artifactId>
    <version>${version.jetty.server}</version>
</dependency>

All the above dependencies are using the same version. So as given above, when using the property based approach (${version.jetty.server}), the version can be easily managed. Also it is not required to specify the version for these dependencies in the child modules that refer these.

Use only required dependencies

With dependencies definition there are different types. Some dependencies properly declared and used and some comes from transitive dependencies which is not properly declared in your project.

The recommended way is declare dependencies that are used only. Other non related dependencies can be easily identified using the maven dependency plugin. More info on this can be found from here.

Carbon components and features related best practices

Front-End and Back-End separated components/features. 

When you are developing a new carbon component, in most of the cases you will need to provide the UI front for that component. This is where the users will interact with the functionality you provide with the component. There will be a separate UI component, which talk to the back-end component via http/s (SOAP). This separation of front-end and back-end in a carbon component is known as the FE/BE separation. This is the recommended way in developing new carbon components. The same concept is used when developing the feature for your component, where for the UI related bundles, there will be separate UI feature and for back-end related bundles, there will be a back-end feature and there will be a composite feature which combine these features. More on writing carbon components is available here.

Properly defining features

Naming convention

The first thing to note about when defining new features is the naming convention to follow. A feature should have a descriptive name for the users to identify them uniquely and a feature ID for run-time identification. Consider the following example feature. The highlighted section is self describing about the feature.

<build>
    <plugins>
        <plugin>
            <groupId>org.wso2.maven</groupId>
            <artifactId>carbon-p2-plugin</artifactId>
            <version>${carbon.p2.plugin.version}</version>
            <executions>
                <execution>
                    <id>4-p2-feature-generation</id>
                    <phase>package</phase>
                    <goals>
                        <goal>p2-feature-gen</goal>
                    </goals>
                    <configuration>
                        <id>org.wso2.carbon.cluster.mgt.server</id>
                        <propertiesFile>../../../etc/feature.properties</propertiesFile>
                        <adviceFile>
                            <properties>
                                <propertyDef>org.wso2.carbon.p2.category.type:server</propertyDef>
                                <propertyDef>org.eclipse.equinox.p2.type.group:false</propertyDef>
                            </properties>
                        </adviceFile>
                        <bundles>
                            <bundleDef>org.wso2.carbon:org.wso2.carbon.cluster.mgt.admin</bundleDef>
                        </bundles>
                        <importFeatures>
                            <importFeatureDef>org.wso2.carbon.core.server:${wso2carbon.version}</importFeatureDef>
                        </importFeatures>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Feature categories

The general categorization for carbon features are Server Feature, UI (Console) Feature and Common Feature (This is an optional category). All of these categories should be added to Composite (Aggregate) Feature as an aggregate.

Feature construction

Carbon features are constructed using the carbon-p2 maven plugin. It has a set of instruction to create a feature.

  1. Include bundles
  2. Include features
  3. Import bundles
  4. Include features

The above instructions are used based on the requirement for the feature that you are developing.

Including bundles

This is used when a bundle/jar should be included in the feature as sub part which makes the feature to tightly couple with it. The bundle/jar also gets packed with feature. In the above feature example this is carried using the following element.

<bundles>
	<bundleDef>org.wso2.carbon:org.wso2.carbon.cluster.mgt.admin</bundleDef>
</bundles>

Including features

Same as including bundle, other features can also be included into a feature and gets packed with it. The common example for this is the composite feature, which includes both UI and Server feature as given below.

<includedFeatures>
	<includedFeatureDef>org.wso2.carbon:org.wso2.carbon.cluster.mgt.server.feature</includedFeatureDef>
	<includedFeatureDef>org.wso2.carbon:org.wso2.carbon.cluster.mgt.ui.feature</includedFeatureDef>
</includedFeatures>

Importing bundles

This is used when a bundle is required as a dependency to the feature, but not tightly coupled with it. For example apache ode bundle is required  as a dependency for org.wso2.carbon.bpel.ui.feature.

<importBundles>
	<importBundleDef>org.apache.ode.wso2:ode</importBundleDef>
</importBundles>

Importing Features

Like with importing bundles, this is used when a feature depends on other features for correct behavior of that feature. An example will look like below. This is common approach when your feature requiring some other feature for its operation.

<importFeatures>
	<importFeatureDef>org.wso2.carbon.core:${wso2carbon.version}</importFeatureDef>
</importFeatures>

Best practices with defining features

The important point to note here is that, when to use which of the above instruction. If a feature requires other features as dependencies, the correct way is to use “ImportedFeatures”. This will allow the feature of interest to be loosely coupled with dependent features and will allow updates to new version of the dependent feature suing installation. This is same for bundle imports as well. Instead of importing but including features/bundles in wrong place, will lead to a situation where updates to new version of them is not available and some time will result in installation failure. There are more information on the best practices to follow with features is found in this resource.

Business functionality level best practices.

Follow API and Implementation separation when needed

When writing a solution based on OSGi, it is always best to have a separation between implementation and the related API’s. This allows the users of the API not to get affected for any changes in the implementation. For example consider a situation where the clustering framework is designed using the above pattern. This makes easy for a new clustering implementation to be plugged in based on the API. The user of the clustering service is not aware of the underlying implementation changes and continue to use the clustering service.

Use OSGi Declarative Service (DS) approach when needed.

For exposing/reusing other component functionalities the recommended approach is to use OSGi Services. The internal framework which supports the dynamic nature for services is the Declarative Services Framework. In the below example, we can see how to use DS based approach and the things to consider when using it within carbon framework.

An example DS component

/**
 * @scr.component name="component.manager.core.service.comp" immediate="true"
 * @scr.reference name="provisioning.agent.provider"
 * interface="org.eclipse.equinox.p2.core.IProvisioningAgentProvider"
 * cardinality="1..1" policy="dynamic" bind="setProvisioningAgentProvider"
 * unbind="unsetProvisioningAgentProvider"
 * @scr.reference name="server.config.service" interface="org.wso2.carbon.base.api.ServerConfigurationService"
 * cardinality="1..1" policy="dynamic"  bind="setServerConfigurationService" unbind="unsetServerConfigurationService"
 */
public class ComponentMgtCoreServiceComponent {

    protected void activate(ComponentContext ctxt) {
        ctxt.getBundleContext().registerService(CommandProvider.class.getName(), new ProvCommandProviderExt(), null);       
    }

    protected void setProvisioningAgentProvider(IProvisioningAgentProvider provisioningAgentProvider) {
        ServiceHolder.setProvisioningAgentProvider(provisioningAgentProvider);
    }

    protected void unsetProvisioningAgentProvider(IProvisioningAgentProvider provisioningAgentProvider) {
        ServiceHolder.setProvisioningAgentProvider(null);
    }
    
    protected void setServerConfigurationService(ServerConfigurationService serverConfigService) {
	ServiceHolder.setServerConfigurationService(serverConfigService);
    }

    protected void unsetServerConfigurationService(ServerConfigurationService serverConfigService) {
	ServiceHolder.setServerConfigurationService(null);
    }
}

In the above example, the DS component (component.manager.core.service.comp), is referring two different OSGi service references for it to become active. They are “IProvisioningAgentProvider” and “ServerConfigurationService”. These services are registered by some other bundles/components and when they become available, this component is notified and will be able to acquire the references for them. This process is handled by the underlying OSGi DS framework. When the referring services become available in the run-time, the setter methods of those respective scr.reference will be called.

Use of ServiceHolder approach

The point to note here is that what do we do when this method is called?

The widely known approach for most of the components is storing this references in a “Holder” and then use it when required within the bundle. This is known as the “ServiceHolder” approach. In the above example also we can see that the references are saved in the ServiceHolder within the setter methods. Also when the references become unavailable (since OSGi run-time is highly dynamic, the service references may become available / unavailable any time), the corresponding unsetter methods will be called, in which the references are removed from the ServiceHolder.

The ServiceHolder is considered as the internal part of the bundle and should not be exposed outside of bundle. To achieve this, the ServiceHolder classes are kept within the private package section of the bundle.

Advertisements

About kishanthan

I’m currently working as a Software Engineer at WSO2, an open source software company. I hold an Engineering degree, majoring in Computer Science & Engineering field, from University of Moratuwa, Sri Lanka.
This entry was posted in Carbon, OSGi, WSO2 and tagged , , , , , , , , , . Bookmark the permalink.

One Response to Things to consider when writing WSO2 Carbon Components and Features

  1. Gregory says:

    Great article! Thanks!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s