This document can be used as a guide to develop custom plugins.
Scope
-
How to start plugin development?
-
Tooling and APIs to build and deploy plugins
Where to Start?
We provide a sample hello world plugin which can be used as the starting point for developing a custom plugin.
In addition, we have separate docs for different types of plugins. A good starting point would be to assess what the plugin should do and then based on that, read the various docs that describe the different types of plugins offered in Kill Bill:
The next stage is to identify existing (similar) plugins which could be used as a starting point to write the code. At this point, this becomes a normal software development cycle, including writing unit tests for that plugin (independent of Kill Bill).
The rest of this document will explain how to setup and work with the hello-world-plugin
.
Prerequisites
-
Ensure that you have gone through the What is Kill Bill document and are familiar with Kill Bill.
-
Ensure that you have gone through the Plugin Introduction document and are familiar with Kill Bill plugins.
-
Ensure that you have Maven 3.5.2 or higher (It can be downloaded from here).
-
Ensure that you have JDK 8 or higher (It can be downloaded from here).
-
Ensure that JAVA_HOME environment variable is set to the path of your JDK installation (For example, if JDK is installed at C:\Software\jdk1.8.0_102, you need to set JAVA_HOME to C:\Software\jdk1.8.0_102).
Getting Started
Let us now take a look at the steps involved in setting up the hello world plugin
.
Setting up the Code in an IDE
The first step is to set up the plugin code in an IDE. For this, you need to do the following:
-
Clone the
hello-world-plugin
repository from Github. -
Set up the code in the IDE of your choice. You can refer to our Development Document for detailed instructions on how to clone a repository and set up the code in Eclipse.
-
This path where the repository is cloned will be referred to as PROJECT_ROOT from now on. For example, if you choose
C:/MyProjects/killbill-hello-world-java-plugin
, PROJECT_ROOT refers to this path. -
Once the code is set up in Eclipse, your Eclipse workspace should look like this:
Build
The hello-world-plugin
is a standard Maven project. So you can build it as follows:
-
Open a command prompt/terminal window and navigate to the PROJECT_ROOT directory.
-
Run the following command:
mvn clean install -DskipTests
-
Verify that a BUILD SUCCESS message is displayed on the console and that the plugin jar file is created as
PROJECT_ROOT/target/<artifact_id>-<version>.jar
.
Deployment
The hello-world-plugin
can be deployed in an AWS, Docker or Tomcat Kill Bill installation as explained in the Getting Started guide. However, if you are using the plugin as a basis to develop your own plugin, it would be useful to deploy the plugin in a standalone Kill Bill installation. Let us take a look at how to do this.
-
Ensure that the Kill Bill application is configured and running in a Jetty server as explained here.
-
Ensure that you have kpm installed as per the instructions here.
-
Open a command prompt/terminal window and run the following command to install the plugin (Replace
PROJECT_ROOT
with your actual project root,<artifact_id>-<version>
with your JAR file name andpath_to_install_plugin
with the actual path where you would like to install the plugin):kpm install_java_plugin 'dev:hello' --from-source-file=PROJECT_ROOT/target/<artifact_id>-<version>.jar --destination=<path_to_install_plugin>
-
Verify that the
<artifact_id>-<version>.jar
is copied at thepath_to_install_plugin
path. -
Open
PROJECT_ROOT/profiles/killbill/src/main/resources/killbill-server.properties
and specify the following property (Replaceplugin_path
with the actual path where the plugin is installed. Note that if this property is not specified, Kill Bill looks for the plugin at the/var/tmp/bundles/
path):org.killbill.osgi.bundle.install.dir=<plugin_path>
-
Open a command prompt/terminal window and navigate to the PROJECT_ROOT directory. Start Kill Bill by running the following command (Replace
PROJECT_ROOT
with your actual project root):mvn -Dorg.killbill.server.properties=file:///PROJECT_ROOT/profiles/killbill/src/main/resources/killbill-server.properties -Dlogback.configurationFile=./profiles/killbill/src/main/resources/logback.xml jetty:run
-
Verify that the following is displayed in the Kill Bill logs which confirms that the plugin is installed successfully:
GET / [*/*] [*/*] (/HelloWorldServlet.hello) GET /healthcheck [*/*] [*/*] (/HelloWorldHealthcheckServlet.check) listening on: http://localhost:8080/plugins/hello-world-plugin/
-
Open a browser and type http://localhost:8080/plugins/hello-world-plugin/. If the plugin is installed properly, the following should be displayed in the Kill Bill logs:
2020-12-09T04:58:15,750+0000 lvl='INFO', log='HelloWorldServlet', th='http-nio-8080-exec-1', xff='', rId='b79decfb-e809-4c01-9064-cff18722a67c', tok='', aRId='', tRId='', Hello world
A Closer Look at Plugin Code
Let us now take a look at some of the important classes in the hello world plugin.
HelloWorldServlet
Plugins can export their own HTTP endpoints. The HelloWorldServet provides an example of this. It exports the plugins/hello-world-plugin/
endpoint. You can extend the HelloWorldServlet
to add other endpoints as required.
A few pointers about the HelloWorldServlet.hello
method (which provides the code for the plugins/hello-world-plugin/
endpoint):
@GET
public void hello(@Local @Named("killbill_tenant") final Optional<Tenant> tenant) {
// Find me on http://127.0.0.1:8080/plugins/hello-world-plugin
logger.info("Hello world");
if(tenant != null && tenant.isPresent() ) {
Tenant t1 = tenant.get();
logger.info("tenant id:"+t1.getId());
login();
}
else {
logger.info("tenant is not available");
}
}
-
This method provides the code for the http://localhost:8080/plugins/hello-world-plugin endpoint.
-
It accepts a parameter corresponding to
Tenant
which is anOptional
. -
If the headers X-Killbill-ApiKey / X-Killbill-ApiSecret are set while accessing this endpoint as shown below, Kill Bill automatically injects a
Tenant
object into the servlet.curl -v -u admin:password -H "X-Killbill-ApiKey: bob" -H "X-Killbill-ApiSecret: lazar" "http://127.0.0.1:8080/plugins/hello-world-plugin"
-
The
Tenant
object can then be used to retrieve tenant information liketenantId
as demonstrated in the code above. -
If the headers X-Killbill-ApiKey / X-Killbill-ApiSecret are NOT set while accessing this endpoint as shown below, Kill Bill injects an empty
Optional
into the servlet.curl -v -u admin:password "http://127.0.0.1:8080/plugins/hello-world-plugin"
HelloWorldListener
The HelloWorldListener provides sample code for developing a notification plugin. It listens to Kill Bill events and takes actions. You can extend this class to handle other events as required. See the Notification Plugin Tutorial for further information.
HelloWorldPaymentPluginApi
The HelloWorldPaymentPluginApi class provides sample code for developing a payment plugin. It implements the PaymentPluginApi interface. You can extend this class as required to develop a payment plugin. See the Payment Plugin Tutorial for further information.
Other Classes
In addition to the classes listed above, some of the other classes in the hello-world-plugin
are as follows:
-
HelloWorldActivator: While building a plugin, you need to create a class similar to
HelloWorldActivator
. You need to specify your plugin name here. -
HelloWorldConfigurationHandler: Most plugins require custom configuration. A configuration handler similar to the
HelloWorldConfigurationHandler
can be used to read the configuration properties. -
HelloWorldHealthcheck and HelloWorldHealthcheckServlet: Can be used to provide the health status of the plugin.
Enabling Per-tenant Configuration
As explained here, Kill Bill supports per tenant configuration for plugins. In order enable per tenant configuration, the following needs to be done:
-
Create a custom configuration handler similar to HelloWorldConfigurationHandler
-
Initialize it in the
start
method of your activator class as follows (see HelloWorldActivator.start):helloWorldConfigurationHandler = new HelloWorldConfigurationHandler(region, PLUGIN_NAME, killbillAPI); final Properties globalConfiguration = helloWorldConfigurationHandler.createConfigurable(configProperties.getProperties()); helloWorldConfigurationHandler.setDefaultConfigurable(globalConfiguration);
Now, you can use the ConfigurationHandler
to retrieve per-tenant properties. You can see an example of this in the Authentication Steps section below.
Setting Up a Breakpoint and Debugging
When you start developing your own plugin, it would be useful to be able to set up a break point and debug the plugin code. This section explains how you can achieve this.
-
Create a new environment variable MAVEN_OPTS and set it to
-Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n
. -
Open Eclipse and do the following:
-
Set up a break point in the HelloWorldServlet#L41.
-
Click Run > Debug Configurations.
-
Double-click New Remote Java Application.
-
Enter the name that you would like to give to this debug configuration in the Name field.
-
Click Apply.
-
Click Close.
-
-
Restart the Kill Bill application as explained in the "Deployment" section above.
-
Click
Run > Debug Configurations
and double click the the Debug configuration that you created above. -
This runs the application in debug mode. You can also set additional breakpoints as required.
Authentication Within Plugins
In order to invoke write API operations like AccountUserApi#createAccount
, plugin code must authenticate against Kill Bill first. Otherwise, it will result in an org.apache.shiro.authz.UnauthenticatedException
. This section explains how authentication can be done.
Authentication Steps
In order to authenticate against Kill Bill, the following needs to be done:
-
Configure the plugin with custom credentials - Although plugins can use the
admin/password
credentials for authentication, it is typically not advisable to do so. This is to limit the scope of operations that plugins can execute. It is thus recommended to configure plugins with custom credentials. (See Per-tenant Plugin Configuration). So, you can configure thehello-world-plugin
with custom credentials as follows:curl -v \ -X POST \ -u admin:password \ -H "X-Killbill-ApiKey: bob" \ -H "X-Killbill-ApiSecret: lazar" \ -H "Content-Type: text/plain" \ -H "Accept: application/json" \ -H "X-Killbill-CreatedBy: demo" \ -H "X-Killbill-Reason: demo" \ -H "X-Killbill-Comment: demo" \ -d 'org.killbill.billing.plugin.hello-world.credentials.username=hello-world-user org.killbill.billing.plugin.hello-world.credentials.password=hello-world-password' \ "http://127.0.0.1:8080/1.0/kb/tenants/uploadPluginConfig/hello-world-plugin"
-
Retrieve credentials in the code. For example, you can retrieve the credentials in the
HelloWorldListener
class as follows:Properties properties = helloWorldConfigurationHandler.getConfigurable(killbillEvent.getTenantId()); final String username = properties.getProperty("org.killbill.billing.plugin.hello-world.credentials.username"); final String password = properties.getProperty("org.killbill.billing.plugin.hello-world.credentials.password");
-
Invoke SecurityApi - Use the credentials obtained above to login as follows:
killbillAPI.getSecurityApi().login(login, password);
-
Invoke the necessary write API method (The code below invokes the
accountUserApi.createAccount
method):osgiKillbillAPI.getAccountUserApi().createAccount(accountData, context);
-
Invoke the
logout
method. This should typically be done within afinally
clause:osgiKillbillAPI.getSecurityApi().logout();
-
You can also perform authentication within the
HelloWorldPaymentPluginApi
as well asHelloWorldServlet
. Within the servlet, you will need to write code similar to the following:private void login(final HttpServletRequest req) { String authHeader = req.getHeader("Authorization"); if (authHeader == null) { return; } final String[] authHeaderChunks = authHeader.split(" "); if (authHeaderChunks.length < 2) { return; } try { final String credentials = new String(BaseEncoding.base64().decode(authHeaderChunks[1]), "UTF-8"); int p = credentials.indexOf(":"); if (p == -1) { return; } final String login = credentials.substring(0, p).trim(); final String password = credentials.substring(p + 1).trim(); killbillAPI.getSecurityApi().login(login, password); } catch (UnsupportedEncodingException ignored) { } }
Skipping Authentication
It is also possible to skip authentication in the plugin code. For this, the following needs to be done:
-
Set the following property in the Kill Bill config file:
org.killbill.security.skipAuthForPlugins=true
-
Create a
PluginCallContext
class in your code similar to the email notification plugin PluginCallContext class. -
Create a
PluginCallContext
instance withCallOrigin.INTERNAL
andUserType.ADMIN
as follows:final PluginCallContext callContext = new PluginCallContext(UUID.randomUUID(),pluginName, CallOrigin.INTERNAL,UserType.ADMIN,reasonCode,comments, createdDate,updatedDate,accountId, tenantId);
-
Use the above
callContext
while invoking the desired api method:accountUserApi.createAccount(accountData, callContext);
Additional Notes
-
We provide a Java plugin framework that can be used to implement some of the work that plugins need to do - although your plugin does not have to rely on this framework, it is often a good idea to leverage it to avoid boilerplate code.
-
Also, for internal reference, you might want to take a look at KillbillActivatorBase, which provides all the abstractions that plugins require (access to java APIs, database connections, system properties, logging, …).
OSGi Configuration
As explained earlier, Kill Bill plugins are based on the OSGi standard. Let us now take a look at how this works and some additional OSGi configuration which may be required in some situations.
Note
|
Note: OSGi configuration is an advanced configuration option and may be required only in rare situations. So, feel free to skip this section. |
Brief OSGi Overview
Let us first briefly understand how OSGi works. OSGi allows creating modular Java components (known as bundles) that run within an OSGi container. The OSGi container ensures that each bundle is isolated from other bundles. Thus, each bundle can use any external dependencies that it requires without having to worry about conflicts.
A bundle is nothing but a JAR file. However, its manifest.mf
has some additional OSGi related headers.
Although each bundle is isolated from other bundles, sometimes bundles may need to communicate/share classes with other bundles. A bundle can export a package to make the corresponding classes available for use by other bundles. A bundle can also import a package to use the classes of another bundle.
For example if a bundle bundle1
requires a class p1.p2.A
from bundle2
, bundle2
needs to export the p1.p2
package and bundle1
needs to import this package. The packages imported by a bundle are specified as a Import-package
header in the manifest.mf
while packages exported by a bundle are specified as a Export-package
header in the manifest.mf
.
The OSGi container ensures that a given bundle’s package dependencies can be satisfied before the bundle runs. Thus, if the package dependencies cannot be satisfied, the bundle will not run.
Kill Bill OSGi Overview
Before we dive into the details, let us understand at a high-level how the import-export mechanism works in case of the core Kill Bill system and its plugins.
-
The Kill Bill core itself is packaged as an OSGi bundle (referred to as system bundle). It exports several packages. This is explained in the "Packages exported by Kill Bill" section.
-
A plugin automatically imports any packages exported by Kill Bill. This is explained in the "Packages Imported by Plugins by Default" section.
-
However, in some cases, a plugin may need to explicitly import packages exported by Kill Bill. This is explained in the "Importing Additional Packages in Plugins" section.
Packages Exported by Kill Bill
As explained earlier, the Kill Bill system bundle exports the packages which it desires to share with plugins. Refer to the value of the org.killbill.osgi.system.bundle.export.packages.api
property in the Kill Bill Configuration Properties Table to see the complete list of packages exported by default.
Additionally, Kill Bill also offers the org.killbill.osgi.system.bundle.export.packages.extra
property which can be used to specify additional packages to be exported by the system bundle and that could in turn be imported by a plugin. This property can be configured as explained in the Kill Bill configuration document.
Packages Imported by Plugins by Default
As explained earlier, Kill Bill plugins are packaged as OSGi bundles. The maven-bundle-plugin specified in the pom.xml is responsible for packaging a plugin as an OSGi bundle. Thus, the maven-bundle-plugin
takes care of creating the jar with the correct OSGi headers (including adding the required packages to the Import-Package
header). In addition, the killbill-oss-parent pom file (which is the parent of the plugin pom.xml
file) also specifies the packages to be included in the Import-Package
header.
Thus, when a plugin is built, the Import-Package
header is automatically computed based on:
-
Packages computed by the
maven-bundle-plugin
. -
Packages specified in the
killbill-oss-parent
pom file.
Importing Additional Packages in Plugins
Sometimes, a plugin may require to use some additional packages from Kill Bill (other than those automatically imported as specified above). In such cases, you will need to explicitly export the package from Kill Bill and import it in the plugin as explained below.
-
All the packages exported by Kill Bill by default are specified as the value of the
org.killbill.osgi.system.bundle.export.packages.api
property in the Kill Bill Configuration Properties Table. Check whether the desired package is already present in this list. -
If Kill Bill does not already export the package, add the following property in the Kill Bill configuration file:
org.killbill.osgi.system.bundle.export.packages.extra=<package1>,<package2>..<packagen>
-
Open plugin
pom.xml
and specify the following in theproperties
section (Replace<package>
with the fully qualified name of the package that you would like to export):<osgi.extra-import> <package1>; <package2>; .... <packagen> </osgi.extra-import>
-
Build the plugin using Maven as specified above.
This causes the package to be added to the Import-Package
header of the plugin jar. You can see an example of this in the Kill Bill Adyen Plugin pom file.
Exporting Additional Packages from a Plugin
A plugin can also export packages corresponding to the classes that it wants to share with other plugins. This mechanism is particularly useful since it allows plugins to share custom functionality with other plugins.
To export a package from a plugin, you need to follow the steps given below.
-
Open plugin
pom.xml
. Specify the following in theproperties
section (Replace<package>
with the fully qualified name of the package that you would like to export):<osgi.export> <package1>, <package2> .... <packagen> </osgi.export>
-
Build the plugin using Maven as specified above.
This causes the specified packages to be added to the Export-Package
header of the plugin jar. Other plugins can then use the classes in these packages by importing them as explained above.
In order to see an example of a plugin exporting packages, you can refer to the email notification plugin. It exports packages required for creating a custom invoice formatter. The custom invoice formatter plugin (which is a sample plugin) then imports these packages to customize the email invoice.
FAQ
This section lists some common errors that are encountered while developing plugins and how you can fix them.
Authentication Error
Sometimes, you may see the org.apache.shiro.authz.UnauthenticatedException: This subject is anonymous
. This occurs when your plugin code invokes any of the read/write Kill Bill operations without authenticating against Kill Bill. So, you first need to invoke SecurityApi#login
API. See "Authentication Steps" section.
Maven Build Error
Sometimes, when you build the code using mvn clean install
you may see some build errors as follows:
-
Enforcer error:
Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.0.0-M3:enforce (default) on project killbill-plugin-momo: Some Enforcer rules have failed.
This generally occurs when your
pom.xml
contains dependencies that are not recommended. In such cases, we recommend that you fix the POM file and rerun the build, or run the build with the-Dcheck.fail-enforcer=false
option. -
Spotbugs error: Kill Bill automatically performs
spotbugs
checks when runningmvn verify
. So, this may result in some spotbugs errors.In such cases, we recommend that you fix the spotbugs errors and rerun the build, or run the build with the
-Dcheck.fail-spotbugs=false
option. -
Other errors: The build process has several checks in place to make sure the right dependencies are pulled in (for example, there are no duplicate dependencies, there are no obvious bugs, etc.). If you’d like to ignore all these checks and still proceed with the build, you can run the following command:
mvn clean install -Dcheck.fail-enforcer=false -Dcheck.fail-dependency=false -Dcheck.fail-dependency-scope=false -Dcheck.fail-dependency-versions=false -Dcheck.fail-duplicate-finder=false -Dcheck.fail-enforcer=false -Dcheck.fail-spotbugs=false -Dcheck.ignore-rat=true
java.lang.NoClassDefFoundError or java.lang.ClassNotFoundException
Sometimes, when you develop a custom plugin, a java.lang.NoClassDefFoundError
or a java.lang.ClassNotFoundException
exception may occur on starting Kill Bill. This is most probably because the class in question is not present on the classpath.
For a plugin to work, any classes used by the plugin must be present on the classpath. So, the class needs to be present in the plugin jar itself or it must be imported from Kill Bill. Refer to the "Importing Additional Packages in Plugins" section above for further details.
"Payment plugin xxx is not registered" Error
Sometimes, when you develop a custom payment plugin, you may see a Payment plugin xxx is not registered
error. This is typically because the plugin is not registered as a payment plugin in your activator class. Ensure that it is registered as shown here.