澳门新葡萄京官网注册 1

澳门新葡萄京官网注册在异步Java代码中解救已检测异常

Java语言通过已检测异常语法所提供的静态异常检测功能非常实用,通过它程序开发人员可以用很便捷的方式表达复杂的程序流程。

在任何并发性应用程序中,异步事件处理都至关重要。无论事件的来源是什么(不同的计算任务、I/O
操作或与外部系统的交互),您的代码都必须跟踪事件,协调为响应它们而执行的操作。应用程序可以采用两种基本方法之一来实现异步事件处理:

Java的异常处理是通过5个关键字来实现的:try,catch,throw,throws,finally。JB的在线帮助中对这几个关键字是这样解释的:
 
   
  Throws: Lists the exceptions a method could throw.  
   
  Throw: Transfers control of the method to the exception handler.  
   
  Try: Opening exception-handling statement.  
   
  Catch: Captures the exception.  
   
  Finally: Runs its code before terminating the program.  
 try语句   
  try语句用大括号{}指定了一段代码,该段代码可能会抛弃一个或多个例外。
 
   
  catch语句   
 
catch语句的参数类似于方法的声明,包括一个例外类型和一个例外对象。例外类型必须为Throwable类的子类,它指明了catch语句所处理的例外类型,例外对象则由运行时系统在try所指定的代码块中生成并被捕获,大括号中包含对象的处理,其中可以调用对象的方法。
 
   
 
catch语句可以有多个,分别处理不同类的例外。Java运行时系统从上到下分别对每个catch语句处理的例外类型进行检测,直到找到类型相匹配的catch语句为止。这里,类型匹配指catch所处理的例外类型与生成的例外对象的类型完全一致或者是它的父类,因此,catch语句的排列顺序应该是从特殊到一般。
 
   
   
   
 
也可以用一个catch语句处理多个例外类型,这时它的例外类型参数应该是这多个例外类型的父类,程序设计中要根据具体的情况来选择catch语句的例外处理类型。 
 
   
  finally语句   
 
try所限定的代码中,当抛弃一个例外时,其后的代码不会被执行。通过finally语句可以指定一块代码。无论try所指定的程序块中抛弃或不抛弃例外,也无论catch语句的例外类型是否与所抛弃的例外的类型一致,finally所指定的代码都要被执行,它提供了统一的出口。通常在finally语句中可以进行资源的清除工作。如关闭打开的文件等。
 
   
  throws语句   
 
throws总是出现在一个函数头中,用来标明该成员函数可能抛出的各种异常。对大多数Exception子类来说,Java
编译器会强迫你声明在一个成员函数中抛出的异常的类型。如果异常的类型是Error或
RuntimeException, 或它们的子类,这个规则不起作用,
因为这在程序的正常部分中是不期待出现的。
如果你想明确地抛出一个RuntimeException,你必须用throws语句来声明它的类型。
 
   
  throw语句   
 
throw总是出现在函数体中,用来抛出一个异常。程序会在throw语句后立即终止,它后面的语句执行不到,然后在包含它的所有try块中(可能在上层调用函数中)从里向外寻找含有与其匹配的catch子句的try块。

实际上,如果某个函数预期将返回某种类型的数据,通过已检测异常,很容易就可以扩展这个函数,将所提供的输入不适于所请求的计算的各类情况都通知给调用者,以确保每种情况下都能够触发恰当的动作。而且由Java语言所提供的语法级的异常处理执行让这些异常像返回类型的隐式扩展一样,成为合理的函数签名一部分。

  • 阻塞:一个等待事件的协调线程。
  • 非阻塞:事件向应用程序生成某种形式的通知,而没有线程显式等待它。

 

这种异常的抽象对于具有分层结构的程序来说特别方便,调用层只需要知道调用内部层级会出现哪些情况,而不需要了解更多的信息。然后,调用层只需要判定这些情况中的哪些需要其在自身范围内跟进,哪些应该作为其作用范围内的非法情况,递归通知到外部层级。

