xwiki开发者指南-执行异步任务

本文介绍了一种异步空间重命名API的设计方案,详细解释了如何处理大量页面及内容更新,通过异步任务避免阻塞HTTP请求,确保操作效率。文章涵盖了API设计、请求与状态管理、脚本服务实现及作业执行流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

用例

实现对空间重命名,需要考虑到以下问题:

  • 一个空间可以有很多页面
  • 每个页面可以有很多反向链接
  • 一些页面可以有大量的内容,我们要在内容里更新相对链接

这操作可能需要大量的时间,所以我们需要显示进度。这意味着我们不能阻塞触发操作的HTTP请求。换句话说,操作应该是异步的。

API设计

在我们开始实现之前,我们需要设计重命名API。实现异步任务的主要方法有2种:

  1. push: 启动任务,然后等待通知任务进度,成功或失败。为了得到通知,可以:
    • 无论是传递一个回调(callback)给API
    • 或者API返回一个promise,你可以使用一个注册的回调
  2. pull: 启动任务,然后你定时ask for updates直到任务完成(成功或失败)。在这种情况下,API需要提供一些方法来访问任务的状态

第一个选项(push)是很好的,但它需要触发任务代码和执行任务代码之间的双向连接。这是不符合(标准)的HTTP协议,服务端(通常)是不会把数据推送到客户端。客户端是可以从服务器拉数据。因此,我们要使用第二个选项。

## Start the task.
#set ($taskId = $services.space.rename($spaceReference, $newSpaceName))
...
## Pull the task status.
#set ($taskStatus = $services.space.getRenameStatus($taskId))

查看Job Module了解如何实现此API。

Request(请求)

request表示该任务的输入。这包括:

  • 任务所需要的数据(例如空间引用和新的空间名称)
  • 上下文信息(例如触发任务的用户)
  • 任务配置选项。 例如:
    • 是否检查访问权限
    • 任务是否是交互的(在任务执行过程中可能需要用户输入)

每一个请求都有一个用来访问任务状态的标识符。

public class RenameRequest extends org.xwiki.job.AbstractRequest
{
   private static final String PROPERTY_SPACE_REFERENCE = "spaceReference";

   private static final String PROPERTY_NEW_SPACE_NAME = "newSpaceName";

   private static final String PROPERTY_CHECK_RIGHTS = "checkrights";

   private static final String PROPERTY_USER_REFERENCE = "user.reference";

   public SpaceReference getSpaceReference()
   {
       return getProperty(PROPERTY_SPACE_REFERENCE);
   }

   public void setSpaceReference(SpaceReference spaceReference)
   {
        setProperty(PROPERTY_SPACE_REFERENCE, spaceReference);
   }

   public String getNewSpaceName()
   {
       return getProperty(PROPERTY_NEW_SPACE_NAME);
   }

   public void setNewSpaceName(String newSpaceName)
   {
        setProperty(PROPERTY_NEW_SPACE_NAME, newSpaceName);
   }

   public boolean isCheckRights()
   {
       return getProperty(PROPERTY_CHECK_RIGHTS, true);
   }

   public void setCheckRights(boolean checkRights)
   {
        setProperty(PROPERTY_CHECK_RIGHTS, checkRights);
   }

   public DocumentReference getUserReference()
   {
       return getProperty(PROPERTY_USER_REFERENCE);
   }

   public void setUserReference(DocumentReference userReference)
   {
        setProperty(PROPERTY_USER_REFERENCE, userReference);
   }
}

Questions(询问)

正如我们所提到的,在作业执行过程中,作业可以通过asking questions进行互动。例如,如果已经有一个空间与新的空间名称重复,那么我们就必须决定是否:

  • 停止重命名 
  • 或合并两个空间

如果我们决定合并两个空间,则有可能在两个空间有一样名字的文档,在这种情况下,我们必须决定是否覆盖目标文档。

为了让这个例子简单点,我们始终合并这两个空间,但我们会要求用户确认是否覆盖。 

public class OverwriteQuestion
{
   private final DocumentReference source;

   private final DocumentReference destination;

   private boolean overwrite = true;

   private boolean askAgain = true;

   public OverwriteQuestion(DocumentReference source, DocumentReference destination)
   {
       this.source = source;
       this.destination = destination;
   }

   public EntityReference getSource()
   {
       return source;
   }

   public EntityReference getDestination()
   {
       return destination;
   }

   public boolean isOverwrite()
   {
       return overwrite;
   }

   public void setOverwrite(boolean overwrite)
   {
       this.overwrite = overwrite;
   }

