blogjava-凯发k8网页登录

blogjava-凯发k8网页登录http://www.blogjava.net/jiangshachina/a cup of java, cheers!
https://github.com/johnshajiang/blog
zh-cnsat, 08 apr 2023 20:42:53 gmtsat, 08 apr 2023 20:42:53 gmt60探索http/2: 流的状态(原)http://www.blogjava.net/jiangshachina/archive/2016/10/08/431871.htmljohn jiangjohn jiangsat, 08 oct 2016 13:17:00 gmthttp://www.blogjava.net/jiangshachina/archive/2016/10/08/431871.htmlhttp://www.blogjava.net/jiangshachina/comments/431871.htmlhttp://www.blogjava.net/jiangshachina/archive/2016/10/08/431871.html#feedback0http://www.blogjava.net/jiangshachina/comments/commentrss/431871.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/431871.html
探索http/2: 流的状态
的第四篇文章,解读了http/2流的状态,以及状态之间的转化。(2016.10.09最后更新)

1. 概述
    http/2的流(stream)是有状态的。当客户端或服务器端在使用某个流去发送或接收特定帧(frame)或包含特定标签(flag)的帧时,会引起流的状态的转化。定义的流状态,如下所示:
                          --------
                 send pp |        | recv pp
                
,--------|  idle  |--------.
               /         |        |         \
              v           --------           v
        ----------           |            ----------
       |          |          | send h /  |          |
,------| reserved |          | recv h    | reserved |------.
|      | (local)  |          |           | (remote) |      |
|       ----------           v            ----------       |
|          |              --------              |          |
|          |     recv es |        | send es     |          |
|   send h |     ,-------|  open  |-------.     | recv h   |
|          |    /        |        |        \    |          |
|          v   v          --------          v   v          |
|       ----------           |            ----------       |
|      |   half   |          |           |   half   |      |
|      |  closed  |          | send r /  |  closed  |      |
|      | (remote) |          | recv r    | (local)  |      |
|       ----------           |            ----------       |
|           |                |                 |           |
|           | send es /      |       recv es / |           |
|           | send r /       v        send r / |           |
|           | recv r      --------    recv r   |           |
| send r /  `----------->|        |<-----------'  send r / |
| recv r                 | closed |               recv r   |
`----------------------->|        |<----------------------'
                          --------

   send:   endpoint sends this frame
   recv:   endpoint receives this frame

   h:  headers frame (with implied continuations)
   pp: push_promise frame (with implied continuations)
   es: end_stream flag
   r:  rst_stream frame
    总的说,http/2为流的整个生命周期定义了7种状态:idle,reserved (local),reserved (remote),open,half closed (local),half closed (remote)和closed。当一端发送或接收头部块(由一个headers/push_promise帧和紧随它的零到多个continuation帧组成的集合)或rst_stream帧,或包含有end_stream标签的帧(headers和data)之后,将改变流的状态。
    流的状态基于各端自己的视角。由于帧的传输会有网络延迟,在同一时刻,不同端认为的流的状态可能是不同的。比如,当发送端使用一个处于idle状态的流发送一个不包含end_stream标签的headers帧之后会立即认为该流处于open状态,但此时接收端尚未得到该headers帧,所以在那一时刻,接收端依然认为该流的状态是idle。

2. idle
    所有的流在创建之初都处于idle状态。处于idle状态的流,只允许被用于发送headers帧,但可以被用于接收headers和priority帧。在一端使用该状态的流发送或接收headers帧之后,该端会认为此流的状态转变为open。接收priority帧不会改变流的状态。
    一个idle状态的流可被另一个流通过发送/接收push_promise帧保留着,使其在将来被用于服务器端推送。被保留的流的状态则从idle变为reserved (local/remote)。

3. open
    处于open状态的流可被用于发送任何类型的帧。使用该状态的流去发送/接收包含有end_stream标签的帧(headers和data)之后,会使该流的状态变成half closed (local/remote)。使用open状态的流发送或接收rst_stream帧之后,则会使它的状态转变为closed。

4. half closed (local/remote)
    状态half closed (local)与half closed (remote)中的local与remote的区别,完全是基于各端自己的视角。对于同一个流的两端,如果一端认为这个流的状态是half closed (local),那么另一端只能认为这个流的状态是half closed (remote)。
    处于half closed (local)状态的流只能被用于发送window_update,priority和rst_stream帧,但可以被用于接收任何类型的帧。相对应地,处于half closed (remote)状态的流只能被用于接收window_update,priority和rst_stream帧,但可以被用于发送任何类型的帧。

5. reserved (local/remote)
    与half closed (local/remote)状态相似,reserved (local/remote)状态中的local与remote也是基于流两端各自的视角。更具体的是,服务器端发送push_promise将一个idle状态的流保留着以用于未来的推送,并视这个被保留的流的状态为reserved (local),而客户端则视这个流的状态为reserved (remote)。
    服务器端使用reserved (local)状态的流向客户端发送headers帧。该headers帧就是服务器端推送(server push)中被推送的响应的头部。当发送了headers帧之后,服务器端将视该流的状态为half closed (remote)。
相应地,客户端通过reserved (remote)状态的流接收到服务器端推送的响应的头部,然后会视该流的状态为half closed (local)。
    扩展一下,服务器端推送中被保留的流的状态在变为half closed(local/remote)之后才可能被用于接收/发送被推送的响应的体部,也就是data帧。

6. closed
    当一端使用一个流发送或接收到rst_stream帧,或通过状态为half closed (local/remote)的流接收/发送包含有end_stream标签的帧之后,都会视这个流的状态为closed。
    closed状态预示着流的终结,处于该状态的流将只能发送或接收priority帧。但有一个特例。即,如果通过使用half closed (local/remote)状态的流去接收或发送包含有end_stream标签的帧(headers或data),以使该流的状态变为closed,那么在此之后的较短时间内,仍然可以接收window_update或rst_stream帧。


john jiang 2016-10-08 21:17
]]>
探索http/2: hpack协议简述(原)http://www.blogjava.net/jiangshachina/archive/2016/09/24/431837.htmljohn jiangjohn jiangsat, 24 sep 2016 12:29:00 gmthttp://www.blogjava.net/jiangshachina/archive/2016/09/24/431837.htmlhttp://www.blogjava.net/jiangshachina/comments/431837.htmlhttp://www.blogjava.net/jiangshachina/archive/2016/09/24/431837.html#feedback0http://www.blogjava.net/jiangshachina/comments/commentrss/431837.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/431837.html
探索http/2: hpack协议简述
的第一篇文章已经介绍了http 2协议,本文则将简述用于http/2头部压缩的。(2016.10.01最后更新)

1. 基本原理
    hpack头部压缩的基本原理就是使用索引表和。在压缩(编码)与解压(解码)过程,可将指定的头部字段(包含字段名与字段值)存储在索引表中。索引表中的每一个条目由索引(一个整数),字段名和字段值组成。对于存在索引表中的头部字段,在编码时可以仅使用索引作为该字段的代表,在解码时通过该索引从表中查找出对应的字段。对于其它的字符串,则可以使用huffman编码进行压缩。
1.1 索引表
    索引表由静态表与动态表组成。静态表由hpack协议预定义的61个常用的头部字段组成,其中大部分字段的值为空。静态表是只读的,其中的条目及其位置均不可更改。hpack协议中的列出了全部的静态表条目。动态表也由一系列头部字段组成,但其中的元素不固定,在实际操作中可以插入新的条目,也允许删除已有的条目。
    hpack协议要求静态表与动态表合并在同一个存储空间中,其中静态表置于前部,动态表紧随其后。那么在整个索引表空间中,动态表的第一个条目的索引将是62。动态表的维护原则是先进先出(fifo)。当向动态表中增加条目时,将总是从第62位插入,原有的条目将全部向右移动一个位置。当从动态表中删除条目时,将总是从最后一位进行删除。
    虽说,协议要求将静态表与动态表合并在一起,但这只是逻辑上的要求。只要动态表的索引是从62开始,那么各个实现可以根据自己的喜好自由地使用存储数据结构。比如,可以将静态表单独放在一个不可变的数组中,而动态表由另一个链表进行存储,这样可能会便于插入和删除条目。只不过,这个链表中元素的下标与动态表中条目的索引之间相差62。
    (动态)索引表中的条目允许重复。
1.2 huffman编码
    huffman编码是一种用于无损数据压缩的权路径编码算法。在使用该算法时,需要一张所有被编码字符的权重(出现频率)代码表。在对大量的http头部样本进行统计之后,得出了一份适用于hpack的huffman代码表,由协议中的列出。

    必须注意的是,hpack协议并不要求该协议的实现一定要使用索引表,即便某个字段已经存在于索引表中了。而且也不要求一定要对字符串实施huffman压缩。也就是说,理论上,在编码时可以不对头部字段进行任何形式的压缩,而只需将所有的字符转化成字节形式。

2. 基本数据类型表示法
    hpack协议使用的基本数据类型只有两种:整数;字符串。该协议使用整数去表示索引和字符串的长度。头部字段名和值中出现的数字,只会被当作字符串进行处理。
2.1 整数表示法
    hpack在表示整数时并不是把它简单的转换成二进制形式。因为hpack希望每一个整数的表示能够从某个8比特位字节(octet,下文将其简写为"字节")中的任何一个比特位开始,但总是要在某个字节的最后一个比特位结束。比如表示127,让它从字节的第一个比特位开始填充,肯定会在最后一个比特位结束,如下图所示:
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
--- --- --- --- --- --- --- ---
如果第一个比特位被其它值占用(用"?"代表),只能从第二个比特位开始填充呢?结果依然只需要一个字节,如下所示:
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| ? | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
--- --- --- --- --- --- --- ---
但如果是从第三个比特位开始填充呢?这时会发现一个字节已经不够了,必须要第二个字节。但能否表示成如下形式呢?
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| ? | ? | 1 | 1 | 1 | 1 | 1 | 1 |
--- --- --- -------------------
| 1 | ? | ? | ? | ? | ? | ? | ? |
--- --- --- --- --- --- --- ---
这显然不符合hpack协议的要求,因为它希望能够在某个字节的最后一个比特位结束这个表示。为达到这一目的,hpack协议设计出了一种如下图所示的表示法,
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| ? | ? | 1 | 1   1   1   1   1 |
--- --- --- -------------------
| 1 |    value-(2^n-1) lsb      |
--- ---------------------------
               ...
--- ---------------------------
| 0 |    value-(2^n-1) msb      |
--- ---------------------------
第一个字节中能够被用来填充整数表示位的比特位数(上图中的为6)被称为prefix。下面是该表示法的java语言实现,
public void encodeinteger(int value, int prefix) {
    
if (value >> prefix <= 0) {
        printbinary(value);
    } 
else {
        
int number = (1 << prefix) - 1;
        printbinary(number);
        
for (value -= number; value >= 128; value /= 128) {
            printbinary(value 
% 128  128);
        }
        printbinary(value);
    }
}

private void printbinary(int value) {
    system.out.println(string.format(
"%8s", integer.tobinarystring(value)).replace(" ""0"));
}
根据上述算法可知,当prefix为6时,127的表示法如下图所示:
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| ? | ? | 1 | 1 | 1 | 1 | 1 | 1 |
--- --- --- -------------------
| 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
--- --- --- -------------------
2.2 字符串表示法
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| h |    string length (7 )     |
--- ---------------------------
|  string data (length octets)  |
-------------------------------
hpack协议使用上图展示的表示法,它由三部分组成:
[1]huffman标志,表示该字符串是否为huffman编码,占用一个比特位。
[2]字符串长度,一个使用2.1节所述方法表示的整数,其中prefix为7。
[3]字符串值。若huffman标志为0,该值就是原始字符串的字节,否则该值是经huffman编码过的数据。由于经huffman编码过的数据并不总是能在一个字节的最后一个比特位处结束,所以可能会使用eos(end-of-string)符号进行填充。

3. 头部字段表示法
    有了第2节介绍的基本数据类型的表示法作为基础,现在就可以阐述头部字段的表示法了。hpack协议将字段表示法分成3种类型。在表示法开头有一个或若干个比特位用于表示类型。
3.1 已在索引表的头部字段
    类型标识占用1个比特位,值为1。索引使用prefix为7的整数表示法。在解码时,不会更新动态表。
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| 1 |        index (7 )         |
--- ---------------------------
3.2 将置入索引表的头部字段
    类型标识占用2个比特位,值为01。在解码时,会向动态表内插入新条目。这种类型又被分成两种情况:
[1]头部字段名已在索引表中,字段名索引使用prefix为6的整数表示法,而字段值使用字符串表示法。
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| 0 | 1 |      index (6 )       |
--- --- -----------------------
| h |     value length (7 )     |
--- ---------------------------
| value string (length octets)  |
-------------------------------
[2]头部字段名不在索引表中,字段名和字段值均使用字符串表示法,而第一个字节的后6个比特位均使用0填充。
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| 0 | 1 |           0           |
--- --- -----------------------
| h |     name length (7 )      |
--- ---------------------------
|  name string (length octets)  |
--- ---------------------------
| h |     value length (7 )     |
--- ---------------------------
| value string (length octets)  |
-------------------------------
3.2 暂不置入索引表的头部字段
    类型标识占用4个比特位,值为0000。在解码时,不向动态表内插入新条目。这种类型又被分成两种情况:
[1]头部字段名已在索引表中,字段名索引使用prefix为4的整数表示法,而字段值使用字符串表示法。
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| 0 | 0 | 0 | 0 |  index (4 )   |
--- --- -----------------------
| h |     value length (7 )     |
--- ---------------------------
| value string (length octets)  |
-------------------------------
[2]头部字段名不在索引表中,字段名和字段值均使用字符串表示法,而第一个字节的后4个比特位均使用0填充。
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| 0 | 0 | 0 | 0 |       0       |
--- --- -----------------------
| h |     name length (7 )      |
--- ---------------------------
|  name string (length octets)  |
--- ---------------------------
| h |     value length (7 )     |
--- ---------------------------
| value string (length octets)  |
--- ---------------------------
3.3 永不置入索引表的头部字段
    类型标识占用4个比特位,值为0001。在解码时,不向动态表内插入新条目。这种类型又被分成两种情况:
[1]头部字段名已在索引表中,字段名索引使用prefix为4的整数表示法,而字段值使用字符串表示法。
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| 0 | 0 | 0 | 1 |  index (4 )   |
--- --- -----------------------
| h |     value length (7 )     |
--- ---------------------------
| value string (length octets)  |
-------------------------------
[2]头部字段名不在索引表中,字段名和字段值均使用字符串表示法,而第一个字节的后4个比特位均使用0填充。   
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| 0 | 0 | 0 | 1 |       0       |
--- --- -----------------------
| h |     name length (7 )      |
--- ---------------------------
|  name string (length octets)  |
--- ---------------------------
| h |     value length (7 )     |
--- ---------------------------
| value string (length octets)  |
-------------------------------
    可以发现,3.2节与3.3节中的表示法除了类型标识不同之外,其它的都完全相同。那么它们的区别是什么呢?类型0000表示的字段在经过多次解码与编码时,可能会被某个中介者置入索引表中。而类型0001表示法强调了该字段无论在任何时候都不可置入索引表。类型0001可用于表示包含有敏感信息,如密码,的字段值,以避免对这些值进行压缩时产生的风险。

4. 动态表的管理
    动态表中的条目被认为是有尺寸的,其计算公式为:字段名的字节长度 字段值的字节长度 32。字段名/值的长度是指它们的原始字节的长度,而非经过huffman编码后的字节的长度。
    动态表的尺寸就是其中所有条目的尺寸之和。动态表的最大尺寸是有限的,可以通过下面的整数表示法来通知协议的现实去改变动态表的最大尺寸。
  0   1   2   3   4   5   6   7
--- --- --- --- --- --- --- ---
| 0 | 0 | 1 |   max size (5 )   |
--- ---------------------------
    当插入新的条目或改变动态表的最大尺寸时,可能导致已有的一个或多个条目被逐出,甚至清空整个动态表。将动态表的最大尺寸设置为0是合法的,实际上,这是一种常用的清空动态表的途径。

john jiang 2016-09-24 20:29
]]>
探索http/2: 初试http/2(原)http://www.blogjava.net/jiangshachina/archive/2016/09/20/431814.htmljohn jiangjohn jiangtue, 20 sep 2016 08:42:00 gmthttp://www.blogjava.net/jiangshachina/archive/2016/09/20/431814.htmlhttp://www.blogjava.net/jiangshachina/comments/431814.htmlhttp://www.blogjava.net/jiangshachina/archive/2016/09/20/431814.html#feedback1http://www.blogjava.net/jiangshachina/comments/commentrss/431814.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/431814.html
探索http/2: 初试http/2
目前支持http/2的服务器端与客户端实现已有不少,的第二篇就分别以jetty和curl作为服务器端和客户端,描述了http/2测试环境的搭建过程。本文还将使用这个测试环境去展示jetty在实现http/2时的一个局限和一个bug。(2016.09.22最后更新)

1. http/2的实现
    目前已经有众多的服务器端和客户端实现了对http/2的支持。在服务器端,著名的apache httpd从2.4.17版,nginx从1.9.5版,开始支持http/2。在客户端,主流的浏览器,如chrome,firefox和ie,的最新版均支持http/2,但它们都只支持运行在tls上的http/2(即h2)。使用java语言实现的,则有jetty和netty,它们都实现了服务器端和客户端。此处有一份http/2实现的列表:
    另外,还有一些工具支持对http/2的分析与调试,如curl和wireshark。这里也有一份此类工具的列表:

2. 服务器端
    作为java程序员,选用一款使用java语言编写的开源http/2服务器端实现似乎是很自然的结果。实际上,在日后的研究中,我们也需要查看服务器端的源代码。这对于深入地理解http/2,并发现实现中可能的问题,具有现实意义。
    本文选择jetty的最新版本9.3.11作为服务器端。jetty是一个成熟的servlet容器,这为开发web应用程序提供了极大便利。而本文第1节中提到的netty是一个传输层框架,它专注于网络程序。可以使用netty去开发一个servlet容器,但这显然不如直接使用jetty方便。
    安装和配置jetty是一件很容易的事情,具体过程如下所示。
    假设此时已经下载并解压好了jetty 9.3.11的压缩文件,目录名为jetty-9.3.11。在其中创建一个test-base子目录,作为将要创建的jetty base的目录。
$ cd jetty-9.3.11
$ mkdir test-base
$ cd test-base
在创建base时,加入支持http,https,http2(h2),http2c(h2c)和deploy的模块。
$ java -jar ../start.jar --add-to-startd=http,https,http2,http2c,deploy

alert: there are enabled module(s) with licenses.
the following 1 module(s):
   contains software not provided by the eclipse foundation!
   contains software not covered by the eclipse public license!
   has not been audited for compliance with its license

 module: alpn
    alpn is a hosted at github under the gpl v2 with classpath exception.
    alpn replaces/modifies openjdk classes in the java.sun.security.ssl package.
    http://github.com/jetty-project/jetty-alpn
    http://openjdk.java.net/legal/gplv2 ce.html

proceed (y/n)? y
info: server          initialised (transitively) in ${jetty.base}\start.d\server.ini
info: http            initialised in ${jetty.base}\start.d\http.ini
info: ssl             initialised (transitively) in ${jetty.base}\start.d\ssl.ini
info: alpn            initialised (transitively) in ${jetty.base}\start.d\alpn.ini
info: http2c          initialised in ${jetty.base}\start.d\http2c.ini
info: https           initialised in ${jetty.base}\start.d\https.ini
info: deploy          initialised in ${jetty.base}\start.d\deploy.ini
info: http2           initialised in ${jetty.base}\start.d\http2.ini
download: http://central.maven.org/maven2/org/mortbay/jetty/alpn/alpn-boot/8.1.5.v20150921/alpn-boot-8.1.5.v20150921.jar to ${jetty.base}\lib\alpn\alpn-boot-8.1.5.v20150921.jar
download: https://raw.githubusercontent.com/eclipse/jetty.project/master/jetty-server/src/test/config/etc/keystore?id=master to ${jetty.base}\etc\keystore
mkdir: ${jetty.base}\webapps
info: base directory was modified
    注意,在上述过程中,会根据当前环境变量中使用的java版本(此处为1.8.0_60)去下载一个对应的tls-alpn实现jar文件(此处为alpn-boot-8.1.5.v20150921.jar),该jar会用于对h2的支持。当启动jetty时,该jar会被java的bootstrap class loader加载到类路径中。
    创建一个最简单的web应用,使它在根目录下包含一个文本文件index,内容为"http/2 test"。再包含一个简单的servlet,代码如下:
package test;

import java.io.ioexception;

import javax.servlet.servletexception;
import javax.servlet.http.httpservlet;
import javax.servlet.http.httpservletrequest;
import javax.servlet.http.httpservletresponse;

public class testservlet extends httpservlet {

    
private static final long serialversionuid = 5222793251610509039l;

    @override
    
public void doget(httpservletrequest request, httpservletresponse response)
            
throws servletexception, ioexception {
        response.getwriter().println("test");
    }

    @override
    
public void dopost(httpservletrequest request, httpservletresponse response)
            
throws servletexception, ioexception {
        doget(request, response);
    }
}
web.xml主要是定义了一个servlet,具体内容如下:
xml version="1.0" encoding="utf-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
    xsi:schemalocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    metadata-complete="false" version="3.1">

    
<welcome-file-list>
        
<welcome-file>indexwelcome-file>
    
welcome-file-list>

    
<servlet>
        
<servlet-name>testservlet-name>
        
<servlet-class>test.testservletservlet-class>
    
servlet>
    
<servlet-mapping>
        
<servlet-name>testservlet-name>
        