合成事件

scala.concurrent.Promise 和 scala.concurrent.Future 类为 Scala
开发人员提供了一些与 Java 8 开发人员的 CompletableFuture
使用方式类似的选项。具体地讲,Future
同时提供了阻塞和非阻塞的事件完成方式。但是,尽管在此级别上很相似,但用于处理两种
future 的技术是不同的。

我们先来看一个并发任务设置:

class myException extends Exception{
  String msg;
  myException(int age){
  msg=”age can not be positive!”;
  }
  public String toString(){
  return msg;
  }  
}

这种针对自上而下的流程,识别和处理特殊情况的抽象通常是程序规格最自然的非正式表述方式。因此已检测异常的存在,能够让程序实现在视觉形态上可以尽可能的与最初的程序规格保持一致。

任务和排序

在一个特定操作中,应用程序通常必须执行多个处理步骤。例如,在向用户返回结果之前,Web
应用程序可能需要:

  1. 在一个数据库中查找用户的信息
  2. 使用查找到的信息来执行 Web 服务调用,并执行另一次数据库查询。
  3. 根据从前两个操作中获得的结果来执行数据库更新。
    图 1 演示了这种结构类型。

图 1. 应用程序任务流

澳门新葡萄京官网注册 1

图 1 将处理过程分解为 4
个不同的任务,它们通过表示顺序依赖关系的箭头相连接。任务 1
可以直接执行,任务 2 和任务 3 都在任务 1 完成后执行,任务 4 在任务 2
和任务 3 都完成后执行。

class Age{
  public void intage(int n) throws myException{//
  if(n<0||n>120){
  myException e=new myException(n);
  throw e; //是一个转向语句,抛出对象实例,停止执行后面的代码
  }
  if(n>=0){
  System.out.print(“合理的年龄!”);
  }
  }
   
public static void main(String args[]) {
  int a=-5;
  try { //try catch 必需有
  Age age = new Age();
  age.intage(a);//触发异常
  System.out.print(“抛出异常后的代码”)
;//这段代码是不会被执行的,程序已经被转向
  } catch (myException ex) {
  System.out.print(ex.toString());
  }
 
finally{//无论抛不抛异常,无论catch语句的异常类型是否与所抛出的例外的类型一致,finally所指定的代码都要被执行,它提供了统一的出口。
  System.out.print(“进入finally! “);
  }
  }
}

举例来说,某个Internet服务的自上而下的规格说明可能会在多个层级中确定一个专用层级用于处理某个自定义的表示请求和响应的协议。可以用如下代码来描述这一层的正常行为:

建模异步事件

在真实的系统中,异步事件的来源一般是并行计算或一种形式的 I/O
操作。但是,使用简单的时间延迟来建模这种系统会更容易一些,这也是这里所采用的方法。清单
1 显示了我用于生成事件的基本的赋时事件 (timed-event)
代码,这些事件采用了已完成的 Future 格式。

清单 1. 赋时事件代码

