Hello everyone,
We are currently migrating our legacy Data Center plugin to Jira 11.x, adapting it to: Jakarta (jakarta.*), Updated Spring versions, Removal of legacy javax.*, modern plugin framework changes ..
However, we are stuck on a dependency injection issue when accessing (for example) one of our servlets.
When accessing our servlet (used for initial addon configuration), Jira throws:
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'com.sykorait.vmcon.ConnectorSettingsServlet':
Unsatisfied dependency expressed through constructor parameter 0:
No qualifying bean of type 'com.atlassian.sal.api.user.UserManager' available:
expected at least 1 bean which qualifies as autowire candidate.
No matter what combinations we try, we are not able to import and inject these components (UserManager, ApplicationProperties, ..)
In previous Jira versions (javax-based, older Spring), this worked correctly using: @ComponentImport, @Inject, @Scanned ..
We also tried what seems to be the now recommended JavaConfig-based OSGi approach using:
pom.xlm
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sykorait.jira.plugins</groupId>
<artifactId>USU-Service-Management-Connector</artifactId>
<version>2.2.3</version>
<organization>
<name>Sykora IT s.r.o.</name>
<url>https://www.sykorait.com</url>
</organization>
<name>USU Service Management Connector for Jira</name>
<description>Integration between USU Service Management (Valuemation) and Jira.</description>
<packaging>atlassian-plugin</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>com.sun.istack</groupId>
<artifactId>istack-commons-runtime</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.fastinfoset</groupId>
<artifactId>FastInfoset</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Jira 11.x changes-->
<dependency>
<groupId>com.atlassian.plugins</groupId>
<artifactId>atlassian-plugins-osgi-javaconfig</artifactId>
<version>${osgi.javaconfig.version}</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.sun.activation</groupId>
<artifactId>jakarta.activation</artifactId>
<version>1.2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<version>2.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.10-atlassian-2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>4.0.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>2.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.applinks</groupId>
<artifactId>applinks-api</artifactId>
<version>8.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.renderer</groupId>
<artifactId>atlassian-renderer</artifactId>
<version>8.0.5</version>
<scope>provided</scope>
<exclusions>
</exclusions>
</dependency>
<dependency>
<groupId>com.atlassian.templaterenderer</groupId>
<artifactId>atlassian-template-renderer-api</artifactId>
<version>5.0.2</version>
<scope>provided</scope>
<exclusions>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.collections</groupId>
<artifactId>google-collections</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.activeobjects</groupId>
<artifactId>activeobjects-plugin</artifactId>
<version>${ao.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.upm</groupId>
<artifactId>licensing-api</artifactId>
<version>5.1.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.upm</groupId>
<artifactId>upm-api</artifactId>
<version>5.1.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.jira</groupId>
<artifactId>jira-api</artifactId>
<version>${jira.version}</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>jta</groupId>
<artifactId>jta</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.atlassian.jira</groupId>
<artifactId>jira-core</artifactId>
<version>${jira.version}</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>jndi</groupId>
<artifactId>jndi</artifactId>
</exclusion>
<exclusion>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-impl</artifactId>
</exclusion>
<exclusion>
<groupId>jta</groupId>
<artifactId>jta</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atlassian.plugin</groupId>
<artifactId>atlassian-spring-scanner-annotation</artifactId>
<version>${atlassian.spring.scanner.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.plugin</groupId>
<artifactId>atlassian-spring-scanner-runtime</artifactId>
<version>${atlassian.spring.scanner.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.atlassian.plugins</groupId>
<artifactId>atlassian-plugins-osgi-testrunner</artifactId>
<version>${plugin.testrunner.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atlassian.plugins.rest</groupId>
<artifactId>atlassian-rest-v2-api</artifactId>
<version>8.0.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.sal</groupId>
<artifactId>sal-api</artifactId>
<version>7.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.wink</groupId>
<artifactId>wink-client</artifactId>
<version>1.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.8.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.upm</groupId>
<artifactId>plugin-license-storage-lib</artifactId>
<version>${upm.license.compatibility.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.atlassian.upm</groupId>
<artifactId>plugin-license-storage-plugin</artifactId>
<version>${upm.license.compatibility.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
<repository>
<id>atlassian-public</id>
<url>https://packages.atlassian.com/maven-public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>atlassian-public</id>
<url>https://packages.atlassian.com/maven-public/</url>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>com.atlassian.maven.plugins</groupId>
<artifactId>jira-maven-plugin</artifactId>
<version>${amps.version}</version>
<extensions>true</extensions>
<configuration>
<!--<applications>
<application>
<applicationKey>jira-servicedesk</applicationKey>
<version>${jira.servicedesk.application.version}</version>
</application>
</applications>-->
<productVersion>${jira.version}</productVersion>
<productDataVersion>${jira.version}</productDataVersion>
<!-- Uncomment to install TestKit backdoor in JIRA. -->
<!--
<pluginArtifacts>
<pluginArtifact>
<groupId>com.atlassian.jira.tests</groupId>
<artifactId>jira-testkit-plugin</artifactId>
<version>${testkit.version}</version>
</pluginArtifact>
</pluginArtifacts>
-->
<enableQuickReload>true</enableQuickReload>
<!-- See here for an explanation of default instructions: -->
<!-- https://developer.atlassian.com/docs/advanced-topics/configuration-of-instructions-in-atlassian-plugins -->
<instructions>
<Atlassian-Plugin-Key>${atlassian.plugin.key}</Atlassian-Plugin-Key>
<!-- Add package to export here -->
<Export-Package>com.sykorait.jira.plugins.vmconnector.api,</Export-Package>
<!-- Add package import here -->
<Import-Package>
!javax.*,
jakarta.servlet*;version="[6.0.0,7.0.0)",
jakarta.ws.rs*;version="[3.0.0,4.0.0)",
jakarta.inject*;version="[2.0.0,3.0.0)",
jakarta.annotation*;version="[2.0.0,3.0.0)",
jakarta.xml.bind*;version="[4.0.0,5.0.0)",
com.atlassian.jira.plugin.webfragment.conditions,
org.springframework.osgi.*;resolution:="optional",
org.eclipse.gemini.blueprint.*;resolution:="optional",
com.atlassian.upm.*;resolution:="optional",
*
</Import-Package>
<!-- Ensure plugin is spring powered -->
<Spring-Context>*</Spring-Context>
<Private-Package>com.atlassian.upm.license.storage.lib*</Private-Package>
<!-- <DynamicImport-Package>com.atlassian.upm.api.license.entity;version="2.0.1", com.atlassian.upm.api.license;version="2.0.1", com.atlassian.upm.api.util;version="2.0.1", com.atlassian.upm.license.storage.plugin;version="${upm.license.compatibility.version}"</DynamicImport-Package>-->
</instructions>
<bundledArtifacts>
<bundledArtifact>
<groupId>com.atlassian.upm</groupId>
<artifactId>atlassian-universal-plugin-manager-plugin</artifactId>
<version>5.1.2</version>
</bundledArtifact>
<bundledArtifact>
<groupId>com.atlassian.upm</groupId>
<artifactId>plugin-license-storage-plugin</artifactId>
<version>${upm.license.compatibility.version}</version>
</bundledArtifact>
</bundledArtifacts>
</configuration>
</plugin>
<plugin>
<groupId>com.atlassian.plugin</groupId>
<artifactId>atlassian-spring-scanner-maven-plugin</artifactId>
<version>${atlassian.spring.scanner.version}</version>
<executions>
<execution>
<goals>
<goal>atlassian-spring-scanner</goal>
</goals>
<phase>process-classes</phase>
</execution>
</executions>
<configuration>
<includeExclude>-com.atlassian.plugin.spring.scanner.annotation.*</includeExclude>
<scannedDependencies>
<dependency>
<groupId>com.atlassian.plugin</groupId>
<artifactId>atlassian-spring-scanner-external-jar</artifactId>
</dependency>
</scannedDependencies>
<verbose>false</verbose>
</configuration>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-storage-plugin</id>
<phase>process-resources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
<includeArtifactIds>plugin-license-storage-plugin</includeArtifactIds>
<stripVersion>true</stripVersion>
</configuration>
</execution>
</executions>
</plugin>
<!--
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>7.0.4</version>
<configuration>
<skipProvidedScope>true</skipProvidedScope>
<suppressionFiles>
<suppressionFile>https://dcapt-downloads.s3.amazonaws.com/atlassian-security-scanner-dc-apps-suppressions.xml</suppressionFile>
</suppressionFiles>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
-->
</plugins>
</build>
<properties>
<ao.version>6.1.2</ao.version>
<jira.version>11.3.0</jira.version>
<amps.version>9.9.1</amps.version>
<osgi.javaconfig.version>0.6.0</osgi.javaconfig.version>
<atlassian.spring.scanner.version>3.0.3</atlassian.spring.scanner.version> <!-- proven stable -->
<spring.version>5.3.39-atlassian-3</spring.version>
<jira.servicedesk.application.version>4.15.0 </jira.servicedesk.application.version>
<plugin.testrunner.version>2.0.1</plugin.testrunner.version>
<!-- This property ensures consistency between the key in atlassian-plugin.xml and the OSGi bundle's key. -->
<atlassian.plugin.key>${project.groupId}.${project.artifactId}</atlassian.plugin.key>
<!-- TestKit version 6.x for JIRA 6.x -->
<testkit.version>6.3.11</testkit.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Jira 11.x changes-->
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<upm.license.compatibility.version>2.15.3</upm.license.compatibility.version>
</properties>
</project>
servlet:
package com.sykorait.vmcon;
import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.jira.util.json.JSONException;
import com.atlassian.jira.util.json.JSONObject;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import com.atlassian.sal.api.ApplicationProperties;
import com.atlassian.sal.api.auth.LoginUriProvider;
import com.atlassian.sal.api.user.UserManager;
import com.atlassian.templaterenderer.TemplateRenderer;
import com.atlassian.upm.api.license.PluginLicenseManager;
import com.atlassian.upm.api.license.entity.PluginLicense;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import static com.google.common.base.Preconditions.checkNotNull;
@Named
public class ConnectorSettingsServlet extends HttpServlet {
private static final String TEMPLATE = "templates/connector-settings.vm";
private final PluginLicenseManager pluginLicenseManager;
private final TemplateRenderer templateRenderer;
private final UserManager userManager;
private final LoginUriProvider loginUriProvider;
private final ActiveObjects ao;
private final ApplicationProperties applicationProperties;
public ConnectorSettingsServlet(UserManager userManager, ApplicationProperties applicationProperties, LoginUriProvider loginUriProvider, PluginLicenseManager pluginLicenseManager, TemplateRenderer templateRenderer, ActiveObjects ao) {
this.pluginLicenseManager = pluginLicenseManager;
this.templateRenderer = templateRenderer;
this.userManager = userManager;
this.loginUriProvider = loginUriProvider;
this.applicationProperties = applicationProperties;
this.ao = ao;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
Map<String, Object> context = new HashMap<>();
context.put("baseUrl", applicationProperties.getBaseUrl());
res.setContentType("text/html;charset=utf-8");
if (pluginLicenseManager.getLicense().isDefined()) {
.....
}
}
private void redirectToLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.sendRedirect(loginUriProvider.getLoginUri(getUri(request)).toASCIIString());
}
private URI getUri(HttpServletRequest request) {
StringBuffer builder = request.getRequestURL();
if (request.getQueryString() != null) {
builder.append("?").append(request.getQueryString());
}
return URI.create(builder.toString());
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
...
}
}
JavaConfig (following the attlassian myPlugin tutorial)
package com.sykorait.vmcon;
import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.sal.api.ApplicationProperties;
import com.atlassian.sal.api.auth.LoginUriProvider;
import com.atlassian.sal.api.user.UserManager;
import com.atlassian.templaterenderer.TemplateRenderer;
import com.atlassian.upm.api.license.PluginLicenseManager;
import com.atlassian.plugins.osgi.javaconfig.ExportOptions;
import com.atlassian.plugins.osgi.javaconfig.configs.beans.ModuleFactoryBean;
import com.atlassian.plugins.osgi.javaconfig.configs.beans.PluginAccessorBean;
import org.osgi.framework.ServiceRegistration;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import jakarta.servlet.http.HttpServlet;
import static com.atlassian.plugins.osgi.javaconfig.OsgiServices.exportOsgiService;
import static com.atlassian.plugins.osgi.javaconfig.OsgiServices.importOsgiService;
@Configuration
@Import({ModuleFactoryBean.class, PluginAccessorBean.class})
public class SpringConfig {
// Import OSGi services
@Bean
public ApplicationProperties applicationProperties() {
return importOsgiService(ApplicationProperties.class);
}
@Bean
public UserManager userManager() {
return importOsgiService(UserManager.class);
}
@Bean
public LoginUriProvider loginUriProvider() {
return importOsgiService(LoginUriProvider.class);
}
@Bean
public PluginLicenseManager pluginLicenseManager() {
return importOsgiService(PluginLicenseManager.class);
}
@Bean
public TemplateRenderer templateRenderer() {
return importOsgiService(TemplateRenderer.class);
}
@Bean
public ActiveObjects activeObjects() {
return importOsgiService(ActiveObjects.class);
}
// Create servlet using the OSGi services
@Bean
public ConnectorSettingsServlet connectorSettingsServlet(
UserManager userManager,
ApplicationProperties applicationProperties,
LoginUriProvider loginUriProvider,
PluginLicenseManager pluginLicenseManager,
TemplateRenderer templateRenderer,
ActiveObjects activeObjects) {
return new ConnectorSettingsServlet(
userManager,
applicationProperties,
loginUriProvider,
pluginLicenseManager,
templateRenderer,
activeObjects
);
}
// Optional: Export servlet as OSGi service (if needed)
@Bean
public FactoryBean<ServiceRegistration> registerConnectorServlet(ConnectorSettingsServlet servlet) {
return exportOsgiService(servlet, ExportOptions.as(ConnectorSettingsServlet.class));
}
}
Is there something fundamentally different about how services must be imported or exposed in Jira 11?