<url-pattern>/test/*url-pattern>
    
servlet-mapping>
web-app>
该应用的部署路径为jetty-9.3.11/test-base/webapps/test.war。在该war文件所在的目录下,创建一个test.xml,其内容如下所示:
xml version="1.0"  encoding="iso-8859-1"?>
doctype configure public "-//jetty//configure//en" "http://www.eclipse.org/jetty/configure_9_0.dtd">

<configure class="org.eclipse.jetty.webapp.webappcontext">
  
<set name="contextpath">/set>
  
<set name="war"><systemproperty name="jetty.base" default="."/>/webapps/test.warset>
configure>
启动jetty服务器,使用默认的http和https端口,分别为8080和8443。
$ java -jar ../start.jar
2016-09-15 21:15:51.190:info:oejs.server:main: jetty-9.3.11.v20160721
2016-09-15 21:15:51.237:info:oejdp.scanningappprovider:main: deployment monitor [file:///d:/http2/jetty/jetty-9.3.11/test-base/webapps/] at interval 1
2016-09-15 21:15:52.251:info:oejw.standarddescriptorprocessor:main: no jsp support for /test.war, did not find org.eclipse.jetty.jsp.jettyjspservlet
2016-09-15 21:15:52.313:info:oejsh.contexthandler:main: started o.e.j.w.webappcontext@4520ebad{/test.war,file:///d:/http2/jetty/jetty-9.3.11/test-base/webapps/test.war/,available}{d:\http2\jetty\jetty-9.3.11\test-base\webapps\test.war}
2016-09-15 21:15:52.391:info:oejw.standarddescriptorprocessor:main: no jsp support for /, did not find org.eclipse.jetty.jsp.jettyjspservlet
2016-09-15 21:15:52.391:info:oejsh.contexthandler:main: started o.e.j.w.webappcontext@711f39f9{/,file:///d:/http2/jetty/jetty-9.3.11/test-base/webapps/test.war/,available}{/test.war}
2016-09-15 21:15:52.532:info:oejs.abstractconnector:main: started serverconnector@1b68ddbd{http/1.1,[http/1.1, h2c, h2c-17, h2c-16, h2c-15, h2c-14]}{0.0.0.0:8080}
2016-09-15 21:15:52.735:info:oejus.sslcontextfactory:main: x509=x509@e320068(jetty,h=[jetty.eclipse.org],w=[]) for sslcontextfactory@1f57539(file:///d:/http2/jetty/jetty-9.3.11/test-base/etc/keystore,file:///d:/http2/jetty/jetty-9.3.11/test-base/etc/keystore)
2016-09-15 21:15:52.735:info:oejus.sslcontextfactory:main: x509=x509@76f2b07d(mykey,h=[],w=[]) for sslcontextfactory@1f57539(file:///d:/http2/jetty/jetty-9.3.11/test-base/etc/keystore,file:///d:/http2/jetty/jetty-9.3.11/test-base/etc/keystore)
2016-09-15 21:15:53.234:info:oejs.abstractconnector:main: started serverconnector@4b168fa9{ssl,[ssl, alpn, h2, h2-17, h2-16, h2-15, h2-14, http/1.1]}{0.0.0.0:8443}
2016-09-15 21:15:53.249:info:oejs.server:main: started @3940ms
    根据上述日志可知,jetty启用了web应用test.war,还启动了两个serverconnector,一个支持h2c,另一个支持h2。值得注意的是,这两个serverconnector还分别支持h2c-17, h2c-16, h2c-15, h2c-14和h2-17, h2-16, h2-15, h2-14。这是因为,http/2在正式发布之前,先后发布了18个草案,其编号为00-17。所以,这里的h2c-xx和h2-xx指的就是第xx号草案。

3. 客户端
    其实最方便的客户端就是浏览器了。只要使用的firefox或chrome版本不是太老,肯定都已经支持了http/2,而且这一功能是默认打开的。也就是说,当使用firefox去访问前面所部署的web应用时,就是在使用http/2,但你不会感觉到这种变化。使用firefox提供的developer tools中的network工具查看服务器端的响应,会发现http版本为http/2.0。但此处希望这个客户端能够提供更为丰富的与服务器端进行交互的功能,那么浏览器就并不合适了。
    jetty也实现了支持http/2的客户端,但这个客户端是一个api,需要编写程序去访问http/2服务器端。而且,目前该api的设计抽象层次较低,需要应用程序员对http/2协议,比如各种帧,有较深入的了解。这对于初涉http/2的开发者来说,显然很不合适。本文选择使用c语言编写的一个工具,其实也是http/2的客户端实现之一,curl。

    curl在支持http/2时,实际上是使用了nghttp2的c库,所以需要先安装nghttp2。另外,为了让curl支持h2,就必须要有tls-alpn的支持。那么,一般地还需要安装openssl 1.0.2 。
    网络上关于在linux下安装支持http/2的curl的资源有很多,过程并不难,但有点儿繁,要安装的依赖比较多,本文就不赘述了。如果是使用windows,笔者比较推荐通过cygwin来安装和使用curl。在windows中安装cygwin非常简单,在cygwin中执行各种命令时,感觉上就如同在使用linux,尽管它并不是一个虚拟机。通过cygwin安装curl,它会自动地安装所需的各种依赖程序和库。
    在笔者的机器上,通过查看curl的版本会出现如下信息:
curl 7.50.2 (x86_64-unknown-cygwin) libcurl/7.50.2 openssl/1.0.2h zlib/1.2.8 libidn/1.29 libpsl/0.14.0 ( libidn/1.29) libssh2/1.7.0 nghttp2/1.14.0
protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp
features: debug idn ipv6 largefile gss-api kerberos spnego ntlm ntlm_wb ssl libz tls-srp http2 unixsockets metalink psl
由上可知,笔者使用的curl版本是7.50.2,nghttp2版本是1.14.0,而openssl版本是1.0.2h。

4. 第一次尝试
    在第一次尝试中,只需要简单地访问第2节中部署的web应用中的静态文本文件index,以感受下h2c,完整命令如下:
$ curl -v --http2 http://localhost:8080/index
在输出中包含有如下的内容:
...
> get /index http/1.1
> host: localhost:8080
> user-agent: curl/7.50.2
> accept: */*
> connection: upgrade, http2-settings
> upgrade: h2c
> http2-settings: aamaaabkaaqaap__
>
...
< http/1.1 101 switching protocols
* received 101
* using http2, server supports multi-use
* connection state changed (http/2 confirmed)
...
< http/2 200
< server: jetty(9.3.11.v20160721)
< last-modified: wed, 14 sep 2016 12:52:32 gmt
< content-length: 11
< accept-ranges: bytes
<
...
http/2 test
">"是客户端发送的请求,"<"是服务器端发送的响应,而"*"是curl对当前过程的说明。结合本系列中所简述的http 2协议,可以有以下的基本理解。
[1]客户端发起了一个http/1.1的请求,其中携带有upgrade头部,要求服务器端升级到http/2(h2c)。
> get /index http/1.1
> host: localhost:8080
> user-agent: curl/7.50.2
> accept: */*
> connection: upgrade, http2-settings
> upgrade: h2c
> http2-settings: aamaaabkaaqaap__
>
[2]服务器端同意升级,返回响应"101 switching protocols",然后客户端收到了101响应,http/2连接进行确认。
< http/1.1 101 switching protocols
* received 101
* using http2, server supports multi-use
* connection state changed (http/2 confirmed)
[3]服务器端响应最终结果。状态行中出现的http版本为http/2,状态代码为200,且后面没有跟着"ok"。最后输出了index文件的内容"http/2 test"。
< http/2 200
< server: jetty(9.3.11.v20160721)
< last-modified: wed, 14 sep 2016 12:52:32 gmt
< content-length: 11
< accept-ranges: bytes
<
...
http/2 test

5. 一个局限
    这次,在发起的请求中包含体部,命令如下:
$ curl -v --http2 -d "body" http://localhost:8080/index
在输出中包含有如下的内容:
...
> post /index http/1.1
> host: localhost:8080
> user-agent: curl/7.50.2
> accept: */*
> connection: upgrade, http2-settings
> upgrade: h2c
> http2-settings: aamaaabkaaqaap__
> content-length: 4
> content-type: application/x-www-form-urlencoded
>
...
< http/1.1 200 ok
< last-modified: wed, 14 sep 2016 12:52:32 gmt
< accept-ranges: bytes
< content-length: 11
...
http/2 test
    和第4节中的输出进行比较,会发现缺少了"101 switching protocols"那一段,而且最终响应状态行中出现的http版本是http/1.1。这就说明服务器端不同意升级,后面继续使用http/1.1。刚刚部署的jetty未做任何改变怎么会突然不支持http/2了呢?或者这是curl的问题?其实,这是因为jetty服务器端在实现h2c时不支持请求中包含体部。另外,apache httpd也有同样的问题。如果是使用h2,则没有这个限制。这背后的原因超出了本文的范畴,不作表述。

6. 一个bug
    在这次尝试中,测试一下两端对100-continue的支持。如果请求中使用了头部"expect: 100-continue",那么正常地该请求要有体部。但由于在第5节中介绍的问题,此时不能再使用h2c,而只能使用h2。另外,这次不访问静态文件,而是访问servlet(此处为/test)。完整命令如下:
$ curl -vk --http2 -h "expect: 100-continue" -d "body" https://localhost:8443/test
在输出的最后出现了如下信息:
curl: (92) http/2 stream 1 was not closed cleanly: cancel (err 8)
这其实是jetty的一个,正在开发中的9.3.12已经修复了它。

7. 小结
    http/2依然算是新潮的技术,对各家的实现,无论是服务器端,客户端,还是分析工具,都要持有一份怀疑态度。这些实现和工具都是程序,都有可能存在bug。而且协议对许多细节没有作出规定,各家都会发挥自己的想像力。比如,apache httpd和jetty在实现服务器端推送时,其方式就不尽相同。
    在开发自己的http/2实现或应用的时候,需要同时使用已有的不同服务器端和客户端去部署多套测试环境进行对比分析。


john jiang 2016-09-20 16:42
]]>
探索http/2: http 2协议简述(原)http://www.blogjava.net/jiangshachina/archive/2016/09/19/431811.htmljohn jiangjohn jiangmon, 19 sep 2016 03:36:00 gmthttp://www.blogjava.net/jiangshachina/archive/2016/09/19/431811.htmlhttp://www.blogjava.net/jiangshachina/comments/431811.htmlhttp://www.blogjava.net/jiangshachina/archive/2016/09/19/431811.html#feedback0http://www.blogjava.net/jiangshachina/comments/commentrss/431811.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/431811.html
探索http/2: http/2协议简述
http/2的协议包含着两个rfc:hypertext transfer protocol version 2 (),即http/2;hpack: header compression for http/2 (),即hpack。rfc7540描述了http/2的语义,rfc7541则描述了用于http/2的头部压缩的格式。本文只涉及http/2协议,本系列的后续文章将会涉及hpack协议。(2016.10.13最后更新)

1. http/2要解决的问题
     http/1.0只允许在一个tcp连接中出现一个请求。后来的http/1.1虽然引入了请求流水线,以允许在一个连接中发送多个请求,但这只是部分地解决了请求并发的问题。服务器端在返回响应时,还是必须要按照它接收到的请求的顺序进行返回。如果排在前面的响应要消耗较长的时间,那依然会对后面的响应的造成阻塞,亦即线头阻塞(head-of-line blocking)。所以,客户端必须要使用多条连接去发起多个的请求以实现并发,并进而减小延迟。更大的并发会增大服务器的负载,也会占用更大的网络带宽。另外,头部通常会包含有大量的信息,如cookie,而这也会增加网络传输的开销。
     http/2允许在同一个tcp连接中交错地出现多个请求与响应,亦即多工(multiplex)。同时,它使用了一个高效的编码方法对头部进行压缩。http/2还允许对请求进行优先级排序,以便让更为重要的请求得以更快的完成,这会进一步提高性能。http/2还改变了服务器端只能被动地向客户端返回响应的定式,允许服务器端主动地向客户端推送数据,这就可以减少客户端发起请求的数量。
     总之,http/2主要是解决性能问题。

2. 发起http/2
     http/2会使用与http/1相同的uri scheme,即http和https。而且实现http/2的服务器端也不会使用不同的端口去分别支持http/1和http/2。这样有利于平滑地从http/1升级到http/2。毕竟目前已部署的绝大部分网络服务都只支持http/1,当未来它们升级到http/2时,如果换用了不同uri scheme或端口,那么肯定会对客户端产生极大的影响。但是http/2协议为运行在http和https上的http/2分别定义了两个不同的标识符:h2c和h2。h2c中的"c"指的是cleartext,即明文。本文后面会使用h2c指代运行在http2上(直接使用tcp)的http/2,而用h2指代运行在https上(使用tls)的http/2。
     那么,支持http/2的客户端如何知道它所连接的服务器端是否也支持http/2呢?
     对于h2c,支持http/2的客户端可以在发起的请求中使用http/1.1的upgrade头部去尝试要求服务器升级到http/2。该请求的格式如下:
get / http/1.1
host: server.example.com
connection: upgrade, http2-settings
upgrade: h2c
http2-settings:
http2-settings是一个经由base64编码过的字符串,其原始内容是客户端将要发送的settings帧的载荷,即一些配置参数。
     如果服务器端支持http/2,它就响应"101 switching protocols",表示可以进行升级。该响应的格式如下:
http/1.1 101 switching protocols
connection: upgrade
upgrade: h2c
     如果服务器端不支持http/2,则会忽略upgrade请求头部,后续依然使用http/1.1。
     对于h2,会使用到协议transport layer security (tls) application-layer protocol negotiation extension (),即tls-alpn。该协议允许客户端和服务器端就使用何种版本的http进行协商。如果tls-alpn在现实中运行良好的话,也许某天还会使用该方法去协商使用别的协议。
     当客户端与服务器端都同意使用http/2时,双方都需要各自发出一个连接序言(connection preface)以进行最后的确认。
     客户端在接收到服务器端的"101 switching protocols"响应(针对h2c)或tls连接的第一个应用数据字节(针对h2)之后会立即发出连接序言。该序言的开头是"pri * http/2.0\r\n\r\nsm\r\n\r\n"(其十六进制形式为"0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a")(1),后面必须再跟一个settings帧,哪怕这个帧是空的。
     服务器端的连接序言则由一个settings帧构成,该帧必须是服务器端在http/2连接中发送的第一个帧。这个settings帧可以为空,也可以包含一些希望客户端如何与自己进行通信的必要配置信息。

3. 帧(frame)
     http/2消息使用二进制格式(实际编码时使用十六进制书写),相比于文本格式,这样可以提高消息处理的效率。http/2消息的最小单元为帧,它由头部与载荷(payload)组成。每个帧的长度必须是一个或多个8比特位字节(octet,下文将其简写为"字节")。
     帧头部依次包含有如下的5个字段:
     长度(length):该字段占用24个比特位,代表帧载荷的长度。该长度是一个24位的无符号整数。
     类型(type):该字段占用8个比特位,代表帧的类型。
     标志(flags):该字段占用8个比特位,代表帧所定义的一个或多个标志。并不是所有的帧都定义了标志。
     保留位(r):该字段占用1个比特位,其语义尚未被定义。在读取帧时,该位需要被忽略;但在发送帧时,该位需要保持为0(0x0)。
     流标识符(stream identifier):该字段占用31个比特位,代表该帧所在流的标识符。
     在头部之后,紧接着的就是载荷。载荷的结构与内容完全由帧的类型决定,它的长度也是不定的。

     http/2定义了如下10种不同类型的帧。
     data:用于携带一组长度不定的字节。一个或多个data可作为请求或响应的载荷。
     headers:用于开启一个流,并可携带一个头部块片断。头部块指由一个headers/push_promise帧和紧随它的零到多个continuation帧组成的集合,因为只有它们才可能携带头部信息。这个集合可被分割成一个或一组字节,这样的字节被称为头部块片断。头部块中各个特定类型的帧必须紧紧相邻,不能出现其它类型的帧。
     priority:用于指定发送端建议的流优先级。
     rst_stream:用于立即终止流。当希望取消一个流或发生错误时,就可发送rst_stream帧。
     settings:用于携带可以影响两端之间通信方式的配置参数。settings帧定义了一个ack标志,用于指示该帧所设置的参数是否已被接收端获知。当收到一个settings且其中的ack标志为0时,接收端必须尽可能快的应用其中已被更新的参数。
     push_promise:用于向接收端通知发送端将要创建的流。当接收端接收到该帧时,新的流尚未被发送端创建,但发送端承诺会创建该流。该帧用于实现http/2的重要特性"服务器端推送(server push)"。
     ping:用于测量发送端与接收端之间的最小往返时间。这与使用众所周知的ping命令的目的相似,是为了测试某个空闲的连接是否还可用。
     goaway:用于发起对连接的关闭,或触发严重的错误条件。该帧允许一端,在完成对之前已创建的流的处理的同时,优雅地停止接收新的流。一端在创建新的流,另一端在发送goaway,这两者之间天然存在着竞争关系。为了就对这种情况,发送端在发送goaway时会让它携带上(该发送端所知晓的)接收端最后创建的流的标识符,当该goaway被发送之后,发送端将会忽视由接收端创建的任何一个标识符比该标识符大的流。
     window_update:用于流量控制。该帧的载荷由一个单比特保留位和一个31比特位的无符号整数组成。该整数向该帧的接收端指示了其向当前流量控制窗口所能增加传输量的值。
     continuation:用于继续发送头部块片断。只要同一个流中前面的帧是headers,push_promise或continuation,并且该帧没有设置end_headers标志,那么可无限量地发送continuation帧。
     部分帧,data,headers和push_promise,的载荷中可能包含填白(padding)。填白在业务上没有实际的用处,它的出现是基于安全目的。比如,可以用它来扰乱实际数据的长度,以减轻特定的http攻击。

     发送端发送的帧的最大长度要尊重接收端设定的settings_max_frame_size的值。但该值的范围要介于2^14至2^24-1个字节之间。

4. 流(stream)
     流是用于在客户端与服务器端之间进行帧传送的通道,同一个tcp连接中可以同时有多个流,如下图所示,
┌────────┐          connection           ┌────────┐
│        │ ============================= │        │
│        │    --------------------- <-- stream    │
│        │    ┌─────┐┌─────────┐┌─┐      │        │
│        │    └─────┘└─────────┘└─┘ <-- frame     │
│        │    ---------------------      │        │
│ client │                               │ server │
│        │    ----------                 │        │
│        │    ┌──┐┌────┐                 │        │
│        │    └──┘└────┘                 │        │
│        │    ----------                 │        │
│        │ ============================= │        │
└────────┘                               └────────┘
服务器端和客户端可以交错地向同一个连接中的不同流中传送帧。可以把一个流看作http/1中的一个连接。客户端与服务器端在同一个流中的交互依然遵循发送请求-等待响应模式。两端都可以创建新的流,共享对方创建的流,也可以关闭对方创建的流。帧在流中的顺序是有意义的,接收端会以接收到的顺序去处理帧。
     每个流都有一个标识符,是一个31比特位的无符合整数。在同一个连接中,流标识符是唯一的。由客户端创建的流的标识符为奇数,由服务器创建的流的标识符为偶数。但标识符为0的流可看作连接,用于连接控制信息,创建新的流时不可使用该标识符。同一个连接中的任何一个流的标识符都不可重用,即便这个流已被关闭了。对于长时间没有中断的连接,可能会出现标识符不够用的情况,那时就必须强制创建一个新的连接。
     http/2协议为流的生命周期定义了7种状态(2):idle,reserved(local),reserved(remote),open,half closed(local),half closed(remote)和closed。当一端接收或发送头部块或(帧data和headers的)标志rst_stream后可使流的状态发生转变。
     使用流来实现多工就会引起对tcp连接使用的竞争,这会造成流的阻塞。基于帧window_update的流量控制方案可以确保相同连接中的流相互之间不会产生破坏性干扰。流量控制可以作用于两个层面,即单个流或整个连接。只有帧data需要遵守流量控制,所有其它的帧所有消耗的空间均不会占用流量控制窗口。http/2协议只是定义了window_update帧的结构和语义,协议的实现可以选择任何适用自己的流量控制算法。
     流可以有优先级。客户端在创建一个新的流时,可在headers中指定优先级权重。在后续任何时间,通过priority可以改变流的优先级权重。在并发能力有限的情况下,高权重流的帧会被优先传送。权重的值必须介于1至256之间,默认权重为16。流与流之间还可以有依赖关系,这种关系会组成一棵依赖关系树。一个流能够指定自己成为另一个流的子流。这一过程,可以是非排他的,也可以是排他的。非排他性依赖,是指一个流在将自己变成另一个流的子流的过程中,允许另一个流还有别的子流,即允许有自己的兄弟流存在。排他性依赖,指在前述过程中,不允许另一个流还有别的子流。如果另一个流已经有子流了,那么该流会把所有潜在的兄弟流先变成自己的子流,然后再使自己成为另一个流的唯一子流。其实,排他性依赖的作用就是为了能够打破已有的关系树,在既成的父子节点中插入新的节点。否则,只能为已有节点添加子节点,那么关系树将不可能进行重构。所有的流在被创建时,默认成为标识符为0x0的流的子流。在"服务器端推送"中生成的"推送"流将自动地成为生成该推送流的流的子流,其默认权重也为16。

5. 消息交换
5.1 请求/响应交换
     http/2沿袭了http/1的语义,即所有的请求与响应语义均得到了保留,尽管传递这些语义的语法已经改变了。
     一个http/2消息由如下几个部分组成:
     [1]仅对于响应消息,可以包含一个携带有1xx响应头部的头部块。该头部块由一个headers帧和紧随它的零到多个continuation帧组成。
     [2]一个头部块。该头部块由一个headers帧和紧随它的零到多个continuation帧组成。
     [3]零到多个携带有体部(body)消息的data帧。http/1中使用的"分块(chunked)"体部将不适用于http/2。因为一个体部可由多个data帧组成,所以http/2的体部天然就是可分块的。
     [4]一个可能存在的包含着尾部消息的头部块。该头部块由一个headers帧和紧随它的零到多个continuation帧组成。

     http/2仍然沿用http/1中的头部字段,但字段名称中的字母必须全部为小写。另外,还将http/1消息开始行(请求中的与响应中的)中的消息,分解成了若干伪头部字段,此类字段均以冒号(:)开头。
     http/1请求行格式为"method request-target http-version",对应的http/2伪头部字段有:method=method和:path=request-target,但http-version无对应字段,默认为http/2。
     http/1状态行格式为"http-version status-code reason-phrase",对应的http/2伪头部字段有:status=status-code。但http-version无对应字段,默认为http/2;reason-phrase也无对应字段,因为可以通过状态代码查找到其对应的reason-phrase。http/2协议是在尽量减少冗余消息。
     http/2协议还为请求头部定义了另外两个伪字段:
     :scheme:uri中的scheme部分。它可以不仅仅是http或https,因为有时候可能会与非http服务进行交互。
     :authority:uri中的授权部分。即,scheme://user:password@host:port/path?query#fragment中的"user:password@host:port"。
     http/2协议节中给出一些简单示例,展示了如何将http/1消息对应到http/2消息。
