Spring Java-based configuration and AOP in Jira plugins

In this article I would like to create a Jira plugin, where I could define beans, using Java-based container configuration, and apply AOP principles to log information about method invocation in the plugin.

Let's do it step by step.

1. Create a Jira plugin.

Open terminal and run the following command:

 atlas-create-jira-plugin

You will be asked about the plugin properties. Here are the values for the properties:

Define value for groupId: : ru.matveev.alexey.plugins.spring
Define value for artifactId: : spring-tutorial
Define value for version:  1.0.0-SNAPSHOT: :
Define value for package:  ru.matveev.alexey.plugins.spring: :

groupId: ru.matveev.alexey.plugins.spring
artifactId: spring-tutorial
version: 1.0.0-SNAPSHOT
package: ru.matveev.alexey.plugins.spring
 Y: : Y

2. Adjust the pom.xml file.

Change the scope  of the atlassian-spring-scanner-annotation dependency from compile to provided:

<dependency>
<groupId>com.atlassian.plugin</groupId>
<artifactId>atlassian-spring-scanner-annotation</artifactId>
<version>${atlassian.spring.scanner.version}</version>
<scope>compile</scope>
</dependency>

Delete the atlassian-spring-scanner-runtime dependency.

Change the value of the property atlassian.spring.scanner.version to 2.0.0

Add the following dependencies:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.2.5.RELEASE</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.5.RELEASE</version>
<scope>provided</scope>
</dependency>

Add the following lines to the maven-jira-plugin in the instructions section:

<DynamicImport-Package>*</DynamicImport-Package>

This line will let your plugin resolve spring classes at runtime.

3. Create the interface and an implementation of the HelloWorld object:

HelloWorld.java:

package ru.matveev.alexey.plugins.spring.api;

public interface HelloWorld {
String getMessage();
void setMessage(String value);
}

HelloWorldImpl.java:

package ru.matveev.alexey.plugins.spring.impl;

public class HelloWorldImpl implements HelloWorld {
private static final Logger LOG = LoggerFactory.getLogger(HelloWorldImpl.class);
private String message = "Hello World!!!";
private final ApplicationProperties applicationProperties;

public HelloWorldImpl(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}

public String getMessage() {
LOG.debug("getMessage executed");
return applicationProperties.getDisplayName() + " " + this.message;
}

public void setMessage(String value) {
LOG.debug("setMessage executed");
message = value;
}
}

4. Create Java classes, which will log information about the HelloWorld methods on their execution.

HijackAroundMethod.java

package ru.matveev.alexey.plugins.spring.aop;

import java.util.Arrays;

public class HijackAroundMethod implements MethodInterceptor {

private static final Logger LOG = LoggerFactory.getLogger(HijackAroundMethod.class);


public Object invoke(MethodInvocation methodInvocation) throws Throwable {

LOG.debug("HijackAroundMethod : Method name : "
+ methodInvocation.getMethod().getName());
LOG.debug("HijackAroundMethod : Method arguments : "
+ Arrays.toString(methodInvocation.getArguments()));

LOG.debug("HijackAroundMethod : Before method hijacked!");

try {
Object result = methodInvocation.proceed();

LOG.debug("HijackAroundMethod : Before after hijacked!");

return result;

} catch (IllegalArgumentException e) {
LOG.debug("HijackAroundMethod : Throw exception hijacked!");
throw e;
}
}
}

HijackBeforeMethod.java

package ru.matveev.alexey.plugins.spring.aop;

public class HijackBeforeMethod implements MethodBeforeAdvice
{

private static final Logger LOG = LoggerFactory.getLogger(HijackBeforeMethod.class);


public void before(Method method, Object[] objects, Object o) throws Throwable {
LOG.debug("HijackBeforeMethod : method {} in", method.toString());

}
}

5. Create a Java-based container configuration file.

Config.java

package ru.matveev.alexey.plugins.spring.config;