   public boolean isAskAgain()
   {
       return askAgain;
   }

   public void setAskAgain(boolean askAgain)
   {
       this.askAgain = askAgain;
   }
}

Job Status(作业状态)

提供作业状态,默认情况下,访问:

  • 作业状态(例如 NONE, RUNNING, WAITING, FINISHED)
  • 作业请求
  • 作业日志 ("INFO: Document X.Y has been renamed to A.B")
  • 作业进展 (72%完成)

大多数时候,你并不需要扩展作业模块提供的DefaultJobStatus,除非你想存储:

  • 更多进展的信息(例如已到目前为止改名的文件清单)
  • 任务结果/输出

请注意,请求和作业状态必须是可序列化,所以要小心你在自定义作业状态存储什么样信息。例如,对于任务输出,可能在输出存储一个引用,路径或URL更好而不是存储输出本身。

作业状态也是job沟通通道:

  • 如果作业发起一个询问,我们
    • 从作业状态访问询问(question)
    • 通过作业状态答复询问
  • 如果你想取消的作业必须通过作业状态来执行

public class RenameJobStatus extends DefaultJobStatus<RenameRequest>
{
   private boolean canceled;

   private List<DocumentReference> renamedDocumentReferences = new ArrayList<>();

   public RenameJobStatus(RenameRequest request, ObservationManager observationManager,
        LoggerManager loggerManager, JobStatus parentJobStatus)
   {
       super(request, observationManager, loggerManager, parentJobStatus);
   }

   public void cancel()
   {
       this.canceled = true;
   }

   public boolean isCanceled()
   {
       return this.canceled;
   }

   public List<DocumentReference> getRenamedDocumentReferences()
   {
       return this.renamedDocumentReferences;
   }
}

脚本服务

现在,我们需要实现一个ScriptService,使用Velocity触发重命名,并得到重命名状态。

@Component
@Named(SpaceScriptService.ROLE_HINT)
@Singleton
public class SpaceScriptService implements ScriptService
{
   public static final String ROLE_HINT = "space";

   public static final String RENAME = "rename";

   @Inject
   private JobExecutor jobExecutor;

   @Inject
   private JobStatusStore jobStatusStore;

   @Inject
   private DocumentAccessBridge documentAccessBridge;

   public String rename(SpaceReference spaceReference, String newSpaceName)
   {
        setError(null);

        RenameRequest renameRequest = createRenameRequest(spaceReference, newSpaceName);

       try {
           this.jobExecutor.execute(RENAME, renameRequest);

            List<String> renameId = renameRequest.getId();
           return renameId.get(renameId.size() - 1);
       } catch (Exception e) {
            setError(e);
           return null;
       }
   }

   public RenameJobStatus getRenameStatus(String renameJobId)
   {
       return (RenameJobStatus) this.jobStatusStore.getJobStatus(getJobId(renameJobId));
   }

   private RenameRequest createRenameRequest(SpaceReference spaceReference, String newSpaceName)
   {
        RenameRequest renameRequest = new RenameRequest();
        renameRequest.setId(getNewJobId());
        renameRequest.setSpaceReference(spaceReference);
        renameRequest.setNewSpaceName(newSpaceName);
        renameRequest.setInteractive(true);
        renameRequest.setCheckRights(true);
        renameRequest.setUserReference(this.documentAccessBridge.getCurrentUserReference());
       return renameRequest;
   }

   private List<String> getNewJobId()
   {
       return getJobId(UUID.randomUUID().toString());
   }

   private List<String> getJobId(String suffix)
   {
       return Arrays.asList(ROLE_HINT, RENAME, suffix);
   }
}

Job实现

Jobs是个组件。让我们来看看我们如何能够实现它们。

@Component
@Named(SpaceScriptService.RENAME)
public class RenameJob extends AbstractJob<RenameRequest, RenameJobStatus> implements GroupedJob
{
   @Inject
   private AuthorizationManager authorization;

   @Inject
   private DocumentAccessBridge documentAccessBridge;

   private Boolean overwriteAll;

   @Override
   public String getType()
   {
       return SpaceScriptService.RENAME;
   }

   @Override
   public JobGroupPath getGroupPath()
   {
        String wiki = this.request.getSpaceReference().getWikiReference().getName();
       return new JobGroupPath(Arrays.asList(SpaceScriptService.RENAME, wiki));
   }