5.2 服务器端推送
     http/2的服务器端推送是传统的请求/响应模式的一种特殊形式。服务器端在收到客户端的请求(主请求)之后,为了主动向客户端推送更多的内容,会自动地生成若干新的请求(推送请求)。服务器向客户端发送的响应中,不仅包含对主请求的响应(主响应),还包含对推送请求的响应(推送响应)。
     客户端可以通过发送包含有settings_max_concurrent_streams参数的settings帧去禁用服务器端推送,也可以通过发送rst_stream帧去取消已经发起的服务器端推送,但不能发送包含有end_stream标志的帧。

(1)"pri * http/2.0\r\n\r\nsm\r\n\r\n"中的"pri"与"sm"合起来就是"rrism(棱镜)"。呵呵,httpbis工作组这是想表达什么意思呢 ;-)
(2)本系列的解读了流的状态。


john jiang 2016-09-19 11:36
]]>
play openjdk: 允许你的包名以"java."开头(原)http://www.blogjava.net/jiangshachina/archive/2015/11/01/428010.htmljohn jiangjohn jiangsun, 01 nov 2015 12:06:00 gmthttp://www.blogjava.net/jiangshachina/archive/2015/11/01/428010.htmlhttp://www.blogjava.net/jiangshachina/comments/428010.htmlhttp://www.blogjava.net/jiangshachina/archive/2015/11/01/428010.html#feedback0http://www.blogjava.net/jiangshachina/comments/commentrss/428010.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/428010.html
play openjdk: 允许你的包名以"java."开头

本文是play openjdk的第二篇,介绍了如何突破jdk不允许自定义的包名以"java."开头这一限制。这一技巧对于基于已有的jdk向java.*中添加新类还是有所帮助的。(2015.11.02最后更新)

无论是经验丰富的java程序员,还是java的初学者,总会有一些人或有意或无意地创建一个包名为"java"的类。但出于安全方面的考虑,jdk不允许应用程序类的包名以"java"开头,即不允许java,java.foo这样的包名。但javax,javaex这样的包名是允许的。

1. 例子
比如,以openjdk 8为基础,臆造这样一个例子。笔者想向openjdk贡献一个同步的hashmap,即类synchronizedhashmap,而该类的包名就为java.util。synchronizedhashmap是hashmap的同步代理,由于这两个类是在同一包内,synchronizedhashmap不仅可以访问hashmap的public方法与变量,还可以访问hashmap的protected和default方法与变量。synchronizedhashmap看起来可能像下面这样:
package java.util;

public class synchronizedhashmap<k, v> {

    
private hashmap<k, v> hashmap = null;

    
public synchronizedhashmap(hashmap<k, v> hashmap) {
        
this.hashmap = hashmap;
    }

    
public synchronizedhashmap() {
        
this(new hashmap<>());
    }

    
public synchronized v put(k key, v value) {
        
return hashmap.put(key, value);
    }

    
public synchronized v get(k key) {
        
return hashmap.get(key);
    }

    
public synchronized v remove(k key) {
        
return hashmap.remove(key);
    }

    
public synchronized int size() {
        
return hashmap.size; // 直接调用hashmap.size变量,而非hashmap.size()方法
    }
}

2. classloader的限制
使用javac去编译源文件synchronizedhashmap.java并没有问题,但在使用编译后的synchronizedhashmap.class时,jdk的classloader则会拒绝加载java.util.synchronizedhashmap。
设想有如下的应用程序:
import java.util.synchronizedhashmap;

public class syncmaptest {

    
public static void main(string[] args) {
        synchronizedhashmap
<string, string> syncmap = new synchronizedhashmap<>();
        syncmap.put(
"key""value");
        system.out.println(syncmap.get(
"key"));
    }
}
使用java命令去运行该应用时,会报如下错误:
exception in thread "main" java.lang.securityexception: prohibited package name: java.util
    at java.lang.classloader.predefineclass(classloader.java:
659)
    at java.lang.classloader.defineclass(classloader.java:
758)
    at java.security.secureclassloader.defineclass(secureclassloader.java:
142)
    at java.net.urlclassloader.defineclass(urlclassloader.java:
467)
    at java.net.urlclassloader.access$
100(urlclassloader.java:73)
    at java.net.urlclassloader$
1.run(urlclassloader.java:368)
    at java.net.urlclassloader$
1.run(urlclassloader.java:362)
    at java.security.accesscontroller.doprivileged(native method)
    at java.net.urlclassloader.findclass(urlclassloader.java:
361)
    at java.lang.classloader.loadclass(classloader.java:
424)
    at sun.misc.launcher$appclassloader.loadclass(launcher.java:
331)
    at java.lang.classloader.loadclass(classloader.java:
357)
    at syncmaptest.main(syncmaptest.java:
6)
方法classloader.predefineclass()的源代码如下:
private protectiondomain predefineclass(string name,
        protectiondomain pd)
{
    
if (!checkname(name))
        
throw new noclassdeffounderror("illegalname: "  name);

    
if ((name != null&& name.startswith("java.")) {
        
throw new securityexception
            (
"prohibited package name: " 
            name.substring(
0, name.lastindexof('.')));
    }
    
if (pd == null) {
        pd 
= defaultdomain;
        }

    
if (name != null) checkcerts(name, pd.getcodesource());

    
return pd;
}
很清楚地,该方法会先检查待加载的类全名(即包名 类名)是否以"java."开头,如是,则抛出securityexception。那么可以尝试修改该方法的源代码,以突破这一限制。
从jdk中的src.zip中拿出java/lang/classloader.java文件,修改其中的predefineclass方法以去除相关限制。重新编译classloader.java,将生成的classloader.class,classloader$1.class,classloader$2.class,classloader$3.class,classloader$nativelibrary.class,classloader$parallelloaders.class和systemclassloaderaction.class去替换jdk/jre/lib/rt.jar中对应的类。
再次运行syncmaptest,却仍然会抛出相同的securityexception,如下所示:
exception in thread "main" java.lang.securityexception: prohibited package name: java.util
    at java.lang.classloader.defineclass1(native method)
    at java.lang.classloader.defineclass(classloader.java:
760)
    at java.security.secureclassloader.defineclass(secureclassloader.java:
142)
    at java.net.urlclassloader.defineclass(urlclassloader.java:
467)
    at java.net.urlclassloader.access$
100(urlclassloader.java:73)
    at java.net.urlclassloader$
1.run(urlclassloader.java:368)
    at java.net.urlclassloader$
1.run(urlclassloader.java:362)
    at java.security.accesscontroller.doprivileged(native method)
    at java.net.urlclassloader.findclass(urlclassloader.java:
361)
    at java.lang.classloader.loadclass(classloader.java:
424)
    at sun.misc.launcher$appclassloader.loadclass(launcher.java:
331)
    at java.lang.classloader.loadclass(classloader.java:
357)
    at syncmaptest.main(syncmaptest.java:
6)
此时是由方法classloader.defineclass1()抛出的securityexception。但这是一个native方法,那么仅通过修改java代码是无法解决这个问题的(jdk真是层层设防啊)。原来在hotspot的c 源文件hotspot/src/share/vm/classfile/systemdictionary.cpp中有如下语句:
const char* pkg = "java/";
if (!has_pending_exception &&
    !class_loader.is_null() &&
    parsed_name !
= null &&
    !strncmp((const char*)parsed_name->bytes()
, pkg, strlen(pkg))) {
  // it is illegal to define classes in the 
"java." package from
  // jvm_defineclass or jni_defineclass unless you're the bootclassloader
  resourcemark rm(thread)
;
  char* name = parsed_name->as_c_string();
  char* index = strrchr(name, '/');
  *index = '\0'; // chop to just the package name
  while ((index = strchr(name, '/')) != null) {
    *index 
= '.'; // replace '/' with '.' in package name
  }
  const char* fmt 
= "prohibited package name: %s";
  size_t len = strlen(fmt)   strlen(name);
  char* message = new_resource_array(char, len);
  jio_snprintf(message, len, fmt, name);
  exceptions::_throw_msg(thread_and_location,
    vmsymbols::java_lang_securityexception()
, message);
}
修改该文件以去除掉相关限制,并按照本系列的中介绍的方法去重新构建一个openjdk。那么,这个新的jdk将不会再对包名有任何限制了。

3. 覆盖java核心api?
开发者们在使用主流ide时会发现,如果工程有多个jar文件或源文件目录中包含相同的类,这些ide会根据用户指定的优先级顺序来加载这些类。比如,在eclipse中,右键点击某个java工程-->属性-->java build path-->order and export,在这里调整各个类库或源文件目录的位置,即可指定加载类的优先级。
当开发者在使用某个开源类库(jar文件)时,想对其中某个类进行修改,那么就可以将该类的源代码复制出来,并在java工程中创建一个同名类,然后指定eclipse优先加息自己创建的类。即,在编译时与运行时用自己创建的类去覆盖类库中的同名类。那么,是否可以如法炮制去覆盖java核心api中的类呢?
考虑去覆盖类java.util.hashmap,只是简单在它的put()方法添加一条打印语。那么就需要将src.zip中的java/util/hashmap.java复制出来,并在当前java工程中创建一个同名类java.util.hashmap,并修改put()方法,如下所示:
package java.util;

public class hashmap<k,v> extends abstractmap<k,v>
    
implements map<k,v>, cloneable, serializable {
    .
    
public v put(k key, v value) {
        system.out.printf(
"put - key=%s, value=%s%n", key, value);
        
return putval(hash(key), key, value, falsetrue);
    }
    
}
此时,在eclipse环境中,synchronizedhashmap使用的java.util.hashmap被认为是上述新创建的hashmap类。那么运行应用程序syncmaptest后的期望输出应该如下所示:
put - key=key, value=value
value
但运行syncmaptest后的实际输出却为如下:
value
看起来,新创建的java.util.hashmap并没有被使用上。这是为什么呢?能够"想像"到的原因还是类加载器。关于java类加载器的讨论超出了本文的范围,而且关于该主题的文章已是汗牛充栋,但本文仍会简述其要点。
java类加载器由下至上分为三个层次:引导类加载器(bootstrap class loader),扩展类加载器(extension class loader)和应用程序类加载器(application class loader)。其中引导类加载器用于加载rt.jar这样的核心类库。并且引导类加载器为扩展类加载器的父加载器,而扩展类加载器又为应用程序类加载器的父加载器。同时jvm在加载类时实行委托模式。即,当前类加载器在加载类时,会首先委托自己的父加载器去进行加载。如果父加载器已经加载了某个类,那么子加载器将不会再次加载。
由上可知,当应用程序试图加载java.util.map时,它会首先逐级向上委托父加载器去加载该类,直到引导类加载器加载到rt.jar中的java.util.hashmap。由于该类已经被加载了,我们自己创建的java.util.hashmap就不会被重复加载。
使用java命令运行syncmaptest程序时加上vm参数-verbose:class,会在窗口中打印出形式如下的语句:
[opened /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]
[loaded java.lang.object from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]

[loaded java.util.hashmap from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]
[loaded java.util.hashmap$node from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]

[loaded java.util.synchronizedhashmap from file:/home/ubuntu/projects/test/classes/]
value
[loaded java.lang.shutdown from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]
[loaded java.lang.shutdown$lock from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]
从中可以看出,类java.util.hashmap确实是从rt.jar中加载到的。但理论上,可以通过自定义类加载器去打破委托模式,然而这就是另一个话题了。


john jiang 2015-11-01 20:06
]]>
play openjdk: 构建你自己的jdk(原)http://www.blogjava.net/jiangshachina/archive/2015/10/30/427994.htmljohn jiangjohn jiangfri, 30 oct 2015 15:17:00 gmthttp://www.blogjava.net/jiangshachina/archive/2015/10/30/427994.htmlhttp://www.blogjava.net/jiangshachina/comments/427994.htmlhttp://www.blogjava.net/jiangshachina/archive/2015/10/30/427994.html#feedback0http://www.blogjava.net/jiangshachina/comments/commentrss/427994.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/427994.html
play openjdk: 构建你自己的jdk

计划使play openjdk成为一组介绍如何使用并参与openjdk项目的系列文章。本文是该系列的第一篇文章,它基于openjdk 8的源代码介绍了构建一个属于自己的jdk方法。(2015.10.30最后更新)

1. openjdk
曾经的sun microsystems. inc,也就是java语言的发明者,将它的jdk代码贡献出来,成立了一个开源项目,即。
    同时,它也是一个社区。相关的公司,组织和个人在这个社区中协作开发openjdk。社区根据不同的领域或项目提供了一系列的,利益相关方或对其感兴趣的个人都可以订阅这些邮件列表去进行关注和讨论。例如,java核心api的邮件列表是core-libs-dev@openjdk.java.net,关于java.lang,java.util等核心api的新特性都会在这里进行讨论,并对其最终的实现代码进行审查。任何将要进入openjdk版本库的源代码,无论是产品代码(即,要随jdk发布的程序),还是测试代码,都需要在社区中进行公开的代码审查。
    选择一个自己感兴趣的领域或项目,加入它的邮件列表,长期跟踪它的发展,看着专家们的讨论、争论推动jdk的演进,学习开发者们的api设计与代码实现,...,这些对自己的成长都是极有帮助的。也许,还能看到一些有趣的八卦;-)

2. 准备工作
在几种主流操作系统,linux(如ubuntu和fedora),windows(7和8),macos(lion和moutain lion),中都可以构建openjdk,具体的细节可以参见。
    本文选择使用ubuntu 14.04。坦白地说,选择使用ubuntu,实是因为在linux环境中构建openjdk非常简单。若在windows中进行构建,则需要安装visual studio c 编译器。macos?嗯,我没有mbp。可能更多人平时是使用windows,但安装一个linux也不麻烦。先安装免费的vm工具,再去下载ubuntu的,然后使用virtualbox去安装ubuntu。virtualbox简单易用,网上的相关资料也非常的多。
    openjdk的源文件版本库基于(hg),它是一个与git相似的分布式版本控制工具。在ubuntu中安装mercurial只需要执行命令
$ sudo apt-get install mercurial
    openjdk中各项目的源代码版本库的路径均在http://hg.openjdk.java.net/之下,在这里可以找到6,7,8,9和jigsaw的源代码。其中jdk 8的最新开发版本库路径为http://hg.openjdk.java.net/jdk8u/jdk8u-dev/。
    另外,构建openjdk时需要一个启动jdk,本文选择构建openjdk 8,那么启动jdk的版本应不低于7。

3. 下载源代码
克隆版本库,
$ hg clone http://hg.openjdk.java.net/jdk8u/jdk8u-dev/ jdk8-src
requesting all changes
adding changesets
adding manifests
adding file changes
added 
1570 changesets with 1958 changes to 141 files
updating to branch default
85 files updated, 0 files merged, 0 files removed, 0 files unresolved
进入源代码目录
$ cd jdk8-src/
openjdk的源代码版本库实际上包含多个独立的子版本库,需要执行如下脚本去分别下载各个子版本库的源代码,
$ sh get_source.sh
# repositories:  corba jaxp jaxws langtools jdk hotspot nashorn
                corba:   hg clone http://hg.openjdk.java.net/jdk8u/jdk8u-dev/corba corba
                 jaxp
:   hg clone http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jaxp jaxp
                corba
:   requesting all changes
                 jaxp
:   requesting all changes

4. 构建
调用configure进行预构建,其中的参数--with-boot-jdk用于指定启动jdk的路径。如果启动jdk的java命令已存在于path环境变量中,该参数可以忽略。
$ sh configure --with-boot-jdk=/path/to/boot/jdk
该过程会检测构建环境是否符合要求,如有问题,它会给出提示。比如,
configure: error: could not find x11 libraries. you might be able to fix this by running 'sudo apt-get install libx11-dev libxext-dev libxrender-dev libxtst-dev libxt-dev'.
此时根据提示安装所需要的库即可。再重新执行上述configure命令,可能还会提示缺少其它的库,那么再次根据提示进行安装。如此反复,只到预构建成功完成。
最后就是进行构建,直接执行如下命令,
$ make all
在此过程中可以会遇到一些警告,不必理会,耐心等待...完成后,会出现如下的汇总信息,
----- build times -------
start 2015-10-30 22:11:10
end   2015-10-30 22:52:54
00:01:01 corba
00:01:08 demos
00:06:49 docs
00:19:37 hotspot
00:01:47 images
00:00:35 jaxp
00:00:49 jaxws
00:08:23 jdk
00:01:09 langtools
00:00:25 nashorn
00:41:44 total
-------------------------
finished building openjdk for target 'all'
在当前路径下会生成一个build目录,构建好的jdk就在那里面。新jdk的具体路径类似于build/linux-x86_64-normal-server-release/images/jdk。可以执行如下命令去测试这个jdk,
$ build/linux-x86_64-normal-server-release/images/jdk/bin/java -version
openjdk version "1.8.0-internal"
openjdk runtime environment (build 1.8.0-internal-ubuntu_2015_10_30_22_07-b00)
openjdk 64-bit server vm (build 25.66-b00, mixed mode)


john jiang 2015-10-30 23:17
]]>
利用java se 8流处理数据ii(译)http://www.blogjava.net/jiangshachina/archive/2014/08/15/417011.htmljohn jiangjohn jiangfri, 15 aug 2014 11:57:00 gmthttp://www.blogjava.net/jiangshachina/archive/2014/08/15/417011.htmlhttp://www.blogjava.net/jiangshachina/comments/417011.htmlhttp://www.blogjava.net/jiangshachina/archive/2014/08/15/417011.html#feedback2http://www.blogjava.net/jiangshachina/comments/commentrss/417011.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/417011.html
利用java se 8流处理数据
-- 结合stream api的高级操作去表示富数据处理查询

本文是 201405/06刊中的一篇文章,也是文章系列"利用java se 8流处理数据"中的第二篇,它基于flatmap()和collect()介绍了java流的高级用法(2014.08.15最后更新)

在本系列的第一篇文章中,你看到了java流让你能够使用与数据库操作相似的方法去处理集合。作为一个复习,清单1的例子展示了如何使用stream api去求得大交易的金额之和。我们组建了一个管道,它由中间操作(filter和map)与最终操作(reduce)构成,图1形象地展示它。
清单1
int sumexpensive =
        transactions.stream()
        .filter(t -> t.getvalue() > 1000)
        .map(transaction::getvalue)
        .reduce(0, integer::sum);
图1

然而在系列的第一部分中,并没有研究这两个方法:
flatmap:这是一个中间操作,它允许将一个"map"和一个"flatten"操作结合在一起
collect:这是一个最终操作,它依据不同的方式,将流中的元素归集为一个结果。
这两个方法对于表达更为复杂的查询是十分有用的。例如,你可以将flatmap和collect结合起来,生成代表一个文字流中每个字母出现的次数的map对象,如清单2所示。如果第一次看到这段代码觉得很惊奇时,但请不要担心。本文的目的就是要解释并探究这两个方法更多的细节。
清单2
import static java.util.function.function.identity;
import static java.util.stream.collectors.*;

stream
<string> words = stream.of("java""magazine""is""the""best");
map
<string, long> lettertocount =
        words.map(w 
-> w.split(""))
        .flatmap(arrays::stream)
        .collect(groupingby(identity(), counting()));
清单2中的代码将会生成如清单3示的结果。棒极了,不是吗?让我们开始探究flatmap和collect方法是如何工作的。
清单3
[a:4, b:1, e:3, g:1, h:1, i:2, ..]

flatmap方法
假设你想找出文件中所有独一唯二的字。你会怎么做呢?
你可能认为这很简单;我们可以files.lines(),在前面的文章中已见过了这个方法,因为它会返回一个包含文件中所有行的流。然后我们就可以使用map方法将每一行拆分成字,最后再使用distinct方法去除重复的字。第一次尝试得到的代码可能如清单4所示。
清单4
files.lines(paths.get("stuff.txt"))
        .map(line 
-> line.split("\\s ")) // stream
        .distinct() // stream
        .foreach(system.out::println);