@Component
@Configuration
public class Config{


@Bean(name = "helloWorld")
@Scope("prototype")
public HelloWorld helloWorld(@ComponentImport ApplicationProperties applicationProperties) {
return new HelloWorldImpl(applicationProperties);
}

@Bean(name="hijackBeforeMethodBean")
public HijackBeforeMethod hijackBeforeMethod() {
return new HijackBeforeMethod();
}

@Bean(name="hijackAroundMethodBean")
public HijackAroundMethod hijackAroudnMethod() {
return new HijackAroundMethod();
}

@Bean (name = "helloWorldBeforeProxy")
@Scope("prototype")
public ProxyFactoryBean proxyBeforeFactoryBean(@ComponentImport ApplicationProperties applicationProperties) {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(helloWorld(applicationProperties));
proxyFactoryBean.setProxyTargetClass(true);
proxyFactoryBean.setInterceptorNames("hijackBeforeMethodBean");
return proxyFactoryBean;
}

@Bean (name = "helloWorldAroundProxy")
@Scope("prototype")
public ProxyFactoryBean proxyAroundFactoryBean(@ComponentImport ApplicationProperties applicationProperties) {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(helloWorld(applicationProperties));
proxyFactoryBean.setProxyTargetClass(true);
proxyFactoryBean.setInterceptorNames("hijackAroundMethodBean");
return proxyFactoryBean;
}


}

6. Create two servlet modules.

The Servlet modules will be used for testing the application.

Open terminal and run the command below:

atlas-create-jira-plugin-module

When you are asked to choose the module number choose 21 (Servlet)

Choose a number (1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/24/25/26/27/28/29/30/31/32/33/34): 21

When you are asked about the properties of the module, enter:

Enter New Classname MyServlet: : MyServlet1
Enter Package Name ru.matveev.alexey.plugins.spring.servlet: :
Show Advanced Setup? (Y/y/N/n) N: : N

 Then you will be asked, if you want to create another module, answer Y

Add Another Plugin Module? (Y/y/N/n) N: : Y

Choose again the Servlet module (Number 21) and fill the module properties as below:

Enter New Classname MyServlet: : MyServlet2
Enter Package Name ru.matveev.alexey.plugins.spring.servlet: :
Show Advanced Setup? (Y/y/N/n) N: : N

Answer N, when you are asked about adding another module:

Add Another Plugin Module? (Y/y/N/n) N: : N

7. Add code to the Servlet modules.

MyServlet1.java

package ru.matveev.alexey.plugins.spring.servlet;

public class MyServlet1 extends HttpServlet{
private static final Logger log = LoggerFactory.getLogger(MyServlet1.class);

private final HelloWorld helloWorld;

@Inject
public MyServlet1(@Qualifier("helloWorldBeforeProxy") HelloWorld helloWorld) {
this.helloWorld = helloWorld;
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
log.debug("MyServlet1 called");
resp.setContentType("text/html");
String message = "<html><body>" + helloWorld.getMessage() + "</body></html>";
helloWorld.setMessage("message changed MyServlet");
resp.getWriter().write(message);
}

}
 

MyServlet2.java

package ru.matveev.alexey.plugins.spring.servlet;

public class MyServlet2 extends HttpServlet{
private static final Logger log = LoggerFactory.getLogger(MyServlet2.class);

private final HelloWorld helloWorld;

@Inject
public MyServlet2(@Qualifier("helloWorldAroundProxy") HelloWorld helloWorld) {
this.helloWorld = helloWorld;
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
log.debug("MyServlet2 called");
resp.setContentType("text/html");
String message = "<html><body>" + helloWorld.getMessage() + "</body></html>";
helloWorld.setMessage("message changed MyServlet");
resp.getWriter().write(message);
}

}

8. Run the plugin

Open terminal and run the command below:

atlas-run

When Jira started, you should open your browser at:

 http://localhost:2990/jira/

Go to Cog item -> System -> Logging and Profiling and set the Debug logging level for  the ru.matveev.alexey package:

Selection_034.png

 Go to http://localhost:2990/jira/plugins/servlet/myservlet1:

Selection_035.pngOur Servet Worked.

Let's see the logs to make sure that the helloWorldBeforeProxy worked too:

