Last updatedNov 30, 2018

Rate this page:

Plugins2 add-ons

A JIRA add-on (also known as a plugin) does exactly what you think it might do: it adds to the functionality of JIRA. It might add a single feature, like a report, or it might provide enough features to constitute a product in its own right. An add-on is installed separately to JIRA, via the Universal Plugin Manager. If you want to make it available for others, you can list it on the Atlassian Marketplace

Hello world

If you already know the theory and want to jump straight into development, read our Getting started guide to build your first JIRA Plugins2 add-on.

About Plugins2 add-ons

A Plugins2 add-on is a single JAR containing code, an add-on descriptor (XML) and usually some Velocity template files to render HTML. 

Add-on descriptor

The add-on descriptor is the only mandatory part of the add-on. It must be called atlassian-plugin.xml and be located in the root of your JAR file. Here is a sample of the descriptor with highlighted elements:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- the key must be unique, think of it as the 'package' of the add-on -->
<atlassian-plugin key="com.atlassian.addon.sample" name="Sample Add-on" plugins-version="2">
    <!-- a short block describing the plugin itself -->
    <plugin-info>
        <description>This is a brief textual description of the add-on</description>
        <!-- the version of the add-on -->
        <version>1.1</version>
        <!-- details of the add-on vendor -->
        <vendor name="Atlassian Software Systems Pty Ltd" url="http://www.atlassian.com"/>
    </plugin-info>

    . . . 1 or more add-on modules . . .
</atlassian-plugin>

Add-on keys

Each add-on has an add-on key which is unique among all add-ons (e.g. "com.atlassian.addon.sample"). Typically the root package name of the primary Java class is used.

Each module (see JIRA modules below) within the add-on also has a module key, which is unique within the add-on (eg. "myreport"). Semantically, this equates to the name of a Java class.

The add-on key + module key are combined to make the complete key of the add-on module (e.g. "com.atlassian.addon.sample:myreport"). Note, a colon is used to separate the add-on key from the module key.

JIRA modules

A JIRA add-on consists of one or more modules (see 'JIRA modules' below). These are of different types (e.g. a report) and each has an individual XML element describing it. The add-on modules supported by JIRA Server are described in Reference section. See Web fragments.

JIRA add-on lifecycle

Once you start building your own add-ons, it is likely that you'll need to call on JIRA code to accomplish certain tasks; for example, to retrieve a list of users, make workflow changes or add new data to issues. This section describes the lifecycle of a JIRA add-on, and the stages that a add-on can hook into to perform its initialization tasks, closing down tasks, and so on. 

Stages in the add-on lifecycle

There are several stages in the add-on lifecycle, and in the lifecycle of your add-on's components (i.e. component plugin modules), that you can hook into in your add-on. During these stages, you can perform various tasks such as wiring up dependencies, initialising caches or programmatically configuring JIRA. It is important to note however that it is not always appropriate to perform some tasks at certain stages, because the state of JIRA and the add-ons system may not be ready for certain tasks.

The stages are described in the Component initialization and Component destruction sections below, including notes about what you can, cannot, or should not do at each stage.

Component initialization

More advanced add-ons will contain one or more components, typically to implement services, managers, stores or other things. When an add-on unit is loaded into the add-on system, its components must first be initialised before the add-on can be successfully registered. This happens when:

  • The add-ons system is being started:
    • JIRA is started
    • A data export is being imported into JIRA
  • An add-on is being installed
  • An add-on, or the specific add-on module, is being enabled

There are three possible phases to component initialisation. The first is construction. This is typically where dependencies for your component are injected, if you are using constructor-based injection. Any basic initialisation of your component's fields also happens here. For example, defining caches.

The second and third phases are very similar: the execution of methods annotated with @PostConstruct, followed by the execution of the afterPropertiesSet method from the InitializingBean interface, in that order. Naturally, the third phase is only executed if your component implements the InitializingBean interface. These two phases are essentially equivalent, and the particular phase you wish to use depends on your Spring configuration. During these phases, all dependencies of your components have already been injected (whether you are using constructor-based or setter-based injection). For example, if your component is an event listener, you can register it with the EventPublisher (that you would have declared as a dependency) in your @PostConstruct method.