不幸的是,这段程序并不十分正确。如果运行它,会得到令人生疑的结果,与下面的输出有些类似:
[ljava.lang.string;@7cca494b
[ljava.lang.string;@7ba4f24f
...
我们的第一次尝试确实打印出了代表几个流对象的字符串。那发生了什么呢?该方法的问题是,传给map方法的lambda表达式返回的是文件中每一行的string数组(string[])。而我们真正想要的是一个表示文字的流的stream对象。
幸运的是,对于该问题有一个凯发天生赢家一触即发官网的解决方案,就是使用flatmap方法。让我们一步一步地看看如何得到正确的解决方法。
首先,我们需要字的流,而不是数组的流。有一个名为arrays.stream()的方法,它将使用一个数组作为参数,并生成一个流。请看清单5中的例子。
清单5
string[] arrayofwords = {"java""magazine"};
stream
<string> streamofwords = arrays.stream(arrayofwords);
让我们在前面的流管道中使用该方法,看看会发生什么(见清单6)。这个方案依然行不通。那是因为我们最终得到的是一组流的流(准确地说,就是stream>)。确切地是,我们首先将每一行转换为一个字的数组,然后使用方法arrays.stream()将每一个数组转换成一个流。
清单6
files.lines(paths.get("stuff.txt"))
       .map(line 
-> line.split("\\s ")) // stream
       .map(arrays::stream) // stream>
       .distinct() // stream>
       .foreach(system.out::println);
我们使用flatmap()方法去解决这个问题,如清单7所示。使用flatmap()方法能够用流中的内容,而不是流去替换每一个生成的数组。换言之,通过map(arrays::stream)方法生成的全部独立的流被合并或"扁平化"为一个流。图2形象地展示了使用flatmap()方法的效果。
清单7
files.lines(paths.get("stuff.txt"))
       .map(line 
-> line.split("\\s ")) // stream
       .flatmap(arrays::stream) // stream
       .distinct() // stream
       .foreach(system.out::println);
本质上,flatmap让你可以使用其它流去替换另一个流中的每个元素,然后再将所有生成的流连合并为一个流。
请注意,flatmap()是一个通用的模式,在使用optaional或completablefuture时,你还会看到它。

collect方法
现在让我们看看collect方法的更多细节。在本系列的第一篇文章中你所看到的方法,要么返回另一个流(即,这些方法是中间操作),要么返回一个值,例如一个boolean,一个int,或一个optional对象(即,这些方法是最终操作)。
collect就是一个最终方法,但它有点儿不同,因为你可以用它将一个stream对象转为一个list对象。例如,为了得到一个包含有所有高金额交易id的列表,你可以使用像清单8那样的代码。
清单8
import static java.util.stream.collectors.*;

list
<integer> expensivetransactionsids =
        transactions.stream()
        .filter(t 
-> t.getvalue() > 1000)
        .map(transaction::getid)
        .collect(tolist());
传递给collect方法的参数就是一个类型为java.util.stream.collector的对象。这个collector对象是干什么的?本质上看,它描述了如何按照需要去收集流中的元素,再将它们生成为一个最终结果。之前用到的工厂方法collector.tolist()会返回一个collector对象,它描述了如何将一个stream对象归集为一个list对象。而且,collctors内建有有许多相似的方法。例如,使用toset()方法可以将一个stream对象转化为一个set对象,它会删除所有重复的元素。清单9中的代码展示了如何生成一个仅仅包含高金额交易所在城市的set对象。(注意:在后面的例子中,我们假设collectors类中的工厂方法都已通过语句import static java.util.stream.collectors.*被静态引入了)
清单9
set<string> cities =
        transactions.stream()
        .filter(t 
-> t.getvalue() > 1000)
        .map(transaction::getcity)
        .collect(toset());
注意,无法保证会返回何种类型的set对象。但是,通过使用tocollection(),你可以进行更多的控制。例如,若你想得到一个hashset,可以传一个构造器给tocollection方法(见清单10)。
清单10
set<string> cities =
        transactions.stream()
        .filter(t 
-> t.getvalue() > 1000)
        .map(transaction::getcity)
        .collect(tocollection(hashset::
new));
然而,这并不是你能用collect和collector所做的全部事情。实际上,这只是你能用它们所做的事情中的极小部分。下面是一些你所能表达的查询的例子:
将交易按货币分类,并计算每种货币的交易金额之和(返回一个map对象)
将交易划分成两组:高金额交易和非高金额交易(返回一个map>对象)
创建多层分组,例如先按交易发生的城市分组,再进一步按它们是否为高金额交易进行分组(返回一个map>>)
兴奋吗?很好。让我们看看,你是如何使用stream api和collector来表达上述查询的。我们首先从一个简单的例子开始,这个例子要对这个流进行"总结":计算它的平均值,最大值和最小值。然后我们再看看如何表达简单的分组,最后,再看看如何将collector组合起来去创建更为强大的查询,例如多层分组。
总结。让我们用一些简单的例子来热身一下。在之前的文章中,你已经看到如何使用reduce方法去计算流中元素的数量,最小值,最大值和平均值,以及如何使用基本数据类型元素的流。有一些预定义的collector类也能让你完成那些功能。例如,可以使用counting()方法去计算元素的数量,如清单11所示。
清单11
long howmanytransactions = transactions.stream().collect(counting());
你可以使用summingdouble(),summingint()和summinglong()分别对流中元素类型为double,int或long的属性求和。在清单12中,我们计算出了所有交易的金额之和。
清单12
int totalvalue = transactions.stream().collect(summingint(transaction::getvalue));
类似的,使用averagingdouble(),averagingint()和averaginglong()去计算平均值,如清单13所示。
清单13
double average = transactions.stream().collect(averagingint(transaction::getvalue));
另外,使用maxby()和minby()方法,可以计算出流中值最大和最小的元素。但这里有一个问题:你需要为流中元素定义一个顺序,以能够对它们进行比较。这就是为什么maxby()和minby()方法使用使用一个comparator对象作为参数,图3表明了这一点。
图3

在清单14的例子中,我们使用了静态方法comparing(),它将传入的函数作为参数,从中生成一个comparator对象。该函数用于从流的元素中解析出用于进行比较的关键字。在这个例子中,通过使用交易金额作为比较的关键字,我们找到了那笔最高金额的交易。
清单14
optional<transaction> highesttransaction =
        transactions.stream()
        .collect(maxby(comparing(transaction::getvalue)));
还有一个reducing()方法,由它产生的collector对象会把流中的所有元素归集在一起,对它们重复的应用同一个操作,直到产生结果。该方法与之前看过的reduce()方法在原理上一样的。例如,清单15展示了使用了基于reducing()方法的另一种方式去计算所有交易的金额之和。
清单15
int totalvalue = transactions.stream().collect(reducing(0, transaction::getvalue, integer::sum));
reducing()方法使用三个参数:
初始值(如果流为空,则返回它);此处,该值为0。
应用于流中每个元素的函数对象;此处,该函数会解析出每笔交易的金额。
将两个由解析函数生成的金额合并在一起的方法;此处,我们只是把金额加起来。
你可能会说,"等等,使用其它的流方法,如reduce(),max()和min(),我已经可以做到这些了。那么,你为什么还要给我看这些方法呢?"后面,你将会看到我们将collector结合起来去构建更为复杂的查询(例如,对加法平均数进行分组),所以,这也能更易于理解这些内建的collector。
分组。这是一个普通的数据库查询操作,它使用属性去数据进行分组。例如,你也许想按币种对一组交易进行分组。若你使用如清单16所示的代码,通过显式的遍历去表达这个查询,那会是很痛苦的。
清单16
map<currency, list<transaction>> transactionsbycurrencies = new hashmap< >();
for(transaction transaction : transactions) {
    currency currency 
= transaction.getcurrency();
    list
<transaction> transactionsforcurrency =
    transactionsbycurrencies.get(currency);

    
if (transactionsforcurrency == null) {
        transactionsforcurrency 
= new arraylist<>();
        transactionsbycurrencies.put(currency, transactionsforcurrency);
    }
    transactionsforcurrency.add(transaction);
}
你首先需要创建一个map对象,它将收集所有的交易记录。然后,你需要遍历所有的交易记录,并解析出每笔交易的币种。在将交易记录使用一个值插入map中之前,需要先检查一下,这个list是否已经创建过了,等等。
真是令人汗颜啊,因为我们想要是"按币种对交易进行分组"。为什么不得不涉及这么多代码呢?有好消息:有一个称为groupingby()的collector,它允许我们以简洁的方式来表达这个例子。我们可以使用清单17中的例子来表达这个相同的查询,现在代码的阅读更接近问题语句了。
清单17
map<currency, list<transaction>> transactionsbycurrencies =
        transactions.stream().collect(groupingby(transaction::getcurrency));
工厂方法groupingby()使用一个函数对象作为参数,该函数会解析出用于分类交易记录的关键字。我们称为这个函数为分类函数。在此处,为了按币种对交易进行分组,我们传入一个方法引用,transaction::getcurrency。图4演示了这个分组操作。
图4

分割。有一个称为partitioningby()的工厂方法,它可被视为一种特殊的groupingby()方法。它使用一个谓语作为参数(该参数返回一个boolean值),然后根据元素是否满足这个谓语对它们进行分组。换言之,它将组成流的交易分割成了结构map>。例如,如若你想将交易分割成两组--低廉的和昂贵的--你就可以像清单18那样去使用partitioningby()产生的collector。此例中的lambda表达式,t->t.getvalue() > 1000,就是一个将交易分成低廉和昂贵的谓语。
清单18
map<boolean, list<transaction>> partitionedtransactions =
        transactions.stream().collect(partitioningby(t 
-> t.getvalue() > 1000));
组合collector。如果你熟悉sql,你应该知道可以将group by与函数count()和sum()一块儿使用,以按币种和交易金额之和进行分组。那么,使用stream api是否也可以实现相似的功能呢?当然可以。确切地说,有一个重载的groupingby()方法,它使用另一个collector作为第二个参数。这个额外的collector对象用于定义在使用由groupingby()产生的collector时如何汇集所有与关键字相关的元素。
好吧,这听起来有些抽象,那么让我们看一个简单的例子。我们想基于每个城市的交易金额之和生成一个城市的map对象(见清单19)。在此处,我们告诉groupingby()方法使用getcity()方法作为分类方法。那么,得到的map结果的key就为城市。正常地,我们期望对map中每个键所对应的值,即list对象,使用groupingby()方法。
清单19
map<string, integer> citytosum =
        transactions.stream().collect(groupingby(transaction::getcity,
        summingint(transaction::getvalue)));
然后,我们却是传入了另一个collector对象,它由summingint()方法产生,该方法会将所有与特定城市相关的交易记录的金额加起来。结果,我们得到了一个map对象,它将每个城市与它们对应的所有交易的金额之和进行了映射。酷,不是吗?想想这个:基本的groupingby(transaction:getcity)方法其实就只是groupingby(transaction:getcity, tolist())的简写。
让我们看看另一个例子。如果你想生成这样一个map,它对每个城市与它的最大金额的交易记录进行映射,那要怎么做呢?你可能已经猜到了,我们可以重用前面过的由maxby()方法产生的collector,如清单20所示。
清单20
map<string, optional<transaction>> citytohighesttransaction =
        transactions.stream().collect(groupingby(transaction::getcity,
        maxby(comparing(transaction::getvalue))));
你已经看到stream api很善于表达,我们正在构建的一些十分有趣的查询都可以写的简洁些。你还能想象出回到从前去遍历地处理一个集合吗?让我们看一个更为复杂的例子,以结束这篇文章。你已看到groupingby()方法可以将一个collector对象作为参数,再根据进一步的分类规则去收集流中的元素。因为groupingby()方法本身得到的也是一个collector对象,那么通过传入另一个由groupingby()方法得到的collector对象,该collector定义了第二级的分类规范,我们就能够创建多层次分组。
在清单21的代码中,先按城市对交易记录进行分组,再进一步对每个城市中的交易记录按币种进行分组,以得到每个城市中每个币种的所有交易记录的平均金额。图5就形象地展示了这种机制。
清单21
map<string, map<currency, double>> citybycurrencytoaverage =
        transactions.stream().collect(groupingby(transaction::getcity,
        groupingby(transaction::getcurrency,  
        averagingint(transaction::getvalue))));
图5

创建你自己的collector。到目前为止,我们展示的全部collector都实现了接口java.util.stream.collector。这就意味着,你可以实现自己的collector,以"定制"归一操作。但是对于这个主题,再写一篇文章可能更合适一些,所以我们不会在本文中讨论这个问题。

结论
在本文中,我们探讨了stream api中的两个高级:flatmap和collect。它们是可以加到你的兵器库中的工具,可以用来表述丰富的数据处理查询。
特别地,你也已经看到了,collect()方法可被用于归纳,分组和分割操作。另外,这些操作还可能被结合在一起,去构建更为丰富的查询,例如"生产一个两层map对象,它代表每个城市中每个币种的平均交易金额"。
然而,本文也没有查究到所有的内建collector实现。请你去看看collectors类,并试试其它的collector实现,例如由mapping(),joining()和collectingandthen(),也许你会发现它们也很有用。

john jiang 2014-08-15 19:57
]]>
利用java se 8流处理数据(i)(译)http://www.blogjava.net/jiangshachina/archive/2014/07/27/416235.htmljohn jiangjohn jiangsun, 27 jul 2014 12:54:00 gmthttp://www.blogjava.net/jiangshachina/archive/2014/07/27/416235.htmlhttp://www.blogjava.net/jiangshachina/comments/416235.htmlhttp://www.blogjava.net/jiangshachina/archive/2014/07/27/416235.html#feedback6http://www.blogjava.net/jiangshachina/comments/commentrss/416235.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/416235.html
利用java se 8流处理数据
-- 使用java流操作去表达复杂的数据查询

本文是 201403/04刊中的一篇文章,也是文章系列"利用java se 8流处理数据"中的第一篇,它概述了java流的基本原理与基本应用,是一篇很好的java streams api的入门文章。(2014.07.27最后更新)

    没有集合对象,你会怎么样?几乎每个java应用都会创建并处理集合。它们是许多编程任务的基础:集合使你能够对数据进行分组和处理。例如,你也许会创建一个关于银行交易的集合,该集合代表了某个用户的银行流水清单。然后,你可能想要处理整个集合去算出该用户花了多少钱。尽管数据处理十分重要,但java在此方面表现的远不完美。
    首先,典型的集合处理模式与类sql操作相似,诸如"查找"(如找出最大金额的那笔交易)或者"分组"(如,将与购买杂货相关的交易进行分组)。大部分数据库允许你以声明的形式去指定这些操作。例如,后面的sql查询会让你找到那笔最大金额交易的id:"select id, max(value) from transctions"。
    如你所见,我们并不需要去实现如何计算最大值(例如,使用循环和一个变量去追踪这个最大值)。我们仅需要表达什么是我们想要的。这种基本思想就意味着,你不太需要担心去显式地实现这些查询--它们已经为你处理好了。为什么我们不能在处理集合时也这样做呢?你发现自己有多少次都是在一遍又一遍地使用循环去重复实现这些操作呢?
    其次,我们如何才能更高效地去处理大型集合?理想情况下,在加速进行处理时,你会想到利用多核架构。然而,编写并行程序既困难又容易出错。
    java se 8赶来帮忙了!java api的设计者们在升级api时引入了一种新的称之为java流(流)的抽象,它允许你以声明形式去处理数据。另外,java流可以利用到多核架构而不必编写一行多线程代码。听起来不错,不是吗?这就是本文章系列所要探究的主题。
    java流能为我们做些什么呢?在探究这些细节之前,让我们先看一个例子,这样你才能对这种新的使用java se 8 java流的编程风格有感觉。假设我们要找到所有类型为grocery的交易并返回它们的id列表,并按交易金额的递减顺序对该列表进行排序。在java se 7中,我们应该会把清单1所示的程序那样去做。而在java se 8中,我们则会像清单2所示的那样去实现。
清单1
list<transaction> grocerytransactions = new arraylist<>();
for(transaction t: transactions){
  
if(t.gettype() == transaction.grocery){
    grocerytransactions.add(t);
  }
}
collections.sort(grocerytransactions, 
new comparator(){
  
public int compare(transaction t1, transaction t2){
    
return t2.getvalue().compareto(t1.getvalue());
  }
});
list
<integer> transactionids = new arraylist<>();
for(transaction t: grocerytransactions){
  transactionsids.add(t.getid());
}

清单2
list<integer> transactionsids =
    transactions.stream()
                .filter(t 
-> t.gettype() == transaction.grocery)
                .sorted(comparing(transaction::getvalue).reversed())
                .map(transaction::getid)
                .collect(tolist());

    图1形象地解释了那段java se 8程序。首先,我们调用list对象中的java流()方法从交易列表(数据)中获取一个java流对象。然后,多个操作(过滤,排序,映射,归集)链接在一起形成了一条线,这条线可以被看作构成了一条数据查询。


    那么如何并行地执行该程序呢?在java se 8中这很简单:只需要使用paralleljava流()方法去替换java流()方法,如清单3所示。java流 api会在内部对你的查询进行解构,并利用上你机器中的多核处理器。
清单3
list<integer> transactionsids =
    transactions.parallelstream()
                .filter(t 
-> t.gettype() == transaction.grocery)
                .sorted(comparing(transaction::getvalue).reversed())
                .map(transaction::getid)
                .collect(tolist());

    在该关于java se 8 java流的文章系列结束时,你将能够使用java流 api编写出像清单3那样的功能强大的查询程序。

java流入门
    让我们先从一点理论开始。java流的定义是什么?一个简短的定义就是"来自于一个数据源的能够支持聚合操作的一串元素"。让我们把它拆开来说:
    一串元素:java流为一串特定类型值的集合提供了一个接口。然后,java流实际上并不存储元素,它们会在需要时被用上。
    数据源:java流要使用一个提供数据的源,诸如集合对象,数组或i/o资源。
    聚合操作:java流支持类sql的操作,以及来自于函数编程语言的通用操作,诸如过滤,映射,归一,查找,匹配,排序,等等。
    另外,与集合操作非常不同的是,java流操作拥有两项基本特质:
    管道:许多java流操作会返回它们自己,这就使得这些操作能够链接在一起以组成一个大型管道。这样就可以进行一些诸如惰性和短路之类的优化,后面我们会进行探究。
    内部遍历:集合是显式地进行遍历(外部遍历),但不同于集合,java流是在幕后进行遍历。让我们重新看看之前的示例代码来解释这些原理。图2形象地解释了清单2的更多细节。


    首先,通过调用java流()方法,我们从交易列表中得到了一个java流对象。那么数据源就是交易列表,它将向java流中提供一串元素。然后,我们对该java流应用了一系列的聚合操作:过滤(提供一个谓语去过滤元素),排序(提供一个比较器去对元素进行排序),以及映射(解析出信息)。所有的操作都会返回该java流,以便能够链接这些操作去组成一个管道,这可被看作是对数据源的一个查询。
    在调用collect()操作之前,没有实际工作会被执行。collect()方法将开始处理这个管道以返回一个结果(某个不是java流的对象,在此处,是一个list对象)。现在还不需要去关注collect()方法,我们会在以后的文章去一探究竟。此时,你会发现collect会将各种数据加工方法作为参数,将收集到的java流元素归结为一个结果。此处,tolist()就描述了一个将java流对象转化为list对象的加工方法。
    在探究与java流有关的各个方法之前,最好是停下来深入思考一下java流和集合之间观念上的不同之处。

java流 vs. 集合
    已有的java集合概念与新的java流概念都为一串元素提供了接口。那它们有何不同吗?简单地说,集合是关于数据的,而java流是关于计算的。
    想想这种情况,一部存储在dvd中的电影。这就是一个集合(可能是字节,可能是帧--在此处,我们不必关心这些),因为它包含有全部的数据结构。现在再想想这种情况,这部电影被转化成了数据流,通过互联网去观看它。此时它就是一个(字节或帧的)流。流视频播放器只需要下载一些晚于用户当前所观看位置的帧就可以了。这样,你就可以在大部分值被计算出来之前先展示流开头处的值(想想流化一场现场直播的足球比赛)。
    粗看之,集合与流的区别就是与何时处理数据有关。集合是内存中的数据结构,它包含有当前数据结构中的全部值--将所有元素加入到集合之前,必须先对所有元素进行处理,相反地,java流只是逻辑上固定的数据结构,它里面的元素只会根据需要进行处理。
    使用collection接口,要求用户实现遍历(例如,使用增强的for循环,即foreach);这被称之为外部循环。相反地,stream类库使用内部遍历--它已经为你实现好了遍历,它会关心存储流的结果值的位置;你仅需要提供一个函数告诉它要做些什么就行了。清单4(对集合的外部遍历)和清单5(对java流的内部遍历)中的代码形象地展示了这一不同之处。
清单4
list> transactionids = new arraylist<>();
for(transaction t: transactions){
    transactionids.add(t.getid());
}

清单5
list<integer> transactionids =
    transactions.stream()
                .map(transaction::getid)
                .collect(tolist());

    在清单4中,我们显式且顺序地遍历了交易列表,抽取了每个交易id,然后将它加到一个收集器中。相反地,当使用流时,没有显式的遍历。清单5中的代码构建了一个查询,其中的map操作被设定为一个参数,它会抽取交易id,然后collect操作会把结果stream对象转化成一个list对象。
你现在应该知道什么是java流,以及如何去使用它。现在让我们看看java流所支持的操作之间的区别,这样你就能构建自己的数据查询了。

java流操作:使用流去处理数据

    java.util.stream.stream接口定义了许多操作,它们可被归集为两类。在图1所示的例子中,你可以看到如下操作:
    过滤,排序和映射,它们可被连接在一起组成一个管道
    收集,它关闭了这个管道并返回结果
    能够被连接在一起的java流操作被称为中间操作。这些操作之所以能被连接在一起,是因为它们都会返回stream对象。这些操作从这个管道中返回结果,结果的类型可以是list,integer,甚至是void(任何stream以外的类型)
    你也许很好奇为什么这种区别很重要。是这样的,在这个java流管道的最终操作被调用之前,中间操作并不会执行任何处理;它们是"惰性"方法。这是因为中间方法经常会被"合并",在最终操作中它们会被合成为单一的执行路径。
清单6
list<integer> numbers = arrays.aslist(12345678);
list
<integer> twoevensquares =
    numbers.stream()
           .filter(n 
-> {
                    system.out.println(
"filtering "  n);
                    
return n % 2 == 0;
                  })
           .map(n 
-> {
                    system.out.println(
"mapping "  n);
                    
return n * n;
                  })
           .limit(
2)
           .collect(tolist());

    例如,思考下清单6中的程序,它是从给定的数列中计算奇数的平方。你可能会很惊讶,它打印出如下结果:
filtering 1
filtering 
2
mapping 
2
filtering 
3
filtering 
4
mapping 
4

    这是因为limit(2)使用了短路;我们只需要处理流的一部分,而不是全部,去得到一个结果。这就类似于测评一个由and操作符关联起来的大型布尔表达式链:一旦某个表达式返回了false,那么就可以认为整个表达式链就是false,而不必测评所有的表达式了。在这个例子中,limit()方法将返回的java流的长度限定为2。另外,filter与map被合并在了同一条执行路径中了。

    归纳一下到目前为止,在使用java流时我们所学到的内容,总言之,涉及三个方面:
    一个数据源(例如一个集合),对它执行查询
    一个中间操作的链,它组成一个流的管道
    一个最终操作,它执行流的管道并产生结果
    现在让我们看看java流所支持的一些操作。参考java.util.stream.stream接口可以得到这些方法的完整清单,再看看本文末尾所给出的资源,它包含有更多的例子。

    过滤。有多种方法可以对流中的元素进行过滤:
    filter(predicate):使用一个谓语(java.util.function.predicate)作为参数,它会返回一个包含所有匹配给定谓语条件元素的java流。
    distinct:返回一个包含有唯一元素的java流。
    limit(n):返回的流的长度不能超过n。
    skip(n):返回的流将不包括前n个元素。

    查找与匹配。一个通用的数据处理模式就要考虑一些元素是否匹配给定的属性。你可以使用anymatch,allmatch和nonematch方法帮你做到这一点。这些方法都会使用一个谓语参数并返回boolean值作为结果(所以,它们是最终操作)。例如,使用allmatch去查出交易流中所有金额大于100的交易,如清单7所示。
清单7
boolean expensive =
    transactions.stream()
                .allmatch(t 
-> t.getvalue() > 100);

    另外,stream接口提供了方法findfirst和findany,以取出流中任一元素。它们可以与其它的流操作,如filter,结合起来使用。findfirst和findany都会返回一个optinal对象(见清单8)。
清单8
optional<transaction> =
    transactions.stream()
                .findany(t 
-> t.gettype() == transaction.grocery);

    optional类(java.util.optional)是一个容器类,它代表一个存在或不存在的值。清单8中的程序,findany方法可能没有找到任何类型为grocery的交易。optional类包含多个方法去测试一个元素是否存在。例如,如果交易存在,通过使用ifpresent方法,我们可以选择一个操作去应用这个optaional对象,如清单9所示(此处只是打印交易)。
清单9
transactions.stream()
              .findany(t 
-> t.gettype() == transaction.grocery)
              .ifpresent(system.out::println);

    映射。java流支持map方法,它使用一个函数(java.util.function.function)作为参数,将流元素投影到其它形式。这个函数会被应用到每个元素,并将元素"映射"到新的元素。
    例如,你可能会想到使用它去抽取流中每个元素的信息。在清单10的例子中,我们返回了一个列表中每个字的长度。
