scala雾中风景(25): try-finally表达式的类型推导

一段实际代码简化后如下:

class A {
  def foo():String = {
    try{
      Thread.sleep(1000)
    }finally{
      "ok"
    }
  }
}

本来期望这个foo方法返回finally块中的”ok”,但编译的时候却给出了类型不匹配的错误:

 ➜  scalac A.scala
A.scala:4: error: type mismatch;
 found   : Unit
 required: String
      Thread.sleep(1000)
                  ^

按说scala类型推断这么强大,不应该推断不出最终的返回值类型,从编译器的错误来看似乎它非要求在try代码块里最后一行表达式必须也是String类型的值,为什么finally里的表达式没有参与类型推断呢?

把上述代码稍作改动,在try代码块里明确的给出一个String结果

def foo():String = {
    try{
        Thread.sleep(1000)
        "res"
    }finally{
        "ok"
    }
}

再编译一下,却给出了一行警告:

 ➜  scalac A.scala
A.scala:7: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
    "ok"
    ^
one warning found

分析一下为什么编译器认为这句表达式”does nothing”,而不把它当作返回值对待:

 ➜  scalac -Xprint:typer A.scala
 ...

def foo(): String = try {
  java.this.lang.Thread.sleep(1000L);
  "res"
} finally {
  "ok";
  ()
}

看到编译器在finally块的”ok”表达式后边自行增加了一个返回Unit类型的值(),看上去编译器认为finally块里的逻辑是一个“procedure”,一定要满足Unit

从scala语言规范来看,try-catch-finally表达式也是有返回值的,且返回值主要是取决于trycatch里的最后一行表达式,而finally被认为是做一些收尾的工作的,不应该在里面去改变返回结果。

具体到这个案例,foo方法声明的返回值类型是Stringfoo方法体里的try-finally表达式的值就是最终的返回值,而try-finally表达式的值是取决于try代码块里的最后一行表达式,而非finally块里的。

看几个例子:

scala> val a = try { 100 } finally { 200 }
<console>:7: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
   val a = try { 100 } finally { 200 }
                                 ^
a: Int = 100

上面try-finally语句的结果是try里的值。

scala> val a = try { throw new Exception() } catch { case e:Exception => 200 }
a: Int = 200

上面try里发生了异常,最终的结果是catch里的。

scala> val a = try { throw new Exception() } catch { case e:Exception => 200 } finally { 300 }
<console>:7: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
   val a = try { throw new Exception() } catch { case e:Exception => 200 } finally { 300 }
                                                                                     ^
a: Int = 200

上面finally里的表达式并不会被当作最终返回值。

当然,在finally块里是可以使用return关键字的,但return关键字在这里并不能改变try-finally表达式的结果类型,我们对原代码增加return再编译:

def foo():String = {
    try{
        Thread.sleep(1000)
    }finally{
      return "ok"
    }
}

 ➜  scalac A.scala
A.scala:4: error: type mismatch;
 found   : Unit
 required: String
      Thread.sleep(1000)
                  ^
one error found

依然编译错误,注意return是面向method的,属于流控层面,并不影响表达式的类型推断。因为在foo方法里try-finally就是最后一句表达式,所以编译器要求这句表达式的类型必须也满足foo的返回值类型签名。如果try-finally不是最后一句,就没有这个约束了,比如:

def foo():String = {
    try{
        Thread.sleep(1000)
    }finally{
      return "ok"
    }

    "no"
}

上面对foo方法在最后一行增加了一句返回”no”的表达式,使得前边的try-finally表达式类型推导不受方法签名的约束,编译可以通过了。当然这个代码逻辑肯定不会走到那里,我更希望编译器给出代码不可达的警告。

如果打开typer-debug编译选项,可以看到编译器总会期待方法里的最后一个表达式满足方法返回值类型,如果最后的这个表达式又是由多个更小粒度的表达式组合成的(比如这个try-finally,我们暂称它为大表达式),则进一步对这个大表达式拆分推导,约束其中的决定整个大表达式类型的小表达式也必须符合方法的返回类型,对于try-finally这个大表达式来说就是其中try块里的最后一行表达式。

对于try-catch-finallyfinally,编译器总是预期它里面的表达式类型为Unit,所以如果在里面的最后一条语句不是一个Unit类型的值,编译会自动给你加上。