   @Override
   protected void runInternal() throws Exception
   {
        List<DocumentReference> documentReferences = getDocumentReferences(this.request.getSpaceReference());
       this.progressManager.pushLevelProgress(documentReferences.size(), this);

       try {
           for (DocumentReference documentReference : documentReferences) {
               if (this.status.isCanceled()) {
                   break;
               } else {
                   this.progressManager.startStep(this);
                   if (hasAccess(Right.DELETE, documentReference)) {
                        move(documentReference, this.request.getNewSpaceName());
                       this.status.getRenamedDocumentReferences().add(documentReference);
                       this.logger.info("Document [{}] has been moved to [{}].", documentReference,
                           this.request.getNewSpaceName());
                   }
               }
           }
       } finally {
           this.progressManager.popLevelProgress(this);
       }
   }

   private boolean hasAccess(Right right, EntityReference reference)
   {
       return !this.request.isCheckRights()
           || this.authorization.hasAccess(right, this.request.getUserReference(), reference);
   }

   private void move(DocumentReference documentReference, String newSpaceName)
   {
        SpaceReference newSpaceReference = new SpaceReference(newSpaceName, documentReference.getWikiReference());
        DocumentReference newDocumentReference =
            documentReference.replaceParent(documentReference.getLastSpaceReference(), newSpaceReference);
       if (!this.documentAccessBridge.exists(newDocumentReference)
           || confirmOverwrite(documentReference, newDocumentReference)) {
            move(documentReference, newDocumentReference);
       }
   }

   private boolean confirmOverwrite(DocumentReference source, DocumentReference destination)
   {
       if (this.overwriteAll == null) {
            OverwriteQuestion question = new OverwriteQuestion(source, destination);
           try {
               this.status.ask(question);
               if (!question.isAskAgain()) {
                   // Use the same answer for the following overwrite questions.
                   this.overwriteAll = question.isOverwrite();
               }
               return question.isOverwrite();
           } catch (InterruptedException e) {
               this.logger.warn("Overwrite question has been interrupted.");
               return false;
           }
       } else {
           return this.overwriteAll;
       }
   }
}

服务端控制器

我们需要从JavaScript能够触发重命名操作和远程定时获得状态更新。这意味着重命名API应该可以通过一些URL访问:

  • ?action=rename -> redirects to ?data=jobStatus
  • ?data=jobStatus&jobId=xyz -> return the job status serialized as JSON
  • ?action=continue&jobId=xyz -> redirects to ?data=jobStatus
  • ?action=cancel&jobId=xyz -> redirects to ?data=jobStatus

{{velocity}}
#if ($request.action == 'rename')
  #set ($spaceReference = $services.model.resolveSpace($request.spaceReference))
  #set ($renameJobId = $services.space.rename($spaceReference, $request.newSpaceName))
  $response.sendRedirect($doc.getURL('get', $escapetool.url({
    'outputSyntax': 'plain',
    'jobId': $renameJobId
  })))
#elseif ($request.action == 'continue')
  #set ($renameJobStatus = $services.space.getRenameStatus($request.jobId))
  #set ($overwrite = $request.overwrite == 'true')
  #set ($discard = $renameJobStatus.question.setOverwrite($overwrite))
  #set ($discard = $renameJobStatus..answered())
#elseif ($request.action == 'cancel')
  #set ($renameJobStatus = $services.space.getRenameStatus($request.jobId))
  #set ($discard = $renameJobStatus.cancel())
  $response.sendRedirect($doc.getURL('get', $escapetool.url({
    'outputSyntax': 'plain',
    'jobId': $renameJobId
  })))
#elseif ($request.data == 'jobStatus')
  #set ($renameJobStatus = $services.space.getRenameStatus($request.jobId))
  #buildRenameStatusJSON($renameJobStatus)
  $response.setContentType('application/json')
  $jsontool.serialize($renameJobStatusJSON)
#end
{{/velocity}}

客户端控制器

在客户端,JavaScript代码负责:

  • 通过一个AJAX请求到服务端控制器触发任务   
  • 定时检索任务状态更新和更新显示进度 
  • 向用户传递job询问和传递用户的答复到服务端控制器

var onStatusUpdate = function(status) {
  updateProgressBar(status);
 if (status.state == 'WAITING') {
   // Display the question to the user.
   displayQuestion(status);
  } else if (status.state != 'FINISHED') {
   // Pull task status update.
   setTimeout(function() {
      requestStatusUpdate(status.request.id).success(onStatusUpdate);
    }, 1000);
  }
};

// Trigger the rename task.
rename(parameters).success(onStatusUpdate);

// Continue the rename after the user answers the question.
continueRename(parameters).success(onStatusUpdate);

// Cancel the rename.
cancelRename(parameters).success(onStatusUpdate);

一般流程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程小泓哥

你的鼓励是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值