How to register DiscoverableForgeListener for CMA app migration in a plugin with plugin-context.xml?

Summary

I’m implementing the App Migration Platform for my Confluence Server plugin to migrate app data to a Forge cloud app via CMA. My plugin uses a hand-written plugin-context.xml for Spring bean wiring (not pure Spring Scanner annotations). I cannot get CMA to discover my DiscoverableForgeListener implementation.

Setup

  • Confluence Server 9.5.4

  • atlassian-app-cloud-migration-listener 1.8.7

  • atlassian-spring-scanner-annotation 5.1.0

  • CMA (migration-agent) 3.13.8

  • Dev mode enabled

My migration listener class:

@ConfluenceComponent
@ExportAsService({DiscoverableForgeListener.class, ConfluenceAppCloudMigrationListenerV1.class})
public class DigitalSignatureMigrationListener
    implements DiscoverableForgeListener, ConfluenceAppCloudMigrationListenerV1 {

    public DigitalSignatureMigrationListener(@ComponentImport BandanaManager bandanaManager) {
        this.bandanaManager = bandanaManager;
    }
    // ... getForgeAppId(), onStartAppMigration(), etc.
}

Problem

My plugin has two other beans (DigitalSignatureMacro and DigitalSignatureService) that are defined in META-INF/spring/plugin-context.xml using manual Spring XML wiring. Because this file exists, Spring Scanner does not process the @ExportAsService annotation on my migration listener — so the bean is never registered as an OSGi service, and CMA shows appStatus: NO_APPS.

What I’ve tried

Approach Result
@ExportAsService + @ConfluenceComponent Annotations ignored because plugin-context.xml exists
Delete plugin-context.xml, use annotations everywhere Plugin starts but macro/REST beans break (HK2 conflict)
<osgi:service> in plugin-context.xml Plugin won’t start — CCMA doesn’t export com.atlassian.migration.app.listener or .confluence sub-packages, only the flat com.atlassian.migration.app
<component> in atlassian-plugin.xml Build fails: “not allowed when Atlassian-Plugin-Key is set”
<scanner:scan-packages> in a second Spring XML file Plugin won’t start — duplicate bean conflicts
Programmatic BundleContext.registerService() from constructor Plugin starts, service registered, but CMA can’t find it (classloader mismatch — our bundled interfaces vs CCMA’s interfaces)
Bean-only in plugin-context.xml (no service export) Plugin works perfectly, but CMA shows NO_APPS

Key observation

CCMA (bundle 272) exports com.atlassian.migration.app version 1.0.0, but does NOT export:

  • com.atlassian.migration.app.listener (where DiscoverableForgeListener lives)

  • com.atlassian.migration.app.confluence (where ConfluenceAppCloudMigrationListenerV1 lives)

  • com.atlassian.migration.app.gateway (where AppCloudForgeMigrationGateway lives)

This means the CMA listener dependency must be bundled (compile scope), which creates a split-package situation — our bundle’s classloader has different class instances than CCMA’s classloader, so OSGi service lookups never match.

Questions

  1. How do other Marketplace vendors register their DiscoverableForgeListener? Is there something I’m missing about the service discovery mechanism?

  2. Should the CMA listener interfaces be imported from CCMA or bundled? The documentation says “the Cloud Migration Assistants export the library’s classes in the runtime” — but in practice, the sub-packages are not exported.

  3. Is there a way to use plugin-context.xml alongside Spring Scanner’s @ExportAsService? My plugin’s macro and REST beans require manual XML wiring, but the migration listener needs annotation-based service export.

Environment details

  • pom.xml CMA dependency: <scope>provided</scope> (also tried compile with !com.atlassian.migration.app.* in Import-Package)

  • OSGi manifest: Spring-Context: *

  • Plugin key: com.baloise.confluence.digital-signature

  • Plugin is open source: https://github.com/baloise/digital-signature

Considering your plugin-context.xml file, I was wondering if there was a reason why you couldn’t just completely switch over to spring scanner.

Your config seems rather simple:

  • Two components
  • A couple of service imports

That’s nothing spring-scanner couldn’t handle.

We use DiscoverableListener (not the Forge one, but I guess it’s similar) in several apps having such a bare-bones spring config file:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:atlassian-scanner="http://www.atlassian.com/schema/atlassian-scanner/2"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.atlassian.com/schema/atlassian-scanner/2 http://www.atlassian.com/schema/atlassian-scanner/2/atlassian-scanner.xsd">

    <atlassian-scanner:scan-indexes/>

