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

2021-01-14 14:33:46 浏览数 (1)

用例

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

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

这操作可能需要大量的时间,所以我们需要显示进度。这意味着我们不能阻塞触发操作的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(

查看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(

客户端控制器

在客户端,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);

一般流程

0 人点赞