注意,return ok这句表达式的类型是Nothing,不要混淆方法返回值类型和表达式自身类型。return, throw等跟流控相关的表达式都是Nothing,它可以满足任何类型,自发可以符合finally里的Unit预期。

其实这个问题是scala类型推导实现的问题,我们期望它更聪明一些,比如:

scala> def bar:String = { return "A";  "B"  }
bar: String

在Java里编译器会报错后边的语句不可达,但Scala里却编译通过。虽然后边的表达式没有意义,不会走到那儿,但并不意味着你能给出任意的值:

scala> def bar:String = { return "A";  200 }
<console>:7: error: type mismatch;
 found   : Int(200)
 required: String
   def bar:String = { return "A";  200 }

尽管后边的表达式不会被执行到,但它在编译时参与类型推导,因为对于该方法来说 { return "A"; 200 }整体是一个大表达式也必须满足String类型才行。

HttpURLConnection在底层是否复用socket的简单验证方式

关于JDK自身的HttpURLConnection在底层是否复用socket的测试方式,可以快速用repl和lsof来检测:

// 本地启动一个 http server,它返回十几个字符
 ➜  curl "http://localhost:8080/sleep?time=1000"
{"code":"ok"}

// 在repl下连续请求这个url 若干次
scala> val is = new java.net.URL("http://localhost:8080/sleep?time=100").openConnection.getInputStream; for(i <- 1 to 15) is.read; is.close
is: java.io.InputStream = sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@1ba9117e

scala> val is = new java.net.URL("http://localhost:8080/sleep?time=100").openConnection.getInputStream; for(i <- 1 to 15) is.read; is.close
is: java.io.InputStream = sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@a82c5f1

与此同时在另一个终端用lsof查看socket,每秒刷新一次,可看到客户端socket是同一个

 ➜  /usr/sbin/lsof -Pan -iTCP -r 1 -p 43280
=======
=======
COMMAND   PID      USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    43280 hongjiang   47u  IPv6 0x43acdfd2ea5b0c01      0t0  TCP 127.0.0.1:57304->127.0.0.1:8080 (ESTABLISHED)
=======
COMMAND   PID      USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    43280 hongjiang   47u  IPv6 0x43acdfd2ea5b0c01      0t0  TCP 127.0.0.1:57304->127.0.0.1:8080 (ESTABLISHED)
=======
COMMAND   PID      USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    43280 hongjiang   47u  IPv6 0x43acdfd2ea5b0c01      0t0  TCP 127.0.0.1:57304->127.0.0.1:8080 (ESTABLISHED)
=======
COMMAND   PID      USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    43280 hongjiang   47u  IPv6 0x43acdfd2ea5b0c01      0t0  TCP 127.0.0.1:57304->127.0.0.1:8080 (ESTABLISHED)  

这个话题是由URLConnection在关闭的时候应该调用close还是disConnect所引起的,关于jdk里keep-alive相关的一些参数不展开了。

验证disconnect方法:

// 执行若干次
scala> val conn = new java.net.URL("http://localhost:8080/sleep?time=100").openConnection.asInstanceOf[java.net.HttpURLConnection]; val is=conn.getInputStream; for(i <- 1 to 15) is.read; conn.disconnect