</beans>

For component imports we just collect them in a single Java class:

@SuppressWarnings("unused")
public class ComponentImporter {

    @ComponentImport private AttachmentManager attachmentManager;
    @ComponentImport private I18nResolver i18nResolver;
    @ComponentImport private LocaleManager localeManager;
    @ComponentImport private ModuleFactory moduleFactory;
    @ComponentImport private PluginLicenseManager pluginLicenseManager;
    ...
}

Alternatively you can also add the @ComponentImport annotations directly in your bean constructors.

We annotate the listener like this:

@ExportAsService(DiscoverableListener.class)
@Component
public class CloudMigrationListener implements DiscoverableListener {
    ...
}

Other non-public components are only annotated with @Component.

Make sure to read atlassian-spring-scanner README and don’t forget to add <Atlassian-Plugin-Key>${addOn.key}</Atlassian-Plugin-Key> in the confluence-maven-plugin in pom.xml.

Another potential alternative - which I haven’t used yet - would be https://developer.atlassian.com/server/framework/atlassian-sdk/spring-java-config/

Btw. your macro class probably shouldn’t be added as a spring bean. Confluence automatically injects dependencies into macros when it creates instances.

Hi @jens ,
thanks for your detailed answer. I tried the scan-indexes approach but the plugin fails to enable (hangs during startup with no errors in logs).

What I tried

  1. Replaced plugin-context.xml with:
<beans xmlns:atlassian-scanner="http://www.atlassian.com/schema/atlassian-scanner/2" ...>
    <atlassian-scanner:scan-indexes/>
</beans>

  1. Created a ComponentImporter class with @ComponentImport fields for all 9 OSGi services (BandanaManager, UserManager, etc.)

  2. Migration listener annotated with @ExportAsService + @Component

  3. Macro and REST classes have @ComponentImport on constructor params but NO @Component/@ConfluenceComponent

  4. Dependencies: atlassian-spring-scanner-annotation 5.1.0 (provided) + atlassian-spring-scanner-runtime 5.1.0 (runtime)

Result

Plugin fails to enable — same WaitUntil.invokeThread.sleep hang pattern with zero log output. I tested progressively:

  • scan-indexes + @ExportAsService + @Component → fails

  • scan-indexes + @Component only (no @ExportAsService) → still fails

  • scan-indexes alone (no annotated migration listener) → still fails

So the issue is scan-indexes itself, not @ExportAsService.

  1. What version of atlassian-spring-scanner are you using? Is it 5.x or an older 2.x version?
  2. Do you need atlassian-spring-scanner-processor or another compile-time artifact to generate the annotation index?
  3. What Confluence Server version do you test against?

Thanks and regards,
Matthias

If it hangs during installation that is usually caused by a failing service import.

You can decrease the timeout in pom.xml by adding this to the AMPS plugin’s <instructions> element:

            <plugin>
                <groupId>com.atlassian.maven.plugins</groupId>
                <artifactId>confluence-maven-plugin</artifactId>
                <configuration>
                    <instructions>
                        <Spring-Context>
                            *;timeout:=10
                        </Spring-Context>
                        ...

This should give you a better error message much quicker.

We use at buildtime:

            <plugin>
                <groupId>com.atlassian.plugin</groupId>
                <artifactId>atlassian-spring-scanner-maven-plugin</artifactId>
                <version>2.0.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>atlassian-spring-scanner</goal>
                        </goals>
                        <phase>process-classes</phase>
                    </execution>
                </executions>
            </plugin>

And as only dependency:

        <dependency>
            <groupId>com.atlassian.plugin</groupId>
            <artifactId>atlassian-spring-scanner-annotation</artifactId>
            <scope>provided</scope>
        </dependency>

The dependency version is taken from our Confluence compile target, e.g. 3.0.3 for Confluence 8.5.3, but typically that version number doesn’t really matter.

I’d recommend to check the text files generated by the scanner after building. They are located in target/classes/META-INF/plugin-components. The contents should reflect your @Component, @ComponentImport and @ExportAsService annotations.

For example our exports file has a line for the DiscoverableListener:

com.k15t.scroll.exporter.migration.PdfCloudMigrationListener#com.atlassian.migration.app.listener.DiscoverableListener