清单10
list<string> words = arrays.aslist("oracle""java""magazine");
 list
<integer> wordlengths =
    words.stream()
         .map(string::length)
         .collect(tolist());

    归一。到目前为止,我们已见过的最终操作会返回boolean(allmatch等等),void(foreach)或optaional对象(findany等等)。我们也使用collect方法将stream对象中的所有元素放到一个list对象中。
    然而,你也可以将流中的元素放到一个查询中,该查询可表达更为复杂的数据处理,例如"拥有最大id"或者"算出所以交易金额的和"。这就可能对java流用上reduce方法,该方法会对每个元素重复地应用一个操作(例如,加上两个数字),直到生成结果。在函数式编程中,这常被称为折叠操作。因为该操作可被看作重复地"折叠"一张很长的纸(stream对象),直到这张纸的面积变得只有一点儿了。这就是折叠操作的结果。
    看看我们是如何使用循环去计算一个组数字的和会有助于理解这个问题:
int sum = 0;
for (int x : numbers) {
  sum 
= x;
}

    列表中的每一个数字元素都被迭代地组合在一起,并使用一个额外的操作符去产生结果。本质上,我们就是把一组数字"归一"成一个数字。在这段代码中有两个参数:数字和变量的初始值,即该例中的0,以及用于合并所有元素的操作符,即本例中的 。
清单11
int sum = numbers.stream().reduce(0, (a, b) -> a  b);

    对java流使用reduce方法,我们可以计算出流中的所有元素值之和,如清单11所示。reduce方法使用两个参数:
    初始值,0
    binaryoperation,合并两个元素,并产生一个新值
    reduce方法本质上就是重复应用模式的抽象。其它的查询,如"计算产量"或"计算最大值"(如清单12所示)则是reduce方法的特别实例。
清单12
int product = numbers.stream().reduce(1, (a, b) -> a * b);
int product = numbers.stream().reduce(1, integer::max);

数字流
    你已经看到可以使用reduce方法去计算整数流的和。然后,这也是有成本的:我们重复执行了许多拆箱操作以将integer对象加到一起。如果我们能调用一个sum方法,使程序的意图更为明显,就像清单13那样,岂不是更好?
清单13
int statement =
    transactions.stream()
                .map(transaction::getvalue)
                .sum(); 
// error since stream has no sum method

    java 8引入的三个特定的基本数据类型的流接口来应对这个问题--intstream,doublestream和longstream--它们专注于元素分别为int,double和long型的java流。将一个流转化为特定类型的流,你最常使用的方法就是maptoint,maptodouble和maptolong。这些方法与我们较早前看到的map方法是一样的,但它们会返回特定类型的stream对象,而不是stream对象。例如,我们可以改进下清单13中的代码,如清单14所示那样。你也可以使用装箱操作将一个基本数据类型的流转化成一个使用包装对象的流。
清单14
int statementsum =
    transactions.stream()
                .maptoint(transaction::getvalue)
                .sum(); 
// works!

    最后,数字流的另一种有用的形式是数字区间。比如,你可能想生成介于1到100之间的所有数字。为了帮助生成这种区间,java se 8在intstream,doublestream和longstream中分别引入了两个静态方法:range和rangeclosed。
    这两个方法都会使用两个参数,第一个参数是起始值,第二个参数是终止值。但是range方法生成的区间不会包含终止值本身,但rangeclosed生成的区间则会包含。清单15是一个使用rangeclosed方法的例子,它返回一个包含有全部介于10到30之间奇数的流。
清单15
intstream oddnumbers =
    intstream.rangeclosed(
1030)
             .filter(n 
-> n % 2 == 1);

构建流
    有多种途径可以去构建一个流。你已经看过如何从集合对象中构建流。另外,我们还操控过数字流。你也可以从值,数组或文件中去创建流。另外,你甚至于可以从一个函数中生成无限流。
    可以直截了当地从值或数组中创建流:只需要使用一些静态方法即可,对于值,是stream.of();而对于数组,则要调用arrays.stream()。如清单16所示。
清单16
stream<integer> numbersfromvalues = stream.of(1234);
int[] numbers = {1234};
intstream numbersfromarray 
= arrays.stream(numbers);

    你也可以将一个文件转化为其内容行的流,使用静态方法files.lines()即可。清单17就使用该方法计算了文件中行的数量。
清单17
long numberoflines =
    files.lines(paths.get(“yourfile.txt”), charset.defaultcharset())
         .count();

    无限流。最后,在总结本文之前,有一个令人非常兴奋的主意。到现在为止,你应该理解到流中的元素是按需生成的。有两个静态方法--stream.iterate()和stream.generate()--可以让你从一个函数中创建流。然而,因为被使用的元素是按需生成的,所以这两个方法可以"永远地"生成元素。这就是为什么我们称它为无限流:它就是没有固定大小的流,但它做的事情与一个从固定集合生成的流是一样的。
    清单18就是一个使用iterate方法的例子,它会包含10的所有倍数。iterate方法使用一个起始值(此处的0)和一个lambda表达式(类型为unaryoperator)去顺序地生成每一个新值。
清单18
stream<integer> numbers = stream.iterate(0, n -> n  10);

    我们也可以使用limit方法,以从一个无限流中得到一个固定流。如清单19所示,可以将流的长度限制为5。
清单19
numbers.limit(5).foreach(system.out::println); // 0, 10, 20, 30, 40

结论
    java se 8引入了streams api,它让你能够表达更为复杂的数据处理查询。在本文中,你已见到流可以支持许多操作,诸如过滤,映射,归一和迭代,把它们结合在一起可以写出简洁的、更富表现力的数据处理查询。这种新的编程方法远不同于java se 8之前的集合处理。但是,它有许多好处。首先,它利用到了诸如惰性或短路这样的技术,以优化数据处理查询的性能。其次,能够自动地利用上多核架构,以并行地处理流。在本文章系统的第二部分中,我们将探索更高级的操作,例如flatmap和collect。请继续关注。


john jiang 2014-07-27 20:54
]]>
2014年04月美国非农业就业情况报告(译)http://www.blogjava.net/jiangshachina/archive/2014/05/05/413246.htmljohn jiangjohn jiangsun, 04 may 2014 16:12:00 gmthttp://www.blogjava.net/jiangshachina/archive/2014/05/05/413246.htmlhttp://www.blogjava.net/jiangshachina/comments/413246.htmlhttp://www.blogjava.net/jiangshachina/archive/2014/05/05/413246.html#feedback1http://www.blogjava.net/jiangshachina/comments/commentrss/413246.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/413246.html
2014年04月美国非农业就业情况报告
本文是上周五(2014.05.02)才发布的中的概要部分,与软件技术毫无干系。这份报告在出台后的极短时间内对外汇市场造成了极大影响,纯粹是出于好奇看了一下这份报告,学到了不少英文行业名称,以及英文在数字方面的用法。(2014.05.05最后更新)

    今天,美国劳工统计署报告,4月份的全部非农业就业人数增加了288000,而且失业率降低了0.4个百分点,达到百分之6.3。就业增长的扩大受益于专业与商业服务业,零售业,餐饮业和建筑业。

家庭调查数据
    在4月份,失业率从百分之6.7降到了百分之6.3,失业人数则为980万,减少了73.3万。这两项统计展示了前4个月的动态。今年以来,失业率和失业人数分别减少了1.2个百分点和190万人。(详见表a-1)
    在主要的劳动者分组中,4月的失业率分别下降了:成年男子(5.9%),成年女子(5.7%),青少年(19.1%),白人(5.3%),黑人(11.6%),以及西班牙裔(7.3%)。亚裔的无业率为5.7%(未扣除季节性因素),今年以来,该值的变化很小。(详见表a-1,a-2和a-3)
    在4月里,再次失业者与新工作者分别减少了41.7万和12.6万。(再次失业者之前工作过,但在开始重新找工作之前他们并不算作劳动力;而新工作者是指之前从未工作过的人。)丢掉工作的和只完成临时工作的劳动者的数量减少了25.3万,达到520万。(详见表a-11)
    长期失业者(指失业时间达到或超过27周的人)的数量在4月份减少了28.7万,达到350万;这些个体占失业人数的35.3%。在过去的12个月里,长期失业者的数量已经减少了90.8万。(详见表a-12)
    随着民用劳动力在3月增长50.3万之后,民用劳动力在4月里减少了80.6万。在最近几个月里,就业参与率并没有显示出清晰的趋势,当前的值也去年10月的值是相同的。在过去这个月中,就业者与人口比例没有发生变化(58.9%),但在过一年中略有变化。(详见表a-1)
    出于经济原因的兼职劳动者(有时候也称之为不情愿的兼职劳动者)的数量在4月略有变化,达到750万。这些个体之所以兼职是由于他们的工作时间被削减,或者是他们无法找到全职工作。(详见表a-8)
    4月份有220万准待业劳动力,比今年早些时候有轻微下降(该数据未扣除季节性因素)。这些个体并不算作劳动力,尽管他们愿意并且也可以去工作,而且在前12个月的某些时候也找过工作。但是他们并不被计算在失业者中,因为在该调查开始的前4周内,他们并没有去找工作。(详见表a-16)
    在这些准待业者中,本月有78.3万就业信心丧失者,比今年早些时候略有变化(该数据未扣除季节性因素)。就业信心丧失者目前并没有去寻找工作,因为他们认为没有合适他们的工作。剩余的140万准待业者没有去寻找工作的原因则是诸如就学或家庭责任。(详见表a-16)

机构调查数据
    全部的非农业就业者在4月份增长了28.8万。在前12个月中,平均每个月增长19.9万个工作岗位。就业增长在4月里得到了扩大,这得益于专业与商业服务业,零售业,餐饮业和建筑业的就业增长。(详见表b-1)
    专业与商业服务业在4月份增加了7.5万个就业岗位。在前12个月内,该行业每个月会平均增加5.5万个就业。在本月,该行业的就业增长仍在继续,其中临时辅助服务业(2.4 万),公司与企业管理(1.2 万),计算机系统设计及其相关服务业(0.9 万)。
    零售业的就业岗位在本月增加了3.5万个。在过去的12个月中,该行业的就业已经增加了32.7万。在零售业中,本月的工作岗位增长发生在饮食商店(0.9 万),日用百货商店(0.8 万),汽车与零部件商(0.6 万)和非实体商店(0.4 万)。电子与电器商店在本月则丢失了1.1万工作岗位。批发业在过去一个月内增加了1.6万个工作岗位,而它在过去一年内增加了12.6万个工作岗位。
    餐饮业在本月增加的就业数(3.3 万)则与过去12个月的平均增长(2.8万/月)处于同一水平线上。
    建筑行业的就业在4月份增加了3.2万个,这些工作增长分别为重型和民用工程建筑业(1.1 万)和住宅建筑业(0.7 万)。在过去一年中,建筑业已经增加了18.9万个工作岗位,而且其中几乎四分之三的增长都发生在过去6个月中。
    医疗行业本月增加了1.9万个岗位,与过去12个月的平均增长(1.7万/月)水平相持平。包括会员协会,个人服务业与洗衣业等其它服务性行业的就业在这个月内增加了15000个。
    采矿业在4月份增加了10000个工作岗位,大部分的增长都是在针对采矿的支持活动中(7000 )。
    其它主要行业,包括制造业,运输与仓储业,信息服务业,金融服务,以及政府服务业,的就业在这个月内少有变化。
    全部私营非农业就业者的平均周工作时间没有变化,仍为34.5小时。制造业的周工作时间减少了0.2个小时,达到40.8个小时。工厂的加班时间没有变化,仍为3.5个小时。私营非农业就业者中的生产与非管理阶层员工的平均周工作时间也没有发生变化,依然为33.7个小时。(详见b-2和b-7)
    本月,私营非农业就业者的平均时薪仍然为24.31美元。在过去12个月中,平均时薪已经增长了1.9%。私营的生产与非管理阶层员工的平均时薪则增长了3%,达到20.50美元。(详见表b-3和b-8)
    二月份的全部的非农业就业岗位从19.7 万修正为22.2 万,而对于三月份的这一数据,则是从19.2 万修正为20.3 万。根据这些修正,二月份与三月份的就业增长比之前报告的要高出3.6万个。



john jiang 2014-05-05 00:12
]]>
java 8的语言变化(译)http://www.blogjava.net/jiangshachina/archive/2014/04/19/412695.htmljohn jiangjohn jiangsat, 19 apr 2014 15:48:00 gmthttp://www.blogjava.net/jiangshachina/archive/2014/04/19/412695.htmlhttp://www.blogjava.net/jiangshachina/comments/412695.htmlhttp://www.blogjava.net/jiangshachina/archive/2014/04/19/412695.html#feedback0http://www.blogjava.net/jiangshachina/comments/commentrss/412695.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/412695.html
java 8的语言变化
--理解lambda表达式和变化的接口类是如何使java 8成为新的语言
本文是ibm developerworks中的一篇介绍java 8关键新特性的,它主要关注lambda表达式和改进的接口。(2014.04.19最后更新)

    java 8包含了一组重要的新的语言特性,使你能够更方便地构造程序。lambda表达为内联的代码块定义了一种新的语法,给予你与匿名内部类相同的灵活性,但又没有那么多模板代码。接口的改变使得能够为已有接口加入新的特性,而不必打破现有代码的兼容性。了解这些语言变化是怎样一起工作的,请阅读本系列另一篇文章"java 8并发基础",可以看到如何在java 8流中使用lambda。
    java 8的最大改变就是增加了对lambda表达式的支持。lambda表达式一种通过引用进行传递的代码块。它类似于某些其它语言的闭包:代码实现了一个功能,可以传入一个或多个参数,还可以返回一个结果值。闭包被定义在一个上下文中,它可以访问(在lambda中是只读访问)上下文中的值。
    如果你不熟悉闭包,也不必担心。java 8的lambda表达式是几乎每个java开发者都熟悉的匿名内部类的一个高效版规范。如果你只想在一个位置实现一个接口,或是创建一个基类的子类时,匿名内部类为此提供了一种内联实现。lambda表达式也用于相同的方式,但是它使用一种缩略的语法,使得这些实现比一个标准的内部类定义更为简洁。
    在本文中,你将看到如何在不同的场景下使用lambda表达式,并且你会学到与java接口定义相关的扩展。在本文章的姊妹篇jvm并发系列的"java 8并发基础"一文中,可以看到更多使用lambda表达式的例子,包括在java 8流特性中的应用。

进入lambda
    lambda表达式就是java 8所称的函数接口的实现:一个接口只定义一个抽象方法。只定义一个抽象方法的限制是非常重要的,因为lambda表达式的语法并不会使用方法名。相反,该表达式会使用动态类型识别(匹配参数和返回类型,很多动态语言都这么做)去保证提供的lambda能够与期望的接口方法兼容。
    在清单1所示的简单例子中,一个lambda表达式被用来对name实例进行排序。main()方法中的第一个代码块使用一个匿名内部类去实现comparator接口,第二个语句块则使用lambda表达式。
清单1. 比较lambda表达式与匿名内部类
public class name {
    
public final string firstname;
    
public final string lastname;

    
public name(string first, string last) {
        firstname 
= first;
        lastname 
= last;
    }

    
// only needed for chained comparator
    public string getfirstname() {
        
return firstname;
    }

    
// only needed for chained comparator
    public string getlastname() {
        
return lastname;
    }

    
// only needed for direct comparator (not for chained comparator)
    public int compareto(name other) {
        
int diff = lastname.compareto(other.lastname);
        
if (diff == 0) {
            diff 
= firstname.compareto(other.firstname);
        }
        
return diff;
    }
    
}

public class namesort {
    
    
private static final name[] names = new name[] {
        
new name("sally""smith"),
        
    };
    
    
private static void printnames(string caption, name[] names) {
        
    }

    
public static void main(string[] args) {

        
// sort array using anonymous inner class
        name[] copy = arrays.copyof(names, names.length);
        arrays.sort(copy, 
new comparator<name>() {
            @override
            
public int compare(name a, name b) {
                
return a.compareto(b);
            }
        });
        printnames(
"names sorted with anonymous inner class:", copy);

        
// sort array using lambda expression
        copy = arrays.copyof(names, names.length);
        arrays.sort(copy, (a, b) 
-> a.compareto(b));
        printnames(
"names sorted with lambda expression:", copy);
        
    }
}
    在清单1中,lambda被用于取代匿名内部类。这种匿名内部类在应用中非常普遍,所以lambda表达式很快就赢得了java8程序员们的青睐。(在本例中,同时使用匿名内部类和lambda表达式去实现name类中的一个方法,以方便对这两种方法进行比较。如果在lambda中对compareto()方法进行内联的话,该表达式将会更加简洁。)

标准的函数式接口
    为了应用lambda,新的包java.util.function中定义了广泛的函数式接口。它们被归结为如下几个类别:
    函数:使用一个参数,基于参数的值返回结果。
    谓语:使用一个参数,基于参数的值返回布尔结果。
    双函数:使用两个参数,基于参数的值返回结果。
    供应器:不使用任何参数,但会返回结果。
    消费者:使用一个参数,但不返回任何结果。
多数类别都包含多个不同的变体,以便能够作用于基本数据类型的参数和返回值。许多接口所定义的方法都可被用于组合对象,如清单2所示:
清单2. 组合谓语
// use predicate composition to remove matching names
list<name> list = new arraylist<>();
for (name name : names) {
    list.add(name);
}
predicate
<name> pred1 = name -> "sally".equals(name.firstname);
predicate
<name> pred2 = name -> "queue".equals(name.lastname);
list.removeif(pred1.or(pred2));
printnames(
"names filtered by predicate:", list.toarray(new name[list.size()]));
    清单2定义了一对predicate变量,一个用于匹配名为sally的名字,另一个用于匹配姓为queue的名字。调用方法pred1.or(pred2)会构造一个组合谓语,该谓语先后使用了两个谓语,当它们中的任何一个返回true时,这个组合谓语就将返回true(这就相当于早期java中的逻辑操作符||)。list.removeif()方法就应用这个组合谓语去删除列表中的匹配名字。
    java 8定义了许多有用的java.util.function包中接口的组合接口,但这种组合并不都是一样的。所有的谓语的变体(doublepredicate,intpredicate,longpredicate和predicate)都定义了相同的组合与修改方法:and(),negate()和or()。但是function的基本数据类型变体就没有定义任何组合与修改方法。如果你拥有使用函数式编程语言的经验,那么你可能就发会发现这些不同之处和奇怪的忽略。

改变接口
    在java 8中,接口(如清单1的comparator)的结构已发生了改变,部分原因是为了让lambda更好用。java 8之前的接口只能定义常量,以及必须被实现的抽象方法。而java 8中的接口则能够定义静态与默认方法。接口中的静态方法与抽象类中的静态方法是完全一样的。默认方法则更像旧式的接口方法,但提供了该方法的一个实现。该方法实现可用于该接口的实现类,除非它被实现类覆盖掉了。
    默认方法的一个重要特性就是它可以被加入到已有接口中,但又不会破坏已使用了这些接口的代码的兼容性(除非已有代码恰巧使用了相同名字的方法,并且其目的与默认方法不同)。这是一个非常强大的功能,java 8的设计者们利用这一特性为许多已有java类库加入了对lambda表达式的支持。清单3就展示了这样的一个例子,它是清单1中对名字进行排序的第三种实现方式。
清单3. 键-提取比较器链
// sort array using key-extractor lambdas
copy = arrays.copyof(names, names.length);
comparator
<name> comp = comparator.comparing(name -> name.lastname);
comp 
= comp.thencomparing(name -> name.firstname);
arrays.sort(copy, comp);
printnames(
"names sorted with key extractor comparator:", copy);
    清单3首先展示了如何使用新的comparator.comparing()静态方法去创建一个基于键-提取(key-extraction) lambda的比较器(从技术上看,键-提取lambda就是java.util.function.function接口的一个实例,它返回的比较器的类型适用于类型t,而提取的键的类型r则要实现comparable接口)。它还展示了如何使用新的comparator.thencomparing()默认方法去组合使用比较器,清单3就返回了一个新的比较器,它会先按姓排序,再按名排序。
    你也许期望能够对比较器进行内联,如:
comparator<name> comp = comparator.comparing(name -> name.lastname)
    .thencomparing(name 
-> name.firstname);
但不幸地是,java 8的类型推导不允许这么做。为从静态方法中得到期望类型的结果,你需要为编译器提供更多的信息,可以使用如下任何一种形式:
comparator<name> com1 = comparator.comparing((name name1) -> name1.lastname)
    .thencomparing(name2 
-> name2.firstname);
comparator
<name> com2 = comparator.<name,string>comparing(name1 -> name1.lastname)
    .thencomparing(name2 
-> name2.firstname);
    第一种方式在lambda表达式中加入参数的类型:(name name1) -> name1.lastname。有了这个辅助信息,编译才能知道下面它该做些什么。第二种方式是告诉编译器要传递给function接口(在此处,该接口通过lambda表达式实现)中comparing()方法的泛型变量t和r的类型。
    能够方便地构建比较器以及比较器链是java 8中很有用的特性,但它的代价是增加了复杂度。java 7的comparator接口定义了两个方法(compare()方法,以及遍布于每个对象中的equals()方法)。而在java 8中,该接口则定义了18个方法(除了原有的2个方法,还新加入了9个静态方法和7个默认方法)。你将发现,为了能够使用lambda而造成的这种接口膨胀会重现于相当一部分java标准类库中。

像lambda那样使用已有方法
    如果一个存在的方法已经实现了你的需求,你可以直接使用一个方法引用对它进行传递。清单4展示了这种方法。
清单4. 对已有方法使用lambda

// sort array using existing methods as lambdas
copy = arrays.copyof(names, names.length);
comp 
= comparator.comparing(name::getlastname).thencomparing(name::getfirstname);
arrays.sort(copy, comp);
printnames(
"names sorted with existing methods as lambdas:", copy);
    清单4做着与清单3相同的事情,但它使用了已有方法。使用java 8的形为"类名:方法名"的方法引用语法,你可以使用任意方法,就像lambda表达式那样。其效果就与你定义一个lambda表达式去调用该方法一样。对类的静态方法,特定对象或lambda输入类型的实例方法(如在清单4中,getfirstname()和getlastname()方法就是name类的实例方法),以及类构造器,都可以使用方法引用。
    方法引用不仅方便,因为它们比使用lambda表达式可能更高效,而且为编译器提供了更好的类型信息(这也就是为什么在上一节的lambda中使用.thencomparing()构造comparator会出现问题,而在清单4却能正常工作)。如果既可以使用对已有方法的方法引用,也可以使用lambda表达式,请使用前者。

