Spring AOP и JavaConfig в плагинах для Atlassian Jira

d2b_ph3ect7vfoaqvbet9gqpbgg.jpeg В этой статье разработаем плагин для Atlassian Jira, где с помощью JavaConfig определим бин с областью видимости прототип, залогируем вызовы методов бина, используя AOP, и выведем информацию из внешних бинов (ApplicationProperties, JiraAuthenticationContext и ConstantsManager).

Исходный код плагина можно взять вот здесь.

1. Создадим плагин.


Для этого нужно открыть терминал и ввести:

atlas-create-jira-plugin


На заданные в терминале вопросы нужно ответить вот так:

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. Внесем изменения в pom.xml


Необходимо изменить область видимости atlassian-spring-scanner-annotation с compile на provided.


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


Удалить зависимость atlassian-spring-scanner-runtime.
Изменить свойство atlassian.spring.scanner.version на 2.0.0

Добавить следующие зависимости:


    org.springframework
    spring-core
    4.2.5.RELEASE
    provided


    org.springframework
    spring-context
    4.2.5.RELEASE
    provided


Добавить в maven-jira-plugin в тэг instructions следующую строчку:

*


Данная строчка позволит плагину находить классы Spring во время исполнения.

3. Создадим интерфейс и имплементацию сущности HelloWorld.


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;
    }
}


Класс принимает три внешних бина и выводит в методе getMessage данные из этих бинов.

4. Создадим класс для импотра экспортируемых Jira бинов.


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
                           ) {
    }

}


Данный класс нужен лишь для того, чтобы JavaConfig увидел требуемые нам внешние бины.

5. Создадим классы для логирования данных о вызываемых методах объектов.


HijackBeforeMethod.java
Данный класс логирует информацию перед вызовом метода объекта.

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
Данный класс логирует информацию перед вызовом и после вызова метода объекта.

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. Создадим 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;
    }
}


В JavaConfig мы создаем:

  • бин helloWorld с областью видимости прототип, что означает, что инстанс бина будет создаваться каждый раз при обращении к нему.
  • бины hijackBeforeMethodBean и hijackAroundMethodBean, которые логируют информацию перед и после вызовов методов объектов.
  • бины helloWorldBeforeProxy и helloWorldAroundProxy, которые проксируют бин helloWorld и при обращении к методам бина helloWorld логируют информацию с помощью бинов hijackBeforeMethodBean и hijackAroundMethodBean


7. Создадим два сервелета.


Сервлеты будем использовать для тестирования нашего приложения.

Открываем терминал и выполняем:

atlas-create-jira-plugin-module


Когда будет задан вопрос о типе создаваемого модуля, то выбираем 21 (Сервлет):

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


В результате у нас сформируются файлы MyServlet1.java и MyServlet2.java. Меняем код в этих файлах:
MyServlet1.java
Мы передаем в сервлет наш прокси бин helloWorldBeforeProxy. То есть при обращении к HelloWorld будет логироваться информация перед вызовом методов HelloWorld.

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
Мы передаем в сервлет наш прокси бин helloWorldAroundProxy. То есть при обращении к HelloWorld будет логироваться информация перед и после вызова методов HelloWorld.

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. Проверим результат.


Мы должны проверить следующее:

  1. Наш плагин запускается.
  2. Выдается информации из имортируемых нами внешних бинов (ApplicationProperties, JiraAuthenticationContext, ConstantsManager)
  3. helloWorld рабоает в области видимости прототип.
  4. Логируется информация о вызовах методов helloWorld.


Откроем терминал и введем:

atlas-run


После того, как Jira запустилась, откроем Jira в браузере по адресу localhost:2990/jira/ и залогинимся в Jira.

fs3hzso6wb_aedfcvfon1jt7d_e.png

Установим уровень логирования пакета ru.matveev.alexey в DEBUG. Для этого нужно зайти в System→ Logging and Profiling:

eysjai-zvyw7gy3fgqgndvxzjra.png

Переходим по адресу localhost:2990/jira/plugins/servlet/myservlet1 для вызыва MyServlet1.java.

yqj9scq88ojib-ngu6wzwtrqclc.png

Из скриншота мы видим, что наш плагин работает, и информация из внешних бинов передается. Мы успешно проверили первые два пункта.

Для проверки того, что helloWorld имеет область видимости прототип, мы обновим страницу в браузере:

ygx_hhwoijgzcln3f0pguu16ib4.png

Мы видим, что сообщение «Hello World!!!» заменилось на «message changed MyServlet». Так как у нас у helloWorld область видимости прототип, то при обращении к MyServlet2.java мы должны получить значение «Hello World!!!» (если ли бы область видимости была синглетон, то мы получили бы сообщение «message changed MyServlet»).

Обращаемся к MyServlet2.java по адресу localhost:2990/jira/plugins/servlet/myservlet2:

qwx5_eb36wd_jd-nb5njbdvijnq.png

Мы видим, что в надписи появилось сообщение «Hello World!!!», а значит действительно область видимости helloWorld прототип.

Дальше проверим была ли залогирована информация при обращении к методам helloWorld. В логах мы находим:

[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!


Из логов мы видим, что логирование информации о вызываемых методах произошло.
Таким образом мы успешно справились с поставленными целями.

© Habrahabr.ru