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