import java.util.Timerimport java.util.TimerTask import scala.concurrent._ object TimedEvent {  val timer = new Timer   /** Return a Future which completes successfully with the supplied value after secs seconds. */  def delayedSuccess[T](secs: Int, value: T): Future[T] = {    val result = Promise[T]    timer.schedule(new TimerTask() {      def run() = {        result.success      }    }, secs * 1000)    result.future  }   /** Return a Future which completes failing with an IllegalArgumentException after secs    * seconds. */  def delayedFailure(secs: Int, msg: String): Future[Int] = {    val result = Promise[Int]    timer.schedule(new TimerTask() {      def run() = {        result.failure(new IllegalArgumentException      }    }, secs * 1000)    result.future  }

清单 1 中的 Scala 代码使用一个 java.util.Timer 来安排
java.util.TimerTask 在一个延迟之后执行。每个 TimerTask
在运行时完成一个有关联的 future。delayedSuccess
函数定制了一个任务,在运行时成功完成一个 Scala Future[T],然后将该
future 返回给调用方。delayedSuccess 函数返回相同类型的
future,但使用了一个在完成 future 时发生 IllegalArgumentException
异常的失败任务。

清单 2 展示了如何使用 清单 1 中的代码创建 Future[Int]
格式的事件,使之与 图 1 中的 4 个任务相匹配。(此代码来自示例代码中的
AsyncHappy 类。)

清单 2. 示例任务的事件

// task definitionsdef task1(input: Int) = TimedEvent.delayedSuccess(1, input + 1)def task2(input: Int) = TimedEvent.delayedSuccess(2, input + 2)def task3(input: Int) = TimedEvent.delayedSuccess(3, input + 3)def task4(input: Int) = TimedEvent.delayedSuccess(1, input + 4)

清单 2 中 4
个任务方法中的每一个都为该任务的完成时刻使用了特定的延迟值:task1 为 1
秒,task2 为 2 秒,task3 为 3 秒,task4 重新变为 1
秒。每个任务还接受一个输入值,是该输入加上任务编号作为 future
的结果值。这些方法都使用了 future
的成功形式;稍后您会看到一些使用失败形式的例子。

这些任务要求您按 图 1
中所示的顺序运行它们,向每个任务传递上一个任务返回的结果值(或者对于
task4,传递前两个任务结果的和)。如果中间两个任务同时执行,总的执行时间大约为
5 秒(1 秒 + (2 秒、3 秒中的最大值)+ 1 秒。如果 task1 的输入为
1,那么结果为 2。如果该结果被传递给 task2 和 task3,那么结果将为 4 和
5。如果这两个结果的和 被作为输入传递给 task4,那么最终结果将为 13。

结果:年龄非法! 进入finally! 

String processMessage(String req) {
   MyExpression exp = parseRequest(req);
   MyValue val = elaborate(exp);
   return composeResponse(val);
}

阻塞等待

在设定好操作环境之后,是时候来查看 Scala
如何处理事件的完成情况了。与上一期的 Java 代码中一样,协调 4
个任务的执行的最简单的方法是使用阻塞等待:主要线程等待每个任务依次完成。清单
3(同样来自示例代码中的 AsyncHappy 类)给出了此方法。

清单 3. 阻塞等待任务执行

def runBlocking() = {  val v1 = Await.result, Duration.Inf)  val future2 = task2  val future3 = task3  val v2 = Await.result(future2, Duration.Inf)  val v3 = Await.result(future3, Duration.Inf)  val v4 = Await.result(task4, Duration.Inf)  val result = Promise[Int]  result.success  result.future}

清单 3 使用 Scala scala.concurrent.Await 对象的 result()
方法来完成阻塞等待。该代码首先等待 task1 的结果,然后同时创建 task2 和
task3 future,并等待两个任务依次返回 future,最后等待 task4 的结果。最后
3 行(创建和设置 result)使得该方法能够返回一个 Future[Int]。返回该
future,让此方法与我接下来展示的非阻塞形式一致,但该 future
将在该方法返回之前完成。

又如:
void fun()throws IOException,SQLException 

… 

这表示
fun方法可能会丢两个异常出来,那么在调用fun的时候就会做好准备,比如可以这样 
try 

fun(); 
}catch(IOException e) 

}catch(SQLException e) 

}
区别一:

除此之外,还需要能够识别各种出错的情况,每种情况可能都会导致不同的与客户端的交互方式。假设:

组合 future

清单 4(同样来自示例代码中的 AsyncHappy 类)展示了一种将 future
联系在一起的方式,以便按正确顺序并使用正确的依赖关系执行任务,而不使用阻塞。

清单 4. 使用 onSuccess() 处理事件的完成

def runOnSuccess() = {  val result = Promise[Int]  task1.onSuccess(v => v match {    case v1 => {      val a = task2      val b = task3      a.onSuccess(v => v match {        case v2 =>          b.onSuccess(v => v match {            case v3 => task4.onSuccess(v4 => v4 match {              case x => result.success            })          })      })    }  })  result.future}