[INFO] [talledLocalContainer] 2018-03-31 05:47:26,957 http-nio-2990-exec-10 DEBUG admin 347x281x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet1 [r.m.a.p.spring.servlet.MyServlet1] MyServlet1 called
[INFO] [talledLocalContainer] 2018-03-31 05:47:26,957 http-nio-2990-exec-10 DEBUG admin 347x281x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet1 [r.m.a.p.spring.aop.HijackBeforeMethod] HijackBeforeMethod : method public java.lang.String ru.matveev.alexey.plugins.spring.impl.HelloWorldImpl.getMessage() in
[INFO] [talledLocalContainer] 2018-03-31 05:47:26,996 http-nio-2990-exec-10 DEBUG admin 347x281x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet1 [r.m.a.p.spring.impl.HelloWorldImpl] getMessage executed
[INFO] [talledLocalContainer] 2018-03-31 05:47:26,997 http-nio-2990-exec-10 DEBUG admin 347x281x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet1 [r.m.a.p.spring.aop.HijackBeforeMethod] HijackBeforeMethod : method public void ru.matveev.alexey.plugins.spring.impl.HelloWorldImpl.setMessage(java.lang.String) in
[INFO] [talledLocalContainer] 2018-03-31 05:47:26,997 http-nio-2990-exec-10 DEBUG admin 347x281x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet1 [r.m.a.p.spring.impl.HelloWorldImpl] setMessage executed

We can see that the HijackBeforeMethod bean was called before each method. It means we also succeeded in AOP.

Let's refresh the page:

Selection_036.pngWe can see that the message variable in the HelloWorldImpl class changed. We declared the HelloWorld bean as prototype. It means, that if we call the MyServlet2 servlet, a new instance of the helloWorld bean will be created and the message variable will have the "Hello World" value. Let's check it.

Go to http://localhost:2990/jira/plugins/servlet/myservlet2:

Selection_037.png

We can see that the message variable has the expected value. The prototype scope worked.

Now let's see in the logs:

[INFO] [talledLocalContainer] 2018-03-31 05:56:24,100 http-nio-2990-exec-6 DEBUG admin 356x283x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.servlet.MyServlet2] MyServlet2 called
[INFO] [talledLocalContainer] 2018-03-31 05:56:24,100 http-nio-2990-exec-6 DEBUG admin 356x283x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Method name : getMessage
[INFO] [talledLocalContainer] 2018-03-31 05:56:24,100 http-nio-2990-exec-6 DEBUG admin 356x283x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Method arguments : []
[INFO] [talledLocalContainer] 2018-03-31 05:56:24,100 http-nio-2990-exec-6 DEBUG admin 356x283x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Before method hijacked!
[INFO] [talledLocalContainer] 2018-03-31 05:56:24,113 http-nio-2990-exec-6 DEBUG admin 356x283x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.impl.HelloWorldImpl] getMessage executed
[INFO] [talledLocalContainer] 2018-03-31 05:56:24,114 http-nio-2990-exec-6 DEBUG admin 356x283x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Before after hijacked!
[INFO] [talledLocalContainer] 2018-03-31 05:56:24,114 http-nio-2990-exec-6 DEBUG admin 356x283x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Method name : setMessage
[INFO] [talledLocalContainer] 2018-03-31 05:56:24,114 http-nio-2990-exec-6 DEBUG admin 356x283x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Method arguments : [message changed MyServlet]
[INFO] [talledLocalContainer] 2018-03-31 05:56:24,114 http-nio-2990-exec-6 DEBUG admin 356x283x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Before method hijacked!
[INFO] [talledLocalContainer] 2018-03-31 05:56:24,114 http-nio-2990-exec-6 DEBUG admin 356x283x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.impl.HelloWorldImpl] setMessage executed
[INFO] [talledLocalContainer] 2018-03-31 05:56:24,114 http-nio-2990-exec-6 DEBUG admin 356x283x1 1o6djq5 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Before after hijacked!

We can see that the helloWorldAround bean worked as well.

Everything worked as we planned.

You can find the code here:

https://bitbucket.org/alex1mmm/spring-tutorial/src/97c4ac7a145336fcadc2c3922046d09406696361?at=V1

 

2 comments

artafon January 11, 2019

Hi Alexey!

You are using spring 2.0. Could you show the plugin-context.xml? In my case, there are errors.

pom.xml:

