Atlassian Spring Scanner

Atlassian Spring Scanner is a set of libraries that make plugins faster to load and easier to develop.

It has two aspects. The first is a build time aspect where special annotations are found in your source to specify the private components and OSGi services your plugin wants to give off and consume.

A well known set of index files are placed in the JAR's META-INF directory, reflecting these annotations.

The second aspect is a runtime aspect where the index files are read and Spring components and OSGi services are created based on those instructions.

These indexes remove the need for the Atlassian plugin system to scan your plugin's bytecode at runtime and "transform" it into an OSGi bundle. Instead it has all the information it needs (from build time) to invoke components and services.

We call plugins that use this technique "transformerless" plugins. They load significantly faster than traditional P2 plugins that require runtime transformation. Local tests for example show plugins that took 5 seconds to load now load in under 2 seconds.

In exchange for the extra speed, you need to be more explicit about the code you write and the code you consume. We think this is a fair trade-off.

Versions and compatibility

Note: Scanner 2.x+ must be used as an OSGi provided dependency, so plugins using Scanner 2.x+ can only run in versions of products that provide it, and won't run in older versions.

Scanner 2.x is able to run against Confluence 6.0+, JIRA 7.2+, Bitbucket Server 5.0+ and Bamboo 5.14+

Please use spring-scanner 1.x if you are working against an older product, and use the 1.x README instead of this one - different rules :)

Problems? See 'Troubleshooting' below.