清单 4 代码使用 onSuccess()
方法将一个函数(技术上讲是一个部分函数,因为它仅处理成功完成的情况)设置为在每个
future 完成时返回。因为 onSuccess()
调用是嵌套式的,所以它们将按顺序执行(即使 future 未完全按顺序完成)。

清单 4 的代码比较容易理解,但很冗长。清单 5 展示了一种使用 flatMap()
方法处理这种情况的更简单的方法。

清单 5. 使用 flatMap() 处理事件的完成

def runFlatMap() = {  task1 flatMap {v1 =>    val a = task2    val b = task3    a flatMap { v2 =>      b flatMap { v3 => task4 }}  }}

清单 5 中的代码实际上执行了与 清单 4 相同的事情,但 清单 5 使用了
flatMap() 方法从每个 future 中提取单一结果值。使用 flatMap() 消除了 清单
4 中所需的 match / case
结构,提供了一种更简洁的格式,但采用了同样的逐步执行路线。

  throw 是语句抛出一个异常;throws 是方法抛出一个异常;

  • parseRequest可能会识别出“语法问题”
    • 这种情况下,应该立即中断通信流;
  • 当某个请求所假定的可用资源不可用时,elaborate可能会识别出这个请求的“资源问题”
    • 在这种情况下,我们希望通过底层的传输协议(如HTTP
      404错误)通知上层这种资源缺乏的情况
  • 假如某个用户试图执行她没有权限执行的操作时,elaborate可能还会识别出“授信问题”
    • 在这种情况下,在我们自定义的协议中,会给客户端一个特定的响应

试用示例

示例代码使用了一个 Scala App
来依次运行事件代码的每个版本,并确保完成事件和结果 是正确的。您可以使用
Maven 从命令行运行此代码,如清单 6 所示(删除了无关的 Maven 输出):

清单 6. 运行事件代码

dennis@linux-9qea:~/devworks/scala4/code> mvn scala:run -Dlauncher=happypath...[INFO] launcher 'happypath' selected => com.sosnoski.concur.article4.AsyncHappyStarting runBlockingrunBlocking returned 13 in 5029 ms.Starting runOnSuccessrunOnSuccess returned 13 in 5011 ms.Starting runFlatMaprunFlatMap returned 13 in 5002 ms.

  throw语法:throw <异常对象>

利用已检测异常,我们可以用下面这种方式表示这一层级的代码:

不顺利的道路

目前为止,您看到了以 future
形式协调事件的代码,这些代码总是能够成功完成。在真实应用程序中,不能寄希望于事情总是这么顺利。处理任务过程中可能会出现问题,而且在
JVM 语言术语中,这些问题通常表示为 Throwable。

更改 清单 2 中的任务定义很容易,只需使用 delayedFailure() 代替
delayedSuccess() 方法,如这里的 task4 所示:

def task4(input: Int) = TimedEvent.delayedFailure(1, “This won’t
work!”)

如果运行仅将 task4 修改为完成时抛出异常的 清单 3,那么您会得到 task4
上的 Await.result() 调用所抛出的预期的 IllegalArgumentException。如果在
runBlocking()
方法中没有捕获该问题,该异常会在调用链中一直传递,直到最终捕获问题(如果未捕获问题,则会终止线程)。幸运的是,修改该代码很容易,因此,如果任何任务完成时抛出异常,该异常会通过返回的
future 传递给调用方来处理。清单 7 展示了这一更改。

清单 7. 具有异常的阻塞等待

def runBlocking() = {  val result = Promise[Int]  try {    val v1 = Await.result, Duration.Inf)    val future2 = task2    val future3 = task3    val v2 = Await.result(future2, Duration.Inf)    val v3 = Await.result(future3, Duration.Inf)    val v4 = Await.result(task4, Duration.Inf)    result.success  } catch {    case t: Throwable => result.failure  }  result.future}