<?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.atlassian.tutorial</groupId>
<artifactId>ProjectFields</artifactId>
<version>1.0.0-snapshot</version>
<organization>
<name>Example Company</name>
<url>http://www.example.com/</url>
</organization>
<name>ProjectFields</name>
<description>This is the com.atlassian.tutorial:ProjectFields plugin for Atlassian JIRA.</description>
<packaging>atlassian-plugin</packaging>
<dependencies>
<dependency>
<groupId>com.atlassian.jira</groupId>
<artifactId>jira-api</artifactId>
<version>${jira.version}</version>
<scope>provided</scope>
</dependency>
<!-- Add dependency on jira-core if you want access to JIRA implementation classes as well as the sanctioned API. -->
<!-- This is not normally recommended, but may be required eg when migrating a plugin originally developed against JIRA 4.x -->
<!--
-->
<dependency>
<groupId>com.atlassian.jira</groupId>
<artifactId>jira-core</artifactId>
<version>${jira.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<!--<scope>test</scope>-->
<scope>provided</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.sal</groupId>
<artifactId>sal-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope> <!-- Uses the application's SAL instead of bundling it into the plugin. -->
</dependency>
<!--<dependency>-->
<!--<groupId>com.atlassian.plugin</groupId>-->
<!--<artifactId>atlassian-spring-scanner-runtime</artifactId>-->
<!--<version>${atlassian.spring.scanner.version}</version>-->
<!--&lt;!&ndash;<scope>runtime</scope>&ndash;&gt;-->
<!--<scope>provided</scope>-->
<!--</dependency>-->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
<scope>provided</scope>
</dependency>
<!-- WIRED TEST RUNNER DEPENDENCIES -->
<dependency>
<groupId>com.atlassian.plugins</groupId>
<artifactId>atlassian-plugins-osgi-testrunner</artifactId>
<version>${plugin.testrunner.version}</version>
<!--<scope>test</scope>-->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<version>1.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.2-atlassian-1</version>
<scope>provided</scope>
</dependency>
<!-- Uncomment to use TestKit in your project. Details at https://bitbucket.org/atlassian/jira-testkit -->
<!-- You can read more about TestKit at https://developer.atlassian.com/display/JIRADEV/Plugin+Tutorial+-+Smarter+integration+testing+with+TestKit -->
<!--
<dependency>
<groupId>com.atlassian.jira.tests</groupId>
<artifactId>jira-testkit-client</artifactId>
<version>${testkit.version}</version>
<scope>test</scope>
</dependency>
-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.8.5</version>
<!--<scope>test</scope>-->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.10</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.templaterenderer</groupId>
<artifactId>atlassian-template-renderer-api</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.5.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.2.5.RELEASE</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.5.RELEASE</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.atlassian.maven.plugins</groupId>
<artifactId>maven-jira-plugin</artifactId>
<version>${amps.version}</version>
<extensions>true</extensions>
<configuration>
<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>
<enableFastdev>false</enableFastdev>
<!-- 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.atlassian.tutorial.ProjectFields.api,</Export-Package>
<!-- Add package import here -->
<Import-Package>org.springframework.osgi.*;resolution:="optional", org.eclipse.gemini.blueprint.*;resolution:="optional", *</Import-Package>
<!-- Ensure plugin is spring powered -->
<Spring-Context>*</Spring-Context>
<DynamicImport-Package>*</DynamicImport-Package>
</instructions>
</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>
</plugins>
</build>
<properties>
<jira.version>7.13.0</jira.version>
<amps.version>6.3.15</amps.version>
<plugin.testrunner.version>1.2.3</plugin.testrunner.version>
<atlassian.spring.scanner.version>2.0.0</atlassian.spring.scanner.version>
<!-- This key is used to keep the consistency between the key in atlassian-plugin.xml and the key to generate bundle. -->
<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>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
</project>

plugin-context.xml

<?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>

After run - atlas-package, I get:

[ERROR] Found a type [org.springframework.stereotype.Service] annotated as a component, but the type is not a concrete class. NOT adding to index file!!
[ERROR] Found a type [org.springframework.stereotype.Repository] annotated as a component, but the type is not a concrete class. NOT adding to index file!!
[ERROR] Found a type [org.springframework.stereotype.Controller] annotated as a component, but the type is not a concrete class. NOT adding to index file!!

 

 Thanks 

hellboy May 9, 2019

I have a problem locating config file:

@Component
@Configuration
class Config

Comment

Log in or Sign up to comment
TAGS
AUG Leaders

Atlassian Community Events