From:https://developer.atlassian.com/server/framework/atlassian-sdk/scheduling-events-via-sal-tutorialjava
本向导将向你演示如何在你的插件中定时调度一个Java任务在后台运行。为此,咱们将使用SAL(Shared Access Layer)提供的跨产品组件 PluginScheduler
。git
后台任务调度在任务成本较高的场景或者须要定时执行的维护工做中会颇有用处。在本向导中,咱们在后台定时运行一个任务,每5秒从Twitter中搜索一次并将最后一次的搜索结果保存在内存中(在本教程中咱们假设Twitter搜索是一个代价很高的任务)。web
为了使向导更有趣,不可见的后台搜索任务伴随着一个JIRA管理页面,该页面呈现最新的搜索结果,并向用户提供更改搜索查询和间隔时间的能力。它还实现了取消和从新安排事件的必要途径。apache
为了实现上述这些,插件包含以下模块:api
全部模块打包到一个JAR文件中,在接下来的示例中会对每一个模块作深刻的介绍。浏览器
咱们鼓励你完成本教程的学习。若是你想要跳过或者检查你的学习成果,你能够从Atlassian Bitbucket上找到插件的源码。Bitbucket服务器是一个开源Git仓,包含了本教程代码。要克隆这个仓,执行以下命令:服务器
$ git clone https://atlassian_tutorial@bitbucket.org/atlassian_tutorial/jira-scheduled-events.git
此外,你能够在下载页面下载源码。下载页面:bitbucket.org/atlassian_tutorial/jira-scheduled-eventsapp
为了完成本教程,你应该已经理解了Java开发的基础知识:classes, interfaces, methods, 如何使用编译器等等。你还应该理解:框架
本教程会教你:jsp
使用适合的 atlas-create-
application
-plugin
命令建立你的插件。如:atlas-create-jira-plugin
或 atlas-create-confluence-plugin
.
在本教程中,咱们会使用Atlassian Plugin SDK,因此请确保你已经安装,并能正常运行。要检查你是否已经准备好了环境,尝试运行atlas-version名, 你将看到以下输出:
$ atlas-version ATLAS Version: 3.0.4 ATLAS Home: /Users/administrator/usr/atlassian-plugin-sdk-3.0.4 ATLAS Scripts: /Users/administrator/usr/atlassian-plugin-sdk-3.0.4/bin ATLAS Maven Home: /Users/administrator/usr/atlassian-plugin-sdk-3.0.4/apache-maven -------- Executing: /Users/administrator/usr/atlassian-plugin-sdk-3.0.4/apache-maven/bin/mvn --version Apache Maven 2.1.0 (r755702; 2009-03-19 06:10:27+1100) Java version: 1.6.0_15 Java home: /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home Default locale: en_US, platform encoding: MacRoman OS name: "mac os x" version: "10.6" arch: "x86_64" Family: "mac" $
而后经过atlas-create-jira-plugin建立一个新的JIRA插件,并根据提示给插件的groupId和artifactId填入合适的值。
下面是一个例子:
$ atlas-create-jira-plugin Executing: /Users/administrator/usr/atlassian-plugin-sdk-3.0.4/apache-maven/bin/mvn com.atlassian.maven.plugins:maven-jira-plugin:3.0.4:create [INFO] Scanning for projects... [INFO] ------------------------------------------------------------------------ [INFO] Building Maven Default Project [INFO] task-segment: [com.atlassian.maven.plugins:maven-jira-plugin:3.0.4:create] (aggregator-style) [INFO] ------------------------------------------------------------------------ [INFO] [jira:create] [INFO] Setting property: classpath.resource.loader.class => 'org.codehaus.plexus.velocity.ContextClassLoaderResourceLoader'. [INFO] Setting property: velocimacro.messages.on => 'false'. [INFO] Setting property: resource.loader => 'classpath'. [INFO] Setting property: resource.manager.logwhenfound => 'false'. [INFO] [archetype:generate] [INFO] Generating project in Interactive mode [INFO] Archetype repository missing. Using the one from [com.atlassian.maven.archetypes:jira-plugin-archetype:5 -> https://maven.atlassian.com/public] found in catalog internal Define value for groupId: : com.atlassian.example Define value for artifactId: : scheduling Define value for version: 1.0-SNAPSHOT: : Define value for package: com.atlassian.example: : com.atlassian.example.scheduling Confirm properties configuration: groupId: com.atlassian.example artifactId: scheduling version: 1.0-SNAPSHOT package: com.atlassian.example.scheduling Y: : [INFO] ---------------------------------------------------------------------------- [INFO] Using following parameters for creating OldArchetype: jira-plugin-archetype:3.0.4 [INFO] ---------------------------------------------------------------------------- [INFO] Parameter: groupId, Value: com.atlassian.example [INFO] Parameter: packageName, Value: com.atlassian.example.scheduling [INFO] Parameter: package, Value: com.atlassian.example.scheduling [INFO] Parameter: artifactId, Value: scheduling [INFO] Parameter: basedir, Value: /private/tmp [INFO] Parameter: version, Value: 1.0-SNAPSHOT [INFO] ********************* End of debug info from resources from generated POM *********************** [INFO] OldArchetype created in dir: /private/tmp/scheduling [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESSFUL [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1 minute 1 second [INFO] Finished at: Mon Feb 22 18:13:41 EST 2010 [INFO] Final Memory: 42M/252M [INFO] ------------------------------------------------------------------------ $
在本教程中,咱们会使用SAL和开源的Twitter Java库twitter4j. 把他们都加到pom.xml文件中:
<dependencies> ... <dependency> <groupId>net.homeip.yusuke</groupId> <artifactId>twitter4j</artifactId> <version>2.0.10</version> </dependency> <dependency> <groupId>com.atlassian.sal</groupId> <artifactId>sal-api</artifactId> <version>2.0.0</version> <scope>provided</scope> </dependency> ... </dependencies>
为了插件框架能够注入 SAL PluginScheduler
, 咱们须要在atlassian-plugin.xml
明确的引入该模块,因此添加以下节点:
<component-import key="pluginScheduler"> <description>SAL Scheduler</description> <interface>com.atlassian.sal.api.scheduling.PluginScheduler</interface> </component-import>
如今编写一个模块来获取已经注入的SAL PluginScheduler,
而后在启动的时候注册这个周期性的后台任务。
首先,实现任务自己,它必须是一个公开类并实现了接口 com.atlassian.sal.api.scheduling.PluginJob
:
package com.atlassian.example.scheduling; import com.atlassian.sal.api.scheduling.PluginJob; import org.apache.log4j.Logger; import twitter4j.Query; import twitter4j.Twitter; import twitter4j.TwitterException; import java.util.Date; import java.util.Map; public class TwitterQueryTask implements PluginJob { private final Logger logger = Logger.getLogger(TwitterQueryTask.class); /** * Executes this job. * * @param jobDataMap any data the job needs to execute. Changes to this data will be remembered between executions. */ public void execute(Map<String, Object> jobDataMap) { final TwitterMonitorImpl monitor = (TwitterMonitorImpl)jobDataMap.get(TwitterMonitorImpl.KEY); assert monitor != null; try { final Twitter twitter = new Twitter(); monitor.setTweets(twitter.search(new Query(monitor.getQuery())).getTweets()); monitor.setLastRun(new Date()); } catch (TwitterException te) { logger.error("Error talking to Twitter: " + te.getMessage(), te); } } }
注意调度器在运行时传递给execute()方法的map,它为咱们提供了一种与任务沟通的途径。
调度器工做的方法是,注册任务时,咱们将任务的类名传递给调取器而不是一个实例,调度器来完成类的实例化。这致使的一个后果就是,它必须包含一个默认的公开的构建方法,咱们若是想要进行运行时配置,须要使用jobDataMap。
当经过jobDataMap将数据传给任务时,使用惟一的字符串键来标识。在咱们的实现里,保存了一个指向TwitterMonitorImpl插件的索引,这个索引负责咱们的任务调度以及接收Twitter的搜索结果。
咱们使用TwitterMonitorImpl.KEY
来保存这个索引。咱们将在下一部分中实现这个类。
最后,看看如何使用twitter4j类库,它容许咱们仅用两行代码就能够公开的,匿名的查询。
这是咱们在atlassian-plugin.xml中注册为插件的类。它会在启动时由框架实例化并负责注册任务。它也会保存Twitter的搜索结果,而且能够经过咱们稍后添加的web站点进行访问。
package com.atlassian.example.scheduling; import com.atlassian.sal.api.lifecycle.LifecycleAware; import com.atlassian.sal.api.scheduling.PluginScheduler; import org.apache.log4j.Logger; import twitter4j.Tweet; import java.util.Date; import java.util.HashMap; import java.util.List; public class TwitterMonitorImpl implements TwitterMonitor, LifecycleAware { /* package */ static final String KEY = TwitterMonitorImpl.class.getName() + ":instance"; private static final String JOB_NAME = TwitterMonitorImpl.class.getName() + ":job"; private final Logger logger = Logger.getLogger(TwitterMonitorImpl.class); private final PluginScheduler pluginScheduler; // provided by SAL private String query = "Atlassian"; // default Twitter search private long interval = 5000L; // default job interval (5 sec) private List<Tweet> tweets; // results of the last search private Date lastRun = null; // time when the last search returned public TwitterMonitorImpl(PluginScheduler pluginScheduler) { this.pluginScheduler = pluginScheduler; } // declared by LifecycleAware public void onStart() { reschedule(query, interval); } public void reschedule(String query, long interval) { this.query = query; this.interval = interval; pluginScheduler.scheduleJob( JOB_NAME, // unique name of the job TwitterQueryTask.class, // class of the job new HashMap<String,Object>() {{ put(KEY, TwitterMonitorImpl.this); }}, // data that needs to be passed to the job new Date(), // the time the job is to start interval); // interval between repeats, in milliseconds logger.info(String.format("Twitter search task scheduled to run every %dms", interval)); } public String getQuery() { return query; } /* package */ void setTweets(List<Tweet> tweets) { this.tweets = tweets; } /* package */ void setLastRun(Date lastRun) { this.lastRun = lastRun; } }
注意咱们是如何实现 SAL’s com.atlassian.sal.api.lifecycle.LifecycleAware
接口的,以及如何使用它的 onStart()
方法来注册任务。
关键是咱们不能在构造方法中注册(注销)任务,由于当构造方法被调用时,调度器(和SAL自己)可能尚未彻底初始化。所以,实现 com.atlassian.sal.api.lifecycle.LifecycleAware
接口并在 onStart()
注册任务。
与全部插件同样,咱们建立一个接口用于与其余插件共享咱们的模块:
package com.atlassian.example.scheduling; public interface TwitterMonitor { public void reschedule(String query, long interval); }
... <component key="schedulerComponent" class="com.atlassian.example.scheduling.TwitterMonitorImpl" system="true" public="true"> <description>The plugin component that schedules the Twitter search.</description> <interface>com.atlassian.sal.api.lifecycle.LifecycleAware</interface> <interface>com.atlassian.example.scheduling.TwitterMonitor</interface> </component> ...
注意这里显示的声明com.atlassian.sal.api.lifecycle.LifecycleAware
接口,以及把模块声明成公开的,只有这样SAL的生命周期管理器才能够访问到。
在本节中,你应该能够调度任务工做,完成Twitter搜索。
启动JIRA,连接调试器,并在模块构造方法,它的 reschedule()
方法和 execute()
中设置断点,而后查看它的运行。
插件SDK方便快速简单的部署和调试。要是你的插件在debug模式下运行,只需执行: $ atlas-debug 或者使用Maven执行: $ mvn jira:debug
若是你对这个中间产物已经满意,继续向前调度你的任务。若是你想要作更多,稍做停留,而后查看剩下的内容。学习如何添加一个Web 单元,一个webwork站点,velocity模版以及国际化支持,使之有一些交互,更有趣。
在这以前,咱们没有使用任何特定产品的特性或API,所以能够运行在任何Atlassian产品上,不限定于JIRA。
为了在管理界面显示Twitter结果,咱们须要在TwitterMonitor接口中添加一些方法。
这是有必要的,由于咱们已经将 TwitterMonitorImpl
模块注入到咱们的webwork站点中,而且这个接口中额外的方法将容许这个站点与模块进行交流,接收最后一次的查询结果,以及间隔时间。
package com.atlassian.example.scheduling; import twitter4j.Tweet; import java.util.Date; import java.util.List; public interface TwitterMonitor { public String getQuery(); public long getInterval(); public List<Tweet> getTweets(); public Date getLastRun(); public void reschedule(String query, long interval); }
并在TwitterMonitorImpl实现它们:
... public class TwitterMonitorImpl implements TwitterMonitor, LifecycleAware { ... public long getInterval() { return interval; } public Date getLastRun() { return lastRun; } public List<Tweet> getTweets() { return tweets; } ...
在接下来的教程中,咱们仅限于在JIRA中,添加一个管理员页面来显示Twitter搜索结果。咱们也将容许用户修改搜索规则以及搜索频率。
首先,实现webwork站点:
package com.atlassian.example.scheduling; import com.atlassian.jira.web.action.JiraWebActionSupport; import twitter4j.Tweet; import java.util.Date; import java.util.List; public class SchedulerAction extends JiraWebActionSupport { private final TwitterMonitor twitterMonitor; private String query; private long interval; public SchedulerAction(TwitterMonitor twitterMonitor) { this.twitterMonitor = twitterMonitor; this.query = twitterMonitor.getQuery(); this.interval = twitterMonitor.getInterval(); } @Override protected String doExecute() throws Exception { return SUCCESS; } public String doReschedule() { twitterMonitor.reschedule(query, interval); return getRedirect("TwitterScheduler!default.jspa"); } public List<Tweet> getTweets() { return twitterMonitor.getTweets(); } public String getQuery() { return query; } public void setQuery(String query) { this.query = query; } public long getInterval() { return interval; } public void setInterval(long interval) { this.interval = interval; } public Date getLastRun() { return twitterMonitor.getLastRun(); } }
咱们有两个进入这个站点的方法:doExecute()
方法没有任何效果,仅仅提供访问当前搜索结果的入口。doReschedule()
方法用来修改频率或搜索条件,它会取消后台任务并从新运行。
注意当从新调度站点后,咱们不会呈现界面,可是咱们会将浏览器重定向到只读站点 doExecute()
以免在浏览器中显示从新调度的URL,由于这会致使用户经过刷新按钮刷新浏览器时,咱们的任务不断的被从新调度。
咱们将在atlassian-plugin中注册webwork站点,并添加一个Web单元,以向JIRA管理页面的上下文菜单添加连接,该菜单将连接到咱们的新页面:
... <resource type="i18n" name="i18n" location="com.atlassian.example.scheduling.TwitterSchedulerBundle"/> <web-item key="schedulerActionLink" section="system.admin/system" i18n-name-key="com.atlassian.example.scheduling.adminLink" name="Scheduled Twitter Search" weight="1"> <label key="com.atlassian.example.scheduling.adminLink"/> <link linkId="schedulerActionLink">/secure/admin/TwitterScheduler.jspa</link> </web-item> <webwork1 key="schedulerAction" name="SAL Scheduler Example"> <actions> <action name="com.atlassian.example.scheduling.SchedulerAction" alias="TwitterScheduler"> <view name="success">/templates/scheduler.vm</view> <view name="input">/templates/scheduler.vm</view> </action> </actions> </webwork1> ...
注意咱们还添加了支持国际化的i18n资源包,因此咱们能够根据用户的设置引用不一样语种的字符串。
一般使用i18n是一个好的习惯,即便你只提供一种语言包。固然,若是你以为这个操做很麻烦,能够忽略包声明,只须要硬编码文本便可。
最后,咱们为页面添加src/main/resources/templates/scheduler.vm
模版。下面的代码片断只关注有趣的部分,省略了大部分布局。所有的模板见The full template is on Bitbucket.
... <form method="post" action="TwitterScheduler!reschedule.jspa"> <p> <table> <tr> <td>$i18n.getText("com.atlassian.example.scheduling.queryCell")</td> <td><input type="text" name="query" value="$query"></td> </tr> <tr> <td>$i18n.getText("com.atlassian.example.scheduling.intervalCell")</td> <td><input type="text" name="interval" value="$interval"></td> </tr> <tr> <td colspan="2"><input type="submit" value="$i18n.getText("com.atlassian.example.scheduling.applyButton")"></td> </tr> </table> </p> </form> ... <table class="jiraform maxWidth"> <thead class="jiraformheader"> <tr> <th colspan="2">$i18n.getText("com.atlassian.example.scheduling.result.header.from")</th> <th>$i18n.getText("com.atlassian.example.scheduling.result.header.tweet")</th> <th>$i18n.getText("com.atlassian.example.scheduling.result.header.date")</th> </tr> </thead> <tbody id="tweets"> #foreach ( $tweet in $tweets ) <tr> <td><img src="$tweet.profileImageUrl" width="48" height="48"></td> <td>$tweet.fromUser</td> <td>$tweet.text</td> <td>$tweet.createdAt</td> </tr> #end </tbody> </table> <div style="text-align: center;">$i18n.getText("com.atlassian.example.scheduling.lastRun") <b>$lastRun</b></div> ...
这就是咱们教程的所有内容,如今咱们启动JIRA,验证结果:
$ mvn jira:run