Note: @PostConstruct and InitializingBean are concepts provided by the Spring framework. For more information on them, see their documentation at @PostConstruct and InitializingBean.

Even though your component's dependencies have been injected, this does not necessarily mean that those dependencies are in a "ready-to-use" state. For example, your component might depend on a JIRA component which caches information about add-ons. During your component's initialisation, there may still be other add-ons which have not yet been initialised, and thus enabled in the add-ons system. Therefore, accessing that JIRA component's methods will cause it to act without knowledge of other as-yet uninitialised add-ons. To avoid this scenario, it is best to defer accessing of dependencies which require knowledge of the add-on system until after all add-ons have been initialised and enabled. See the section on add-on system events below.

If initialisation fails for any reason, the add-on will effectively be disabled. Initialisation may fail if the component takes too long to complete (the default timeout is 30 seconds). Therefore, it is not a good idea to attempt any potentially long-running tasks during component initialisation, such as connecting to an external resource, or loading/processing large amounts of data. If such tasks are required but can be asynchronous, you could instead create a scheduled task to perform them at a more convenient time.

Attempting to access a component's dependencies during the initialisation stage can also lead to deadlocks. For example, if there happens to be a cyclic dependency between one component and another, and one accesses the other during initialisation, then the Spring container will get into a deadlock attempting to wait for both components to become initialised. After the timeout expires (as above), the container will realise that both components could not be initialised, and the loading of the plugin will fail.

Component destruction

Along with component initialisation, component destruction is a key part of the lifecycle of an add-on's components. Destruction of components is required in order to make sure that the add-on can clean up after itself. For example, releasing resources, deregistering listeners. Component destruction occurs when:

  • The add-on system is being shut down:
    • A data export is being imported into JIRA
    • JIRA is shutting down
  • An add-on is being uninstalled
  • A newer version of the add-on is being installed over an existing one
  • An add-on, or the specific add-on module, is being disabled

There are two possible phases to component destruction: the execution of methods annotated with @PreDestroy, followed by the execution of the destroy method from the DisposableBean interface, in that order. Naturally, the second phase is only executed if your component implements the DisposableBean interface. These two phases are essentially equivalent, and the particular phase you wish to use depends on your Spring configuration.

Note: @PreDestroy and DisposableBean are concepts provided by the Spring framework. For more information on them, see their documentation at @PreDestroy and DisposableBean.

It is important that components properly destroy themselves so that JIRA is in a clean state afterwards. This is particularly important when a component is potentially referred to by another plugin or JIRA itself. For example, if a component registers itself as an event listener with the EventPublisher, then it is crucial that it deregisters itself during destruction. If this is not done, then the EventPublisher will hold onto the object past the time when the plugin's classes might be unloaded from the class loader. This can lead to memory leaks and other undesirable consequences.

Similar to component initialisation, there is also a timeout on component destruction (the default is 10 seconds). If destruction fails to complete in that time, the plugins system will log an error and continue.

In the scenario of the entire add-on system being shut down, the order in which add-ons are destroyed is not guaranteed. Thus if your add-on's component is dependent on components from other add-ons, and you need to access those components during destruction, this can cause errors.

Events

Both the events system and JIRA itself fire events at different stages of the add-on lifecycle. These are referred to as add-on system events and JIRA events respectively.

Add-on system events

Add-on system events are fired by the events system used by JIRA and the Atlassian Plugins Framework, atlassian-events. These events are fired at different stages in an add-on's lifecycle. Internally, JIRA listens to these events to ensure the proper working of various system components, for example CustomFieldManager. Add-ons can also listen to these events to execute tasks when something happens to an add-on or an add-on module (whoever that plugin belongs to). The add-on system events are described below.

PluginEnabledEvent and PluginModuleEnabledEvent

When an add-on has been successfully installed into the add-ons system - that is, after it has been successfully initialised - the add-on system notifies all listeners that each add-on's modules have been enabled by firing the PluginModuleEnabledEvent. After all notifications have been made for a add-on's modules, the PluginEnabledEvent is then fired. Specifically, these events are fired when:

  • The add-on system is being started:
    • JIRA is started
    • A data export is being imported into JIRA
  • An add-on is being installed
  • An add-on, or the specific add-on module, is being enabled