这时没法用lsof观察了,它最小刷新单位是1秒,因为每次连接立即关闭导致没机会看到,得用tcpdump来观察

 ➜  sudo tcpdump -i lo0  -s 1024 -l -A  port 8080

 ^[[A04:59:57.066577 IP localhost.57355 > localhost.http-alt: Flags [S]
 ...
 -`=.-`=.GET /sleep?time=100 HTTP/1.1
User-Agent: Java/1.8.0_51
Host: localhost:8080
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

...

05:00:05.407691 IP localhost.57356 > localhost.http-alt: Flags [P.], seq 1:168, ack 1, win 12759, options [nop,nop,TS val 761290281 ecr 761290281], length 167: HTTP: GET /sleep?time=100 HTTP/1.1
E...LF@.@.........................1........
-`^)-`^)GET /sleep?time=100 HTTP/1.1
User-Agent: Java/1.8.0_51
Host: localhost:8080
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive 

... 

05:00:07.045830 IP localhost.57357 > localhost.http-alt: Flags [P.], seq 1:168, ack 1, win 12759, options [nop,nop,TS val 761291915 ecr 761291915], length 167: HTTP: GET /sleep?time=100 HTTP/1.1
E.....@.@................l.;.\.,..1........
-`d.-`d.GET /sleep?time=100 HTTP/1.1
User-Agent: Java/1.8.0_51
Host: localhost:8080
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

看到三次连接每次客户端socket端口都变了。

再谈AsyncHttpClient

以前曾经说过为何我们不使用AsyncHttpClient,当时主要是从需求出发,性能上不需要。不过在业务做活动推广的场景下,性能上是必须要考虑的;另外在Actor模型里,要调用第三方的http接口,为了避免Actor收发消息的底层线程池被阻塞也必须用异步的方式。在去年年底我们的一次活动,Akka里使用AsyncHttpClient(1.9版本)已经很好的验证过它们的组合以及惊诧的性能了。

最近看到AsyncHttpClient在2.0后被重构了,包名从com.ning变为了org.asynchttpclient,里面的一些类也有所重构,也升级了所依赖的netty的版本。最近正好一些新项目里调用第三方的http接口,尝试了一下新的AsyncHttpClient,结果发现它的readTimeout参数并不work,下面是两个版本的测试程序:

import com.ning.http.client.AsyncCompletionHandler;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.Response;

public class AsyncV1Test {

    static class MyCallback extends AsyncCompletionHandler<Boolean> {

        @Override
        public Boolean onCompleted(Response response) throws Exception {
            System.out.println(response.getResponseBody());
            return true;
        }

        public void onThrowable(Throwable t) {
            t.printStackTrace(System.err);
        }
    }

    public static void main(String[] args) throws Exception {
        AsyncHttpClientConfig cfg = new AsyncHttpClientConfig.Builder().setReadTimeout(100).build();
        com.ning.http.client.AsyncHttpClient cli = new com.ning.http.client.AsyncHttpClient(cfg);
        cli.prepareGet("http://localhost:8080/sleep?time=200").execute(new MyCallback());

        // wait and quit
        Thread.sleep(1000);
        cli.close();
    }
}

上面是老版本的com.ning的客户端的readTimeout测试,设置的是100毫秒,请求的url在server端会sleep 200毫秒,超时的异常符合预期:

java.util.concurrent.TimeoutException: Read timeout to localhost/127.0.0.1:8080 of 100 ms
    at com.ning.http.client.providers.netty.request.timeout.TimeoutTimerTask.expire(TimeoutTimerTask.java:47)
    at com.ning.http.client.providers.netty.request.timeout.ReadTimeoutTimerTask.run(ReadTimeoutTimerTask.java:57)
    at org.jboss.netty.util.HashedWheelTimer$HashedWheelTimeout.expire(HashedWheelTimer.java:556)
    at org.jboss.netty.util.HashedWheelTimer$HashedWheelBucket.expireTimeouts(HashedWheelTimer.java:632)
    at org.jboss.netty.util.HashedWheelTimer$Worker.run(HashedWheelTimer.java:369)
    at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
    at java.lang.Thread.run(Thread.java:745)

在新版本的org.asynchttpclient里测试同样的逻辑:

import org.asynchttpclient.AsyncCompletionHandler;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.DefaultAsyncHttpClientConfig;
import org.asynchttpclient.Response;

public class AsyncV2Test {

    static class MyCallback extends AsyncCompletionHandler<Boolean> {

        @Override
        public Boolean onCompleted(Response response) throws Exception {
            System.out.println(response.getResponseBody());
            return true;
        }

        public void onThrowable(Throwable t) {
            t.printStackTrace(System.err);
        }
    }

    public static void main(String[] args) throws Exception{

        DefaultAsyncHttpClientConfig cfg = new DefaultAsyncHttpClientConfig.Builder().setReadTimeout(100).build();
        org.asynchttpclient.AsyncHttpClient cli = new DefaultAsyncHttpClient(cfg);
        cli.prepareGet("http://localhost:8080/sleep?time=200").execute(new MyCallback());

        // wait and quit
        Thread.sleep(1000);
        cli.close();
    }
}       

却没有发生超时的异常,而是正常打印出了最终的请求结果。估计是新版本重构后所带来的bug,我还是回退到老的com.ning.http.client.AsyncHttpClient