java异步代码
Java语言通过其检查的异常语法提供的静态异常检查功能非常有用,因为它使程序员可以方便地表达程序工作流程的复杂方面。
实际上,通过检查的异常,可以轻松扩展应该返回某种数据的功能,以替代地将所有不同情况通知调用者,其中所提供的输入不适用于所请求的计算,因此每种情况都可以触发适当的操作。 语言提供的异常处理的语法级强制实施使这些异常成为函数签名的合法部分,例如返回类型的隐式扩展。
所提供的抽象在具有分层结构的程序中特别方便,在该结构中,调用层仅需知道调用内部层的情况可能是什么情况,而无需进一步的知识。 然后,调用层仅应决定在其自身范围内需要跟踪这些情况中的哪些,而在其范围内应被认为是非法情况,因此应递归通知给外层。
要在底层自上而下的工作流程中识别和处理的特殊情况的抽象通常是用非正式术语表示程序规范的最自然的方法。 因此,检查异常的可用性允许以其可视形式尽可能多地遵循原始规范的实现。
例如,互联网服务的自上而下的规范可以在各个层中标识一个专门用于处理自定义协议的处理,在该自定义协议中表达请求和响应。 该层的正常行为的描述可能会导致以下代码:
StringprocessMessage (String req) {
MyExpression exp = parseRequest(req);
MyValue val = elaborate(exp);
return composeResponse (val);
}
此外,可以发现各种错误情况,每种错误情况都会导致与客户端的交互以不同的方式进行。 让我们假设:
- parseRequest可以识别“语法问题”
- 在这种情况下,通信流应该突然中断;
- 详细说明可以在假定资源可用性(而不是不可用)的请求后识别“资源问题”
- 在这种情况下,我们要依靠基础传输协议(例如,HTTP 404错误)来通知资源不足
- 如果用户试图执行她无权执行的操作,则精心设计的方法还可以标识“凭据问题”
- 在这种情况下,我们的自定义协议中会提供针对客户的特定响应
通过利用检查的异常,我们可以通过以下方式表示层代码:
片段1 :
MyExpressionparseRequest (String req) throws MySyntaxException { ... }
String composeResponse (MyValue val) { ... }
String composeErrorResponse (MyCredentialException ce) { ... }
MyValue elaborate (MyExpression exp) throws MyCredentialException, MyResourceException { ... }
String processMessage (String req) throws MySyntaxException, MyResourceException {
MyExpression exp = parseRequest(req);
try {
MyValue val = elaborate(exp);
return composeResponse (val);
} catch (MyCredentialException ce) {
return composeErrorResponse (ce);
}
}
如果没有可用的检查异常,那么,为了保持相同的信息,我们将需要引入专用类型来表示可能检测到特殊情况的每个函数的结果。 这些类型将使我们能够存储所有可能的情况,包括正常情况下产生的值。
而且,为了达到相同级别的基于类型的强制执行,我们将不得不扩展结果类型以封装对其可用的所有操作,以便将所有情况都考虑在内。
不幸的是,Java似乎没有提供现成的机制来定义这种聚合结果类型,即:
结果<T,Exc1,Exc2,Exc3>
其中T是正常返回值,任何添加的Exc1,Exc2,...是可能的替代错误情况,因此,只有其中一个会在返回时携带值。
我们所需要的最接近的工具是Java 8的CompletionStage <T> ,它封装了函数可能引发异常的可能性,并确保如果检测到异常,则跳过对先前结果的任何进一步操作。 但是,此接口旨在启用“ monadic”样式的代码,该代码将异常隐藏为与正常工作流程完全分离的计算方面。 因此,它旨在处理无法恢复的异常(主要是未检查的异常),而不是用于工作流中不可或缺的自定义已检查异常。 结果,即使CompletionStage <T>允许选择性地处理某些类型的异常,而保留其他异常,但是对于任何特定情况,都不能强制执行该处理。
因此,为了使用CompletionStage <T>为我们的案例建模并保持基于类型的强制执行,我们应将检查的异常包含在基本类型T中,并且仍然需要专用的结果类型。
通过坚持朴素的方法并引入自定义的专用结果类型(但仍利用Java 8的语法),代码将变成如下所示:
片段2 :
class ProcessingOutcome {
private String resp;
private MySyntaxErrorNotif se;
private MyResourceErrorNotif re;
......
}
class ParsingOutcome {
private MyExpression exp;
private MySyntaxErrorNotif se;
......
public ElaborationOutcome applyElaboration (
Function<MyExpression, ElaborationOutcome> elabFun) {
if ( se != null) {
return new ExtendedElaborationOutcome (se);
} else {
return elabFun. apply (exp);
}
}
}
class ElaborationOutcome {
private MyValue val;
private MyCredentialErrorNotif ce;
private MyResourceErrorNotif re;
......
public ProcessingOutcome applyProtocol (
Function<MyValue, String> composeFun,
Function<MyCredentialErrorNotif, String> composeErrorFun) {
if ( re != null) {
return new ProcessingOutcome (re);
} else if (ce != null) {
return new ProcessingOutcome (composeErrorFun. apply (ce));
} else {
return new ProcessingOutcome (composeFun. apply (val));
}
}
}
class ExtendedElaborationOutcome extends ElaborationOutcome {
private MySyntaxErrorNotif se;
......
public ProcessingOutcome applyProtocol (
Function<MyValue, String> composeFun,
Function<MyCredentialErrorNotif, String> composeErrorFun) {
if ( se != null) {
return new ProcessingOutcome (se);
} else {
return super. applyProtocol (composeFun, composeErrorFun);
}
}
}
ParsingOutcome parseRequest (String req) { ... }
String composeResponse (MyValue val) { ... }
String composeErrorResponse (MyCredentialErrorNotif ce) { ... }
ElaborationOutcome elaborate (MyExpression exp) { ... }
ProcessingOutcome processMessage (String req) {
ParsingOutcome expOutcome = parseRequest(req);
ElaborationOutcome valOutcome = expOutcome. applyElaboration (exp -> elaborate(exp));
ProcessingOutcome respOutcome = valOutcome. applyProtocol (
val -> composeResponse(val), ce -> composeErrorResponse(ce));
return respOutcome;
}
实际上,通过比较代码段1和代码段2 ,我们可以将检查后的异常功能视为一种语法糖,旨在用前一种较短的语法重写后一种代码,同时保留所有基于类型的强制执行的好处。
但是,此功能存在一个烦人的问题:仅适用于同步代码。
如果即使工作流中的单个子任务也应涉及异步API调用和可能的严重延迟,则我们可能不希望让处理线程等待异步计算完成(仅出于性能和可伸缩性的考虑)。
因此,在每个调用层中,应该遵循异步API调用的任何代码都必须移入回调。 在那种情况下, 片段1的简单递归结构将不再可用,它可以进行静态异常检查。
因此,对于异步代码,将各种函数结果封装到专用的返回类型中似乎是强制最终将处理每个错误情况的唯一方法。
幸运的是,通过利用Java 8的JDK,我们可以通过保留代码结构的方式解决工作流中引入异步性的问题。 例如,假设精心设计的功能应要求异步处理。 然后可以将其重写以返回CompletableFuture ,并且代码可以变成:
片段3 :
class ProcessingOutcome {
private String resp;
private MySyntaxErrorNotif se;
private MyResourceErrorNotif re;
......
}
class ParsingOutcome {
private MyExpression exp;
private MySyntaxErrorNotif se;
......
public CompletableFuture<ElaborationOutcome> applyElaboration (
Function<MyExpression, CompletableFuture<ElaborationOutcome>> elabFun) {
if ( se != null) {
return CompletableFuture.completedFuture(new ExtendedElaborationOutcome (se));
} else {
return elabFun. apply (exp);
}
}
}
class ElaborationOutcome {
private MyValue val;
private MyCredentialErrorNotif ce;
private MyResourceErrorNotif re;
......
public ProcessingOutcome applyProtocol (
Function<MyValue, String> composeFun,
Function<MyCredentialErrorNotif, String> composeErrorFun) {
if ( re != null) {
return new ProcessingOutcome (re);
} else if ( ce != null) {
return new ProcessingOutcome (composeErrorFun. apply (ce));
} else {
return new ProcessingOutcome (composeFun. apply (val));
}
}
}
class ExtendedElaborationOutcome extends ElaborationOutcome {
private MySyntaxErrorNotif se;
......
public ProcessingOutcome applyProtocol (
Function<MyValue, String> composeFun,
Function<MyCredentialErrorNotif, String> composeErrorFun) {
if ( se != null) {
return new ProcessingOutcome (se);
} else {
return super . applyProtocol (composeFun, composeErrorFun);
}
}
}
ParsingOutcome parseRequest (String req) { ... }
String c omposeResponse (MyValue val) { ... }
String composeErrorResponse (MyCredentialErrorNotif ce) { ... }
CompletableFuture<ElaborationOutcome> elaborate (MyExpression exp) { ... }
CompletableFuture<ProcessingOutcome> processMessage(String req) {
ParsingOutcome expOutcome = parseRequest(req);
CompletableFuture<ElaborationOutcome> valFutOutcome = expOutcome. applyElaboration (exp -> elaborate(exp));
CompletableFuture<ProcessingOutcome> respFutOutcome = valFutOutcome. thenApply (outcome -> outcome. applyProtocol (
val -> composeResponse(val), ce -> composeErrorResponse ( ce )));
return respFutOutcome;
}
尽管引入了异步调用,但仍保留代码结构是非常可取的功能。 实际上,底层执行是在同一线程上进行还是切换(一次或多次)到不同线程可能并不总是一个重要方面。 在我们最初的自上而下的规范中,我们没有提到线程问题,并且只能假设在效率方面存在明显的隐性要求。 在这里,正确的错误处理无疑是更重要的方面。
如果尽管有潜在的线程切换,我们仍然可以保留代码片段1的结构,则类似于我们如何保留代码片段2的结构一样,这可能会提供最喜欢的代码表示形式。
换种说法:由于代码片段2中的代码适合基于已检查的异常的替代性更简单的表示形式,为什么对于代码片段3中经过稍微修改的代码不应该保留相同的代码?
我们既不试图用正式的术语来面对这个问题,也不是主张扩展语言以实现上述目的。 我们只想说说如果有这样的扩展将是多么的好。
为了明确起见,让我们假设Java可以识别何时函数是异步的但仍然是顺序的。 例如,通过以下方式编写函数(使用Fantasy关键字seq )
CompletableFuture <T> seq fun(A1 a1,A2 a2){...}
我们可以通过某种方式让JVM强制返回的CompletableFuture仅完成一次(通过丢弃进一步的虚假调用); 无论所涉及的实际线程如何,这都将被视为功能的“正式”终止。
然后,编译器可以允许我们使用fun函数,就好像它是通过以下简化签名定义的(使用另一个Fantasy关键字async ):
异步乐趣(A1 a1,A2 a2);
使用此签名,我们可以像调用函数一样将其调用为同步函数,但是JVM应该负责提取在fun之后指定的所有代码,并在“正式”终止时(即,在CompletableFuture完成时)执行它,在适当的线程中。
此代码传输将递归应用于调用堆栈中的所有函数。 实际上,如果在定义新功能时使用了fun的简化签名,则应强制后者的签名也包含async关键字,以表明它在幕后是异步的(但是顺序的)。
顺便说一句,递归传播可以在签名为
void async fun(A1 a1,A2 a2);
以便允许调用线程(也许属于ExecutorService )做其他事情。
以上假设功能可以轻松扩展以支持检查的异常。 实际上,根据形式的函数定义:
CompletableFuture <结果<T,E1,E2 >>序列乐趣(A1 a1,A2 a2){...}
如果Outcome是返回类型和一个或多个异常的某种标准包装,则编译器可以允许我们使用该函数,就像使用以下简化签名定义了该函数一样:
异步乐趣(A1 a1,A2 a2)抛出E1,E2;
通过使用此语法,可以以简化形式编写代码段3的等效版本:
片段4 :
MyExpressionparseRequest (String req) throws MySyntaxException { ... }
String composeResponse (MyValue val) { ... }
String composeErrorResponse (MyCredentialException ce) { ... }
CompletableFuture<Outcome<MyValue, MyCredentialException, MyResourceException>> seq elaborate (MyExpression exp) { ... }
/*
equivalent to:
MyValue async elaborate(MyExpression exp) throws MyCredentialException, MyResourceException;
*/
String async processMessage (String req) throws MySyntaxException, MyResourceException {
MyExpression exp = parseRequest(req);
try {
MyValue val = elaborate(exp);
return composeResponse (val);
} catch (MyCredentialException ce) {
return composeErrorResponse (ce);
}
}
并且在精心引入异步之后,将代码 段1转换为代码 段4是很自然的。
有什么方法可以用可用的语法实现类似的效果(进行合理的折衷)?
我们需要实现的一种机制是,对异步调用后的所有代码进行定界(例如,将其转换为回调),并在异步调用产生结果之后在发生此结果的线程中执行该机制。
作为一种直观的方法,只要每层异步调用的数量都很小,可行的尝试就可能涉及以下步骤:
- 从工作流的同步表示开始(如代码片段1所示 ),并标识应该变为异步的功能(在这种情况下: 评估 ,结果是processMessage本身)。
- 如果同一个函数中存在多个潜在的异步调用,则可能通过引入中间函数来安排代码,使得每个函数在中间仅包含一个潜在的异步调用,而其他任何函数都称为最后一个调用返回。
(在我们的简单情况下无需安排)。 - 转换代码的方式是,将每个函数(比如说“外部”)和涉及一个函数(比如“内部”)的调用(假定是异步的)分为“ outerBefore”部分和“ outerAfter”部分。 externalBefore将包含inner之前的所有代码,然后调用inner作为其最后一个操作; 另一方面, outerAfter将首先调用outerBefore ,然后再调用所有剩余的代码。 注意,结果, outerBefore和outerAfter将共享相同的参数。
就我们而言,我们可能会给出以下代码:片段5:
MyExpressionparseRequest (String req) throws MySyntaxException { ... } String composeResponse (MyValue val) { ... } String composeErrorResponse (MyCredentialException ce) { ... } String processMessage (String req) throws MySyntaxException, MyResourceException { return processMessageAfter (req); } String processMessageAfter (String req) throws MySyntaxException, MyResourceException { try { MyValue val = processMessageBefore(req); return composeResponse (val); } catch (MyCredentialException ce) { return composeErrorResponse (ce); } } MyValue processMessageBefore (String req) throws MySyntaxException, MyResourceException, MyCredentialException { MyExpression exp = parseRequest(req); return elaborate (exp); } MyValue elaborate (MyExpression exp) throws MyCredentialException, MyResourceException { return elaborateAfter (exp); } MyValue elaborateAfter (MyExpression exp) throws MyCredentialException, MyResourceException { ... } ......
- 介绍专用类以包含由“ xxxBefore”和“ xxxAfter”部分组成的每对,然后使用一个临时实例来调用任何此类。
我们的代码可以通过以下方式扩展:片段6:
StringprocessMessage (String req) throws MySyntaxException, MyResourceException { return new ProtocolHandler (). processMessageAfter (req); } class ProtocolHandler { MyExpression parseRequest (String req) throws MySyntaxException { ... } String composeResponse(MyValue val) { ... } String composeErrorResponse(MyCredentialException ce) { ... } String processMessageAfter(String req) throws MySyntaxException, MyResourceException { try { MyValue val = processMessageBefore(req); return composeResponse (val); } catch (MyCredentialException ce) { return composeErrorResponse (ce); } } MyValue processMessageBefore (String req) throws MySyntaxException, MyResourceException, MyCredentialException { MyExpression exp = parseRequest(req); return elaborate (exp); } } MyValue elaborate (MyExpression exp) throws MyCredentialException, MyResourceException { return new ExpressionHandler (). elaborateAfter (exp); } class ExpressionHandler { MyValue elaborateAfter (MyExpression exp) throws MyCredentialException, MyResourceException { ... } ...... }
- 用合适的代理对象替换上一步中介绍的实例; 代理的工作将是收集所有“ xxxAfter”函数并仅在相关的“ xxxBefore”函数完成之后(在发生完成的线程中)调用每个函数。
最后一步允许将最内部的功能转换为异步功能。
最终的代码可能变成:片段7:
StringprocessMessage (String req) throws MySyntaxException, MyResourceException { ProtocolHandler proxy = createProxy (new ProtocolHandler ()); return proxy. processMessageAfter (req); } class ProtocolHandler { MyExpression parseRequest (String req) throws MySyntaxException { ... } String composeResponse (MyValue val) { ... } String composeErrorResponse (MyCredentialException ce) { ... } String processMessageAfter (String req) throws MySyntaxException, MyResourceException { try { MyValue val = processMessageBefore(req); return composeResponse (val); } catch (MyCredentialException ce) { return composeErrorResponse (ce); } } MyValue processMessageBefore (String req) throws MySyntaxException, MyResourceException, MyCredentialException { MyExpression exp = parseRequest(req); return elaborate (exp); } } MyValue elaborate (MyExpression exp) throws MyCredentialException, MyResourceException { ExpressionHandler proxy = createProxy( new ExpressionHandler ()); return proxy.elaborateAfter (exp); } class ExpressionHandler { MyValue elaborateAfter (MyExpression exp) throws MyCredentialException, MyResourceException { ... } ...... }
即使进行了转换,生成的最终代码仍然可以作为初始规范的自然映射来读取。
附带说明一下,我们认为这种方法确实可行,尤其是代理所要求的工作是可行的,因为它的本质是通过以下方式覆盖“ xxxBefore”和“ xxxAfter”方法:
(让我们考虑一下我们示例的ProtocolHandler的代理)
- Proxy.processMessageAfter:
[这必须是此代理的第一次调用]- 跟踪收到的论点;
- 找到最后一个被调用的代理,如果有,通知它; 跟踪调查结果; 然后将其设置为最后调用的代理;
- 用接收到的参数调用ProtocolHandler.processMessageBefore ;
- 如果该方法已通知它已经调用了另一个代理,则不执行任何操作;
- 否则,该方法同步终止。 调用onCompleted (请参见下文)并将方法结果传递给它。
- Proxy.processMessageBefore :
[这必须从ProtocolHandler.processMessageAfter内部调用,因此我们处于onCompleted中 (请参见下文),并且只保留了方法结果)- 只是重放结果保持
此外:
- Proxy.onCompleted :
- 跟踪作为论点收到的结果;
- 将其设置为最后调用的代理;
- 当接收到Proxy.processMessageAfter的调用时,使用保留的参数来调用ProtocolHandler.processMessageAfter ;
- 如果该方法已通知它已经调用了另一个代理,则不执行任何操作; 但是,请注意告知其他代理它的先前代理不应该是这个,而是我们自己的先前代理;
- 否则,该方法同步终止。 如果存在以前的代理,请调用其自己的onCompleted并将方法结果传递给它。
以上只是一个不完整的草图。
我们试图将这些想法应用到完整的解决方案中,而我们所获得的是一种可以应用于具体案例的实验技术。
所设想的技术在可用性方面隐含了许多折衷,这可能会将其吸引力限制在极少数情况下。 在我们的案例中,这值得付出努力。
有兴趣的读者可以找到我们技术的详细介绍 在此 ,以及对所涉及可用性优缺点的全面讨论。
java异步代码