Spring AOP and JavaConfig in Atlassian Jira plugins

  • Tutorial
In this article, we will develop a plugin for Atlassian Jira, where using JavaConfig we define a bean with a scope of prototype, we reserve calls to bean methods using AOP, and output information from external beans (ApplicationProperties, JiraAuthenticationContext and ConstantsManager).

The source code of the plugin can be taken here .

1. Create a plugin.


To do this, open the terminal and enter:

atlas-create-jira-plugin

The questions asked in the terminal should be answered like this:

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. Make changes to pom.xml


You must change the scope of atlassian-spring-scanner-annotation from compile to provided.

com.atlassian.pluginatlassian-spring-scanner-annotation${atlassian.spring.scanner.version}compile

Remove the atlassian-spring-scanner-runtime dependency.
Change the atlassian.spring.scanner.version property to 2.0.0

Add the following dependencies:

org.springframeworkspring-core4.2.5.RELEASEprovidedorg.springframeworkspring-context4.2.5.RELEASEprovided

Add the following line to the maven-jira-plugin in the instructions tag:

*

This line will allow the plugin to find Spring classes at runtime.

3. Create an interface and implementation of the HelloWorld entity.


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;
import com.atlassian.jira.config.ConstantsManager;
import com.atlassian.jira.security.JiraAuthenticationContext;
import com.atlassian.sal.api.ApplicationProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.matveev.alexey.plugins.spring.api.HelloWorld;
public class HelloWorldImpl implements HelloWorld {
    private static final Logger LOG = LoggerFactory.getLogger(HelloWorldImpl.class);
    private String message = "Hello World!!!";
    private final ApplicationProperties applicationProperties;
    private final ConstantsManager constantsManager;
    private final JiraAuthenticationContext jiraAuthenticationContext;
    public HelloWorldImpl(ApplicationProperties applicationProperties, JiraAuthenticationContext jiraAuthenticationContext, ConstantsManager constantsManager) {
        this.applicationProperties = applicationProperties;
        this.constantsManager = constantsManager;
        this.jiraAuthenticationContext = jiraAuthenticationContext;
    }
    public String getMessage() {
        LOG.debug("getMessage executed");
        return applicationProperties.getDisplayName() + " logged user: " + jiraAuthenticationContext.getLoggedInUser().getName() + " default priority: " + constantsManager.getDefaultPriority().getName() + " " + this.message;
    }
    public void setMessage(String value) {
        LOG.debug("setMessage executed");
        message = value;
    }
}

The class takes three external beans and displays data from these beans in the getMessage method.

4. Create a class for the impotra exported Jira beans.


JiraBeansImporter.java

import com.atlassian.jira.config.ConstantsManager;
package ru.matveev.alexey.plugins.spring.impl;
import com.atlassian.jira.security.JiraAuthenticationContext;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import com.atlassian.sal.api.ApplicationProperties;
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class JiraBeansImporter {
@Inject
    public JiraBeansImporter(@ComponentImport ApplicationProperties applicationProperties,
                             @ComponentImport JiraAuthenticationContext jiraAuthenticationContext,
                             @ComponentImport ConstantsManager constantsManager
                           ) {
    }
}

This class is needed only for JavaConfig to see the external beans we need.

5. Create classes for logging data about called methods of objects.


HijackBeforeMethod.java
This class logs information before calling an object method.

package ru.matveev.alexey.plugins.spring.aop;
import java.lang.reflect.Method;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.MethodBeforeAdvice;
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());
    }
}

HijackAroundMethod.java
This class logs information before calling and after calling an object's method.

package ru.matveev.alexey.plugins.spring.aop;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
        }
    }
}

6. Create JavaConfig