清单 7 非常浅显易懂,最初的代码包装在一个 try/catch 中,catch 在返回的
future 完成时传回异常。此方法稍微复杂一些,但任何 Scala
开发人员应该仍然很容易理解它。

那么,清单 4 和清单 5
中的事件处理代码的非阻塞变形是怎样的?从名称可以看出,清单 4 中使用的
onSuccess() 方法仅 适用于 future
的成功完成类型。如果想要同时处理成功和失败完成类型,则必须使用
onComplete() 方法,检查哪种完成例行适用。清单 8
展示了此技术如何用在事件处理代码中。

清单 8. 成功和失败的 onComplete() 处理

def runOnComplete() = {  val result = Promise[Int]  task1.onComplete(v => v match {    case Success => {      val a = task2      val b = task3      a.onComplete(v => v match {        case Success =>          b.onComplete(v => v match {            case Success => task4.onComplete(v4 => v4 match {              case Success => result.success              case Failure => result.failure            })            case Failure => result.failure          })        case Failure => result.failure      })    }    case Failure => result.failure  result.future}

清单 8 看起来很凌乱,幸运的是还有一种简单得多的替代方法:使用 清单 5
中的 flatMap() 代码代替。flatMap()
方法同时处理成功和失败完成类型,无需执行任何更改。

  在方法声明中,添加throws子句表示该方法将抛出异常。

代码片段1:

使用 async

最新的 Scala 版本包含在编译期间使用宏
转换代码的能力。目前实现的一个最有用的宏是 async,它在编译期间将使用
future 的看似顺序的代码转换为异步代码。清单 9 展示了 async
如何简化本教程中使用的任务代码。

清单 9. 结合使用 future 与 async {}

def runAsync(): Future[Int] = {  async {    val v1 = await    val a = task2    val b = task3    await(task4 + await  }}

清单 9 中封装的 async {…} 调用了 async
宏。此调用将该代码块声明为异步执行的代码,并在默认情况下异步执行它,然后返回一个
future 表示该代码块的执行结果。在该代码块中,await()
方法(实际上是该宏的一个关键字,而不是一个真正的方法)显示了何处需要一个
future 的结果。async 宏在编译期间修改了 Scala 程序的抽象语法树
,以便将该代码块转换为使用回调的代码,这大体相当于 清单 4 的代码。

除了 async {…} 包装器之外,清单 9 中的代码还与 清单 3
中最初的阻塞代码很相似。这主要是这个宏的成就,它抽象化了异步事件的所有复杂性,使它看起来像您在编写简单的线性代码。在幕后,这涉及到大量复杂性。

 
throws语法:[<修饰符>]<返回值类型><方法名>([<参数列表>])[throws<异常类>]

MyExpression parseRequest(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);
   }
}

async 内部原理

如果查看 Scala 编译器从源代码生成的类,就会看到一些具有类似
AsyncHappy$$anonfun$1.class
的名称的内部类。从名称可以猜到,这些类由编译器为异步函数而生成(比如传递给
onSuccess() 或 flatMap

使用 Scala 2.11.1 编译器和 Async 0.9.2 实现,您还会看到一个名为
AsyncUnhappy$stateMachine$macro$1$1.class 的类。这是 async
宏生成的实际实现代码,采用状态机的形式来处理异步任务。清单 10
给出了此类的一个部分地方进行了反编译(decompiled)的视图。

清单 10. 反编译后的 AsyncUnhappy$stateMachine$macro$1$1.class

public class AsyncUnhappy$stateMachine$macro$1$1  implements Function1<Try<Object>, BoxedUnit>, Function0.mcV.sp{  private int state;  private final Promise<Object> result;  private int await$macro$3$macro$13;  private int await$macro$7$macro$14;  private int await$macro$5$macro$15;  private int await$macro$11$macro$16;  ...  public void resume() {    ...  }   public void apply(Try<Object> tr) {    int i = this.state;    switch  {      default:        throw new MatchError(BoxesRunTime.boxToInteger;      case 3:        if (tr.isFailure {          result().complete;        } else {          this.await$macro$11$macro$16 = BoxesRunTime.unboxToInt;          this.state = 4;          resume();        }        break;      case 2:        if (tr.isFailure {          result().complete;        } else {          this.await$macro$7$macro$14 = BoxesRunTime.unboxToInt;          this.state = 3;          resume();        }        break;      case 1:        if (tr.isFailure {          result().complete;        } else {          this.await$macro$5$macro$15 = BoxesRunTime.unboxToInt;          this.state = 2;          resume();        }        break;      case 0:        if (tr.isFailure {          result().complete;        } else {          this.await$macro$3$macro$13 = BoxesRunTime.unboxToInt;          this.state = 1;          resume();        }        break;    }  }   ...}

清单 10 中的 apply() 方法处理实际的状态更改,估算一个 future
的结果并将输出状态更改为匹配。输入状态会告诉该代码正在估算哪个
future;每个状态值对应于 async 代码块中一个特定的 future。从 清单 10
的部分代码很难了解这一点,但查看其他一些字节码,就可以看到状态代码是与任务匹配的,所以状态
0 表示 task1 的结果符合预期,状态 1 表示 task2
的结果符合预期,依此类推。

resume() 方法并未显示在 清单 10 中,因为反编译器无法确定如何将它转换为
Java 代码。我也不打算探讨这个过程,但通过查看字节码,可以确定 resume()
方法执行了与状态代码上的 Java switch
相似的工作。对于每个非最终状态,resume()
执行适当的代码段来设置下一个预期的 future,最终将
AsyncUnhappy$stateMachine$macro$1$1 实例设置为 future 的 onComplete()
方法的目标。对于最终状态,resume()
将会设置结果值并履行对最终结果的承诺。

您实际上并不需要深入分析生成的代码来理解 async。关于 async
工作原理的完整描述,请查阅 SIP-22 – Async 提案。

async 限制
由于 async
宏将代码转换为状态机类的方式,该宏的使用有一些限制。最明显的限制是,不能将
await() 嵌套在 async 代码块中的另一个对象或闭包内。也不能将 await()
嵌套在一个 try 或 catch 内。

除了这些使用限制之外,async
的最大问题是:在调试时,您同样会体验到一些通常与异步代码有关的问题回调,在这种情况下,需要尝试理解没有反映明显的代码结构的调用堆栈。不幸的是,目前的调试器设计无法解决这些问题。这是
Scala 中一个新的工作区域(请参阅 反思调试器。)与此同时,您可以禁用
async
代码块的异步执行,让调试变得更轻松(假设您尝试修复的问题在按顺序执行操作时仍然存在)。

最后,Scala 宏仍是一项我们正在开展的工作。async 有望在未来的版本中成为
Scala 语言的一个正式部分,但只有在 Scala
语言团队对宏的工作方式感到满意时,这种情况才会出现。到那时,无法确保
async 的格式不会发生改变。

  其中:异常类可以声明多个,用逗号分割。

如果没有已检测异常,想要保存同样的信息,我们就需要引入专用的类型表示每种可能出现的特殊情况的函数输出。这些类型让我们可以保存所有可能的情况,包括在正常情况下所生成的值。

结束语

一些处理异步事件的 Scala 方法与 Java 代码存在很大的区别。借助 flatMap()
和 async 宏,Scala 提供了整洁而且容易理解的技术。async
特别有趣,您可以编写看似正常的顺序的代码,但编译的代码会并发地执行。Scala
不是提供这种方法的惟一语言,但基于宏的实现为其他方法提供了极高的灵活性。

本文转自:

区别二:

此外,为了达到和基于类型的执行相同的层次,我们必须要扩展输出类型,封装这些类型所有可用的操作,这样才能将所有情况都考虑在内。

  throws可以单独使用,但throw不能;

Unfortunately, Java seems not to supply ready-made mechanisms for
defining aggregate outcome types of this kind, that is, something like:

区别:

不幸的是,Java看起来还没有现成的机制来定义下面这种聚合输出类型集合:

 
throw要么和try-catch-finally语句配套使用,要么与throws配套使用。但throws可以单独使
用,然后再由处理异常的方法捕获。
throws E1,E2,E3
只是告诉程序这个方法可能会抛出这些个异常,方法的调用者可能要处理这些异常。而这些异常E1,E2,E3可能是该函数体产生的。

Outcome<T, Exc1, Exc2, Exc3>

而throw是明确之处这个地方要抛出这个异常。

在上面的例子中,T是正常的返回值,增加的Exc1,Exc2等则是可能会出现的错误情况,这样这些输出中只有一个能够在返回时传递返回值。

void doA() throws Exception1, Exception3 {
  try {
  ……
  } catch(Exception1 e) {
  throw e;
  } catch(Exception2 e) {
  System.out.println(“出错了”);
  }
  if (a != b)
  throw new Exception3(“自定义异常”);
}

Java中最类似的工具就是Java
8的CompletionStage<T>,它封装了函数可能抛出的异常并且负责保证在检测到异常的情况下,跳过对前置输出的进一步操作。但是这个接口旨在启用“一元”风格的代码,将异常作为与正常工作流程完全分离的计算的某一方面隐藏。因此,这个工具是为了处理那些不需要恢复的异常而设计,并不适用于自定义已检测异常,因为已检测异常是工作流程不可分割的一部分。因此尽管CompletionStage<T> 可以在保持其他类型异常的同时,选择性的处理某些类型的异常,这种处理并不能在任意特定的情景下执行。

代码块……中可能产生异常Exception1、Exception2和Exception3。
如果产生Exception1异常,则捕捉了之后抛出由该方法的调用者去做处理;
如果产生Exception2异常,则该方法自己做了处理(打印出了说出错了),所以该方法就不会再向外抛出Exception2异常了,void
doA() throws Exception1,,Excpetion3里面的Exception2也就不用写了;
而Exception3异常是该方法的某段逻辑出错,程序员自己作了处理在该段逻辑错误的情况下抛出异常Exception3,则调用者也需要处理。

因此,如果要用CompletionStage<T>对我们之前的情况建模并保持基于类型的执行,就需要在基础类型T中包含我们的已检测异常同时还要保留专用的输出类型。

throw语句用在方法体内,表示抛出异常,由方法体内的语句处理 
throws语句用在方法声明后面,表示再抛出异常,由调用这个方法的上一级方法中的语句来处理

坚持原生方式并引入定制化的专用输出类型后(同时仍然利用Java
8语法的优势),代码展示如下:

throws主要是声明这个方法会抛出这种类型的异常,使其他地方调用它时知道要捕获这个异常。
throw是具体向外抛异常的动作,所以它是抛出一个异常实例。

代码片段2:

throws说明你有哪个可能,倾向 
throw的话,那就是你把那个倾向变成真实的了
同时:
1)throws出现在方法函数头;而throw出现在函数体; 
2)throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常; 
3)两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

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,我们可以以一种能够保留代码结构的方式对在流程中引入异步性负责。例如,假设elaborate函数需要异步处理。那么就可以将其重写为返回一个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 composeResponse(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可以识别一个函数是异步的但仍然是顺序执行的。例如,使用如下方式编写函数(使用一个神奇的关键字seq

CompletableFuture<T> seq fun(A1 a1, A2 a2) { … }

我们可以让JVM以某种方式强制返回的CompletableFuture对象只完成一次(通过丢弃后续的虚假调用);这会被看作是这个函数的“正式”终止,不管实际的线程调用情况如何。

然后,编译器将允许我们使用好像由下述简化的签名所定义的fun函数(用另外一个神奇的关键字async):

T async fun(A1 a1, A2 a2);

有了这个签名,我们就可以像同步函数那样调用这个函数,不过JVM必须要负责提取fun函数之后所有制定的代码,并且在“正式”终止后(如,在CompletableFuture对象完成之后)在适当的线程中执行这些代码。

这种代码转换将递归地应用到函数调用栈中的所有函数之上。实际上,如果在定义一个新的函数时使用了fun函数的简化签名,新函数就需要强制包含async关键字,以表明这一函数本质上是异步的(虽然仍是顺序执行)。

另外,调用如下签名的方法后,递归的传递将会终止

void async fun(A1 a1, A2 a2);

以便调用线程(可能属于某个ExecutorService对象)可以完成其他的工作。

可以很方便地扩展上述假想的功能以支持已检测异常。在实践中,通过如下形式的函数定义:

CompletableFuture<Outcome<T, E1, E2>> seq fun(A1 a1, A2
a2) { … }

其中,Outcome是某个返回类型和异常的标准包装器,异常可以是一个或多个,编译器会把它看做由下述经过简化的签名所定义,从而允许我们使用这个函数:

T async fun(A1 a1, A2 a2) throws E1, E2;

利用这个语法,代码片段3的等价版本可以简化如下:

代码片段4:

MyExpression parseRequest(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);
   }
}

而且,在elaborate中引入异步性的基础上,将代码片段1转化为代码片段4是很自然的事情。

是否有什么其他的方法能够达成与可用的语法能达成的相似的目标(服从合理的妥协)?

我们需要实现一种机制,通过这种机制,某个异步调用之后的所有代码都会在这个调用在其所在的线程中产生输出后被分割(比如,通过将其转入一个回调)并执行。

作为一种直观的方法,一种可能可行的尝试(只要每一层级的异步调用数量都比较少,这种尝试就是可行的)包含如下步骤:

  1. 首先,从工作流程的同步展示开始(如代码片段1所示),然后识别出可能会变成异步的函数(在这个例子中即指:evaluate以及相应的processMessage方法本身)
  2. 如果多个可能的异步调用存在于同一个函数中,就需要合理安排代码,可以通过引入中间函数的方式,每个函数中间仅包含一个可能的异步调用,所有其他的异步调用则作为返回前的最后一步操作被调用。(在我们的简单示例中,不需要做任何安排)
  3. 用这样的方式转化代码,每个可能成为异步函数并且参与了内部(inner)函数调用的外部(outer)函数都将会被分割为“outerBefore”和“outerAfter”两部分。outerBefore将包含所有在内部函数之前执行的代码,然后调用内部函数作为其最后一步操作;另一方面,outerAfter则将调用outerBefore作为其第一个操作,然后执行全部剩余代码。需要注意的是,这样造成的后果就是outerBeforeouterAfter将共享相同的参数。在我们的示例中,将会生成如下代码:代码片段5:

    MyExpression parseRequest(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 { ... }
    
    ......
    
  4. 引入专用的类用来包含由“xxxBefore”和“xxxAfter”组成的函数对,然后用一个临时实例调用任意函数对。我们的代码可能会扩展成如下形式:代码片段6:

    String processMessage(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 { ... }
    
       ......
    
    }
    
  5. 用适当的代理对象替代前一步引入的示例;代理的工作包括集合所有“xxxAfter”函数然后只在相关的“xxxBefore”函数完成后再调用它们(在“xxxBefore”函数完成的线程中)。最后这一步主要考虑将最内部函数转换为异步函数。最终的代码如下所示:代码片段7:

    String processMessage(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方法并将当前方法的输出作为参数传入。

以上只是一个不完全的概括。

我们试图用这些理念用来创造一个完整的解决方案。目前的阶段性成果是可以应用于具体场景的一种实验性技术。

这一预想的技术隐含着在易用性方面的许多妥协,这可能会限制其在有限的一些场景下的吸引力。在我们的场景中,已经证明我们在这种技术上所花费的努力是值得的。

感兴趣的读者可以从这里找到关于我们的技术的详细介绍,除此之外还包含一个对易用性利弊的全面讨论。

发表评论

电子邮件地址不会被公开。 必填项已用*标注