捕获与非捕获lambda
    你在本文中已见过的lambda表达式都是非捕获的,意即,它们都是把传入的值当作接口方法参数使用的简单lambda表达式。java 8的捕获lambda表达式则是使用外围环境中的值。捕获lambda类似于某些jvm语言(如scala)使用的闭包,但java 8的实现与之有所不同,因为来自在外围环境中的值必须声明为final。也就是说,这些值要么确实为final(就如同以前的java版本中由匿名内部类所引用的值),要么在外围环境中不会被修改。这一规范适用于lambda表达式和匿名内部类。有一些方法可以绕过对值的final限制。例如,在lambda中仅使用特定变量的当前值,你可以添加一个新的方法,把这些值作为方法参数,再将捕获的值(以恰当的接口引用这种形式)返回给lambda。如果期望一个lambda去修改外围环境中的值,那么可以用一个可修改的持有器类(holder)对这些值进行包装。
    相比于捕获lambda,可以更高效地处理非捕获lambda,那是因为编译能够把它生成为类中的静态方法,而运行时环境可以直接内联的调用这些方法。捕获lambda也许低效一些,但在相同上下文环境中它至少可以表现的和匿名内部类一样好。

幕后的lambda
    lambda表达式看起来像匿名内部类,但它们的实现方法不同。java的内部类有很多构造器;每个内部类都会有一个字节码级别的独立类文件。这就会产生大量的重复代码(大部分是在常量池实体中),类加载时会造成大量的运行时开销,哪怕只有少量的代码也会有如此后果。
    java 8没有为lambda生成独立的类文件,而是使用了在java 7中引入的invokedynamic字节码指令。invokedynamic作用于一个启动方法,当该方法第一次被调用时它会转而去创建lambda表达式的实现。然后,该实现会被返回并被直接调用。这样就避免了独立类文件带来的空间开销,以及加载类的大量运行时开销。确切地说,lambda功能的实现被丢给了启动程序。目前java 8生成的启动程序会在运行时为lambda创建一个新类,但在将来会使用不同的方法去实现。
    java 8使用的优化使得通过invokedynamic指令实现的lambda在实际中运行正常。多数其它的jvm语言,包括scala (2.10.x),都会为闭包使用编译器生成的内部类。在将来,这些语言可能会转而使用invokedynamic指令,以便利用到java 8(及其后继版本)的优化。

lambda的局限
    如在本文开始时我所提到的,lambda表达式总是某些特殊函数式接口的实现。你可以仅把lambda当作接口引用去传递,而对于其它的接口实现,你也可以只是把lambda当作这些特定接口去使用。清单5展示了这种局限性,在该示例使用了一对相同的(名称除外)函数式接口。java 8编译接受string::lenght来作为这两个接口的lambda实现。但是,在一个lambd表达式被定义为第一个接口的实例之后,它不能够用于第二个接口的实例。
清单5. lambda的局限
private interface a {
    
public int valuea(string s);
}
private interface b {
    
public int valueb(string s);
}
public static void main(string[] args) {
    a a 
= string::length;
    b b 
= string::length;

    
// compiler error!
    
// b = a;

    
// classcastexception at runtime!
    
// b = (b)a;

    
// works, using a method reference
    b = a::valuea;
    system.out.println(b.valueb(
"abc"));
}
    任何对java接口概念有所了解的人都不会对清单5中的程序感到惊讶,因为那就是java接口一直所做的事情(除了最后一点,那是java 8新引入的方法引用)。但是使用其它函数式编程语言,例如scala,的开发者们则会认为接口的这种限制是不自然的。
    函数式编程语言是用函数类型,而不是接口,去定义变量。在这些编程语言中会很普遍的使用高级函数:把函数作为参数传递给其它的函数,或者把函数当作值去返回。其结果就是你会得到比lambda更为灵活的编程风格,这包括使用函数去组合其它函数以构建语句块的能力。因为java 8没有定义函数类型,你不能使用这种方法去组合lambda表达式。你可以组合接口(如清单3所示),但只能是与java 8中已写好的那些接口相关的特定接口。仅在新的java.util.function包内,就特殊设定了43个接口去使用lambda。把它们加入到数以百计的已有接口中,你将看到这种方法在组合接口时总是会有严重的限制。
    使用接口而不是在向java中引入函数类型是一个精妙的选择。这样就在防止对java类库进行重大改动的同时也能够对已有类库使用lambda表达式。它的坏作用就是对java 8造成了极大的限制,它只能称为"接口编程"或是类函数式编程,而不是真正的函数式编程。但依靠jvm上其它语言,也包括函数式语言,的优点,这些限制并不可怕。

结论
    lambda是java语言的最主要扩展,伴着它们的兄弟新特性--方法引用,随着程序被移植到java 8,lambda将很快成为所有java开发者不可或缺的工具。当与java 8流结合起来时,lambda就特别有用。查看文章"",可以了解到将lambda和流结合起来使用是如何简化并发编程以及提高程序效率的。


john jiang 2014-04-19 23:48
]]>
java流的8个特性(译)http://www.blogjava.net/jiangshachina/archive/2014/02/28/410455.htmljohn jiangjohn jiangfri, 28 feb 2014 07:25:00 gmthttp://www.blogjava.net/jiangshachina/archive/2014/02/28/410455.htmlhttp://www.blogjava.net/jiangshachina/comments/410455.htmlhttp://www.blogjava.net/jiangshachina/archive/2014/02/28/410455.html#feedback0http://www.blogjava.net/jiangshachina/comments/commentrss/410455.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/410455.html
java流的8个特性
本文是稍早前java.net推荐的一篇,描述了java流的8个有用的特性。(2014.03.07最后更新)

lamba表达式是java8到目前为止最棒的特性。但我认为有一个秘密武器展示了lambda这一"语法糖"在提高代码可读性和可写性方面是何其的强大。当你在改进代码的表现力时,那么在对代码的理解方面你就上升到了新的境界,这能使最笨拙的工作变得简单。

是什么秘密武器呢?就是java stream api。最近我参与了一个在线比赛,就是在一个对性能要求较高的环境中简单地使用java stream。令我惊讶的是,这个api让编写主要的循环程序变得十分简单,而且能很好地适应我所做出的众多变化。下面就是我所学到的8个特性。

1. java流不需要lambda表达式
尽管这个api确实从lambda表达式中获准良多,但你并不必非得使用lambda。你可以回过去使用匿名内部类,但为什么要这么做呢?较可能的场景是,使用一个方法引用(例如integer::valueof),或者一个实例对象。使用方法引用可将复杂的多行逻辑置于循环体之外,就如你在优化一个hash set查找时所看到的。而实例对象可用于实现"四人帮"的策略模式。但请不要使用匿名内部类,除非你不得不这么做。

2. 窥入流内进行调试
你可以在流的任何位置放入你所想加进去的媒质,这个媒质称为peek。该操作使用了一个消费者对象,并期望不产生任何结果,因为lambda一般只返回空。我喜欢把peek用于向系统发送调试信息,就如
.peek(system.out::println)
.peek(it 
-> system.out.printf("it is %s%n", it)

3. 流化随机成员
流并不局限于集合或数组,甚至是固定链表。如果你能创建一个iterator或supplier lambda来创建流中的值,然后你就可以使用类java.util.stream.streamsupport中的方法来创建一个流了。可以设想一个使用持续测量值,如内存消耗量或网络吞量,来驱动的流。

4. 流化随机数
如果你正在寻找一个简单的随机数,例如可以通过java.util.random,这个类现在有了三个新的set方法ints(),longs()和doubles()来创建流。这些方法的重载版本可以让你设置边界,随机种子以及流中随机数的总量。

5. 流化i/o reader
java程序员的另一个常见工作就是一行一行地解析文件。现在java.io.bufferedreader有了一个新方法lines(),它会将i/o流转化为一个字符串流,以便于流的处理。

6. 流化文件树
如果访问的文件并不是你的菜,那就试试访问一个文件树会怎么样?类java.nio.file.files中有几个方法都可以返回流。list()方法将列出一个目录下的所有文件,walk()方法将会递归地做到这一点,而filter()方法也会递归地访问这些文件,但会使用一些属性来进行过滤(当你有一个path对象,有些事情会变得复杂起来)。你依然可以使用lines(path)方法来通过流去获取内容。

7. 流化复杂文本
如果你依然念念不忘文本处理,但内容并不是基于行,那么就可以在java.util.regex.pattern实例中使用splitasstream(charsequence)方法。这对于处理有数百万列的csv文件或classpath十分有用。

8. 流化zip文件
说到对长classpath的搜索,你也可以很简单地调用名为stream的方法来流化java.util.zip.zipfiles和java.util.jar.jarfiles,它会相应地返回一个zipentry或jarentry实例。

如果你都已经干过这些事了,那么你肯定知道它们并不是java流的基本用途。不过将来会有足够多的博文去涉及java流的基础。我只是认为上述这些都是被掩藏起来的宝藏,它们揭示了java流的潜质。

john jiang 2014-02-28 15:25
]]>
使用gradle构建java web应用(译)http://www.blogjava.net/jiangshachina/archive/2014/01/23/409285.htmljohn jiangjohn jiangthu, 23 jan 2014 13:22:00 gmthttp://www.blogjava.net/jiangshachina/archive/2014/01/23/409285.htmlhttp://www.blogjava.net/jiangshachina/comments/409285.htmlhttp://www.blogjava.net/jiangshachina/archive/2014/01/23/409285.html#feedback8http://www.blogjava.net/jiangshachina/comments/commentrss/409285.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/409285.html
使用gradle构建java web应用
本文是发布在上的一篇摘自于一书中的,介绍了使用构建java web应用的过程。刚刚接触gradle,看到了这篇小文,随手译了出来:-) (2014.01.23最后更新)

当今世界,一派繁忙。在职业生涯和私人生活中,我们中间的许多人要同时管理多个项目。你可能常常发现自己处于不知所措及失控的状态。保持规整并专注于价值的关键是一个维护良好的工作清单。当然,你可能总是把你的任务写在一张纸上,但是你也许不可能在你所处的任何地方都可方便地获得这些工作条目?对互联网的访问几乎是无处不在的,无论是通过你的移动电话,还是公共的网络接入点。在一书中,如图1所示的说明性示例是一个很有吸引力的可视化web应用。

图1 to do应用可以通过互联网进行访问,并使用它去管理数据存储中的工作条目


gradle插件表现的如同一个使能器,它会自动地执行这些任务。一个插件通过引入特定领域的规范以及对缺省值敏感的任务去对工程进行扩展。随gradle发布的插件之一就是java插件。该java插件绝不仅仅是提供了源码编译和打包这样的基础功能。它为工程建立了一整套标准的目录布局,它会确保以正确的顺序去执行任务,这样,这些任务在java工程环境中才是有意义的。现在是时候为我们的应用去创建一个构建脚本并去使用这个java插件了。

构建java应用
一开始,每个gradle工程都会创建一个名为build.gradle的构建脚本。为了创建该脚本,并告诉该工程使用java插件,应该像这样去做:
apply plugin: 'java'
为了构建你的java代码,一行代码就够了。但gradle怎么知道去哪儿找你的源文件呢?该java插件引入的规范之一就是源代码的路径。默认地,该插件会到目录src/main/java中搜寻产品的源代码。

构建web应用
通过war插件,gradle也提供了构建web应用的扩展支持。war插件扩展了java插件,它加入了针对web应用程序开发的规范,并支持归集war文件。让我们也在这个工程中用用war插件:
apply plugin: 'war'
web应用源文件的默认路径是src/main/webapp。假设你已经明确了该应用所必要的java类。那么要使产品的全部源代码和web资源文件处于正确路径下,该工程的目录布局应该像下面这样:
.
├── build.gradle
└── src
    └── main
        ├── java
        │   └── com
        │       └── manning
        │           └── gia
        │               └── todo
        │                   ├── model
        │                   │   └── todoitem.java
        │                   ├── repository
        │                   │   ├── inmemorytodorepository.java
        │                   │   └── todorepository.java
        │                   └── web
        │                       └── todoservlet.java
        └── webapp                                               #a
            ├── web-inf
            │   └── web.xml                                      #b
            ├── css                                              #c
            │   ├── base.css
            │   └── bg.png
            └── jsp                                              #d
                ├── index.jsp
                └── todo-list.jsp

#a web源文件默认目录
#b web应用描述符文件
#c 存储描述如何展现html元素的样式单文件的目录
#d 存放jsp形式的动态脚本化视图组件

声明外部依赖
在实现这个web应用的过程,我们使用的一些类,例如javax.servlet.httpservlet,并非java标准版(java se)的一部分。在构建工程之前,我们需要确保已经声明了这些外部依赖。在java系统中,依赖类库是以jar文件的形式去发布和使用的。许多类库可以从仓库,如一个文件系统或中央服务器,中获得。为了使用依赖,gradle要求你至少定义一个仓库。出于一些考虑,我们将使用公共的可通过互联网进行访问的maven central仓库。
repositories {
   mavencentral()                   #a
}
#a 通过http://repo1.maven.org/maven2访问maven2中央仓库的简短标记
在gradle中,依赖是通过配置项来进行分组的。我们将来servlet依赖使用的配置项是providedcompile。该配置项用于那些在编译时而非运行时所需的依赖。像jstl这样的运行时依赖,在编译时不会被用到,但在运行时则会被用到。它们都会成为war文件的一部分。下面的配置语句块声明了我们应用所需的外部类库:
dependencies {
   providedcompile 'javax.servlet:servlet-api:2.5'
   runtime 'javax.servlet:jstl:1.1.2'
}

构建工程
我们已经准备好构建这个工程了。另到工程中的一个java插件任务名为build。该任务将编译源代码,运行测试程序并归集war文件--所有的这些任务都将以正确的顺序被执行。执行命令gradle build之后,你可能会得到形如下面的输出:
$ gradle build
:compilejava                                      #a
:processresources up-to-date
:classes
:war                                              #b
:assemble
:compiletestjava up-to-date                       #c
:processtestresources up-to-date
:testclasses up-to-date
:test                                             #d
:check
:build

#a 编译产品的java源代码
#b war插件提供的任务,用于归集war文件
#c 编译java测试源代码
#d 运行单元测试

上述输出的每一行都代表执行了一个由java或war插件提供的任务。你可能会注意到,有一些任务被标记为up-to-date。它的意思是指该任务被跳过去了。gradle的增量构建支持策略会自动识别不需要执行的工作。特别是在大型商业项目中,该特性会极大地节省时间。
在该工程的根节目录中,你将会发现一个名为build的子目录,它包含有执行构建之后的全部输出,包括类文件,测试报告,归集的war文件,以及像manifest这样的在打包时需要的临时文件。如下就是执行构建工作之后的工程目录结构:
.
├── build
│   ├── classes
│   │   └── main                                          #a
│   │       └── com
│   │           └── manning
│   │               └── gia
│   │                   └── todo
│   │                       ├── model
│   │                       │   └── todoitem.class
│   │                       ├── repository
│   │                       │   ├── inmemorytodorepository.class
│   │                       │   └── todorepository.class
│   │                       └── web
│   │                           ├── todoservlet$todoliststats.class
│   │                           └── todoservlet.class
│   ├── dependency-cache
│   ├── libs
│   │   └── todo-webapp.war                               #b
│   ├── reports
│   │   └── tests
│   │       ├── base-style.css
│   │       ├── css3-pie-1.0beta3.htc
│   │       ├── index.html
│   │       ├── report.js
│   │       └── style.css
│   ├── test-results
│   │   └── binary
│   │       └── test
│   │           └── results.bin
│   └── tmp
│       └── war
│           └── manifest.mf                                #c
├── build.gradle
└── src

#a 包含java类文件的默认目录
#b 归集的war文件
#c 用于war的临时manifest文件
你已经知道如何从一个基于标准目录结构的web工程去构建war文件。现在是时候将它布署到一个servlet容器中去了。在下一节中,我们将在本地开发机器中启动jetty去运行这个web应用。

运行应用
在本地机器中运行一个web应用应该很容易,能够实践快速应用开发(rad),并能够提供快速的启动时间。最棒地是,它不要求你部署一个web容器运行时环境。jetty一个流行的轻量级开源web容器,它支持前面提到的所有特性。在这个web应用中加入一个http模块,它就变成了一个嵌入式实现。gradle的jetty插件扩展了war插件,它提供的任务可以将一个web应用部署到嵌入式容器中,并能够启动该应用。在你的构建脚本中,可以像如下那样使用这个插件:
apply plugin: 'jetty'
这个将被我们用于启动web应用的任务名为jettyrun。它甚至可以在无需创建war文件的情况下启动一个jetty容器。执行上述命令后会得到如下形式的输出:
$ gradle jettyrun
:compilejava
:processresources up-to-date
:classes
> building > :jettyrun > running at http://localhost:8080/todo-webapp-jetty
在上述输出的最后一行中,该插件告诉了你jetty即将侦听的请求地址。打开一个你喜欢的浏览器,并输入上述地址。最后,我们会看到这个to do web应用的行为。图2展示在一个浏览器中查看到该应用界面的截屏。

图2 to do应用的web界面及其行为

在你通过组合键ctrl c去停止这个应用之前,gradle会让它一直运行。jetty如何知道使用哪个端口和上下文环境去运行这个web应用?再说一遍,这就是规范。jetty运行web应用所使用的默认端口就是8080。


总结
只需要较少的努力,你就可以使用gradle去构建并运行一个java web应用。只要你严格遵循标准目录结构,那么你的构建脚本仅需要两行代码。


john jiang 2014-01-23 21:22
]]>
java并发基础实践--死锁(原)http://www.blogjava.net/jiangshachina/archive/2013/12/29/408180.htmljohn jiangjohn jiangsun, 29 dec 2013 12:19:00 gmthttp://www.blogjava.net/jiangshachina/archive/2013/12/29/408180.htmlhttp://www.blogjava.net/jiangshachina/comments/408180.htmlhttp://www.blogjava.net/jiangshachina/archive/2013/12/29/408180.html#feedback1http://www.blogjava.net/jiangshachina/comments/commentrss/408180.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/408180.html
java并发基础实践--死锁
本文是系列中的一篇,介绍了最简单的死锁场景,并使用jstack产生的thread dump来查找死锁。(2013.12.29最后更新)

1. 死锁
为了能够维护线程的安全性,java提供的锁机制,但不恰当地使用锁则可能产生死锁。死锁是并发编程中一个无法绕开的问题。只要在一个任务中使用了一个以上的锁,那么就存在死锁的风险。
死锁产生的直接原因非常简单,即两个线程在相互等待对方所执有的锁。

2. 锁顺序死锁
在死锁场景中,最典型的就是锁顺序死锁,代码清单1就是一个很常见的示例。
清单1
public class deadlock {

    
private object leftlock = new object();
    
private object rightlock = new object();

    
public void leftright() {
        
synchronized (leftlock) {
            
try {
                timeunit.seconds.sleep(
3);
            } 
catch (interruptedexception e) {
                e.printstacktrace();
            }

            
synchronized (rightlock) {
                system.out.println(
"leftright");
            }
        }
    }

    
public void rightleft() {
        
synchronized (rightlock) {
            
try {
                timeunit.seconds.sleep(
3);
            } 
catch (interruptedexception e) {
                e.printstacktrace();
            }

            
synchronized (leftlock) {
                system.out.println(
"leftright");
            }
        }
    }

    
public static void main(string[] args) {
        
final deadlock deadlock = new deadlock();

        thread t1 
= new thread(new runnable() {

            @override
            
public void run() {
                deadlock.leftright();
            }
        });

        thread t2 
= new thread(new runnable() {

            @override
            
public void run() {
                deadlock.rightleft();
            }
        });

        t1.start();
        t2.start();
    }
}

3. thread dump
jdk提供了一组命令行工具,其中就包括jstack。通过jstack可以获取当前正运行的java进程的java stack和native stack信息。如果java进程崩溃了,也可以通过它来获取core file中的java stack和native stack信息,以方便我们定位问题。
为了能够使用jstack去输出目标java进程的thread dump,首先必须要弄清楚在执行清单1的程序时,该程序的进程号。jdk提供的另一个命令行工具jps可以获取系统中所有java进程的相关信息。
在命令行窗口中执行命令jps,即可以得到清单2所示的结果
清单2
c:\documents and settings\administrator>jps
2848
4552 deadlock
5256 jps
其中4552就是在笔者机器上执行程序deadlock时所生成java进程的进程号。
然后再执行命令jstack 4552,在笔者的机器上就会得到清单3所示的结果
清单3
c:\documents and settings\administrator>jstack 
4552
2013-12-29 18:45:41
full thread dump java hotspot(tm) client vm (
23.25-b01 mixed mode, sharing):

"destroyjavavm" prio=6 tid=0x00878800 nid=0xd00 waiting on condition [0x00000000]
   java.lang.thread.state: runnable

"thread-1" prio=6 tid=0x02b56c00 nid=0x14ec waiting for monitor entry [0x02fdf000]
   java.lang.thread.state: blocked (on object monitor)
        at concurrency.deadlock.deadlock.rightleft(deadlock.java:
33)
        - waiting to lock <0x22be6598> (a java.lang.object)
        - locked <0x22be65a0> (a java.lang.object)
        at concurrency.deadlock.deadlock$
2.run(deadlock.java:53)
        at java.lang.thread.run(thread.java:
724)

"thread-0" prio=6 tid=0x02b55c00 nid=0x354 waiting for monitor entry [0x02f8f000]
   java.lang.thread.state: blocked (on object monitor)
        at concurrency.deadlock.deadlock.leftright(deadlock.java:
19)
        - waiting to lock <0x22be65a0> (a java.lang.object)
        - locked <0x22be6598> (a java.lang.object)
        at concurrency.deadlock.deadlock$
1.run(deadlock.java:45)
        at java.lang.thread.run(thread.java:
724)

"service thread" daemon prio=6 tid=0x02b34800 nid=0x133c runnable [0x00000000]
   java.lang.thread.state: runnable

"c1 compilerthread0" daemon prio=10 tid=0x02b13800 nid=0x10fc waiting on condition [0x00000000]
   java.lang.thread.state: runnable

"attach listener" daemon prio=10 tid=0x02b11c00 nid=0x1424 waiting on condition [0x00000000]
   java.lang.thread.state: runnable

"signal dispatcher" daemon prio=10 tid=0x02b10800 nid=0x1100 runnable [0x00000000]
   java.lang.thread.state: runnable

"finalizer" daemon prio=8 tid=0x02af4c00 nid=0x1238 in object.wait() [0x02daf000]
   java.lang.thread.state: waiting (on object monitor)
        at java.lang.object.wait(native method)
        - waiting on <0x22b60fb8> (a java.lang.ref.referencequeue$lock)
        at java.lang.ref.referencequeue.remove(referencequeue.java:
135)
        - locked <0x22b60fb8> (a java.lang.ref.referencequeue$lock)
        at java.lang.ref.referencequeue.remove(referencequeue.java:
151)
        at java.lang.ref.finalizer$finalizerthread.run(finalizer.java:
189)

"reference handler" daemon prio=10 tid=0x02af0000 nid=0x12e8 in object.wait() [0x02d5f000]
   java.lang.thread.state: waiting (on object monitor)
        at java.lang.object.wait(native method)
        - waiting on <0x22b60da0> (a java.lang.ref.reference$lock)
        at java.lang.object.wait(object.java:
503)
        at java.lang.ref.reference$referencehandler.run(reference.java:
133)
        - locked <0x22b60da0> (a java.lang.ref.reference$lock)

"vm thread" prio=10 tid=0x02aee400 nid=0x129c runnable

"vm periodic task thread" prio=10 tid=0x02b48000 nid=0x89c waiting on condition

jni global references: 
117


found one java-level deadlock:
=============================
"thread-1":
  waiting to lock monitor 0x02af4a3c (object 0x22be6598
, a java.lang.object),
  which is held by 
"thread-0"
"thread-0":
  waiting to lock monitor 0x02af310c (object 0x22be65a0
, a java.lang.object),
  which is held by 
"thread-1"

java stack information for the threads listed above:
===================================================
"thread-1":
        at concurrency.deadlock.deadlock.rightleft(deadlock.java:
33)
        - waiting to lock <0x22be6598> (a java.lang.object)
        - locked <0x22be65a0> (a java.lang.object)
        at concurrency.deadlock.deadlock$
2.run(deadlock.java:53)
        at java.lang.thread.run(thread.java:
724)
"thread-0":
        at concurrency.deadlock.deadlock.leftright(deadlock.java:
19)
        - waiting to lock <0x22be65a0> (a java.lang.object)
        - locked <0x22be6598> (a java.lang.object)
        at concurrency.deadlock.deadlock$
1.run(deadlock.java:45)
        at java.lang.thread.run(thread.java:
724)

found 
1 deadlock.
在上述输出中,我们可以很明确地看到一个死锁
"thread-1":
  waiting to lock monitor 0x02af4a3c (object 0x22be6598
, a java.lang.object),
  which is held by 
"thread-0"
"thread-0":
  waiting to lock monitor 0x02af310c (object 0x22be65a0
, a java.lang.object),
  which is held by 
"thread-1"
并且它还标明了程序是在哪个地方时发现了上述死锁
"thread-1":
        at concurrency.deadlock.deadlock.rightleft(deadlock.java:
33)
        - waiting to lock <0x22be6598> (a java.lang.object)
        - locked <0x22be65a0> (a java.lang.object)
        at concurrency.deadlock.deadlock$
2.run(deadlock.java:53)
        at java.lang.thread.run(thread.java:
724)
"thread-0":
        at concurrency.deadlock.deadlock.leftright(deadlock.java:
19)
        - waiting to lock <0x22be65a0> (a java.lang.object)
        - locked <0x22be6598> (a java.lang.object)
        at concurrency.deadlock.deadlock$
1.run(deadlock.java:45)
        at java.lang.thread.run(thread.java:
724)

4. 小结
死锁产生的直接原因非常简单,即两个线程在相互等待对方所执有的锁。锁顺序死锁是其中最经典的场景,此外还有动态的锁顺序死锁。虽然表现形式有所不同,但本质上都是两个线程在以不同的顺序来获取相同锁时,发生了死锁问题。
使用thread dump可以帮助我们分析死锁产生的原因。除了直接使用jstack命令来获取thread dump输出以外,jdk还提供了jvisualvm工具,它能以可视化的方式展示java程序的进程号并导出thread dump。

john jiang 2013-12-29 20:19
]]>
java concurrent animated(译)http://www.blogjava.net/jiangshachina/archive/2013/12/07/407310.htmljohn jiangjohn jiangsat, 07 dec 2013 09:45:00 gmthttp://www.blogjava.net/jiangshachina/archive/2013/12/07/407310.htmlhttp://www.blogjava.net/jiangshachina/comments/407310.htmlhttp://www.blogjava.net/jiangshachina/archive/2013/12/07/407310.html#feedback1http://www.blogjava.net/jiangshachina/comments/commentrss/407310.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/407310.html
java concurrent animated
    在最新一期的中有一篇访谈,介绍了一个学习java并发编程的动画应用。该应用以十分直观的方式展示了java并发工具包中的每一个重要组件,降低了学习java并发编程的难度。(2013.12.07最后更新)

java magazine:有多少人已经试用过了你的java concurrent animated应用?
grazi:该应用是在2009年7月被引入的,从那时算起,已经有了大约20000的下载量。但考虑到已有约一千万的java开发者,这个下载量才只是开始。按国家区分,下载最多的分别是美国(23%),印度(14)和中国(7%)。
    你可以下载一个可以执行的jar文件,然后仅需双击它就可以运行了。该应用是由菜单驱动的,或者也可以使用向上或向下键在不同的图像和动画之间进行导航。它能运行在诸如windows,mac,linux等等所有的平台上。它要求安装java se 6或更高的版本。

java magazine:对这个应用最典型的反馈是什么?
grazi:大家告诉我这个工具很好用。许多人确实对此感到兴奋,尤其是那些正试图向团队教授合适并发技术的老师与领导们。java是最早在核心类库中引入并发的语言之一。在当时,这是一个很强大的特性,但我们很快就发现一个非常优秀的程序员与会写出很糟糕的并发代码。进行恰当的并发编程是一件困难甚至是不可能的事情,但是如何人们能花些时间去理解一些现有的框架,那么在进行并发编码时所产生潜在错误就会变得极少。
    例如,去看看java内存模型。开发者经常忽视java内存模型,而像个幸福的傻瓜一样在编码,那么他们的程序会不太正常,因为java虚拟机(jvm)和服务器可能无法利用到由java内存模型所提供的优化。由于内核在速度与数量上都有了增长,厂商们期望能够高效地利用到这些内核,然而由于错误的并发管理,本来如期运行的程序却开始遇到了一些零星的错误。

java magazine:你是说,这个应用会以我们所虚构的方式去使开发者们能够更快且直观地掌握java并发的原理与实践?
grazi:那是达到这一目的一个有趣的途径。你知道的,java concurrent animated并不是一个flash动画。它是一组可交互的java程序,也即,每个动画都是真地在使用它所要演示的并发组件。在屏幕的右边是一个展示代码片断的面板,由于动画的运行,它会动态地高亮显示及恢复正在执行的代码。
    让你给你一个例子,这个例子发生在readwritelock这个动画中。readwritelock用于确保数据的一致性。它允许不受数量限制的线程去获取读锁,并能并发地对这个锁进行操作。但是,写线程在获取这个锁之前只能等待所有的读线程执行结束。一旦一个写线程获得了这个锁,那么其它的读线程或写线程将无法获取它。
    假设一个写线程正在等待正在执行中的读线程去释放这个读锁,但突然一个新的读线程跑过来了。那么谁应该获得这个锁会比较好呢?这个新的读线程应该跑到写线程前面去吗?毕竟,如果其它的读线程已经获得了这个锁,那么新来的读线程为什么要去等一个尚在等待中的写线程呢?而这实际上这正是java 5所干的事儿。但某次我在java 6上运行这个动画时,我注意到行为发生了改变。即,随后而来的读线程在获取到这个锁之前可能要等待所有的写线程先释放锁。
    我认为这个新的行为是一个bug,且向并发专家heinz kabutz博士提及了此事。博士解释道,这不是一个错误,而一个特性。如果允许新到的读线程跳到正处于等待中的写线程的前面去,这就存在产生线程饥饿条件的高风险。因为,存在一种很大的可能性,可能没有任何写线程能获得这个锁,它们将永远等待着。这就是一个如何使用动画去警示依赖于jvm运行时版本的线程行为的例子。

java magazine:以动画教程的形式来展示特殊值,在java并发编程中有何与众不同吗?
grazi:miller定律教会我们,我们的大脑在某一时刻能处理的思维的数量是有限的。人类大脑倾向于进行顺序的思维处理,那么即便我们能够克服身体上的束缚,并能够去正确地进行理解,在以后也很难返回至前去重新构造前面的思维处理。可以肯定地是,如果另一个开发者在以后能深入对其进行研究,那么仍然非常难以从原有的思维成果中再次捕捉到认知轨迹。这样的话,脆弱的代码就会很突然地不能正常工作了。
    通过使用框架,我们不仅将并发编程委托给了创建和维护该框架的聪明开发者们,而且还为沟通设计时引入了一个词典。所以,我可以说,“下面的代码会当作cyclicbarrier去执行”,而人们会明白那是什么意思。通过为java.util.concurrent中的所有组件都引入一个可交互化的动画应用,开发者们点着鼠标就能很方便地将他们所探究的功能进行可视化,使理解这些算法变得真心简单了。

java magazine:你当时正在研究某些直觉,这些直觉可以帮助更方便地学习并发编程。从开发者的反馈来看,这些直觉看起来是有效的。
grazi:是的。例如,我前面解释的readwritelock基本功能。读者们可能理解了,也可能没有。现在让我们看看这个与其有关的动画,如图表1所示。

    绿色线程是读线程,最上面的白色线程(带着菱形箭头)是一个写线程,它下面的白色线程是一个新的读线程,该线程在获取锁之前必须要等待所有的读线程与写线程执行完毕。如果你点击按钮并观看这些动画,会比通过浏览繁冗的解释性文字去进行理解要简单得多了。

java magazine:heinz kabutz评论道,java被构建成能够一次性做许多事情,而这正与并发完全相关。你的学习系统是如何提高程序员的技能,以便他们能降低并发错误的风险。
grazi:经常地,当我要努力克服一个并发问题时,我知道凯发天生赢家一触即发官网的解决方案就存在于某个设计模式中,但是哪一个呢?在开发者探寻一个正确凯发天生赢家一触即发官网的解决方案时,java concurrent animated为他们提供了一个所有方案的目录;在激发出正确方案的过程中,它扮演着向导的角色。

java magazine:当你管理的团队正在使用java并发,并且你和你的团队都想更好地去理解java并发,java concurrent animated有着它的出发点。是什么导致你使用动画呢,能描述下这个过程吗?
grazi:我的培训是针对投资部门的服务器端java应用,在那里,并发是一个通常都会受到关注的问题。交易员们要求延迟要低,这样可以确保他们在这个需要于一毫秒窗口时间内捕捉交易机会的比赛中不会成为失败者。批量处理也要求快速完成,等等。所以我开始看到那些可怕的只写(write-only)组件,这些组件使人们在并发编程挣扎着。我自己也身处其中。
    某天下午,我正坐在机场内,将要前往芝加哥为我的团队做一个关于并发的讲演。我正对讲演的ppt进行最后的处理,那组幻灯片着重演示了每一个重要的组件。为了引导我浏览java.util.concurrent中每个并发组件的状态,我写了一些状态机,它们展示了一些供我参考用的简单文本消息。在之前的生涯中,我曾在一家互联网创业公司中开发交互式的游戏,所以我懂得许多与动画相关的知识。这使我想到可以将ppt替换成一组交互式的动画应用,那会更为直观。
    在等飞机的过程中,我写了一个初步的动画引擎,然后在我的状态机中调用了这个引擎。到了第二天早晨,我已经有一个可用的原型程序。多年来,我一直致力于这个框架,并且吸引了其他专家的建议。我传递过一份早期版本给brian goetz,令人惊讶的是,他为每个动画程序都给出了建议。我将他的所有建议到吸收到了该框架中。在我的第一次javaone讲演中,kirk pepperdine加入了进来。他建议为动画应用在真正的ppt中加入描述,以便讲演者能记住正在讨论的内容。随后我加上那些描述,这确实非常有用--不只是对讲演者有用,对于终端用户也很有用。heinz kabutz也加入了那场讲演,并建议修改某些动画,以使它们更为直观。
    在另一场讲演中,一个很有激情的软件咨询师oliver zeigermann指出,很显然缺少了针对concurrenthashmap的动画。我问他是否有兴趣贡献这个动画,随后他添加了那个很有价值的动画程序。

java magazine:你能带着我们过一遍java并发工具包中的类吗?并能否解释一下这些动画程序是如何使开发者们更易于深入理解这些类?
grazi:好的,但在没有动画程序的情况下确实很难办到。让我们看看cyclicbarrier,它有两个重要的状态,如图2和图3所示,它们展示了一个障碍和四个成员。在图2中,我们可以看到有三个成员已经到了,所以它们被阻止继续前进。图3展示了,一旦第四个成员也到达了,每个成员又可以向前走了。



    这就形象地诠释了障碍的概念,亦即,在所有成员到达障碍点之前,每个成员必须等待。随着并发组件复杂度的增加--例如fork/join的动画,以及那些演示原生的wait和notify机制的动画--使用动画程序的好处就更不肖说了。

java magazine:谈谈在创建这些动画程序的过程中所遇到的一些挑战。
grazi:有一些挑战。开始时,线程被表示成箭头。对于多数并发组件,这种表示法是有效的。后来我们必须提供一个可视化方案,不仅要表示线程,还要表示blockingqueue中的对象。所以,我不得不引入一个称之为"精灵类型(sprite-type)"的概念,然后我们有了一个箭头型的精灵类型和一个新的"对象"型的精灵类型。后来,concurrenthashmap和atomicinteger又需要新的精灵类型,因为我们试图要对他们的计算与交换行为进行可视化。
    后面又来了fork/join,新的挑战就是如何去表现那些完全不同于现有框架所表现的可视化部件。还有一个挑战,即fork/join动画需要解决一个实际的问题,但这个动画应该解决一个什么样的问题呢?
    开始时,我让这个动画程序去求fibonacci数列,但却行不通。我在这个问题上纠结了两天时间,直到我认识到fibonacci数列(fn 1=fn fn-1)无法高效地并行化,因为每个值都依赖于它前面的值。所以,无论你如何试图对其实施并行化,它天生就是一个顺序化的计算。所以我换成了另一个问题--查找数组中的最大元素,这样就好了。在这个动画中,你可以很精确地看到如何使用一个随机的数列去解决这个问题(如图4所示)。


java magazine:你都在哪里讲演过这些动画程序?
grazi:在javaone中讲演过几次,在其它的许多会议,如奥斯陆中的javazone,苏黎世的jazoon,纽约的qcon,以及许多sig(特别兴趣组)和jug(java用户组)中也都讲演过。我喜欢讲演,我也喜欢周游世界,而java concurrent animated为我提供了一个极好的机会去做这两件事情。它总能获得极高的评价。
    java concurrent animated的讲演提供了一种意识,并且它们也向出席的开发者们展示了下载这一框架的价值,而且它还展示了,如果你拥有了框架和灵感启迪,学习并发编程会是多么的容易。


john jiang 2013-12-07 17:45
]]>
java并发基础实践--分而治之(原)http://www.blogjava.net/jiangshachina/archive/2013/10/23/405577.htmljohn jiangjohn jiangwed, 23 oct 2013 15:27:00 gmthttp://www.blogjava.net/jiangshachina/archive/2013/10/23/405577.htmlhttp://www.blogjava.net/jiangshachina/comments/405577.htmlhttp://www.blogjava.net/jiangshachina/archive/2013/10/23/405577.html#feedback0http://www.blogjava.net/jiangshachina/comments/commentrss/405577.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/405577.html阅读全文

john jiang 2013-10-23 23:27
]]>
java并发基础实践--退出任务ii(原)http://www.blogjava.net/jiangshachina/archive/2013/10/07/404690.htmljohn jiangjohn jiangmon, 07 oct 2013 08:55:00 gmthttp://www.blogjava.net/jiangshachina/archive/2013/10/07/404690.htmlhttp://www.blogjava.net/jiangshachina/comments/404690.htmlhttp://www.blogjava.net/jiangshachina/archive/2013/10/07/404690.html#feedback3http://www.blogjava.net/jiangshachina/comments/commentrss/404690.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/404690.html
java并发基础实践--退出任务ii
在的中所述的退出并发任务的方式都是基于jdk 5之前的api,本文将介绍使用由jdk 5引入的并发工具包中的api来退出任务。(2013.10.08最后更新)

    在本系列的前一篇中讲述了三种退出并发任务的方式--停止线程;可取消的任务;中断,但都是基于jdk 5之前的api。本篇将介绍由jdk 5引入的java.concurrent包中的future来取消任务的执行。

1. future模式
    future是并发编程中的一种常见设计模式,它相当于是proxy模式与thread-per-message模式的结合。即,每次都创建一个单独的线程去执行一个耗时的任务,并且创建一个future对象去持有实际的任务对象,在将来需要的时候再去获取实际任务的执行结果。
依然先创建一个用于扫描文件的任务filescannertask,如代码清单1所示,
清单1
public class filescannertask implements runnable {

    
private file root = null;

    
private arraylist<string> filepaths = new arraylist<string>();

    
public filescannertask(file root) {
        
if (root == null || !root.exists() || !root.isdirectory()) {
            
throw new illegalargumentexception("root must be directory");
        }

        
this.root = root;
    }

    @override
    
public void run() {
        travlefiles(root);
    }

    
private void travlefiles(file parent) {
        string filepath 
= parent.getabsolutepath();
        filepaths.add(filepath);

        
if (parent.isdirectory()) {
            file[] children 
= parent.listfiles();
            
if (children != null) {
                
for (file child : children) {
                    travlefiles(child);
                }
            }
        }
    }

    
public list<string> getfilepaths() {
        
return (list<string>) filepaths.clone();
    }
}
此处的文件扫描任务,提供了一个getfilepaths()方法以允许随时都可以取出当前已扫描过的文件的路径(相当于一个任务快照)。然后,创建一个针对该任务的future类,如代码清单2所示,
清单2
public class filescannerfuture {

    
private filescannertask task = null;

    
public filescannerfuture(filescannertask task) {
        
new thread(task).start();
        
this.task = task;
    }

    
public list<string> getresult() {
        
return task.getfilepaths();
    }
}
filescannerfuture持有filescannertask的引用,并创建一个独立的线程来执行该任务。在任务的执行过程中,应用程序可以在"未来"的某个时刻去获取一个任务的快照,如代码清单3所示,
清单3
public static void main(string[] args) throws exception {
    filescannerfuture future 
= new filescannerfuture(new filescannertask(new file("c:")));

    timeunit.seconds.sleep(
1);
    list
<string> filepaths1 = future.getresult();
    system.out.println(filepaths1.size());

    timeunit.seconds.sleep(
1);
    list
<string> filepaths2 = future.getresult();
    system.out.println(filepaths2.size());
}

2. 使用并发工具包中的future实现
    前面所展示的future实现十分的简陋,没有实际应用的意义。使用filescannerfuture,应用程序在获取filepaths时,无法得知其获取的是否为最终结果,即无法判断filescannertask是否已经完成。而且,也不能在必要时停止filescannertask的执行。毫无疑问,由jdk 5引入的并发工具包肯定会提供此类实用工具,如futuretask。为了使用并发工具包中的future,需要修改前述的filescannertask实现,让其实现callable接口,如代码清单4所示,
清单4
public class filescannertask implements callable<list<string>> {

    
private file root = null;

    
private list<string> filepaths = new arraylist<string>();

    
public filescannertask(file root) {
        
if (root == null || !root.exists() || !root.isdirectory()) {
            
throw new illegalargumentexception("root must be directory");
        }

        
this.root = root;
    }

    @override
    
public list<string> call() {
        travlefiles(root);
        
return filepaths;
    }

    
private void travlefiles(file parent) {
        string filepath 
= parent.getabsolutepath();
        filepaths.add(filepath);

        
if (parent.isdirectory()) {
            file[] children 
= parent.listfiles();
            
if (children != null) {
                
for (file child : children) {
                    travlefiles(child);
                }
            }
        }
    }

    
public list<string> getfilepaths() {
        
return (list<string>) filepaths.clone();
    }
}
应用程序也要相应的修改成如代码清单5所示,使用executorservice来提交任务,并创建一个future/futuretask实例。
清单5
public static void main(string[] args) {
    executorservice executorservice 
= executors.newcachedthreadpool();
    future
<list<string>> future = executorservice.submit(new filescannertask(new file("c:")));

    
try {
        list
<string> filepaths = future.get();
        system.out.println(filepaths.size());
    } 
catch (interruptedexception e) {
        e.printstacktrace();
    } 
catch (executionexception e) {
        e.printstacktrace();
    }

    executorservice.shutdown();
}
此处就是调用future.get()方法来获取任务的执行结果,如果任务没有执行完毕,那么该方法将会被阻塞。该future实现的好处就是,正常情况下,只有在任务执行完毕之后才能获取其结果,以保证该结果是最终执行结果。

3. 使用future取消任务
    future除了定义有可获取执行结果的get方法(get()以及get(long timeout, timeunit unit)),还定义了三个方法:cancel(),iscancelled()以及isdone(),用于取消任务,以及判定任务是否已被取消、已执行完毕。如代码清单6所示,
清单6
public interface future<v> {

    
boolean cancel(boolean mayinterruptifrunning);
    
boolean iscancelled();
    
boolean isdone();
    
}
其中,cancel()方法中的boolean参数若为true,表示在取消该任务时,若执行该任务的线程仍在运行中,则对其进行中断。如代码清单7所示,若任务执行超时了,那么就取消它。
清单7
public static void main(string[] args) {
    executorservice executorservice 
= executors.newcachedthreadpool();
    future
<list<string>> future = executorservice.submit(new filescannertask(new file("c:")));

    
try {
        list
<string> filepaths = future.get(1, timeunit.seconds);
        system.out.println(filepaths.size());
    } 
catch (interruptedexception e) {
        e.printstacktrace();
    } 
catch (executionexception e) {
        e.printstacktrace();
    } 
catch (timeoutexception e) {
        e.printstacktrace();
    } 
finally {
        future.cancel(
true);
    }

    executorservice.shutdown();
}
在实际应用中,取消任务的原由肯定不仅仅只是超时这么简单,还可能是由于接受到了用户的指令。此时,则可能会从另一个独立线程去取消该任务。除了取消任务之外,有时还需要取出任务中已经生成的部分结果。但为了能够响应任务的退出,首先需要修改filescannertask,使得当任务被取消(中断)时,任务能够真正的快速停止并返回,如代码清单8所示,
清单8
public class filescannertask implements callable<list<string>> {

    

    
private void travlefiles(file parent) {
        
if (thread.currentthread().isinterrupted()) {
            
return;
        }

        string filepath 
= parent.getabsolutepath();
        filepaths.add(filepath);

        
if (parent.isdirectory()) {
            file[] children 
= parent.listfiles();
            
if (children != null) {
                
for (file child : children) {
                    travlefiles(child);
                }
            }
        }
    }

    
}
相应地修改应用程序的代码,如代码清单9所示,
清单9
public static void main(string[] args) {
    executorservice executorservice 
= executors.newcachedthreadpool();
    filescannertask task 
= new filescannertask(new file("c:"));
    
final future<list<string>> future = executorservice.submit(task);
    
    
new thread(new runnable() {
        
        @override
        
public void run() {
            
try {
                timeunit.seconds.sleep(
1);
            } 
catch (interruptedexception e) {
                e.printstacktrace();
            }
            future.cancel(
true);
        }
    }).start();
    
    
try {
        list
<string> filepaths = future.get();
        system.out.println(filepaths.size());
    } 
catch (interruptedexception e) {
        e.printstacktrace();
    } 
catch (executionexception e) {
        e.printstacktrace();
    } 
catch (cancellationexception e) {
        list
<string> filepaths = task.getfilepaths();
        system.out.println(
"partly result: "  filepaths.size());
    }
    
    executorservice.shutdown();
}
由上可知,此处使用future.cancel(true)的本质依然是利用了线程的中断机制。

4. 小结
    使用future可以在任务启动之后的特定时机再去获取任务的执行结果。由jdk 5引入的并发工具包中提供的future实现不仅可以获取任务的执行结果,还可以用于取消任务的执行。

john jiang 2013-10-07 16:55
]]>
java并发基础实践--退出任务i(原)http://www.blogjava.net/jiangshachina/archive/2013/09/21/404269.htmljohn jiangjohn jiangsat, 21 sep 2013 11:11:00 gmthttp://www.blogjava.net/jiangshachina/archive/2013/09/21/404269.htmlhttp://www.blogjava.net/jiangshachina/comments/404269.htmlhttp://www.blogjava.net/jiangshachina/archive/2013/09/21/404269.html#feedback0http://www.blogjava.net/jiangshachina/comments/commentrss/404269.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/404269.html
java并发基础实践--退出任务i
计划写一个""系列,算作本人对java并发学习与实践的简单总结。本文是该系列的第一篇,介绍了退出并发任务的最简单方法。(2013.09.25最后更新)

在一个并发任务被启动之后,不要期望它总是会执行完成。由于时间限制,资源限制,用户操作,甚至是任务中的异常(尤其是运行时异常),...都可能造成任务不能执行完成。如何恰当地退出任务是一个很常见的问题,而且实现方法也不一而足。

1. 任务
创建一个并发任务,递归地获取指定目录下的所有子目录与文件的绝对路径,最后再将这些路径信息保存到一个文件中,如代码清单1所示:
清单1
public class filescanner implements runnable {

    
private file root = null;

    
private list<string> filepaths = new arraylist<string>();

    
public filescanner1(file root) {
        
if (root == null || !root.exists() || !root.isdirectory()) {
            
throw new illegalargumentexception("root must be legal directory");
        }

        
this.root = root;
    }

    @override
    
public void run() {
        travlefiles(root);
        
try {
            savefilepaths();
        } 
catch (exception e) {
            e.printstacktrace();
        }
    }

    
private void travlefiles(file parent) {
        string filepath 
= parent.getabsolutepath();
        filepaths.add(filepath);

        
if (parent.isdirectory()) {
            file[] children 
= parent.listfiles();
            
for (file child : children) {
                travlefiles(child);
            }
        }
    }

    
private void savefilepaths() throws ioexception {
        filewriter fos 
= new filewriter(new file(root.getabsolutefile()
                
 file.separator  "filepaths.out"));
        
for (string filepath : filepaths) {
            fos.write(filepath 
 "\n");
        }
        fos.close();
    }
}

2. 停止线程
有一个很直接,也很干脆的方式来停止线程,就是调用thread.stop()方法,如代码清单2所示:
清单2
public static void main(string[] args) throws exception {
    filescanner task 
= new filescanner(new file("c:"));
    thread taskthread 
= new thread(task);
    taskthread.start();

    timeunit.seconds.sleep(
1);
    taskthread.stop();
}
但是,地球人都知道thread.stop()在很久很久之前就不推荐使用了。根据的介绍,该方法存在着固有的不安全性。当停止线程时,将会释放该线程所占有的全部监视锁,这就会造成受这些锁保护的对象的不一致性。在执行清单2的应用程序时,它的运行结果是不确定的。它可能会输出一个文件,其中包含部分的被扫描过的目录和文件。但它也很有可能什么也不输出,因为在执行filewriter.write()的过程中,可能由于线程停止而造成了i/o异常,使得最终无法得到输出文件。

3. 可取消的任务
另外一种十分常见的途径是,在设计之初,我们就使任务是可被取消的。一般地,就是提供一个取消标志或设定一个取消条件,一旦任务遇到该标志或满足了取消条件,就会结束任务的执行。如代码清单3所示:
清单3
public class filescanner implements runnable {

    
private file root = null;

    
private list<string> filepaths = new arraylist<string>();

    
private boolean cancel = false;

    
public filescanner(file root) {
        
    }

    @override
    
public void run() {
        
    }

    
private void travlefiles(file parent) {
        
if (cancel) {
            
return;
        }

        string filepath 
= parent.getabsolutepath();
        filepaths.add(filepath);

        
if (parent.isdirectory()) {
            file[] children 
= parent.listfiles();
            
for (file child : children) {
                travlefiles(child);
            }
        }
    }

    
private void savefilepaths() throws ioexception {
        
    }

    
public void cancel() {
        cancel 
= true;
    }
}
新的filescanner实现提供一个cancel标志,travlefiles()会遍历新的文件之前检测该标志,若该标志为true,则会立即返回。代码清单4是使用新任务的应用程序。
清单4
public static void main(string[] args) throws exception {
    filescanner task 
= new filescanner(new file("c:"));
    thread taskthread 
= new thread(task);
    taskthread.start();

    timeunit.seconds.sleep(
3);
    task.cancel();
}
但有些时候使用可取消的任务,并不能快速地退出任务。因为任务在检测取消标志之前,可能正处于等待状态,甚至可能被阻塞着。对清单2中的filescanner稍作修改,让每次访问新的文件之前先睡眠10秒钟,如代码清单5所示:
清单5
public class filescanner implements runnable {

    

    
private void travlefiles(file parent) {
        
try {
            timeunit.seconds.sleep(
10);
        } 
catch (interruptedexception e) {
            e.printstacktrace();
        }

        
if (cancel) {
            
return;
        }

        
    }

    
private void savefilepaths() throws ioexception {
        
    }

    
public void cancel() {
        cancel 
= true;
    }
}
再执行清单3中的应用程序时,可能发现任务并没有很快速的退出,而是又等待了大约7秒钟才退出。如果在检查cancel标志之前要先获取某个受锁保护的资源,那么该任务就会被阻塞,并且无法确定何时能够退出。对于这种情况,就需要使用中断了。

4. 中断
中断是一种协作机制,它并不会真正地停止一个线程,而只是提醒线程需要被中断,并将线程的中断状态设置为true。如果线程正在执行一些可抛出interruptedexception的方法,如thread.sleep(),thread.join()和object.wait(),那么当线程被中断时,上述方法就会抛出interruptedexception,并且中断状态会被重新设置为false。任务程序只要恰当处理该异常,就可以正常地退出任务。对清单5再稍作修改,即,如果任务在睡眠时遇上了interruptedexception,那么就取消任务。如代码清单6所示:
清单6
public class filescanner implements runnable {

    

    
private void travlefiles(file parent) {
        
try {
            timeunit.seconds.sleep(
10);
        } 
catch (interruptedexception e) {
            cancel();
        }

        
if (cancel) {
            
return;
        }

        
    }

    
}
同时将清单4中的应用程序,此时将调用thread.interrupt()方法去中断线程,如代码清单7所示:
清单7
public static void main(string[] args) throws exception {
    filescanner3 task 
= new filescanner3(new file("c:"));
    thread taskthread 
= new thread(task);
    taskthread.start();

    timeunit.seconds.sleep(
3);
    taskthread.interrupt();
}
或者更进一步,仅使用中断状态来控制程序的退出,而不再使用可取消的任务(即,删除cancel标志),将清单6中的filescanner修改成如下:
清单8
public class filescanner implements runnable {

    

    
private void travlefiles(file parent) {
        
try {
            timeunit.seconds.sleep(
10);
        } 
catch (interruptedexception e) {
            thread.currentthread().interrupt();
        }

        
if (thread.currentthread().isinterrupted()) {
            
return;
        }

        
    }

    
}
再次执行清单7的应用程序后,新的filescanner也能即时的退出了。值得注意的是,因为当sleep()方法抛出interruptedexception时,该线程的中断状态将又会被设置为false,所以必须要再次调用interrupt()方法来保存中断状态,这样在后面才可以利用中断状态来判定是否需要返回travlefiles()方法。当然,对于此处的例子,在收到interruptedexception时也可以选择直接返回,如代码清单9所示:
清单9
public class filescanner implements runnable {

    

    
private void travlefiles(file parent) {
        
try {
            timeunit.seconds.sleep(
10);
        } 
catch (interruptedexception e) {
            
return;
        }

        
    }

    
}

5 小结
本文介绍了三种简单的退出并发任务的方法:停止线程;使用可取消任务;使用中断。毫无疑问,停止线程是不可取的。使用可取消的任务时,要避免任务由于被阻塞而无法及时,甚至永远无法被取消。一般地,恰当地使用中断是取消任务的首选方式。


john jiang 2013-09-21 19:11
]]>
为何喜欢在thoughtworks工作(译)http://www.blogjava.net/jiangshachina/archive/2013/08/22/403156.htmljohn jiangjohn jiangthu, 22 aug 2013 06:43:00 gmthttp://www.blogjava.net/jiangshachina/archive/2013/08/22/403156.htmlhttp://www.blogjava.net/jiangshachina/comments/403156.htmlhttp://www.blogjava.net/jiangshachina/archive/2013/08/22/403156.html#feedback3http://www.blogjava.net/jiangshachina/comments/commentrss/403156.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/403156.html
为何喜欢在thoughtworks工作
本文是thoughtworks首席科学家martin fowler接受informit采访时的,谈到了在thoughtworks工作的情况,对thoughworks感兴趣的朋友们可以看看。(2013.08.23最后更新)

martin fowler分享了他关于thoughtworks的看法,这是一家关注可持续性以及经济和社会公正的软件开发公司。martin谈到了他是如何开始了在thoughtworks的工作,他为什么会喜欢这家公司的文化,以及对新的软件开发者的建议。

informit:你是怎样得到在的工作?
martin fowler:围绕一个当时他们正从事的项目的域模型,他们让我做些咨询工作。我们进展的不错,最后我就自然而然地加入进来了。在此期间,他们大步地转向极限编程这一软件开发风格。约九个月后,他们向我提供了一份offer。由于他们是我最喜欢的客户,我就决定加入他们。

informit:你在那儿工作多长时间了?
martin fowler:12年。

informit:thoughtworks与其它公司有何不同?
martin fowler:根本上,这种不同要归结为人。他们善于雇佣既聪明又乐于合作的人。特别是,这里会更多地以正直的态度去关注人。我发现,相比于过去多年中共事过的大多数客户,我更加信任我的同事。还有对完成高质量工作并期望做到更好的真挚热情,对于像我这样的作者,这是极好的素材。

informit:对于在那儿工作,你最喜欢的是什么?
martin fowler:thoughtworks有许多东西可以。只是困难之处在于,我要挑哪块儿去讲述呢。

informit:工作在thoughtworks,你最自豪的是什么?
martin fowler:我最自豪的是,我们从一家几百人的美国公司成长为在世界范围内拥有两千人的公司,而且原封不动地保持了公司文化中的精髓。我不确定我在其中扮演了什么角色,但这里一直是我喜欢工作的地方,并且公司一直在宣传我,使大家能有兴趣与thoughtworks协作,我对此感到高兴。
尽管如此,我仍不能肯定这种情况是否要大大地归功于我。在与我本人更明确相关的工作中,我必须得说,我很高兴在过去十年中建立了。为了关注今后在改进这个站点时所做的事情,该站点已成为一个丰富的资源和永恒的迷题。
在更需要协作的工作前沿,我确实很高兴地看到我的一些同事已经成为业内举足轻重的"大嘴"。我不认为我在这其中有很大的作用--我给予的任何帮助能够轻易地超过他们自身的努力--但这确实是我最想做的贡献。

informit:为什么会有人想着去thoughtworks工作?
martin fowler:对于经验少的人,我想最大的吸引力是,能在许多不同类型的项目中学会做好软件开发的能力。当然,thoughtworks的项目并非完美,但我认为它们比绝大多数软件项目要好得多。我听过很多thoughtworks前员工们谈到他们在公司工作的岁月中对于软件开发学到了许多。
而在所有的原因当中,旅行机会也是很重要的。如果你想花上一大段时间在世界上不同的地区进行工作,例如在美国人在印度工作,或巴西人在中国工作,那么thoughtworks提供了大量的此类机会。这也与我们日益关心社会正义的问题有关,我认为对于许多有经验的人来说这也是一个重要的因素。

informit:对于一个刚刚开始在thoughtworks工作的新员工,你有什么建议吗?
martin fowler:对于在thoughtworks工作的人们,最沮丧的事情之一就是我们不做职业规划,这就会很容易危险地在项目间漂来漂去。对于有些人来说这不是问题,但如果你想设定一个职业方向,那么你必须得自己去做。这意味着大量的交际,寻找机遇,并积极推进。这不是一种直接的途径,但在我成为独立咨询师之前,没有这种促进我前行的职业计划,就会有相反的效果。

informit:告诉我们一个"只会发生在thoughtworks"的故事。
martin fowler:我记得有次被拉去讨论一个和bigco相关的很有前景的项目。这笔交易确实很大,在初始阶段就要耗费约50 人/年。但存在着一些对bigco在道德记录方面的担忧,尤其是在发展中国家。在这一讨论中,纯粹的thoughtworks要义是,cfo慷慨激昂地反对接下这笔高利润额的工作,而所有的高级领导们则倾听着一位来自南方国家最近才受雇于thoughtworks的初级开发者,他描述了bigco的行径是怎样损害着他的国家。


john jiang 2013-08-22 14:43
]]>
oracle openworld 2013(上海)(原)http://www.blogjava.net/jiangshachina/archive/2013/08/02/402274.htmljohn jiangjohn jiangfri, 02 aug 2013 03:50:00 gmthttp://www.blogjava.net/jiangshachina/archive/2013/08/02/402274.htmlhttp://www.blogjava.net/jiangshachina/comments/402274.htmlhttp://www.blogjava.net/jiangshachina/archive/2013/08/02/402274.html#feedback0http://www.blogjava.net/jiangshachina/comments/commentrss/402274.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/402274.html
oracle openworld 2013(上海)
oracle openworld 2013(上海)已经结束,我全程参与了此次技术大会。本文是我参会的所见、所闻、所思与所感,有文,有图,且无码。(2013.08.03最后更新)

oracle openworld 2013(shanghai) lasted four days, and included over 300 keynotes, sessions and hands-on labs. 18000 attendees, including company leaders, industries experts and developers, joined the event, which was said the biggest it activity in asia-pacific area as never before.

sessions
* java strategy/technical keynote
every javaone conference must has such keynote, which introduces and demonstrates the latest new jdk versions and features, and prospects upcoming jdk version and features. all parts of java ecosystem, including jme, jse, jee and javafx, are involved in the keynote. i was deeply impressed by javafx demo by jim weaver. the java champion played musical instruments with a javafx application; that's really cool. in this javaone, javafx has a separated track, and the technology is very highlighted by oracle. the software giant expects javafx to enhance java's performance on rich application. but outspokenly, it's difficult to take end users to install a jre beforehand if they just want to run a desktop application.


* why should i switch to java se 7?
jdk 6 has rested in peace(exactly, the version isn't completely frozen), and jdk 7 has been released for two years, and even jdk 8 will come next march. but do you migrate your jre to java 7? strings in switch statements, diamond operator, try with resources, multi-catch statement, ... do you like the compiler sugar? anyway, the features could improve development productivity. but fork-join framework really touches my heart. because of complete backward compatibility, you just need to run your application in the new platform, and the performance could be accelerated.

* the road to lambda
java has been being criticised for implementing modern language features, like closure. the argument around closure implementation has lasted for a long time. finally, lambda, the closure for java, has been finished, and will be distributed with jdk 8. lambda is regarded as the biggest change since generics in java 5. function programming (fp) is popular, and java developers can utilise lambda expression to apply fp. and jdk 8 extends collection framework to take advantage of lambda as powerful as possible. for example, currently we need an iterator or loop to iterate a collection container, that's so-called external iteration. jdk 8 adds a new method foreach() for collection interface, and we can iterate a collection object via internal iteration, that means it's no need to care the iteration details by developers. in fact, it's impossible to debug the iterating process. and the process may apply fork-join if possible to improve performance. that sounds cool!

* effective scala
dynamic programming languages are emerging, and more languages, such as groovy, scala and clojure, can run on jvm, and jdk 7 introduced a new instruction "invokedynamic" to support dynamic languages better. therefore, i have to pay some attention to other jvm languages. the session "effective scala" was provided by typesafe, a company founded by scala creators; spring founder rod johnson joined the company as member of board of directors last year. the session simulated the style of effective java, and also introduced some best practices and coding regulations. frankly, i don't know scala syntax, like traits, totally, so i don't understand the lecture exactly. but i still obtained something new, like cake pattern. do you know the design pattern? i never hear it before this session, but the pattern is well-known in scala world. it's time to learn another language, and i think groovy is a good start-up due to it is closer to java.

* taobao gcih

taobao creates gcih (gc invisible heap) based on oracle vm hotspot, and allows different vm to access a shared heap. in taobao business scenario, the shared heap only stores read-only objects. all of the objects are initialized after the application is launched, and not be cleaned before the application shutdowns. therefore, single vm can consume less memory, that means we can deploy more vms in single machine. and gcih cannot be accessed by gc so that reducing the garbage collection overhead.

* what do you mean, backwards compatibility?
10gen, the company behind mongodb, presented the topic. because old mongodb driver isn't well designed, so it's difficult to understand and use the driver api. for example, the driver has a lot of find() methods, but how do i know which one is my want. according to the session, method chaining not only makes codes more readable, and the idiom benefits the backward compatibility as well. generally, method chaining only handle one parameter at each invoking, and application codes don't care the sequence of invoking. in this case, it's no need to provide the methods with several arguments, and the methods are possible to be deprecated in later time due to their ambiguity. of course, method chaining isn't designed for backward compatibility. and applying backward compatibility is still a hard job, because you don't know what would happen in the future. in fact, many and many apis are deprecated in later jdk versions.

appreciation party
in the third evening, oracle catered a small but exciting party to every attendee. beer, snacks, professional band, beautiful girls, hot performances, ... were on the stage. all of performances were shown by oracle employees, and we just rock!



i was on the scene
in otn lounge, i communicated with jim weaver and his assistant about javafx and ui testing. in this event, my small dream, taking a photo with java mascot -- duke, came true ^_^ additionally, don't forget iron man :-)


event organization
this is my sixth suntechdays/oow, and i must say that the conference was organized the best this time. professional agenda, considerate attendee service, delicious dessert and drinking, all of them, except for lunch, must be delighted.



finally, i must say the event is well worth participating; join it, and enjoy it :-)


john jiang 2013-08-02 11:50
]]>
世界一直在变(原)http://www.blogjava.net/jiangshachina/archive/2013/07/14/401546.htmljohn jiangjohn jiangsun, 14 jul 2013 03:15:00 gmthttp://www.blogjava.net/jiangshachina/archive/2013/07/14/401546.htmlhttp://www.blogjava.net/jiangshachina/comments/401546.htmlhttp://www.blogjava.net/jiangshachina/archive/2013/07/14/401546.html#feedback3http://www.blogjava.net/jiangshachina/comments/commentrss/401546.htmlhttp://www.blogjava.net/jiangshachina/services/trackbacks/401546.html
世界一直在变
    最近有点儿闲,看了一些文章,有了一些感触,写成一篇小文。软件世界真的变化很大,没有永恒的终结者。今天的终结者,明天可能就会被别人终结。道理大家都懂,但现实依然很残酷。(2013.07.29最后更新)

    this week, i read some articles about some api and tools that developers, especially java guys, must know. fortunately, i really know some of them, but unfortunately, i really miss something.

    please let me introduce some cases at first:
    1. in our real projects, we only use jdk 6, but the version had been in end-of-life; we never touch jdk 7, but jdk 8 is upcoming. i don't know how much time we would spend on accepting expression. in fact, at present, a lot of java developers cannot understand generics exactly, however the syntax has been introduced for more than 8 years. of course, java generics is a bit ambiguous, so it may be difficult to understand.
    2. ant was ever the standard for building, and it still being used by many projects, even new ones. maven was designed to terminate ant due to the older cannot make life easy. some conceptions of maven, such as build life cycle, dependency management, default directory structure, are very advanced. but maven dependency and transitive dependency management is nightmare, you have to include/exclude this or that. and extending maven is also a hard job. i have real experience on both of them, i even wrote some popular preliminary about maven several years ago. but what i really don't know? i don't know maven is becoming legacy, and worse, a new super star is on stage. outspokenly, i never hear of the artifact before this week :-( outstanding spring framework is a very case about the trend. at beginning, obviously spring is built by ant, then the framework switched to maven some years ago, but last year spring migrated to gradle.
    3. google-collections was well-known if you used it or not, and i know however i never use the api. but what i really don't know? i don't know google-collections was closed several years ago, and even it was combined by guava, which is a new rock star in java ecosystem.

  oh, something is born, and then grows, and then rests in peace. that's nature, and we have to face it, but why i don't know? exactly, i have no idea.
  world has been changing, and is changing faster as never before. how to keep us up-to-date with new fashion? i think the question may be asked by every "old" developer. after a long term career life, some of us may become veteran, but absolutely, it's impossible that everyone become expert, particularly the expert in underlying fields. we just be proficient in some programming languages, frameworks, apis, or tools. so we must update our brains continuously.
  maybe the issue is one of the middle life crisis problems, good luck for you and me :-)

john jiang 2013-07-14 11:15
]]>
网站地图