package ru.matveev.alexey.plugins.spring.config;
import com.atlassian.jira.config.ConstantsManager;
import com.atlassian.jira.security.JiraAuthenticationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import ru.matveev.alexey.plugins.spring.aop.HijackAroundMethod;
import ru.matveev.alexey.plugins.spring.aop.HijackBeforeMethod;
import com.atlassian.sal.api.ApplicationProperties;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ru.matveev.alexey.plugins.spring.api.HelloWorld;
import ru.matveev.alexey.plugins.spring.impl.HelloWorldImpl;
@Component
@Configuration
public class Config{
    @Bean(name = "helloWorld")
    @Scope("prototype")
    public HelloWorld helloWorld(ApplicationProperties applicationProperties,
                                 JiraAuthenticationContext jiraAuthenticationContext,
                                 ConstantsManager constantsManager) {
        return new HelloWorldImpl(applicationProperties, jiraAuthenticationContext, constantsManager);
    }
    @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(ApplicationProperties applicationProperties,
                                                   JiraAuthenticationContext jiraAuthenticationContext,
                                                   ConstantsManager constantsManager) {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        proxyFactoryBean.setTarget(helloWorld(applicationProperties,jiraAuthenticationContext,constantsManager));
        proxyFactoryBean.setProxyTargetClass(true);
        proxyFactoryBean.setInterceptorNames("hijackBeforeMethodBean");
        return proxyFactoryBean;
    }
    @Bean (name = "helloWorldAroundProxy")
    @Scope("prototype")
    public ProxyFactoryBean proxyAroundFactoryBean(ApplicationProperties applicationProperties,
                                                   JiraAuthenticationContext jiraAuthenticationContext,
                                                   ConstantsManager constantsManager) {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        proxyFactoryBean.setTarget(helloWorld(applicationProperties,jiraAuthenticationContext,constantsManager));
        proxyFactoryBean.setProxyTargetClass(true);
        proxyFactoryBean.setInterceptorNames("hijackAroundMethodBean");
        return proxyFactoryBean;
    }
}

In JavaConfig we create:

  • the helloWorld bean with the scope of the prototype, which means that the bean instance will be created every time it is accessed.
  • hijackBeforeMethodBean and hijackAroundMethodBean beans, which log information before and after calling object methods.
  • helloWorldBeforeProxy and helloWorldAroundProxy beans, which proxy the helloWorld bean and, when accessing the helloWorld bean methods, log information using the hijackBeforeMethodBean and hijackAroundMethodBean beans

7. Create two servlets.


We will use servlets to test our application.

Open the terminal and execute:

atlas-create-jira-plugin-module

When asked about the type of module being created, then select 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

Next, we answer the questions as follows:

Enter New Classname MyServlet: : MyServlet1
Enter Package Name ru.matveev.alexey.plugins.spring.servlet: :
Show Advanced Setup? (Y/y/N/n) N: : N
Add Another Plugin Module? (Y/y/N/n) N: : Y
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
Enter New Classname MyServlet: : MyServlet2
Enter Package Name ru.matveev.alexey.plugins.spring.servlet: :
Show Advanced Setup? (Y/y/N/n) N: : N
Add Another Plugin Module? (Y/y/N/n) N: : N

As a result, the files MyServlet1.java and MyServlet2.java will be formed. We change the code in these files:
MyServlet1.java
We pass our helloWorldBeforeProxy proxy bean to the servlet. That is, when accessing HelloWorld, information will be logged before calling HelloWorld methods.

package ru.matveev.alexey.plugins.spring.servlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import ru.matveev.alexey.plugins.spring.api.HelloWorld;
import javax.inject.Inject;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
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 = "" + helloWorld.getMessage() + "";
        helloWorld.setMessage("message changed MyServlet");
        resp.getWriter().write(message);
    }
}

MyServlet2.java
We pass to the servlet our proxy bean helloWorldAroundProxy. That is, when accessing HelloWorld, information will be logged before and after calling the HelloWorld methods.

package ru.matveev.alexey.plugins.spring.servlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import ru.matveev.alexey.plugins.spring.api.HelloWorld;
import javax.inject.Inject;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
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 = "" + helloWorld.getMessage() + "";
        helloWorld.setMessage("message changed MyServlet");
        resp.getWriter().write(message);
    }
}

8. Check the result.


We must check the following:

  1. Our plugin is launched.
  2. Information from external beans imported by us (ApplicationProperties, JiraAuthenticationContext, ConstantsManager) is returned
  3. helloWorld works in the field of prototype visibility.
  4. Information about calls to helloWorld methods is logged.

Open the terminal and enter:

atlas-run

After Jira has started, open Jira in the browser at localhost : 2990 / jira / and log in to Jira.



Set the logging level of the ru.matveev.alexey package to DEBUG. To do this, go to System-> Logging and Profiling:



Go to localhost : 2990 / jira / plugins / servlet / myservlet1 to call MyServlet1.java.



From the screenshot we see that our plugin is working, and information from external bins is transmitted. We successfully checked the first two points.

To verify that helloWorld has a prototype scope, we will refresh the page in the browser:



We see that the message “Hello World !!!” has been replaced by “message changed MyServlet”. Since helloWorld has a scope of prototype, when accessing MyServlet2.java we should get the value “Hello World !!!” (if the scope was singleton, we would get the message “message changed MyServlet”).

We turn to MyServlet2.java at localhost : 2990 / jira / plugins / servlet / myservlet2:



We see that the message “Hello World !!!” appeared in the caption, which means that the visibility of helloWorld is really a prototype.

Next, we will check whether the information was logged when accessing the helloWorld methods. In the logs we find:

[INFO] [talledLocalContainer] 2018-04-01 17:06:52,609 http-nio-2990-exec-4 DEBUG admin 1026x406x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet1 [r.m.a.p.spring.servlet.MyServlet1] MyServlet1 called
[INFO] [talledLocalContainer] 2018-04-01 17:06:52,610 http-nio-2990-exec-4 DEBUG admin 1026x406x1 1fz8ddf 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-04-01 17:06:52,610 http-nio-2990-exec-4 DEBUG admin 1026x406x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet1 [r.m.a.p.spring.impl.HelloWorldImpl] getMessage executed
[INFO] [talledLocalContainer] 2018-04-01 17:06:52,611 http-nio-2990-exec-4 DEBUG admin 1026x406x1 1fz8ddf 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-04-01 17:06:52,611 http-nio-2990-exec-4 DEBUG admin 1026x406x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet1 [r.m.a.p.spring.impl.HelloWorldImpl] setMessage executed
[INFO] [talledLocalContainer] 2018-04-01 17:06:55,024 http-nio-2990-exec-3 DEBUG admin 1026x407x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.servlet.MyServlet2] MyServlet2 called
[INFO] [talledLocalContainer] 2018-04-01 17:06:55,025 http-nio-2990-exec-3 DEBUG admin 1026x407x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Method name : getMessage
[INFO] [talledLocalContainer] 2018-04-01 17:06:55,026 http-nio-2990-exec-3 DEBUG admin 1026x407x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Method arguments : []
[INFO] [talledLocalContainer] 2018-04-01 17:06:55,026 http-nio-2990-exec-3 DEBUG admin 1026x407x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Before method hijacked!
[INFO] [talledLocalContainer] 2018-04-01 17:06:55,026 http-nio-2990-exec-3 DEBUG admin 1026x407x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.impl.HelloWorldImpl] getMessage executed
[INFO] [talledLocalContainer] 2018-04-01 17:06:55,026 http-nio-2990-exec-3 DEBUG admin 1026x407x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Before after hijacked!
[INFO] [talledLocalContainer] 2018-04-01 17:06:55,026 http-nio-2990-exec-3 DEBUG admin 1026x407x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Method name : setMessage
[INFO] [talledLocalContainer] 2018-04-01 17:06:55,026 http-nio-2990-exec-3 DEBUG admin 1026x407x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Method arguments : [message changed MyServlet]
[INFO] [talledLocalContainer] 2018-04-01 17:06:55,026 http-nio-2990-exec-3 DEBUG admin 1026x407x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Before method hijacked!
[INFO] [talledLocalContainer] 2018-04-01 17:06:55,026 http-nio-2990-exec-3 DEBUG admin 1026x407x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.impl.HelloWorldImpl] setMessage executed
[INFO] [talledLocalContainer] 2018-04-01 17:06:55,026 http-nio-2990-exec-3 DEBUG admin 1026x407x1 1fz8ddf 127.0.0.1 /plugins/servlet/myservlet2 [r.m.a.p.spring.aop.HijackAroundMethod] HijackAroundMethod : Before after hijacked!

From the logs we see that the logging of information about the called methods has occurred.
Thus, we have successfully completed our goals.

Also popular now: