通过SAL实现定时计划

From:https://developer.atlassian.com/server/framework/atlassian-sdk/scheduling-events-via-sal-tutorial

概述

本向导将向你演示如何在你的插件中定时调度一个Java任务在后台运行。为此,我们将使用SAL(Shared Access Layer)提供的跨产品组件 PluginScheduler 。

后台任务调度在任务成本较高的场景或者需要定时执行的维护工作中会很有用处。在本向导中,我们在后台定时运行一个任务,每5秒从Twitter中搜索一次并将最后一次的搜索结果保存在内存中(在本教程中我们假设Twitter搜索是一个代价很高的任务)。

为了使向导更有趣,不可见的后台搜索任务伴随着一个JIRA管理页面,该页面呈现最新的搜索结果,并向用户提供更改搜索查询和间隔时间的能力。它还实现了取消和重新安排事件的必要途径。

为了实现上述这些,插件包含如下模块:

  • Java classes encapsulating the plugin logic (a webwork 1 action and the SAL scheduled event) Java类,封装所有的逻辑实现(一个web 1动作处理和SAL调用事件)
  • A velocity template for the admin page that renders the Twitter search results 管理员界面的velocity模版,用于显示Twitter搜索结果。
  • A web item to add a link to the administration context menu web模块,提供跳转到管理员菜单的链接。
  • An internationalisation resource bundle 国际化资源包

所有模块打包到一个JAR文件中,在接下来的示例中会对每个模块做深入的介绍。

插件代码

我们鼓励你完成本教程的学习。如果你想要跳过或者检查你的学习成果,你可以从Atlassian Bitbucket上找到插件的源码。Bitbucket服务器是一个开源Git仓,包含了本教程代码。要克隆这个仓,执行如下命令:

$ git clone https://atlassian_tutorial@bitbucket.org/atlassian_tutorial/jira-scheduled-events.git

此外,你可以在下载页面下载源码。下载页面:bitbucket.org/atlassian_tutorial/jira-scheduled-events

所需知识点

为了完成本教程,你应该已经理解了Java开发的基础知识:classes, interfaces, methods, 如何使用编译器等等。你还应该理解:

  • 如何使用 Atlassian Plugin SDK 创建一个插件工程
  • 如何在IDE中打开你的插件工程
  • 如何编译你的工程以及通过maven打包JAR文件。

本教程会教你:

  • 如何在插件中使用 SAL (Shared Access Library)
  • 如何利用插件框架的生命周期系统
  • 如何使用带velocity模版的webwork
  • 如何创建和使用国际化资源包
  • 如何添加一个web站点 

Step 1. 创建插件工程

使用适合的 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] ------------------------------------------------------------------------
$

Step 2. 添加所需的Maven依赖

在本教程中,我们会使用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>

Step 3. 在插件描述文件中导入SAL模块

为了插件框架可以注入 SAL PluginScheduler, 我们需要在atlassian-plugin.xml明确的引入该模块,所以添加如下节点:

<component-import key="pluginScheduler">
    <description>SAL Scheduler</description>
    <interface>com.atlassian.sal.api.scheduling.PluginScheduler</interface>
</component-import>

Step 4. 编写后台任务

现在编写一个模块来获取已经注入的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类库,它允许我们仅用两行代码就可以公开的,匿名的查询。

Step 5. 编写调度任务的模块

这是我们在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);
}

Step 6. 将模块添加到atlassian-plugin.xml

...
    <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的生命周期管理器才可以访问到。

Step 7. 启动Have a Beer and Put Your Feet Up!

在本节中,你应该可以调度任务工作,完成Twitter搜索。
启动JIRA,链接调试器,并在模块构造方法,它的 reschedule() 方法和 execute() 中设置断点,然后查看它的运行。

插件SDK方便快速简单的部署和调试。要是你的插件在debug模式下运行,只需执行:
$ atlas-debug
或者使用Maven执行:
$ mvn jira:debug

如果你对这个中间产物已经满意,继续向前调度你的任务。如果你想要做更多,稍作停留,然后查看剩下的内容。学习如何添加一个Web 单元,一个webwork站点,velocity模版以及国际化支持,使之有一些交互,更有趣。

在这之前,我们没有使用任何特定产品的特性或API,因此可以运行在任何Atlassian产品上,不限定于JIRA。

Step 8. 扩展模块接口

为了在管理界面显示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;
    }
...

Step 9. 添加一个webwork 站点

在接下来的教程中,我们仅限于在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,因为这会导致用户通过刷新按钮刷新浏览器时,我们的任务不断的被重新调度。

Step 10. 在atlassian-plugin.xml注册WebWork站点

我们将在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是一个好的习惯,即使你只提供一种语言包。当然,如果你觉得这个操作很麻烦,可以忽略包声明,只需要硬编码文本即可。

Step 11. 添加Velocity模版

最后,我们为页面添加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>
...

Step 12. 启动JIRA

这就是我们教程的全部内容,现在我们启动JIRA,验证结果:

$ mvn jira:run

Screenshots

 

 

posted @ 2018-12-04 10:50  临江仙·2007  阅读(369)  评论(0编辑  收藏  举报