Changes required if you're upgrading from atlassian-spring-scanner 1.x to 2.0+


  • VERY IMPORTANT! Change all the atlassian-scanner namespaces in your spring context files (where you have <scan-indexes> - see the "Spring runtime integration" example below) from to (Don't forget this. It may seem to work, but without it, you may accidentally access an unreliable copy of 1.x from inside some other plugin.)
  • Remove your dependency on atlassian-spring-scanner-runtime.
  • Change your dependency on atlassian-spring-scanner-annotation to be <scope>provided</scope>.
  • atlassian-spring-scanner-processor is gone. If you had a dependency on it, remove it.
  • Add atlassian-spring-scanner-maven-plugin to <plugins> - see example below. ** If your build then fails to find org.springframework.stereotype or javax.inject, add spring-context or javax.inject to <dependencies> with <scope>provided</scope> - you'd been relying on those transitively.
  • Remove use of @Scanned - this feature is removed. All classes are now scanned.
  • Remove 'auto-imports' attribute from <scan-indexes>.


Makes things simpler:

  • Remove these from your Import-Package and replace with a (non-"optional") *: org.springframework.osgi.*;resolution:="optional" org.eclipse.gemini.blueprint.*;resolution:="optional"
  • You only need to import com.atlassian.plugin.osgi.bridge.external if you're using @ModuleType.

Getting started with Atlassian Spring Scanner

Example code

This source repository contains an example plugin that uses all the facilities described here.

Update your POM dependencies

Place this dependency entry in your pom.xml:

    This is for the annotations you need to indicate components and OSGI services.

The provided scope is mandatory for spring-scanner 2.0+; runtime or compile scope is not allowed. No dependency on atlassian-spring-scanner-runtime is needed.

Important warning: this README is for the 2.0+ version. If you're still using the 1.x version, you DO need to use runtime scope for atlassian-spring-scanner-runtime: please see the 1.x README here.

Update your POM plugin section

A byte code scanning maven build plugin is used to find the well known annotations. This plugin will build the index files that the runtime needs.

Minimal config

            <!-- process-classes seems to be skipped if you are using scala
                 so perhaps use prepare-package -->
       <!-- Enable this to get build-time logging of annotations atlassian-spring-scanner-maven-plugin has noticed -->

Note that this will scan classes inside the current maven module only. To scan any transitively included jars or modules, you'll need to explicitly add them to scannedDependencies as per following section.

  • verbose
    • To debug whats happening inside the maven plugin

Optional extended config - includeExclude, scannedDependencies

            <!-- process-classes seems to be skipped if you are using scala
                 so perhaps use prepare-package -->
       <!-- Enable this to get build-time logging of annotations atlassian-spring-scanner-maven-plugin has noticed -->

             Demonstrates excluding packages from scanning, and scanning an example dependency jar.
             Please see the docs below. -->

  • includeExclude

    • The paths to ignore - typically you don't need this tag at all
  • scannedDependencies

    • This is a list of compile scope dependencies that you want to ALSO be scanned for components like it was your own source code

The scannedDependencies section is powerful. It allows the build time plugin to find code that is NOT in your source code but in your dependant code jars.

This means your dependant code can be included into the META-INF index files and hence offer components and OSGi Services at runtime themselves.

Add Spring runtime integration

Create a Spring XML file in src/main/resources/META-INF/spring/ of your plugin code. Spring doesn't care what this file is called, as it will process all files ending in .xml in that folder.

The atlassian-scanner:scan-indexes instructions here fire up the atlassian-spring-scanner-runtime (which is provided for you in the product) to process the index files and publish and consume components and OSGi services.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns=""

Add scanner annotations to your code

Annotations are used to indicate what are Spring private components and what are published OSGi services. They are also used to indicate what OSGi services you consume and hence what needs to be imported into your plugin bundle.

The old mechanism was to use <component>and <component-import> statements in atlassian-plugin.xml but now you just use annotations.

You need to go through your atlassian-plugin.xml, find any <component>, <component-import> and <module-type> tags, and put some annotations in your code to represent them and then remove them from your atlassian-plugin.xml.

If you had <component ... public="true"> then you are exposing that component as an OSGi service and hence you need to replace that with the @ExportAsService annotation, which works as follows:

  • if you provide one or more types (typically interfaces) as the annotation value, your component is exported as those types
  • if you provide no value for the annotation and your component implements any interfaces, it's exported as ALL of those interfaces
  • if you provide no value for the annotation and your component implements no interfaces, it's exported as its own class

The default behaviour with interfaces is often what you want. It only uses directly implemented interfaces, and not inherited ones, so there's no base class brittleness. You may need to be careful about interfaces like DisposableBean that you don't want to register for, and maintainers will need to think about this also.

If you had <component ...> (that is non-public component) then it's a plain old component and you can use JSR @Named or Spring @Component to tell Spring that this is a singleton component.

You will also need to add @Autowired or @Inject (see below) to the appropriate constructor if the default constructor is not the right choice. This replaces xml written by the transformation process. You can also add the autowire attribute to the <atlassian-scanner:scan-indexes/> element in your spring runtime integration to set the autowire mode for all beans created by the scanner.

Each of your <component-import /> statements represents OSGi components you want to import. In the new scheme you replace them with @ComponentImport annotations.

Also in the past a plugin could import host-provided components implicitly, but these now have to be explicitly included. Again you use the @ComponentImport annotation.

public class ExposeToOSGIComponent
    private final IssueService issueService;
    private final InternalComponent internalComponent;

    public ExposeToOSGIComponent(@ComponentImport final IssueService issueService,final InternalComponent internalComponent)
        this.issueService = issueService;
        this.internalComponent = internalComponent;

Here's something "normal" plugins can't do... Suppose you have some component that you need exported as a service so that your TEST code can import and make use of it, however, you really don't want this thing to be a public service in production. In a transform-less plugin you can now use the @ExportAsDevService annotation which will tell the system to export your component as an OSGi service ONLY if the product is running in dev mode.

The well known annotations

  • @Component (Spring) - that's org.springframework.stereotype.Component, not org.osgi.service.component.annotations.Component
  • @Named (JSR)

These are standard annotations that indicate a private singleton component.

The scanner detects them and puts an entry in the index for each one so it can fix them up at runtime.

The old analogy was a <component> entry in atlassian-plugin.xml. This is no longer needed.

  • @ClasspathComponent

Sometimes you need to have a component whose class is contained in some other library. The use of @ClasspathComponent allows you to do this.

For example :

public final class OsgiDescribedModuleDescriptorFactoryAccessor implements DescribedModuleDescriptorFactoryAccessor
    private final WaitableServiceTracker<String, DescribedModuleDescriptorFactory> serviceTracker;

    public OsgiDescribedModuleDescriptorFactoryAccessor(@ClasspathComponent WaitableServiceTrackerFactory waitableServiceTrackerFactory)
  • @Autowired (Spring)
  • @Inject (JSR)

These are standard annotations that indicate that this is the constructor or field I would like to use to wire a component.

  • @ComponentImport

This indicates an OSGi service component that you want to import from outside your plugin. The scanner notices them and puts an entry in the META-INF index for each one so it can fix them up at runtime.

You must have a <Import-Package> line entry for the package of the code you are importing. The old style was a <component-import> entry in atlassian-plugin.xml. This is no longer needed.

  • @JiraImport
  • @ConfluenceImport

These are product specific versions of the @ComponentImport annotation.

  • @ExportAsService

This indicates that you want to export this component as an OSGi service to the outside world. You must have a <Export-Package> line entry for the package of the code you are exporting. The old style was a <component public="true"> entry in atlassian-plugin.xml. This is no longer needed.

  • @ModuleType

This indicates that the code defines new kinds of pluggable modules to users of the plugin system. See the section discussing @ModuleType for more details. The old style was a <module-type> entry in atlassian-plugin.xml. This is no longer needed.

Expose Module Types Differently via @ModuleType

In the past you could put <module-type ... /> into atlassian-plugin.xml and it would have created a module factory for you under the covers.

You now have to explicitly have your own factory class and annotate it @ModuleType. Note the special HostContainer dependency here. It's needed for the plugin system but don't worry - it gets fixed up for you by the scanner runtime.

public class BasicModuleTypeFactory extends
    public BasicModuleTypeFactory(final HostContainer hostContainer)
        super(hostContainer, "basic", BasicModuleDescriptor.class);

public class BasicModuleDescriptor extends AbstractModuleDescriptor<String>
    public BasicModuleDescriptor(final @ComponentImport ModuleFactory moduleFactory)

    public String getModule()
        return new Date().toString();

To use @ModuleType, you also need to add this to your <Import-Package> definition, and add a @ComponentImport for ModuleFactory where you use them.


Update OSGi Bundle instructions

You need to take responsibility for your OSGi imports and exports. This is part art and part science, but you can use AMPS facilities (or the Maven BND plugin directly) to help you here.

At a minimum you will need bundle instructions like the following :

<project ...>

Ensure <packaging> is set to atlassian-plugin so that AMPS gets invoked and hence BND processing gets done.

The use of <Atlassian-Plugin-Key> here tells the plugin system that you are a transformerless plugin and that it should skip the slow transformation step. This is VERY IMPORTANT. Without this entry in your Manifest, the plugin system will try to transform your plugin, and you will lose the load time speed benefits. You are also likely to see Spring-related errors. Do not forget to specify this entry.

The <Import-Package> and <Export-Package> entries here are the OSGi class loader boundary instructions that affect you at runtime.

In the past, the transformation steps did this for you, but now you must take responsibility for them.

BND does a pretty good job of finding and building the compile time packages you need to import, however it's not prescient. If you don't have a compile time dependency on a package but you do have a runtime one, then you need to add it yourself. A common example of this is web conditions. You often don't have a compile time dependency on them, but you certainly have a runtime one:

<web-item key="recent-request-types" section="" weight="999">
     <condition class="com.atlassian.jira.plugin.webfragment.conditions.UserLoggedInCondition" />

In the above example you MUST import the package com.atlassian.jira.plugin.webfragment.conditions explicitly, BND cannot do it for you and even if you put com.atlassian.jira.*, BND still cannot expand that to include this runtime dependency.

You will need to find these dependencies and explicitly import them. One trick is to look at the manifest generated by the transformation step if you used that method in a previous incarnation of your plugin.

You should be aware that if your plugin has packaging set to <packaging>bundle</packaging> and is using the maven-bundle-plugin, the default behaviour of the maven-bundle-plugin is to export all packages in the jar. This means there may be code outside your plugin that depends on classes in your plugin that will be broken if you stop exporting these packages. Be sure to check for these dependencies.

Product specific components and imports

The annotations give you the possibility to import components and define components specific to products like JIRA and Confluence.

This ability is embodied by the @BambooComponent, @BambooImport, @ConfluenceComponent, @ConfluenceImport, @FecruComponent, @FecruImport, @JiraComponent, @JiraImport, @RefAppComponent, @RefAppImport, @StashComponent and @StashImport annotations.

There is a wide range of packages you might want to import from the product, depending on the product services you are importing.  For example, if you required packages from Confluence and Jira:


The ;resolution:="optional" here allows your plugin to start in JIRA without the Confluence-specific packages (which you'll carefully avoid using from @JiraComponents), and vice versa.

You can use well known JSR annotations to indicate components

You can also use the JSR @Named and @Inject annotations via:


This is not strictly necessary. It's an alternative to normal Spring annotations such as @Component and @Autowire. It's up to you.

Although the usage of Spring dependency injection of Atlassian components (e.g. PageManager) via Spring works in Confluence, due to the older version of the Spring framework in use (Spring 2.5.6), it was not possible to use JSR standard @Inject style injection and @Named component declaration. This will now work with the Atlassian Spring Scanner.


Important warning: Atlassian Spring Scanner does not work with maven versions 3.3.1/3.3.3 due to a maven bug: MNG-5787. Use version 3.3.9 which contains a fix for this bug.

Common mistakes

First, read the "Changes required if you're upgrading" section above :)

  • Wrong scope for atlassian-spring-scanner-annotation. Must be <provided>.
  • Wrong scope for atlassian-spring-scanner-runtime. You don't need a dependency on this at all in 2.0.
  • Missing atlassian-spring-scanner-maven-plugin in your pom, or it's it in <pluginManagement> but not in <plugins>. (Check that your built plugin jar contains the expected plugin-components as below.)
  • Missing <Spring-Context>*</Spring-Context>. See above.
  • Missing <scan-indexes> in your spring context xml file, or you have the wrong namespace URI for it. Must be ...scanner/2. See above.
  • Your spring context xml file containing <scan-indexes> is in the wrong directory, so isn't being processed. Make sure it's in the directory META-INF/spring in your plugin jar.
  • Using a fixed list of packages with no * in OSGi Import-Package, but missing some packages containing required annotations such as org.springframework.stereotype, javax.inject, org.springframework.beans.factory.annotation. Add them, or better, add a * and let the bytecode scanning decide what to add: it's easiest to maintain. (View your plugin's MANIFEST.MF to see the result of the *.)
  • Using spring-instantiated components (@Component, @Named) but you haven't told it which constructor to use. Add @Autowired or @Inject to the appropriate constructor, or use <scan-indexes autowire="constructor"> or "autodetect".
  • Using @ExportAsService, but forgot to export the interface with Export-Package.
  • Using @ModuleType but missing Import-Package for com.atlassian.plugin.osgi.bridge.external. See above.
  • @ComponentImports for multiple services with interfaces names that differ only by package (e.g. jira UserManager and SAL UserManager). They'll end up with the same bean name so one will fail to inject. Fix: set a different name for one, e.g. @ComponentImport("salUserManager").