Once these events have been fired, then from JIRA's (and the add-on system's) perspective your add-on is enabled. That is to say, any time JIRA asks the PluginAccessor for modules that are present and enabled, your add-on's modules will be returned. At this point in time, you can be sure that any internal dependencies that the add-on's components may use (that is, dependencies provided by your add-on to your add-on) will be in a "ready-to-use" state.

Due to the shortcomings of the component initialisation stage, a listener on the PluginEnabledEvent is the earliest time that you should attempt to do any serious wiring into JIRA. For example, if your add-on needs to ensure there are particular custom fields created in JIRA for the add-on to function properly, the add-on should attempt to create them during this stage, and not earlier.

If however your add-on has tasks which can be executed asynchronously, or even lazily, then that should be your preferred approach. The downside to performing those critical tasks in the PluginEnabledEvent phase is that, in some scenarios, your add-on will be notified that it is enabled, signalling the start of these tasks, but at the same time JIRA will allow requests to be made to your add-on. Thus, if the tasks take too long, and they don't correctly block JIRA from processing a request, then you can get into an unknown state.

Say for example that your add-on needs to load a lot of data into memory, and this process can take a while. If this task is performed when the PluginEnabledEvent is fired, then it is possible that while this task is executing, users are accessing JIRA and requesting information from your add-on. This is unfortunately a known limitation of JIRA's add-on system. As previously stated, if you can defer this task to be executed asynchronously, or lazily when the first request comes in which requires it, then this will avoid problems. An alternative approach is to make the add-on unavailable to requests until the task is completed, but this requires a lot of defensive programming.

Finally, the order of these events being fired in relation to other stages in the lifecycle can change depending on the scenario you are running under. Consult the section on the different scenarios (Example scenarios) to get a better understanding of this.

PluginDisabledEvent and PluginModuleDisabledEvent

When an add-on is being removed from the add-ons system, the PluginModuleDisabledEvent and PluginDisabledEvent events can be triggered to signal that this is happening. This will happen when:

  • The add-ons system is being shut down:
    • A data export is being imported into JIRA
    • JIRA is shutting down
  • An add-on is being uninstalled
  • A newer version of the add-on is being installed over an existing one
  • An add-on, or the specific add-on module, is being disabled

Note: When disabling or uninstalling a add-on, the PluginDisabledEvent corresponding to that plugin is not intended to be caught by the add-on itself. It is provided primarily for JIRA itself and other add-ons that may depend on the add-on. Typically, the sequence of events is:

  1. For each of the add-on's modules - PluginModuleDisabledEvent fired
  2. For the add-on's component - @PreDestroyDisposableBean#destroy()
  3. PluginDisabledEvent fired

As you can see, the add-on you are disabling will no longer be around by the time the PluginDisabledEvent is fired. It can however be used for one add-on to be informed when another add-on is being disabled. This might be necessary if the functionality of one add-on depends on another.

As the component destruction stage is not suitable for all tasks relating to tearing down the state of a add-on, the best time to perform those tasks would be when hooking into a PluginModuleDisabledEvent. That will guarantee that all of your add-on's components will still be available (not destroyed). The event is also fired synchronously - component destruction will not begin until the listeners of the event have finished executing.

LifecycleAware#onStart()

The LifecycleAware interface is offered by the Shared Access Layer library (SAL) to assist add-on developers who want to expose public components to the add-on system and JIRA. It can be implemented by add-on components marked as "public" in the atlassian-plugin.xml file, to hook into the add-on lifecycle. Similar to the event listener model, any components that implement the onStart() method of the interface will have that method invoked at a particular stage in the lifecycle, depending on the scenario.

The onStart() method is guaranteed to be invoked on a component after that component has been through initialisation. However, The time at which it is invoked in relation to other stages depends on the scenario:

  • When JIRA is being started, or when a data export is being imported into JIRA (that is, the whole add-on system is being started) onStart() will be invoked last. That is, it will be invoked after:

    1. PluginModuleEnabledEvent is fired
    2. PluginEnabledEvent is fired
    3. JIRA's data is upgraded by JIRA
    4. JiraStartedEvent is fired

    5. Add-on upgrade tasks recognised by SAL are executed

  • When an add-on is being enabled, or when a plugin is being installed, onStart() will be invoked directly after initialisation. That is, it will be invoked before:

    1. PluginModuleEnabledEvent is fired
    2. Add-on upgrade tasks recognised by SAL are executed (in the "install" scenario)
    3. PluginEnabledEvent is fired

The unreliability of timing of this phase makes it a bad candidate for use beyond the most basic tasks. It may be used in order to schedule tasks with SAL's PluginScheduler, but it still might be best to perform those tasks in the same place that you perform other add-on initialising tasks. This issue is being tracked in the JIRA project on JAC: JRA-26358.

Add-on upgrade tasks

Add-ons can define upgrade tasks that can be executed in order to bring JIRA's data up to date with a newer version of the add-on. This is achieved simply by defining components in the atlassian-plugin.xml which are public and implement the com.atlassian.sal.api.upgrade.PluginUpgradeTask interface from SAL.

If your add-on is complex and stores a lot of data - perhaps using the ActiveObjects (AO) plugin/library - then upgrade tasks will probably become a necessity as you release newer versions of your add-on. Therefore, it is useful to know when add-on upgrade tasks are executed in the lifecycle. A add-on's upgrade tasks can run when:

  • The add-on system is being started:
    • JIRA is started
    • A data export is being imported into JIRA
  • An add-on is being installed
  • An add-on is being enabled

An upgrade task component will only be executed if it has not been successfully executed before (that is, completing without throwing an Exception). SAL keeps track of successful upgrade tasks by using the value of getBuildNumber() on the PluginUpgradeTask interface.

Note: Depending on the scenario, the timing of the execution of upgrade tasks will change relative to other stages in the lifecycle. Specifically, when an add-on is being installed into a running instance of JIRA (for example, via UPM), the add-on's upgrade tasks will be executed after the PluginModuleEnabledEvent is fired for all modules, but during the PluginEnabledEvent for the add-on (SAL's PluginUpgradeManager begins the execution of upgrade tasks when it receives the PluginEnabledEvent). This means that, if a request comes into JIRA which requires a module from an add-on which is currently being installed, that request might be serviced while upgrade tasks are running. This differs from the scenario when JIRA is being started up or when data is being upgraded - in both of those cases, JIRA will block requests until add-on upgrade tasks have completed.

Due to this shortcoming in JIRA and the add-on system, we recommend that you use defensive blocking behaviour for plugins with potentially long-running upgrade tasks, to ensure that your add-on cannot be accessed while upgrade tasks are running. This becomes less of an issue if your add-on is only ever installed in JIRA when no users are accessing it, or by shutting down JIRA first.

JIRA events

JIRA itself fires several events during its own lifecycle. Most are for internal purposes and not relevant to add-on developers. However, they can be listened to, using the regular mechanism provided by Atlassian Events.

One event of note is the JiraStartedEvent event. It is triggered when:

  • The add-ons system is being started:
    • JIRA is started
    • A data export is being imported into JIRA

In both of the above scenarios, the event is triggered as part of SAL's LifecycleManager start phase, where every LifecycleAware object is notified of the starting of the application. This means that, in theory, the JiraStartedEvent happens at the same time as:

  • any add-on upgrade tasks are executed, and
  • LifecycleAware#onStart() is executed on any known components.

Because the event is not triggered when a add-on is installed or enabled, this phase in the lifecycle is essentially a less-capable version of the LifecycleAware#onStart() phase, with the difference that it is event-based. For the same reasons as with the LifecycleAware#onStart() phase, it is probably safest to avoid using this phase, unless there is a specific reason why you need to execute only when JIRA is starting or data is being restored.

Example scenarios

This section describes several scenarios that involve the add-on lifecycle. In each scenario, we list the sequence of stages and events to give you an idea about when each stage comes into play.

  • JIRA is starting up:

    1. For all components of all add-ons - Constructor@PostConstruct#afterPropertiesSet
    2. For each add-on:
      1. For each add-on module, PluginModuleEnabledEvent fired
      2. PluginEnabledEvent fired
    3. JIRA upgrades data
    4. JIRA re-indexes (possibly)
    5. The following happen "at the same time" (in serial, but not in determined order):
      • JiraStartedEvent fired
      • SAL PluginUpgradeTasks executed if necessary
      • LifecycleAware#onStart() executed
    6. JIRA Scheduler started
    7. JIRA responds to web requests
  • Data export is being restored:

    1. Add-on system is shut down:
      1. For each add-on:
        1. For each add-on component - @PreDestroyDisposableBean#destroy()
        2. For each add-on module - PluginModuleDisabledEvent fired
      2. For each add-on - PluginDisabledEvent fired
    2. Data is imported into the database
    3. Add-on system restarted:
      1. For all components of all add-on - Constructor@PostConstruct#afterPropertiesSet
      2. For each add-on:
        1. For each add-on module, PluginModuleEnabledEvent fired
        2. PluginEnabledEvent fired
    4. JIRA upgrades data (if necessary)
    5. JIRA re-indexes (possibly)
    6. The following happen "at the same time" (in serial, but not in determined order):
      • JiraStartedEvent fired
      • SAL PluginUpgradeTasks executed if necessary
      • LifecycleAware#onStart() executed
    7. UI can be accessed again
  • Add-on is disabled via UPM:
    1. For each of the add-on's modules - PluginModuleDisabledEvent fired
    2. For the add-on's component - @PreDestroyDisposableBean#destroy()
    3. PluginDisabledEvent fired
  • Add-on is enabled via UPM:
    1. For each of the add-on's components - Constructor@PostConstruct#afterPropertiesSetLifecycleAware#onStart
    2. For each of the add-on's modules - PluginModuleEnabledEvent fired
    3. The following happen "at the same time" (in serial, but not in determined order):
      • Upgrade tasks are run (only if they have not been run previously)
      • PluginEnabledEvent fired
  • Add-on is uninstalled via UPM:
    1. For each of the add-on's modules - PluginModuleDisabledEvent fired
    2. For the add-on's component - @PreDestroyDisposableBean#destroy()
    3. PluginDisabledEvent fired
    4. Add-on's classes removed from Class Loader
  • Add-on is installed via UPM:
    1. For each of the add-on's components - Constructor@PostConstruct#afterPropertiesSetLifecycleAware#onStart
    2. For each of the add-on's modules - PluginModuleEnabledEvent fired
    3. UI-facing add-on modules can be accessed now
    4. The following happen "at the same time" (in serial, but not in determined order):
      • Upgrade tasks are run (only if they have not been run previously)
      • PluginEnabledEvent fired
    5. UI is accessible to the user who triggered the installation
  • JIRA is being shut down:
    1. Add-on system is shut down
      1. For each add-on:
        1. For each add-on component - @PreDestroyDisposableBean#destroy()
        2. For each add-on module - PluginModuleDisabledEvent fired

You may also want to read this related tutorial: Writing JIRA event listeners with the atlassian-event library

PicoContainer and dependency injection

It is important to understand how dependency injection works before trying to call JIRA functionality from your add-on. JIRA uses PicoContainer to manage object creation throughout the system. PicoContainer is responsible for instantiating objects and resolving their constructor dependencies. This greatly simplifies code, in that any PicoContainer-instantiated object (e.g. a Webwork action) can obtain an instance of another (e.g. a Manager class) simply by requesting one in its constructor. PicoContainer will ensure each object that is required in the constructor is passed in (aka dependency injection). For example, the ViewIssue action:

ViewIssue.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ViewIssue extends AbstractViewIssue
{
    ....
    public ViewIssue(RepositoryManager repositoryManager, PermissionManager permissionManager, TrackbackManager trackbackManager,
                     ThumbnailManager thumbnailManager, SubTaskManager subTaskManager, IssueLinkManager issueLinkManager,
                     IssueLinkTypeManager issueLinkTypeManager, VoteManager voteManager, WatcherManager watcherManager,
                     PluginManager pluginManager)
   {
        super(issueLinkManager, subTaskManager);
        this.trackbackManager = trackbackManager;
        this.thumbnailManager = thumbnailManager;
        this.issueLinkTypeManager = issueLinkTypeManager;
        this.pluginManager = pluginManager;
        this.pagerManager = new PagerManager(ActionContext.getSession()); 
        this.repositoryManager = repositoryManager;
        this.permissionManager = permissionManager;
        this.voteManager = voteManager;
        this.watcherManager = watcherManager;
    }
    ....
}

Non-managed classes

Classes not managed by PicoContainer (e.g. workflow conditions / functions, services and listeners, or JSP scriptlets) can still get pico-instantiated objects statically using static methods on ComponentManager. For example:

1
2
3
4
final ProjectManager projectManager = ComponentManager.getInstance().getProjectManager();
final IssueFactory = ComponentManager.getInstance().getIssueFactory();
//or
final ApplicationProperties applicationProperties = ComponentManager.getComponentInstanceOfType(ApplicationProperties.class);

Registering new PicoContainer-managed classes

PicoContainer-managed classes need to be registered with PicoContainer. This happens automatically for Webwork actions, but other classes need to be registered manually. This is done in ComponentRegistrar's registerComponents() method:

ComponentManager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void registerComponents(final ComponentContainer register, final boolean startupOK)
{
    ...
        register.implementation(INTERNAL, EntityUtils.class);
        register.implementation(PROVIDED, AttachmentManager.class, DefaultAttachmentManager.class);
        register.implementation(PROVIDED, AttachmentService.class, DefaultAttachmentService.class);
        register.implementation(PROVIDED, ProjectService.class, DefaultProjectService.class);
        register.implementation(PROVIDED, FieldManager.class, DefaultFieldManager.class);
        register.implementation(PROVIDED, CustomFieldManager.class, DefaultCustomFieldManager.class);
        register.implementation(PROVIDED, CustomFieldService.class, DefaultCustomFieldService.class);
        register.implementation(PROVIDED, FieldScreenManager.class, DefaultFieldScreenManager.class);
        register.implementation(INTERNAL, DefaultFieldScreenStore.class);
        register.implementation(PROVIDED, MailThreadManager.class, MailThreadManagerImpl.class);
        register.implementation(PROVIDED, CvsRepositoryUtil.class, CvsRepositoryUtilImpl.class);
        register.implementation(INTERNAL, DefaultWebAttachmentManager.class);
        register.implementation(INTERNAL, I18nBean.class);// this is a candidate for removal (may not be used - SF 08/Oct/04)
        register.implementation(PROVIDED, I18nHelper.class, I18nBean.class);
        register.implementation(PROVIDED, I18nHelper.BeanFactory.class, I18nBean.CachingFactory.class);
        register.implementation(INTERNAL, JiraLocaleUtils.class);
        register.implementation(PROVIDED, LocaleManager.class, DefaultLocaleManager.class);
        register.implementation(INTERNAL, PingUrlFilterer.class);
    ...
}

Components can either by INTERNAL meaning that they will be available only to JIRA itself or PROVIDED in which case they will also be available to plugins2 add-ons.

Components are generally only registered in the ComponentRegistrar, if they are required in JIRA internally. Add-on developers who want to write to write their own components that can be injected in their add-on's classes, should use the component plugin module.

Data storage

Active Objects is an ORM layer into our products that enables easy, fast, and scalable data access and storage. See the Active Objects documentation.

Examples of Plugins2 add-ons

Some parts of JIRA are implemented as add-ons. These are called system add-ons. If you want to know more about how to create an add-on, system add-ons can be a handy reference, as they showcase the functionality that can be built in JIRA.

The system add-ons are referenced from the following files (located in /WEB-INF/classes):

  • system-workflow-plugin.xml - the built in workflow conditions, validators and functions.
  • system-customfieldtypes-plugin.xml - the built in custom field types.
  • system-project-plugin.xml - the built in project tab panels (ie roadmap, change log and popular issues).
  • system-reports-plugin.xml - the built in system reports (ie time tracking and developer workload reports).
  • system-portlets-plugin.xml - all of the built in system portlets.

and in other system-*-plugin.xml files in that directory. The add-ons modules are all defined in JIRA in JiraModuleDescriptorFactory.java.

Rate this page: