Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in
Deleted user
0 / 0 points
Next:
badges earned

Your Points Tracker
Challenges
Leaderboard
  • Global
  • Feed

Badge for your thoughts?

You're enrolled in our new beta rewards program. Join our group to get the inside scoop and share your feedback.

Join group
Recognition
Give the gift of kudos
You have 0 kudos available to give
Who do you want to recognize?
Why do you want to recognize them?
Kudos
Great job appreciating your peers!
Check back soon to give more kudos.

Past Kudos Given
No kudos given
You haven't given any kudos yet. Share the love above and you'll see it here.

It's not the same without you

Join the community to find out what other Atlassian users are discussing, debating and creating.

Atlassian Community Hero Image Collage

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

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 

I have a problem locating config file:

@Component
@Configuration
class Config

Comment

Log in or Sign up to comment
TAGS

Community Events

Connect with like-minded Atlassian users at free events near you!

Find an event

Connect with like-minded Atlassian users at free events near you!

Unfortunately there are no Community Events near you at the moment.

Host an event

You're one step closer to meeting fellow Atlassian users at your local event. Learn more about Community Events

Events near you