How to debug

If your components are not loaded, because you missed an essential point, a good place to start debugging is the com.atlassian.plugin.spring.scanner.runtime.impl.ClassIndexBeanDefinitionScanner and ComponentImportBeanFactoryPostProcessor to check if your index files are correctly loaded. Or start earlier, from com.atlassian.plugin.spring.scanner.runtime.impl.AtlassianScannerBeanDefinitionParser.

Moreover make sure your index files are actually included in the deployed artifact under META-INF/plugin-components/. This path in your jar would typically contain files like components, imports or components-confluence.

Spring errors like

No unique bean of type [...] is defined: expected single matching bean but found 2

can be an indication that your plugin is still being transformed. There are duplicate beans because both the transformation process and the spring scanner are producing beans. In particular, check that the <Atlassian-Plugin-Key> is present in your output jar META-INF/MANIFEST.MF, and double check the spelling.

If you're using @Component and it's not getting created, make sure you're using org.springframework.stereotype.Component, not org.osgi.service.component.annotations.Component :)

How it works internally

OK, if you really need to know the technical bits, I'll try to make this quick. The atlassian-spring-scanner libraries contains not only the annotations, but the build time processors that automatically run when you compile your project.

These create files containing the fully qualified class names of your components, imports and exports, as well as any custom names you gave them. These "index files" are also split out by product.

The generated files are put into your jar's META-INF folder so they can be found at runtime.

In a jar with product-specific components, such as that produced by the atlassian-spring-scanner-test/atlassian-spring-scanner-maven-test maven module in the scanner, you'll see something like:


The contents of one of these is a list of class names:


and some entries may include additional information like a bean name:


or export interfaces:


At runtime, Spring sees the <atlassian-scanner:scan-indexes> tag in your Spring XML file and runs a custom BeanDefinitionParser. This parser loads the component files related to whichever product you're currently running within and creates BeanDefinitions for each class in the file. It does this without ever having to load the actual class or inspect it in any way, so it's super fast.

In addition to registering your component beans, it also registers two other PostProcessors. One reads the imports files in a similar manner and registers OSGi service imports for them. This happens in one pass after all the beans have been registered but before they are created.

The second one runs after each bean is created/destroyed, and handles registering/un-registering the OSGi services for your public components. It does this by looking for the bean class in the exports, and registering the service according to the data there.

So at runtime there are no jar files to read and no ASM parsing of code. It starts quickly and wires components. That's it.

How do I check it's really working ?

Running automated tests

The atlassian-spring-scanner-test/atlassian-spring-scanner-maven-test module is a test plugin that exercises all the features and annotations.

AMPS runs integration tests on it:

  • TestPackaging checks the contents of the various index files are as expected: verifies build-time output from atlassian-spring-scanner-maven-plugin.
  • The subclasses of AbstractComponentsInProductTest (TestInRefapp, TestInJira etc) are integration tests that check the components, imports and exports instantiated in the live test plugin, when installed in the product. There's testGroup config for each product.

To run them in refapp: mvn verify

To run them in other products: mvn -DtestGroups=jira verify

To run them in JIRA Cloud with Postgres-in-Docker:

docker login mvn -Pjira-cloud,postgres84 -Djira.version=x.y.z clean verify

Running manually

To run manually so you can debug:

mvn -pl atlassian-spring-scanner-test/atlassian-spring-scanner-maven-test -Dproduct=refapp -Djvm.debug.suspend=true amps:debug

This will start the product with the plugin in it. Browse to http://localhost:5990/product/plugins/servlet/component-status to see a list of beans and services for the plugin. This is an initial set as per the default profile.

You can go to http://localhost:5990/product/plugins/servlet/manage-dynamic-contexts to start and stop the dynamic context and see the resulting change in components.

Or to run in JIRA Cloud with Postgres-in-Docker:

docker login
mvn -DskipTests clean install
mvn -Pjira-cloud,postgres84 -pl atlassian-spring-scanner-test/atlassian-spring-scanner-maven-test -Dproduct=jira -Djira.version=x.y.z -DskipTests -Djvm.debug.suspend=true amps:debug

To use Docker with amps:run or amps:debug, you must set the value of on the commandline, due to issues with AMPS maven phases and property substitution. This may be localhost or may be in your DOCKER_HOST env variable.

You also need to have a running docker-machine and appropriate environment variables set up. If you see this error, that's what's wrong:

docker-maven-plugin:0.15.0:start failed: No url given, no DOCKER_HOST environment variable and no read/writable '/var/run/docker.sock'

How do I add new support for specific product code detection, into atlassian-spring-scanner ?

atlassian-spring-scanner is able to detect product-specific annotations, so that it wires components according to the product it is currently running on, for example with the JiraComponent annotation.

At the time of writing, atlassian-spring-scanner supports code detection for Bamboo, Bitbucket Server, Confluence, Fecru, JIRA, Stash and Refapp. In the future, you may want to add support for whichever product you are working on. Here's how you would do that:

  • create a new YourProductNameComponent annotation in the sub-module "atlassian-spring-scanner-annotation", package com.atlassian.plugin.spring.scanner.annotation.component. This will be the annotation to put on your product specific component that you want to export and be consumed
  • create a new YourProductNameImport annotation in the sub-module "atlassian-spring-scanner-annotation", package com.atlassian.plugin.spring.scanner.annotation.imports. This will be the annotation to put on the interface you inject in your plugin code, when consuming your product specific component
  • add your YourProductNameComponent and YourProductNameImport to SpringIndexWriter.MeaningfulAnnotation to have the spring scanner recognize your new annotations
  • if not already present, add your product enum value to the ProductFilter enum
  • In ProductFilterUtil: add a complete class name that is specific to your product, so that atlassian-spring-scanner can detect that it is actually running on this product at runtime:
static final String CLASS_ON_JIRA_CLASSPATH = "com.atlassian.jira.bc.issue.IssueService";
  • In ProductFilterUtil: add your product class to the magic Map that's used for that detection:
private final static Map<String, ProductFilter> PRODUCTS_TO_HOSTCLASSES = ImmutableMap.<String, ProductFilter>builder()
            .put(CLASS_ON_JIRA_CLASSPATH, ProductFilter.JIRA)
            .put(CLASS_ON_REFAPP_CLASSPATH, ProductFilter.REFAPP)
  • add a test for beans index files writing, to verify that a @YourProductNameComponent annotated files gets written in the plugin-components/components-PRODUCTNAME
  • add a YourProductOnlyComponent class in "atlassian-spring-scanner-maven-test" submodule, package com.atlassian.plugin.spring.scanner.test.product
  • test it in it.allproducts.TestPackaging test class, in a testYourProductComponents
  • add a subclass of it.perproduct.AbstractComponentsInProductTest and add to testGroups config in atlassian-spring-scanner-maven-test pom.xml