神奇好望角 The Magical Cape of Good Hope

庸人不必自扰,智者何需千虑?
posts - 26, comments - 50, trackbacks - 0, articles - 11
  语源科技BlogJava :: 首页 ::  :: 联系 :: 聚合  :: 管理

2007年11月30日

21 世纪初,Spring 框架的诞生和崛起让沉重而腐朽的 J2EE 遭到了当头棒喝,随后大批开发人员转投 Spring 阵营,呼吸间就让 J2EE 阵营大伤元气。然而这种命悬一线的危机并没有造成毁灭性的打击,尤其是对于 Java 这种提倡开放的平台而言,取长补短,互相促进才是正道。于是,JCP 委员会痛定思痛,在 2006 年推出 Java EE 5 规范,主要是对 EJB 的开发进行了极大幅度的简化。2008 年发布的 Java EE 6 引入了 CDI、BV、JAX-RS 等一系列新功能,并且以配置文件(profile)的方式让 Java EE 向轻量级迈进了一步。特别有趣的是,Spring 框架也开始提供对某些 Java EE 注解的支持,是否标志着两大阵营开始合流?Java EE 7 预定于今年下半年发布,目标是支持云计算。最近几年来,云计算一直被炒作,却从来没有一个准确的定义和规范,希望 Java EE 7 能够在 Java 界扭转这种尴尬的局面。

下面开始详细列举 Java EE 7 的新功能前瞻,数据来源于《Java Magazine 2012-01/02》中的《Cloud/Java EE: Looking Ahead to Java EE 7》一文。Java EE 7 是以“日期驱动”的方式开发的,也就是说,在计划日期到达前没有完成的功能都将被推迟到 Java EE 8。

Java EE 7(JSR-342)

  • 主题:让应用程序能够在私有或公共云上容易地运行。
  • 该平台将定义一个应用程序元数据描述符,以描述 PaaS 执行环境(例如多租户、资源共享、服务质量,以及应用程序间的依赖)。
  • 支持 HTML5、WebSocket、JSON 等新标准,并为它们一一提供 API。
  • 消除受管 Bean、EJB、Servlet、JSF、CDI 和 JAX-RS 之间不一致的地方。
  • 可能在 Web 配置文件中包含 JAX-RS 2.0 和 JMS 2.0 API 修订版。
  • 更新一些现有的技术,可能引入用于 Java EE 的并发工具(JSR-236)和 JCache(JSR-107)。

Java Persistence 2.1(JSR-338)

  • 支持多租户。
  • 支持存储过程和厂商函数。
  • 用规则(Criteria)进行更新和删除。
  • 支持数据库大纲(Scheme)的生成。
  • 持久化上下文的同步。
  • 侦听器中的 CDI 注入。

JAX-RS 2.0: The Java API for RESTful Web Services(JSR-339)

  • 客户端 API——底层使用构建者模式,可能提供上层封装。
  • 超媒体——轻松创建和处理关联了资源的链接。
  • 使用 Bean 验证框架来验证表单或查询参数。
  • @Inject 更紧密集成。
  • 服务端的异步请求处理。
  • 使用“qs”进行服务端的内容协商。

Java Servlet 3.1(JSR-340)

  • 为 Web 应用程序优化 PaaS 模型。
  • 用于安全、会话和资源的多租户。
  • 基于 NIO2 的异步 I/O。
  • 简化的异步 Servlet。
  • 利用 Java EE 并发工具。
  • 支持 WebSocket。

Expression Language 3.0(JSR-341)

  • ELContext 分离为解析和求值上下文。
  • 可定制的 EL 强迫规则。
  • 在 EL 表达式中直接引用静态方法和成员。
  • 添加运算符,例如等于、字符串连接和取大小。
  • 与 CDI 集成,例如在表达式求值前/中/后生成事件。

Java Message Service 2.0(JSR-343)

  • 简化开发——改变 JMS 编程模型,让应用程序开发变得更加简单容易。
  • 清除/澄清现有规范中的模糊之处。
  • 与 CDI 集成。
  • 澄清 JMS 和其他 Java EE 规范之间的关系。
  • 新的强制性 API允许任何 JMS 提供者能与任何 Java EE 容器集成。
  • 来自平台的多租户和其他云相关的功能。

JavaServer Faces 2.2(JSR-344)

  • 简化开发——使配置选项动态化,使复合组件中的 cc:interface 可选,Facelet 标记库的速记 URL,与 CDI 集成,JSF 组件的 OSGi 支持。
  • 支持 Portlet 2.0 桥(JSR-329)的实现。
  • 支持 HTML5 的功能,例如 HTML5 表单、元数据、头部和区段内容模型。
  • 流管理,页面导航事件的侦听器,以及 fileUploadBackButton 等新组件。

Enterprise JavaBeans 3.2(JSR-345)

  • 增强 EJB 架构以支持 PaaS,例如多租户。
  • 对在 EJB 外使用容器管理的事务进行工厂化。
  • 更进一步使用注解。
  • 与平台中的其他规范对齐和集成。

Contexts and Dependency Injection 1.1(JSR-346)

  • 拦截器的全局排序和管理内建上下文的装饰器 API。
  • 可在 Java EE 容器外启动的嵌入式模式。
  • 声明式地控制归档中的哪些包和 Bean 将被扫描。
  • 注入日志之类的静态成员。
  • 将 Servlet 事件作为 CDI 事件发送。

Bean Validation 1.1(JSR-349)

  • 与其他 Java EE 规范集成。
  • JAX-RS:在 HTTP 调用中验证参数和返回值。
  • JAXB:将约束条件转换到 XML 模式描述符中。
  • 方法级别的验证。
  • 在组集合上应用约束条件。
  • 扩展模型以支持“与”和“或”风格的组合。

JCache: Java Temporary Caching API(JSR-107)

  • 在内存中暂存 Java 对象的 API 和语义,包括对象的创建、共享访问、缓存池、失效,以及跨 JVM 的一致性。

Java State Management(JSR-350)

  • 应用程序和 Java EE 容器可使用该 API 将状态管理的任务交给具有不同 QoS 特征的第三方提供者。
  • 基于 Java SE 的调用者可通过查询状态提供者来访问状态数据。
  • 可添加具有不同 QoS 的提供者,API 调用者能够按自己的规则进行查询。

Batch Applications for the Java Platform(JSR-352)

  • 用于批处理应用程序的编程模型,以及用于调度和执行工作的运行时。
  • 为标准编程模型定义批处理工作、批处理工作步骤、批处理应用程序、批处理执行器和批处理工作管理器。

Concurrency Utilities for Java EE(JSR-236)

  • 提供一个整洁、简单且独立的 API,使其能用于任何 Java EE 容器中。

Java API for JSON Processing(JSR-353)

  • 处理 JSON 的 Java API。

posted @ 2012-02-13 22:23 蜀山兆孨龘 阅读(5807) | 评论 (0)编辑 收藏

ForkJoinPool 是 Java SE 7 新功能“分叉/结合框架”的核心类,现在可能乏人问津,但我觉得它迟早会成为主流。分叉/结合框架是一个比较特殊的线程池框架,专用于需要将一个任务不断分解成子任务(分叉),再不断进行汇总得到最终结果(结合)的计算过程。比起传统的线程池类 ThreadPoolExecutorForkJoinPool 实现了工作窃取算法,使得空闲线程能够主动分担从别的线程分解出来的子任务,从而让所有的线程都尽可能处于饱满的工作状态,提高执行效率。

ForkJoinPool 提供了三类方法来调度子任务:

execute 系列
异步执行指定的任务。
invokeinvokeAll
执行指定的任务,等待完成,返回结果。
submit 系列
异步执行指定的任务并立即返回一个 Future 对象。

子任务由 ForkJoinTask 的实例来代表。它是一个抽象类,JDK 为我们提供了两个实现:RecursiveTaskRecursiveAction,分别用于需要和不需要返回计算结果的子任务。ForkJoinTask 提供了三个静态的 invokeAll 方法来调度子任务,注意只能在 ForkJoinPool 执行计算的过程中调用它们。

ForkJoinPoolForkJoinTask 还提供了很多让人眼花缭乱的公共方法,其实它们大多数都是其内部实现去调用的,对于应用开发人员来说意义不大。

下面以统计 D 盘文件个数为例。这实际上是对一个文件树的遍历,我们需要递归地统计每个目录下的文件数量,最后汇总,非常适合用分叉/结合框架来处理:

// 处理单个目录的任务
public class CountingTask extends RecursiveTask<Integer> {
    private Path dir;

    public CountingTask(Path dir) {
        this.dir = dir;
    }

    @Override
    protected Integer compute() {
        int count = 0;
        List<CountingTask> subTasks = new ArrayList<>();

        // 读取目录 dir 的子路径。
        try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
            for (Path subPath : ds) {
                if (Files.isDirectory(subPath, LinkOption.NOFOLLOW_LINKS)) {
                    // 对每个子目录都新建一个子任务。
                    subTasks.add(new CountingTask(subPath));
                } else {
                    // 遇到文件,则计数器增加 1。
                    count++;
                }
            }

            if (!subTasks.isEmpty()) {
                // 在当前的 ForkJoinPool 上调度所有的子任务。
                for (CountingTask subTask : invokeAll(subTasks)) {
                    count += subTask.join();
                }
            }
        } catch (IOException ex) {
            return 0;
        }
        return count;
    }
}

// 用一个 ForkJoinPool 实例调度“总任务”,然后敬请期待结果……
Integer count = new ForkJoinPool().invoke(new CountingTask(Paths.get("D:/")));
    

在我的笔记本上,经多次运行这段代码,耗费的时间稳定在 600 豪秒左右。普通线程池(Executors.newCachedThreadPool())耗时 1100 毫秒左右,足见工作窃取的优势。

结束本文前,我们来围观一个最神奇的结果:单线程算法(使用 Files.walkFileTree(...))比这两个都快,平均耗时 550 毫秒!这警告我们并非引入多线程就能优化性能,并须要先经过多次测试才能下结论。

posted @ 2012-02-09 10:40 蜀山兆孨龘 阅读(2751) | 评论 (2)编辑 收藏

前面已经看到,Socket 类的 getInputStream()getOutStream() 方法分别获取套接字的输入流和输出流。输入流用来读取远端发送过来的数据,输出流则用来向远端发送数据。

输入流

使用套接字的输入流读取数据时,当前线程会进入阻塞状态,直到套接字收到一些数据为止(亦即套接字的接收缓冲区有可用数据)。该输入流的 available() 方法只是返回接收缓冲区的可用字节数量,不可能知道远端还要发送多少字节。使用输入流的时候,最好先将它包装为一个 BufferedInputStream,因为读取接收缓冲区将导致 JVM 和底层系统之间的切换,应当尽量减少切换次数以提高性能。BufferedInputStream 的缓冲区大小最好设为套接字接收缓冲区的大小。

如果直接调用输入流的 close() 方法来关闭它,则将导致套接字被关闭。对此,Socket 类提供了一个 shutdownInput() 方法来禁用输入流。调用该方法后,每次读操作都将返回 EOF,无法再读取远端发送的数据。对这个 EOF 的检测,不同的输入流包装体现出不同的结果,可能读到 -1 个字节,可能读到的字符串为 null,还可能收到一个 EOFException 等等。禁用输入流后,远端输出流的行为是平台相关的:

  • 在 BSD 平台上,远端的发送的数据能正常接收,然后直接丢弃。远端无法知道本端的输入流已禁用。这和 JDK 文档描述的行为一致。
  • 在 WINSOCK 平台上,远端发送数据将会导致“连接被重置”的错误。
  • 在 Linux 平台上,远端发送的数据能继续接收,直到套接字输入缓冲区填满,之后远端再也无法发送数据(若使用阻塞模式则进入死锁)。

禁用输入流这种技术并不常用。

输出流

套接字的输出操作实际上仅仅将数据写到发送缓冲区内,当发送缓冲区填满且上次的发送成功后,由底层系统负责发送。如果发送缓冲区的剩余空间不够,当前线程就会阻塞。和输入流类似,最好将输出流包装为 BufferedOutputStream

如果套接字的双发都使用 ObjectInputStreamObjectOutputStream 来读写 Java 对象,则必须先创建 ObjectOutputStream,因为 ObjectInputStream 在构造的时候会试图读取对象头部,如果双发都先创建 ObjectInputStream,则会互相等待对方的输出,造成死锁:

// 创建的顺序不能颠倒!
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
    

类似于输入流,关闭输出流也导致关闭套接字,所以 Socket 类同样提供了一个 shutdownOutput() 来禁用输出流。禁用输出流后,已写入发送缓冲区的数据会正常发送,之后的任何写操作都会导致 IOException,且远端的输入流始终会读到 EOF。禁用输出流非常有用,例如套接字的双发都在发送完毕数据后禁用输入流,然后双方都会收到 EOF,从而知道数据已经全部交换完毕,可以安全关闭套接字。直接关闭套接字会同时关闭输入流和输出流,且断开连接,达不到这种效果。

使用流的阻塞套接字的优缺点

如果要使用流进行输入和输出,就只能用阻塞模式的套接字。这里总结一下阻塞套接字的优缺点。先看看优点:

  1. 编程模型简单,非常适合初学者上手。
  2. 以装饰器模式设计的 Java I/O 使得开发人员可以轻松地从 I/O 流读写任何类型的数据。

但在性能方面有致命的缺点:

  1. 由于服务器套接字接受连接,以及套接字的读写都会阻塞,性能低下。
  2. 如果不对 I/O 流手动进行缓冲,则可能造成一次只处理一个字节,性能低下。
  3. 服务器套接字每次只能接受一个连接,导致 JVM 和底层系统之间频繁的调用切换,性能低下。

下一篇文章开始探讨使用基于 NIO 的套接字通道和缓冲区实现伸缩性更强的 TCP 套接字。

posted @ 2012-01-19 14:37 蜀山兆孨龘 阅读(2317) | 评论 (1)编辑 收藏

ServerSocket 类和 Socket 类都提供了多个公共构造方法。不同的构造方法不仅带的参数不同,所具有的意义也不一样。下面分别解析这两个类的实例初始化过程。

ServerSocket 实例的初始化

ServerSocket 类提供了四个构造器:

public ServerSocket(int port) throws IOException
public ServerSocket(int port, int backlog) throws IOException
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
public ServerSocket() throws IOException

带参构造器用来创建已绑定的服务器套接字,也就是说构造成功后它就已经开始侦听指定的端口,且能够调用 accept() 方法来接受客户端连接。默认构造器则会创建未绑定的服务器套接字,构造成功后必须手动将其绑定到一个本地地址才能用,在绑定之前可以进行一些选项配置。

带参构造器

总的来说,带参构造器提供了三个参数:

port
指定该服务器套接字所要侦听的本地端口。如果为 0,则由系统自动分配一个端口号,这必须以另外的方式让客户端获取端口号。
backlog
这个名词目前还没有合适的译名。底层系统的 TCP 实现会维护一个连接队列,该队列缓存了已被 TCP 处理完毕,但还没有被服务器套接字接受的客户端连接。一旦某个连接被接受(通过调用 accept() 方法),它就会被从队列中移除。backlog 参数就用于指定队列的最大长度,默认值为 50,但这个值只是一个建议,底层系统可能根据需要自动调整。如果队列满了,则其行为是平台相关的:微软的 WINSOCK 会拒绝新的连接,其他实现则什么都不做。严格地说,微软没有遵守规范,破坏了游戏规则……
bindAddr
一台机器可能会有多个本地 IP 地址,例如同时使用多块网卡。使用其他两个带参构造器时,该参数为 null,服务器套接字会在所有的本地 IP 地址(0.0.0.0::0)上侦听。如果希望只侦听一个地址,则可使用该参数。

默认构造器

如果使用默认构造器,在绑定地址前,还可以做些配置。绑定操作由两个 bind 方法定义,参数类似于带参构造器。配置项包括以下方面(都必须在绑定前配置):

设置是否重用本地地址
该选项由 setReuseAddress(boolean on) 方法配置,对应底层系统的 SO_REUSEADDR 套接字选项。JDK 没有定义该选项的默认值。如果该选项为 false,则在关闭 TCP 连接时,为了保证可靠性,该连接可能在关闭后的一段时间(大约两分钟)内保持超时状态(通常称为 TIME_WAIT 状态或 2MSL 等待状态),这段时间里无法将新建的服务器套接字绑定到同一个地址。在开发阶段,服务器可能不断重启,打开改选项会非常有用。
设置接收缓冲区大小
该选项由 setReceiveBufferSize(int size) 方法配置,对应底层系统的 SO_RCVBUF 套接字选项,单位是字节。《RFC 1323 - TCP Extensions for High Performance》将缓冲区大小定义为 64KB。该选项只是一个建议值,底层系统可能根据需要自行调整。
设置超时值
该选项由 setSoTimeout(int timeout) 方法配置,对应底层系统的 SO_TIMEOUT 套接字选项,单位是毫秒。默认值为 0。该选项影响 accept 方法的阻塞时间长度,如果超时将引发 SocketTimeoutException。如果设为 0,则表示永不超时。
设置性能首选项
性能首选项包括连接时间、延迟和带宽三个选项,由 setPerformancePreferences(int connectionTime, int latency, int bandwidth) 方法配置。这三个数值分别表示短连接时间、低延迟和高带宽的相对重要性,数值越大则越重要;其各自的绝对值没有意义。该方法的初衷是为了让 Java 能在用非 TCP/IP 实现的套接字环境下工作得更好,某些需要对网络进行调优的程序也可以将这三个首选项作为配置参数提供给用户。

Socket 实例的初始化

Socket 类提供了六个公共构造器(已过时的除外):

public Socket(String host, int port) throws UnknownHostException, IOException
public Socket(InetAddress address, int port) throws IOException
public Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
public Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
public Socket()
public Socket(Proxy proxy)

前四个构造器创建已连接的客户端套接字,也就是说构造的时候就会去连接服务器。前两个构造器需要提供服务器的地址和端口作为参数,本地地址和端口由系统自动分配;后两个允许手动指定本地地址和端口,但极少使用。后两个构造器创建未连接的套接字,创建后需要调用 connect 方法手动连接,连接之前可以做一些配置。最后一个构造器接受一个代表代理服务其的 Proxy 对象,JDK 支持 HTTP 和 SOCKS(V4 或 V5)两种代理类型。

连接前的配置

在连接前,客户端套接字不仅像服务器套接字那样可以设置是否重用本地地址、缓冲区大小、超时值和性能首选项,还能够配置以下各项(都必须在连接前配置):

设置是否保持活跃
该选项由 setKeepAlive(boolean on) 方法配置,对应底层系统的 SO_KEEPALIVE 套接字选项。默认值为 false。如果打开该选项,则套接字会定期自动发送保持活跃的探测性消息,类似于心跳检测。根据《RFC 1122 - Requirements for Internet Hosts》的规定,保持活跃机制只是 TCP 的一个可选功能,如果支持的话,默认必须为 false,而且这种机制默认在成功建立连接后,且连续两小时没有数据传输的情况下才会被激活。从另一方面来看,通过套接字的 I/O 操作完全可以知道连接是否还有效,所以该选项的实用价值不大。
设置是否收发带外数据
该选项由 setOOBInline(boolean on) 方法配置,对应底层系统的 SO_OOBINLINE 套接字选项。默认值为 off。带外数据(Out-of-band Data)也叫做紧急数据,表示数据很重要,需要使用不同于发送普通数据的一个专用通道来发送。打开该选项后,就可以调用 sendUrgentData(int data) 方法发送一个字节的紧急数据。JDK 对带外数据只提供了有限支持,紧急数据将会和普通数据一起被收到,并且无法自动区分。该选项对应用开发人员意义不大。
设置是否从容关闭连接
该选项由 setSoLinger(boolean on, int linger) 方法配置,对应底层系统的 SO_LINGER 套接字选项。默认为 false。该选项只会影响套接字的关闭,其中的 linger 参数表示超时时间,单位为秒。如果打开改选项:如果将 linger 设为 0,则关闭套接字的时候,未发送的数据会被丢弃,且另一端会出现连接被同位体重置的异常;如果 linger 非 0,则关闭套接字的线程将被阻塞,直到数据全部发送或超时,超时后的行为与底层系统相关,JDK 无法控制。如果关闭该选项,则套接字正常关闭,数据也会全部发送。由于底层实现的差异性,不提倡应用开发人员打开该选项。
设置是否延迟发送数据
该选项由 setTcpNoDelay(boolean on) 方法配置,对应底层系统的 TCP_NODELAY TCP 选项。默认值为 off。打开该选项将禁用 Nagle 算法,TCP 包会立即发送;关闭该选项则会启用 Nagle 算法,多个较小的 TCP 包会被组合成一个大包一起发送,虽然发送延迟了,但有利于避免网络拥塞。默认为 false。该选项对实时性很强的程序可能有用,但一般的程序不需要关心。
设置流量类别
该选项由 setTrafficClass(int tc) 方法配置,对应底层系统的“流量类别”套接字属性。该选项用于向网络(例如路由器)提示从该套接字发送的包需要获取哪些服务类型,对本地 TCP 协议栈没有影响。IPv4 和 IPv6 分别定义了多个不同的值,例如 IPv4 将 0x08 定义为最大吞吐量,0x10 定义为最小延迟,等等。可以用或运算将多个值合并为一个选项。该选项用来调整性能,需要根据实际情况设置。由于只是建议值,可能被网络忽略。

posted @ 2012-01-16 10:31 蜀山兆孨龘 阅读(2344) | 评论 (2)编辑 收藏

网上很多关于单例模式写法的文章,不外乎饿汉和懒汉两种形式的讨论。很多人喜欢用懒汉式,因为觉得它实现了延迟加载,可以让系统的性能更好。但事实果真如此吗?我对此存疑。

首先我们检查一下饿汉和懒汉单例模式最简单的写法(这里不讨论哪种懒汉写法更好):

// 饿汉
public final class HungrySingleton {
    private static final HungrySingleton INSTANCE = new HungrySingleton();

    private HungrySingleton() {
        System.out.println("Initializing...");
    }

    public static HungrySingleton getInstance() {
        return INSTANCE;
    }
}

// 懒汉
public final class LazySingleton {
    private static LazySingleton INSTANCE;

    private LazySingleton() {
        System.out.println("Initializing...");
    }

    public static synchronized LazySingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LazySingleton();
        }
        return INSTANCE;
    }
}
    

从理论上来说,HungrySingleton 的单例在该类第一次使用的时候创建,而 LazySingleton 的单例则在其 getInstance() 方法被调用的时候创建。至于网上有人声称“饿汉式不管用不用都会初始化”,纯属走路的时候步子迈得太大。谁的加载更迟?如果你只是调用它们的 getInstance() 方法来得到单例对象,则它们都是延迟加载,这样懒汉式没有任何意义,而且由于 LazySingleton 采取了同步措施,性能更低(可以说任何懒汉式的性能都低于饿汉式)。当你使用一个单例类的时候,难道第一步不是调用 getInstance() 么?所以在自己的代码里,我更喜欢用饿汉式。

下面用一个例子来测试加载顺序:

// 饿汉
System.out.println("Before");
HungrySingleton.getInstance();
System.out.println("After");

// 懒汉
System.out.println("Before");
LazySingleton.getInstance();
System.out.println("After");
    

输出结果都是:

Before
Initializing...
After

那么,懒汉模式还有什么存在意义?如果系统使用了某些需要在启动时对类进行扫描的框架,使用饿汉式的话,启动时间比懒汉式更长,如果使用了大量单例类,不利于开发阶段。在系统的正式运行阶段,所有的单例类迟早都要加载的,总的说来两者性能持平,但是懒汉式每次都至少多一个判断,所以越到后期越体现饿汉的优越性。

最后,推荐下《Effective Java》第二版指出的用枚举类型实现的饿汉单例模式:

// 饿汉
public enum HungrySingleton {
    INSTANCE;

    private HungrySingleton() {
    }
}

这种写法不但最简洁,还能轻易扩展为实例数量固定的“多例模式”。

posted @ 2012-01-10 17:39 蜀山兆孨龘 阅读(1930) | 评论 (6)编辑 收藏

前面介绍了各种请求参数的注入,这些参数在 HTTP 请求中都是以纯文本的方式存在的。在处理参数的时候,往往需要把这些文本参数转换为 Java 对象。JAX-RS 提供了一些内置的规则里自动完成这种转换。

转换规则一览

JAX-RS 提供了四条自动类型转换规则,下面我们逐条考察。

原始类型

这个早就见识过了,无需多说。举例回顾一下:

@GET
@Path("{id}")
public Movie getXxx(@PathParam("id") int id) {/*...*/}
    

提供接受单个 String 参数的构造器的类型

这个也不难理解,JAX-RS 会自动调用该构造器创建一个对象:

public class Style {
    public Style(String name) {/* ... */}
    // ...
}

@GET
@Path("{name}")
public Movie getXxx(@PathParam("name") Style style) {
    // JAX-RS 已自动调用 xxx = new Style(name)
    // ...
}
    

提供静态工厂方法 valueOf(String) 的类型

也好理解。特别需要注意的是,所有的枚举类型都在此列,因为编译器会自动给枚举类型加上一个这样的工厂方法。例如:

public enum Style {/*...*/}

@GET
@Path("{name}")
public Movie getXxx(@PathParam("name") Style style) {
    // JAX-RS 已自动调用 style = Style.valueOf(name)
    // ...
}
    

类型参数满足前两个条件的 List<T>Set<T>SortedSet<T>

这条规则适用于多值参数,例如查询参数:

@GET
@Path("xxx")
public Movie getXxx(@QueryParam("style") Set<Style> styles) {
    // JAX-RS 已自动转换每个 Style 对象并组装到 Set 中
    // ...
}
    

转换失败的处理

如果转换失败,JAX-RS 会根据情况自动抛出一个包装了初始异常,但是带不同 HTTP 错误码的 WebApplicationException:对矩阵参数(@MatrixParam)、查询参数 (@QueryParam)或路径参数(@PathParam)来说为 HTTP 404 找不到,而对头部参数(@HeaderParam)或 Cookie 参数(@CookieParam)为 HTTP 400 错误请求

posted @ 2012-01-10 13:17 蜀山兆孨龘 阅读(3237) | 评论 (2)编辑 收藏

JDK 提供了对 TCP(Transmission Control Protocol,传输控制协议)和 UDP(User Datagram Protocol,用户数据报协议)这两个数据传输协议的支持。本文开始探讨 TCP。

TCP 基础知识

在“服务器-客户端”这种架构中,服务器和客户端各自维护一个端点,两个端点需要通过网络进行数据交换。TCP 为这种需求提供了一种可靠的流式连接,流式的意思是传出和收到的数据都是连续的字节,没有对数据量进行大小限制。一个端点由 IP 地址和端口构成(专业术语为“元组 {IP 地址, 端口}”)。这样,一个连接就可以由元组 {本地地址, 本地端口, 远程地址, 远程端口} 来表示。

连接过程

在 TCP 编程接口中,端点体现为 TCP 套接字。共有两种 TCP 套接字:主动和被动,“被动”状态也常被称为“侦听”状态。服务器和客户端利用套接字进行连接的过程如下:

  1. 服务器创建一个被动套接字,开始循环侦听客户端的连接。
  2. 客户端创建一个主动套接字,连接服务器。
  3. 服务器接受客户端的连接,并创建一个代表该连接的主动套接字。
  4. 服务器和客户端通过步骤 2 和 3 中创建的两个主动套接字进行数据传输。

下面是连接过程的图解:

TCP 连接
TCP 连接

一个简单的 TCP 服务器

JDK 提供了 ServerSocket 类来代表 TCP 服务器的被动套接字。下面的代码演示了一个简单的 TCP 服务器(多线程阻塞模式),它不断侦听并接受客户端的连接,然后将客户端发送过来的文本按行读取,全文转换为大写后返回给客户端,直到客户端发送文本行 bye

public class TcpServer implements Runnable {
    private ServerSocket serverSocket;

    public TcpServer(int port) throws IOException {
        // 创建绑定到某个端口的 TCP 服务器被动套接字。
        serverSocket = new ServerSocket(port);
    }

    @Override
    public void run() {
        while (true) {
            try {
                // 以阻塞的方式接受一个客户端连接,返回代表该连接的主动套接字。
                Socket socket = serverSocket.accept();
                // 在新线程中处理客户端连接。
                new Thread(new ClientHandler(socket)).start();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

public class ClientHandler implements Runnable {
    private Socket socket;

    public ClientHandler(Socket socket) {
        this.socket = Objects.requireNonNull(socket);
    }

    @Override
    public void run() {
        try (Socket s = socket) {  // 减少代码量的花招……
            // 包装套接字的输入流以读取客户端发送的文本行。
            BufferedReader in = new BufferedReader(new InputStreamReader(
                    s.getInputStream(), StandardCharsets.UTF_8));
            // 包装套接字的输出流以向客户端发送转换结果。
            PrintWriter out = new PrintWriter(new OutputStreamWriter(
                    s.getOutputStream(), StandardCharsets.UTF_8), true);

            String line = null;
            while ((line = in.readLine()) != null) {
                if (line.equals("bye")) {
                    break;
                }

                // 将转换结果输出给客户端。
                out.println(line.toUpperCase(Locale.ENGLISH));
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}
    

阻塞模式的编程方式简单,但存在性能问题,因为服务器线程会卡死在接受客户端的 accept() 方法上,不能有效利用资源。套接字支持非阻塞模式,现在暂时略过。

一个简单的 TCP 客户端

JDK 提供了 Socket 类来代表 TCP 客户端的主动套接字。下面的代码演示了上述服务器的客户端:

public class TcpClient implements Runnable {
    private Socket socket;

    public TcpClient(String host, int port) throws IOException {
        // 创建连接到服务器的套接字。
        socket = new Socket(host, port);
    }

    @Override
    public void run() {
        try (Socket s = socket) {  // 再次减少代码量……
            // 包装套接字的输出流以向服务器发送文本行。
            PrintWriter out = new PrintWriter(new OutputStreamWriter(
                    s.getOutputStream(), StandardCharsets.UTF_8), true);
            // 包装套接字的输入流以读取服务器返回的文本行。
            BufferedReader in = new BufferedReader(new InputStreamReader(
                    s.getInputStream(), StandardCharsets.UTF_8));

            Console console = System.console();
            String line = null;
            while ((line = console.readLine()) != null) {
                if (line.equals("bye")) {
                    break;
                }

                // 将文本行发送给服务器。
                out.println(line);
                // 打印服务器返回的文本行。
                console.writer().println(in.readLine());
            }

            // 通知服务器关闭连接。
            out.println("bye");
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}
    

从 JDK 文档可以看到,ServerSocketSocket 在初始化的时候,可以设定一些参数,还支持延迟绑定。这些东西对性能和行为都有所影响。下一篇文章将详解这两个类的初始化。

posted @ 2012-01-04 22:21 蜀山兆孨龘 阅读(3125) | 评论 (5)编辑 收藏

我竟然到现在才发现《Fundamental Networking in Java》这本神作,真有点无地自容的感觉。最近几年做的都是所谓的企业级开发,免不了和网络打交道,但在实际工作中,往往会采用框架将底层细节和上层应用隔离开,感觉就像是在一个 Word 模板表单里面填写内容,做出来也没什么成就感。虽然没有不使用框架的理由,但我还真是有点怀念当初直接用套接字做网络编程的日子,既能掌控更多东西,还可以学到更多知识,为研究框架的实现原理打基础。闲话完毕,转入今天的正题:IP(Internet Protocol,互联网协议)。

IP 基础知识

说到 IP,大多数人的第一反应估计都是 IP 地址。其实 IP 是一种协议,IP 地址只是协议的一部分。《RFC 791 - INTERNET PROTOCOL》说:“互联网协议是为在包交换计算机通信网络的互联系统中使用而设计的。”IP 包含三方面的功能:

  1. 用于查找主机的寻址系统
  2. 包格式的定义
  3. 传输和接收包的规则

IP 的相关 Java 类

从 Java 的角度来看上面说到的三个功能,只有第一个是开发人员需要关心的。另外两个都依赖底层系统的实现,JDK 也没有提供相关的类去操作。下面一一介绍 JDK 提供的用于处理 IP 地址的类。

InetAddress

此类用来表示 IP 地址,它有两个子类:Inet4AddressInet6Address,分别用于处理 IPv4 和 IPv6 两个版本。在实际应用中,InetAddress 足以应付绝大多数情况。它提供了一些静态方法来构造实例,能根据参数格式自动识别 IP 版本:

public static InetAddress[] getAllByName(String host) throws UnknownHostException
解析指定的主机地址,并返回其所有的 IP 地址;如果传入 IP 地址字符串,则只会校验格式,返回的数组也只包含一个代表该 IP 地址的实例。例如,想看看谷歌有多少马甲的话,InetAddress.getAllByName("www.google.com") 就可以了。
public static InetAddress getByAddress(byte[] addr) throws UnknownHostException
用表示 IP 地址的字节数组(专业术语称为“原始 IP 地址”)构造一个实例。IPv4 地址必须是 4 个字节,IPv6 必须 16 个。不常用。
public static InetAddress getByAddress(String host, byte[] addr) throws UnknownHostException
用主机地址和原始 IP 地址构造一个实例。此方法应该慎用,因为它不会对主机名进行解析。即使主机名为 IP 地址字符串,也不会检查是否与字节数组一致。
public static InetAddress getByName(String host) throws UnknownHostException
用主机地址构造一个实例,也可以直接传入 IP 地址字符串,等同于 getAllByName(host)[0]
public static InetAddress getLocalHost() throws UnknownHostException
返回本机在网络中的地址。
public static InetAddress getLoopbackAddress()
返回环回地址 127.0.0.1,不抛出异常,等同于 getByName("localhost")(不要和 getLocalHost() 搞混)。环回地址使主机能够自己连接自己,常被用来对在同一台机器上测试网络应用程序。在 IPv4 中,环回地址的网段为 127.0.0.0/8,通常用 127.0.0.1;IPv6 中只有一个 ::1

接下来看看 InetAddress 中定义的部分实例方法:

public byte[] getAddress()
返回原始 IP 地址。
public String getCanonicalHostName()
返回全限定域名。这个方法可以用来探查实际的主机名,例如 InetAddress.getByName("www.google.com").getCanonicalHostName() 返回 we-in-f99.1e100.net
public String getHostAddress()
返回构造时传入的主机地址。
public String getHostName()
返回主机名。如果构造时传入的主机地址为 IP 地址字符串,则调用 getCanonicalHostName(),否则直接返回构造时传入的主机地址。
public boolean isAnyLocalAddress()
检查是否为通配符地址。通配符地址为 0.0.0.0(IPv4)或 ::0(IPv6),代表所有的本地 IP 地址。例如,假设电脑有两块网卡,各有一个地址,如果想让一个程序同时监听这两个地址,就需要用通配符地址。
public boolean isLinkLocalAddress()
检查是否为链路本地地址。IPv4 里定义为地址段 169.254.0.0/16,Ipv6 里是以 fe80::/64 为前缀的地址。在电脑没联网的时候查看本机 IP,就能看到这种地址。
public boolean isLoopbackAddress()
检查是否为环回地址。
public boolean isSiteLocalAddress()
检查是否为站点本地地址。站点本地地址这个名词实际上已经过时了,现在叫唯一本地地址。IPv4 中未定义;IPv6 中定义为地址段 fc00::/7。这些地址用于私有网络,例如企业内部的局域网。

此外还有一些有关多播地址的方法,暂时略过。

JDK 默认同时支持 IPv4 和 IPv6。如果只想使用一种,可以根据情况将 java.net.preferIPv4Stackjava.net.preferIPv6Addresses 这两个系统属性之一设为 true。两个属性的默认值都为 false。一般来说不需要去惊动它们。

SocketAddress

该类是一个空壳,事实上应用程序使用的是它的唯一子类 InetSocketAddress,目前还看不出这样设计有什么意义。该类只不过在 InetAddress 的基础上增加了一个端口属性。

NetworkInterface

该类代表网络接口,例如一块网卡。一个网络接口可以绑定一些 IP 地址。具有多个网络接口的主机被称为多宿主主机。下面的代码可打印出所有本机网络接口的信息:

for (NetworkInterface ni : Collections.list(NetworkInterface.getNetworkInterfaces())) {
    System.out.println(ni);
    for (InterfaceAddress ia : ni.getInterfaceAddresses()) {
        System.out.println("\t" + ia);
    }
    System.out.println();
}
    

在我的笔记本上运行结果为:

name:lo (Software Loopback Interface 1)
	/127.0.0.1/8 [/127.255.255.255]
	/0:0:0:0:0:0:0:1/128 [null]
name:net0 (WAN Miniport (SSTP))
name:net1 (WAN Miniport (L2TP))
name:net2 (WAN Miniport (PPTP))
name:ppp0 (WAN Miniport (PPPOE))
name:eth0 (WAN Miniport (IPv6))
name:eth1 (WAN Miniport (Network Monitor))
name:eth2 (WAN Miniport (IP))
name:ppp1 (RAS Async Adapter)
name:net3 (WAN Miniport (IKEv2))
name:net4 (Intel(R) Wireless WiFi Link 4965AGN)
	/fe80:0:0:0:288a:2daf:3549:1811%11/64 [null]
name:eth3 (Broadcom NetXtreme 57xx Gigabit Controller)
	/10.140.1.133/24 [/10.140.1.255]
	/fe80:0:0:0:78c7:e420:1739:f947%12/64 [null]
name:net5 (Teredo Tunneling Pseudo-Interface)
	/fe80:0:0:0:e0:0:0:0%13/64 [null]
name:net6 (Bluetooth Device (RFCOMM Protocol TDI))
name:eth4 (Bluetooth Device (Personal Area Network))
name:eth5 (Cisco AnyConnect VPN Virtual Miniport Adapter for Windows x64)
name:net7 (Microsoft ISATAP Adapter)
	/fe80:0:0:0:0:5efe:a8c:185%17/128 [null]
name:net8 (Microsoft ISATAP Adapter #2)
name:net9 (Intel(R) Wireless WiFi Link 4965AGN-QoS Packet Scheduler-0000)
name:eth6 (Broadcom NetXtreme 57xx Gigabit Controller-TM NDIS Sample LightWeight Filter-0000)
name:eth7 (Broadcom NetXtreme 57xx Gigabit Controller-QoS Packet Scheduler-0000)
name:eth8 (Broadcom NetXtreme 57xx Gigabit Controller-WFP LightWeight Filter-0000)
name:eth9 (WAN Miniport (Network Monitor)-QoS Packet Scheduler-0000)
name:eth10 (WAN Miniport (IP)-QoS Packet Scheduler-0000)
name:eth11 (WAN Miniport (IPv6)-QoS Packet Scheduler-0000)
name:net10 (Intel(R) Wireless WiFi Link 4965AGN-Native WiFi Filter Driver-0000)
name:net11 (Intel(R) Wireless WiFi Link 4965AGN-TM NDIS Sample LightWeight Filter-0000)
name:net12 (Intel(R) Wireless WiFi Link 4965AGN-WFP LightWeight Filter-0000)

posted @ 2011-12-30 17:39 蜀山兆孨龘 阅读(2737) | 评论 (0)编辑 收藏

《JAX-RS 从傻逼到牛叉 3:路径匹配》中,我们已经见过如何使用 @PathParam@QueryParam@MatrixParam 分别注入 URI 中的路径参数、矩阵参数和查询参数,以及如何编程访问这些参数。本文介绍表单参数、HTTP 头部参数和 Cookie 参数的注入。

表单参数

HTTP 请求也可以使用提交表单的方式。这时请求方法一般是 POST,当然春哥也无法阻止你用 GET。在前面我们虽然介绍过处理 POST 请求的例子,但那只是利用了 JAX-RS 对 JAXB 的支持,并没有涉及到对具体请求参数的注入。JAX-RS 提供了 @FormParam 注解来注入 POST 请求的参数,例如:

@POST
public Response createMovie(@FormParam("title") String title) {
    // 此处省略若干行
}
    

这儿省略了 @Consumes 注解,JAX-RS 会自动默认为 @Consumes(MediaType.APPLICATION_FORM_URLENCODED),也就是 application/x-www-form-urlencoded 格式的请求。如果请求格式为 multipart/form-data,就必须显示指明:

@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response createMovie(@FormParam("title") String title) {
    // 此处省略若干行
}
    

JAX-RS 还支持文件的上传和下载,以后再介绍。

HTTP 头部参数

注入 HTTP 头部参数简单得不能再简单了:

@GET
@Path("xxx")
@Produces(MediaType.TEXT_PLAIN)
public String xxx(@HeaderParam("User-Agent") String userAgent) {
    // 此处省略若干行
}
    

如果有很多头部参数,为了避免臃肿的参数列表,可以注入一个头部对象,然后编程访问头部参数:

@GET
@Path("xxx")
@Produces(MediaType.TEXT_PLAIN)
public String xxx(@Context HttpHeaders headers) {
    // 此处省略若干行
}
    

Cookie 参数

注入 Cookie 参数同样的简单:

@GET
@Path("xxx")
@Produces(MediaType.TEXT_PLAIN)
public String xxx(@CookieParam("userName") String userName) {
    // 此处省略若干行
}
    

如果希望编程访问,则可以像编程访问那样注入一个 HttpHeaders 对象,然后通过它的 getCookies() 方法来获取所有的 Cookie。

posted @ 2011-12-29 16:34 蜀山兆孨龘 阅读(4448) | 评论 (4)编辑 收藏

Exchanger 用来让两个线程互相等待并交换计算结果。这个类的用法很简单,因为它就定义了两个重载的 exchange 方法,参数多的那个无非增加了对超时的支持。当一个线程调用 exchange 的时候(以计算结果作为参数),它就开始等待另一个线程调用 exchange,然后两个线程分别收到对方调用 exchange 时传入的参数,从而完成了计算结果的交换。

不用太多的解释,运行下面这个例子就一清二楚:

final Exchanger<String> e = new Exchanger<>();

new Thread() {
    @Override
    public void run() {
        long id = Thread.currentThread().getId();
        String s = "abc";
        System.out.println("线程 [" + id + "] 算出 " + s);

        try {
            TimeUnit.SECONDS.sleep(new Random().nextInt(5));
            System.out.println("线程 [" + id + "] 收到 " + e.exchange(s));
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
}.start();

new Thread() {
    @Override
    public void run() {
        long id = Thread.currentThread().getId();
        String s = "xyz";
        System.out.println("线程 [" + id + "] 算出 " + s);

        try {
            TimeUnit.SECONDS.sleep(new Random().nextInt(5));
            System.out.println("线程 [" + id + "] 收到 " + e.exchange(s));
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
}.start();
    

运行结果(可能为):

线程 [9] 算出 abc
线程 [10] 算出 xyz
线程 [10] 收到 abc
线程 [9] 收到 xyz

最后强调下,该类只适用于两个线程,妄图用它来处理多个生产者和消费者之间的数据交换是注定要失败的……

posted @ 2011-12-27 10:50 蜀山兆孨龘 阅读(1551) | 评论 (0)编辑 收藏

目前我们的电影服务只提供了对电影信息的访问服务,现在我们要再增加两项级服务,分别用来访问导演和演员信息。加上原先的电信信息服务,我们把 URI 统一放到 /ms/rest/service/ 的子路径下。最先想到的方法就是为这三个 URI 分别写 JAX-RS 服务:

@Singleton
@Path("service/movie")
public class MovieService {
    // 此处省略若干行
}

@Singleton
@Path("service/director")
public class DirectorService {
    // 此处省略若干行
}

@Singleton
@Path("service/director")
public class ActorService {
    // 此处省略若干行
}
    

这种写法的缺点就是让三个本来有点关系(父级 URI 相同)的服务被放到了毫不相干的三个类里面,不一个个类地查看注解难以看出这点关系。为此,JAX-RS 提供了动态资源绑定的功能,让我们能够对这种情况做一些整理。

首先,我们引入一个服务定位器来处理集中管理这三个子级服务:

@Singleton
@Path("service")
public class ServiceLocator {
    @Inject
    private MovieService movieService;
    @Inject
    private DirectorService directorService;
    @Inject
    private ActorService actorService;
    private Map<String, Object> serviceMap;

    @PostConstruct
    private initServiceMap() {
        serviceMap = new HashMap<>();
        serviceMap.put("movie", movieService);
        serviceMap.put("director", directorService);
        serviceMap.put("actor", actorService);
    }

    @Path("{name}")
    public Object locateService(@PathParam("name") String name) {
        Object service = serviceMap.get(name);
        if (service == null) {
            throw new WebApplicationException(Status.SERVICE_UNAVAILABLE);
        }
        return service;
    }
}
    

该类中的 locateService 方法根据服务的名称返回相应的服务实例,注意该方法只有一个 @Path 注解,因为它并不清楚请求的具体内容;返回对象的类型为 Object,表明动态资源定位不要求服务类实现相同的接口,只需要它们的方法带有相应的 JAX-RS 注解,就能够被 JAX-RS 自动发现和处理(专业术语称为 introspect,内省),以 MovieService 为例:

@Singleton
public class MovieService {
    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Movie find(@PathParam("id") int id) {
        Movie movie = movieDao.get(id);
        if (movie != null) {
            return movie;
        } else {
            throw new WebApplicationException(Status.NOT_FOUND);
        }
    }

    // 此处省略若干行
}

这样,每个请求实际上都由两个类先后处理。例如,处理请求 GET /ms/rest/service/movie/1 的时候,先由 ServiceLocator 返回相配的服务实例 movieService,然后再由该实例的 find 方法返回结果。比起最开始那三个简单的类,虽然多了一层调用,但换来了更加清晰的结构。

动态资源定位是一个非常灵活强大的功能,用好的话,完全可以把 URI 层次整理成一个类似于文件目录结构的抽象文件系统。

posted @ 2011-12-21 16:00 蜀山兆孨龘 阅读(2983) | 评论 (0)编辑 收藏

笼子大了什么鸟都有。同样的道理,不论多么细心地设计 URI 结构,在系统复杂到一定程度后,仍然难以避免路径冲突。为此,JAX-RS 使用一些规则来定义路径匹配的优先级。

如果某个请求路径可以对上多个 URI 匹配模式,那么 JAX-RS 就把可能匹配上的 URI 模式先拼接完整,按照下列规则依次进行比较,直到找出最适合的匹配模式:

  1. 首先,字面字符数量更多的 URI 模式优先。“字面字符”就是写死的路径段,不包含路径分隔符 / 和模板参数。例如 /ms/rest/movie/{id : \\d+} 包含 11 个字面字符。
  2. 其次,模板参数个数最多的 URI 模式优先。例如 /ms/rest/movie/{id : \\d+} 带一个模板参数。
  3. 最后,含正则表达式的模板参数个数最多的 URI 模式优先。例如 /ms/rest/movie/{id : \\d+} 带一个含正则表达式的模板参数。

现在看一个例子。回顾一下,/ms/rest/movie/{id : \\d+} 已经用来根据 ID 获取电影信息。为了制造麻烦,现在引入 /ms/rest/movie/{title} 来根据电影标题获取电影信息。先请你猜一猜 /ms/rest/movie/300 代表啥?ID 为 300 的神秘电影,还是我们可爱的勇士?只能跟着规则一条一条地看:

  1. 首先,两个 URI 匹配模式的字面字符都是 11,下一步。
  2. 其次,两个 URI 匹配模式都带一个模板参数,下一步。
  3. 最后,只有 /ms/rest/movie/{id : \\d+} 带了一个含正则表达式的模板参数,胜利!所以返回 ID 为 300 的片片。

传说这三条规则能够覆盖 90% 以上的情景。不过我们马上就能造出一个打破规则的东西:/ms/rest/movie/{title : [ \\w]+}。经过测试,/ms/rest/movie/300 会匹配上 /ms/rest/movie/{id : \\d+}。如何解释?JAX-RS 规范文档 3.7.2 定义了完整的匹配规则,对于这两个简单的 URI 匹配模式,似乎一直进行到底都无法比较出优先级。莫非有另外的潜规则?或者是 JAX-RS 的实现(参考实现为 Jersey)自行规定?但无论如何,搞出这种怪物本身就是一个设计错误,所以也不必去深究原因。

posted @ 2011-12-07 15:10 蜀山兆孨龘 阅读(2359) | 评论 (0)编辑 收藏

以前我曾用两个类(ZipItemZipSystem)实现了一个简单的 ZIP 文件系统(以下简称 ZFS)。其实这两个小类挺好用的,而且支持嵌套的 ZIP 文件,但是,但是……JDK 7 丢下来一枚叫做 NIO2 的笑气炸弹,引入了一套标准的文件系统 API,我承认我中弹了,手痒了,又根据这套 API 重新实现了 ZIP 文件系统,终于在今天初步完工,哈。

话说,JDK 7 其实捆绑销售了一个 ZFS,demo 目录下还有源代码。可……它达不到我的奢求,而且 BUG 不少。随便逮两个:

        // com.sun.nio.zipfs.ZipFileSystemProvider 类中的方法
        @Override
        public Path getPath(URI uri) {

            String spec = uri.getSchemeSpecificPart();
            int sep = spec.indexOf("!/");
            if (sep == -1)
                throw new IllegalArgumentException("URI: "
                    + uri
                    + " does not contain path info ex. jar:file:/c:/foo.zip!/BAR");
            // 难怪该方法始终抛 IllegalArgumentException 异常,原来你小子把文件的 URI
            // 当成 ZFS 的 URI 在用……
            return getFileSystem(uri).getPath(spec.substring(sep + 1));
        }

        // com.sun.nio.zipfs.ZipFileSystem 类中的方法
        @Override
        public PathMatcher getPathMatcher(String syntaxAndInput) {
            int pos = syntaxAndInput.indexOf(':');
            // 丫的,pos == syntaxAndInput.length()?!谁写的?抓出来鞭尸。
            if (pos <= 0 || pos == syntaxAndInput.length()) {
                throw new IllegalArgumentException();
    

很明显,官方 ZFS 没有经过代码审阅、没有经过测试、没有经过……然后,@author Xueming Shen,真是丢咱华夏民族的脸……

下面列个表格详细比较官方 ZFS 和山寨 ZFS:

比较内容 官方 ZFS 山寨 ZFS
实现方式 另起炉灶,用纯 Java 重新实现了对 ZIP 文件格式的处理代码。 基于 ZipFileZipInputStream 这两个已经稳定多年的类,但涉及了大量本地代码调用,也许会影响性能。
读操作 支持,且通过解压到临时文件支持随机访问。 支持,但不支持随机访问。
写操作 通过解压到临时文件进行支持,但无法检测到其他进程对同一个 ZIP 文件的写操作,不适用于并发环境。 不支持。ZIP 文件事实上是一个整体,对内部条目的任何修改都可能导致重构整个文件,因此所谓的写操作必须通过临时文件来处理,效率低下,意义不大,而且难以处理嵌套 ZIP 文件。这也符合我的原则:不解压。
嵌套 ZIP 文件 不支持。 支持,当然读取嵌套 ZIP 文件会慢一些。
反斜线分隔符 不支持,直接瓜掉。 支持,且和标准的斜线分隔符区别对待。例如,/abc//abc\ 算不同的文件,实际上这两个能够并存于 ZIP 文件中。
空目录名 不支持,直接瓜掉。 支持。例如 /a/b/a//b 是两个可以并存且不同的文件。

山寨 ZFS 的用法示例:

        Map<String, Object> env = new HashMap<>();
        // 用于解码 ZIP 条目名。默认为 Charset.defaultCharset()。
        env.put("charset", StandardCharsets.UTF_8);
        // 指示是否自动探测嵌套的 ZIP 文件。默认为 false。
        env.put("autoDetect", true);
        // 默认目录,用于创建和解析相对路径。默认为“/”。
        env.put("defaultDirectory", "/dir/");

        // 从文件创建一个 ZFS。
        try (FileSystem zfs = FileSystems.newFileSystem(
                URI.create("zip:" + Paths.get("docs.zip").toUri()), env)) {
            Path path = zfs.getPath("app.jar");
            if ((Boolean) Files.getAttribute(path, "isZip")) {
                // 创建一个嵌套的 ZFS。
                try (FileSystem nestedZfs = zfs.provider().newFileSystem(path, env)) {
                    // 此处省略若干行。
                }
            }
        }
    

最后双手奉上源代码:请猛击此处!

posted @ 2011-12-01 13:12 蜀山兆孨龘 阅读(2465) | 评论 (1)编辑 收藏

先看出错的代码:

        public class Holder<T> {
            private T value;

            public Holder() {
            }

            public Holder(T value) {
                this.value = value;
            }

            public void setValue(T value) {
                this.value = value;
            }

            // 此处省略若干行。
        }

        Holder<Object> holder = new Holder<>("xxx");
    

看起来还好,但编译的时候却报错:

Uncompilable source code - incompatible types
  required: zhyi.test.Holder<java.lang.Object>
  found:    zhyi.test.Holder<java.lang.String>

老老实实把类型写出来就没问题:

        Holder<Object> holder = new Holder<Object>("xxx");
    

如果非要用钻石运算符的话,可以采取下列两种方式之一:

        // 使用默认构造器,再调用 setValue 方法。
        Holder<Object> holder = new Holder<>();
        holder.setValue("xxx");

        // 使用泛型通配符,但之后就不能调用 setValue 了,否则编译出错。
        Holder<? extends Object> holder = new Holder<>("xxx");
    

posted @ 2011-11-11 11:06 蜀山兆孨龘 阅读(1624) | 评论 (0)编辑 收藏

CyclicBarrier 的功能类似于前面说到的 CountDownLatch,用于让多个线程(子任务)互相等待,直到共同到达公共屏障点(common barrier point),在这个点上,所有的子任务都已完成,从而主任务完成。

该类构造的时候除了必须要指定线程数量,还可以传入一个 Runnable 对象,它的 run 方法将在到达公共屏障点后执行一次。子线程完成计算后,分别调用 CyclicBarrier#await 方法进入阻塞状态,直到其他所有子线程都调用了 await

下面仍然以运动员准备赛跑为例来说明 CyclicBarrier 的用法:

            final int count = 8;
            System.out.println("运动员开始就位。");

            final CyclicBarrier cb = new CyclicBarrier(count, new Runnable() {
                @Override
                public void run() {
                    System.out.println("比赛开始...");
                }
            });

            for (int i = 1; i <= count; i++) {
                final int number = i;
                new Thread() {
                    @Override
                    public void run() {
                        System.out.println(number + " 号运动员到场并开始准备...");
                        try {
                            // 准备 2~5 秒钟。
                            TimeUnit.SECONDS.sleep(new Random().nextInt(4) + 2);
                        } catch (InterruptedException ex) {
                        }
                        System.out.println(number + " 号运动员就位。");
                        try {
                            cb.await();
                        } catch (InterruptedException | BrokenBarrierException ex) {
                        }
                    }
                }.start();
            }
            System.out.println("等待所有运动员就位...");
    

运行输出(可能)为:

运动员开始就位。
1 号运动员到场并开始准备...
2 号运动员到场并开始准备...
等待所有运动员就位...
3 号运动员到场并开始准备...
4 号运动员到场并开始准备...
6 号运动员到场并开始准备...
8 号运动员到场并开始准备...
5 号运动员到场并开始准备...
7 号运动员到场并开始准备...
1 号运动员就位。
3 号运动员就位。
8 号运动员就位。
6 号运动员就位。
2 号运动员就位。
7 号运动员就位。
5 号运动员就位。
4 号运动员就位。
比赛开始...

最后看看 CyclicBarrierCountDownLatch 的主要异同:

  1. 两者在构造的时候都必须指定线程数量,而且该数量在构造后不可修改。
  2. 前者可以传入一个 Runnable 对象,在任务完成后自动调用,执行者为某个子线程;后者可在 await 方法后手动执行一段代码实现相同的功能,但执行者为主线程。
  3. 前者在每个子线程上都进行阻塞,然后一起放行;后者仅在主线程上阻塞一次。
  4. 前者可以重复使用;后者的倒计数器归零后就作废了。
  5. 两者的内部实现完全不同。

posted @ 2011-10-17 11:21 蜀山兆孨龘 阅读(1830) | 评论 (0)编辑 收藏

顾名思义,CountDownLatch 是一个用来倒计数的咚咚。如果某项任务可以拆分成若干个子任务同时进行,然后等待所有的子任务完成,可以考虑使用它。

该类的用法非常简单。首先构造一个 CountDownLatch,唯一的参数是任务数量,一旦构造完毕就不能修改。接着启动所有的子任务(线程),且每个子任务在完成自己的计算后,调用 CountDownLatch#countDown 方法将倒计数减一。最后在主线程中调用 CountDownLatch#await 方法等待计数器归零。

例如赛跑的准备阶段,八名运动员先后到达起点做好准备,然后裁判打响发令枪,准备工作就结束了,比赛开始。如果把从运动员就位到发令枪响看做赛跑准备任务,那么每个运动员的准备过程就是其子任务,可以用 CountDownLatch 模拟如下:

        final int count = 8;
        System.out.println("运动员开始就位。");

        // 构造 CountDownLatch。
        final CountDownLatch cdl = new CountDownLatch(count);
        for (int i = 1; i <= count; i++) {
            final int number = i;
            new Thread() {
                @Override
                public void run() {
                    System.out.println(number + " 号运动员到场并开始准备...");
                    try {
                        // 让运动员随机准备 2~5 秒钟。
                        TimeUnit.SECONDS.sleep(new Random().nextInt(4) + 2);
                    } catch (InterruptedException ex) {
                    }
                    System.out.println(number + " 号运动员就位。");
                    // 倒计数减一。
                    cdl.countDown();
                }
            }.start();
        }

        System.out.println("等待所有运动员就位...");
        try {
            // 等待倒计数变为 0。
            cdl.await();
            System.out.println("比赛开始。");
        } catch (InterruptedException ex) {
        }
    

运行输出(可能)为:

运动员开始就位。
1 号运动员到场并开始准备...
2 号运动员到场并开始准备...
4 号运动员到场并开始准备...
等待所有运动员就位...
8 号运动员到场并开始准备...
6 号运动员到场并开始准备...
3 号运动员到场并开始准备...
7 号运动员到场并开始准备...
5 号运动员到场并开始准备...
6 号运动员就位。
1 号运动员就位。
5 号运动员就位。
4 号运动员就位。
7 号运动员就位。
8 号运动员就位。
2 号运动员就位。
3 号运动员就位。
比赛开始。

从上面的例子还可以看出 CountDownLatch 的局限性和 CompletionService 类似,在于无法处理子任务数量不确定的情况,例如统计某个文件夹中的文件数量。另外,如果某个子任务在调用 countDown 之前就挂掉了,倒计数就永远不会归零。对于这种情况,要么用 finally 之类的手段保证 countDown 一定会被调用,要么用带参数的 await 方法指定超时时间。

posted @ 2011-10-14 14:22 蜀山兆孨龘 阅读(1754) | 评论 (1)编辑 收藏

JAX-RS 的核心功能是处理向 URI 发送的请求,所以它提供了一些匹配模式以便简化对 URI 的解析。楼主在本系列的上一篇文章中已经使用了最简单的路径参数,本文将介绍一些稍微高级点的咚咚。

模板参数

前面已经见过用 @Path("{id}")@PathParam("id") 来匹配路径参数 id。这种匹配方式可以被嵌入到 @Path 注解中的任何地方,从而匹配多个参数,例如下面的代码用来查找 ID 在某一范围内的电影:

        @GET
        @Path("{min}~{max}")
        @Produces(MediaType.APPLICATION_JSON)
        public List<Movie> findMovies(@PathParam("min") int min, @PathParam("max") int max) {
    

于是,GET /ms/rest/movie/5~16 就将返回 ID 为 5 到 16 的电影。此处的 minmax 已被自动转换为 int 类型。JAX-RS 支持多种类型的自动转换,详见 @PathParam 的文档。

根据 HTTP 规范,参数可能会编码。默认情况下,JAX-RS 会自动解码。如果希望得到未解码的参数,只需在参数上再加个 @Encoded 注解。该注解适用于大多数 JAX-RS 注入类型,但并不常用。

模板参数虽然灵活,也可能会带来歧义。例如想用 {firstName}-{lastName} 匹配一个人的姓名,但恰好某人的名(lastName)含有“-”字符,像 O-live K 这种,匹配后就会变成姓 live-K,名 O。这种场景很难避免,一种简单的解决方法就是对参数值进行两次编码,然后在服务端代码解码一次,因为 JAX-RS 默认会进行一次解码,或者加上 @Encoded 注解,自己进行两次解码。

另外,在一个复杂系统中,多个 @Path 可能会造成路径混淆,例如 {a}-{b}{a}-z 都能匹配路径 a-z。虽然 JAX-RS 定义了一些规则来指定匹配的优先级,但这些规则本身就比较复杂,并且也不能完全消除混淆。楼主认为,设计一个 REST 系统的核心就是对 URI 的设计,应当小心处理 URI 的结构,合理分类,尽量保证匹配的唯一性,而不要过度使用晦涩的优先级规则。楼主将在下一篇文章介绍优先级规则。

正则表达式

模板参数可以用一个正则表达式进行验证,写法是在模板参数的标识符后面加一个冒号,然后跟上正则表达式字符串。例如在根据 ID 查询电影信息的代码中,模板参数 {id} 只能是整数,于是代码可以改进为:

        @GET
        @Path("{id : \\d+}")
        @Produces(MediaType.APPLICATION_JSON)
        public List<Movie> findMovies(@PathParam("min") int min, @PathParam("max") int max) {
    

冒号左右的空格将被忽略。用正则表达式验证数据很有局限性,可惜 JAX-RS 目前并不能直接集成 Bean 验证框架,因此复杂的验证只能靠自己写代码。

查询参数

查询参数很常见,就是在 URI 的末尾跟上一个问号和一系列由“&”分隔的键值对,例如查询 ID 为 5 到 16 的电影也可以设计为 /ms/rest/movie?min=5&max=16。JAX-RS 提供了 QueryParam 来注入查询参数:

        @GET
        @Produces(MediaType.APPLICATION_JSON)
        public List<Movie> findMovies(@DefaultValue("0") @QueryParam("min") int min,
                @DefaultValue("0") @QueryParam("max") int max) {
    

查询参数是可选的。如果 URI 没有设定某个查询参数,JAX-RS 就会根据情况为其生成 0、空字符串之类的默认值。如果要手动设定默认值,需要像上面的代码一样用 @DefaultValue 注解来指定。另外还可以加上 Encoded 注解来得到编码的原始参数。

有的查询参数是一对多的键值对,例如 /xyz?a=def&a=pqr,这种情况只需将注入的参数类型改为 List 即可。

矩阵参数

矩阵参数应该属于 URI 规范中的非主流类型,但它实际上比查询参数更灵活,因为它可以嵌入到 URI 路径中的任何一段末尾(用分号隔开),用来标识该段的某些属性。例如 GET /ms/rest/movie;year=2011/title;initial=A 表示在 2011 年出品的电影中查找首字母为 A 的标题。year 是电影的属性,而 initial 是标题的属性,这比把它们都作为查询参数放在末尾更直观可读。匹配 URI 的时候,矩阵参数将被忽略,因此前面的 URI 匹配为 /ms/rest/movie/title。矩阵参数可以用 @MatrixParam 来注入:

        @GET
        @Path("title")
        @Produces(MediaType.APPLICATION_JSON)
        public List<String> findTitles(@MatrixParam("year") int year,
                @MatrixParam("initial") String initial) {
    

如果 URI 的多个段中含有相同名称的矩阵参数,例如 /abc;name=XXX/xyz;name=OOO,这种直接注入就失效了,只能用下面要讲的编程式访问来取得。

编程式访问

如果简单的注入不能达到目的,就需要通过注入 PathSegmentUriInfo 对象来直接编程访问 URI 的信息。

一个 PathSegment 对象代表 URI 中的一个路径段,可以从它得到矩阵参数。它可以通过 @PathParam 来注入,这要求该路径段必须整个被定义为一个模板参数。例如下面的代码也可以用来处理 GET /ms/rest/movie/{id}

        @GET
        @Path("{id}")
        @Produces(MediaType.APPLICATION_JSON)
        public Movie findMovie(@PathParam("id") PathSegment ps) {
    

@PathParam 也可以注入多个段,如果想把 /a/b/c/d 匹配到 /a/{segments}/d,直接注入一个字符串显然不行,因为 b/c 是两个路径段。唯一的选择是把注入的类型改为 List<PathSegment>。楼主严重不推荐用一个模板参数匹配多个路径段,因为这很容易干扰其他匹配的设计,最后搞成一团乱麻。URI 路径段应当尽量设计得简单明晰,再辅以矩阵参数或查询参数就能应付大多数场景。不论对服务端还是客户端开发人员来说,简洁的 URI 既便于管理,又便于使用。网上有不少关于 URI 设计指南的文章,此处不再赘述。

如果想完全手动解析路径,则可以用 @Context 注入一个 UriInfo 对象,通过此对象可以得到 URI 的全部信息,详见 API 文档。例如:

        @GET
        @Path("{id}/{segments}")
        @Produces(MediaType.PLAIN_TEXT)
        public String getInfo(@PathParam("id") int id, @Context UriInfo uriInfo) {
    

UriInfo 主要用在某些特殊场合下起辅助作用,设计良好的 URI 用普通的注入就能完成大部分匹配。


工欲善其事必先利其器,为此 JAX-RS 提供了这些利器来解析 URI。至于如何用这些器来做出一个好系统,则还是依赖于 URI 本身的设计。

posted @ 2011-10-09 12:43 蜀山兆孨龘 阅读(4317) | 评论 (1)编辑 收藏

CompletionService 接口的实例可以充当生产者和消费者的中间处理引擎,从而达到将提交任务和处理结果的代码进行解耦的目的。生产者调用 submit 方法提交任务,而消费者调用 poll(非阻塞)或 take(阻塞)方法获取下一个结果:这一特征看起来和阻塞队列(BlockingQueue)类似,两者的区别在于 CompletionService 要负责任务的处理,而阻塞队列则不会。

在 JDK 中,该接口只有一个实现类 ExecutorCompletionService,该类使用创建时提供的 Executor 对象(通常是线程池)来执行任务,然后将结果放入一个阻塞队列中:果然本就是一家亲啊!ExecutorCompletionService 将线程池和阻塞队列糅合在一起,仅仅通过三个方法,就实现了任务的异步处理,可谓并发编程初学者的神兵利器!

接下来看一个例子。楼主有一大堆 *.java 文件,需要计算它们的代码总行数。利用 ExecutorCompletionService 可以写出很简单的多线程处理代码:

        public int countLines(List<Path> javaFiles) throws Exception {
            // 根据处理器数量创建线程池。虽然多线程并不保证能够提升性能,但适量地
            // 开线程一般可以从系统骗取更多资源。
            ExecutorService es = Executors.newFixedThreadPool(
                    Runtime.getRuntime().availableProcessors() * 2);
            // 使用 ExecutorCompletionService 内建的阻塞队列。
            CompletionService cs = new ExecutorCompletionService(es);

            // 按文件向 CompletionService 提交任务。
            for (final Path javaFile : javaFiles) {
                cs.submit(new Callable<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        // 略去计算单个文件行数的代码。
                        return countLines(javaFile);
                    }
                });
            }

            try {
                int loc = 0;
                int size = javaFiles.size();
                for (int i = 0; i < size; i++) {
                    // take 方法等待下一个结果并返回 Future 对象。不直接返回计算结果是为了
                    // 捕获计算时可能抛出的异常。
                    // poll 不等待,有结果就返回一个 Future 对象,否则返回 null。
                    loc += cs.take().get();
                }
                return loc;
            } finally {
                // 关闭线程池。也可以将线程池提升为字段以便重用。
                // 如果任务线程(Callable#call)能响应中断,用 shutdownNow 更好。
                es.shutdown();
            }
        }
    

最后,CompletionService 也不是到处都能用,它不适合处理任务数量有限但个数不可知的场景。例如,要统计某个文件夹中的文件个数,在遍历子文件夹的时候也会“递归地”提交新的任务,但最后到底提交了多少,以及在什么时候提交完了所有任务,都是未知数,无论 CompletionService 还是线程池都无法进行判断。这种情况只能直接用线程池来处理。

posted @ 2011-09-29 13:37 蜀山兆孨龘 阅读(2110) | 评论 (0)编辑 收藏

实体间的多对多的关联需要一张关联表。如果直接使用 ManyToMany 来映射,JPA 就会隐式地帮我们自动管理关联表,代码写出来和其他类型的关联差别不大。例如,某州炒房团需要一个炒房跟踪系统,那么该系统中的炒房客和房子就是多对多的关系:

        public class Speculator implements Serializable {
            @Id
            private Integer id;
            @ManyToMany
            @JoinTable(joinColumns = @JoinColumn(name = "speculator_id"),
                    inverseJoinColumns = @JoinColumn(name = "house_id"))
            private List<House> houses;
            // 此处省略若干行
        }

        public class House implements Serializable {
            @Id
            private Integer id;
            @ManyToMany(mappedBy = "houses")
            private List<Speculator> speculators;
            // 此处省略若干行
        }
    

如果炒房客 s 要卖掉房子 h(严格点说是卖掉房子的产权部分),那么系统执行的代码差不多就是 s.getHouses().remove(h)。看似简单,然而底层的操作却性能低下:JPA 会先从数据库中取出炒房客的所有房产(s.getHouses()),然后再删除指定的那套房子;从数据库层面上看,这将先从关联表(speculator_house)中找到该炒房客的所有房子的外键,然后从 house 表载入这些 House 对象,最后才从 speculator_house 删除关联。在 ORM 出现前,这种操作只需要改关联表,根本不用关心其他房子。这种简单的多对多映射写法将关联表隐藏起来,虽然简化了代码,却也可能带来性能隐患。

很自然地可以想到,如果把关联表也映射成实体类,就能解决这个问题。speculator_house 包含两个外键,可用作联合主键。如果把它映射为 SpeculatorHouse 类,则该类与 SpeculatorHouse 都是多对一的关系。关联表实体类的代码如下(EmbeddedId 的映射技巧见《JPA 应用技巧 2:主键外键合体映射》):

        @Embeddable
        public class SpeculatorHouseId implements Serializable {
            private Integer speculatorId;
            private Integer houseId;
            // 此处省略若干行
        }

        @Entity
        @Table(name = "speculator_house")
        public class SpeculatorHouse implements Serializable {
            @EmbeddedId
            private SpeculatorHouseId id;
            @MapsId("speculatorId")
            @ManyToOne
            private Speculator speculator;
            @MapsId("houseId")
            @ManyToOne
            private House house;
            // 此处省略若干行
        }
    

SpeculatorHouse 也要增加相应的关联信息:

        public class Speculator implements Serializable {
            @Id
            private Integer id;
            @ManyToMany
            @JoinTable(joinColumns = @JoinColumn(name = "speculator_id"),
                    inverseJoinColumns = @JoinColumn(name = "house_id"))
            private List<House> houses;
            @OneToMany(mappedBy = "speculator")
            private List<SpeculatorHouse> speculatorHouses;
            // 此处省略若干行
        }

        public class House implements Serializable {
            @Id
            private Integer id;
            @ManyToMany(mappedBy = "houses")
            private List<Speculator> speculators;
            @OneToMany(mappedBy = "house")
            private List<SpeculatorHouse> speculatorHouses;
            // 此处省略若干行
        }
    

这样既保留了多对多关系,又映射了关联表,然后就可以根据实际情况选择隐式或显示的关联表管理。例如,要得到一个炒房客的全部房子,就使用隐式管理:s.getHouses();而要删除炒房客和某套房子的关联,则用显示管理:delete from SpeculatorHouse sh where sh.speculator = :s and sh.house = :h

posted @ 2011-09-27 11:04 蜀山兆孨龘 阅读(3300) | 评论 (2)编辑 收藏

JAX-RS 使用注解进行配置,所以用它开发 REST 风格的服务非常简单。楼主在本文用一个小例子来说明 JAX-RS 的基本用法。


假设楼主要开发一个小电影服务,客户端可以通过请求 URI 对电影进行 CRUD 操作。为简明起见,这儿不使用数据库,只在内存中模拟。先用一个非常简单的 Movie 类,在后续的文章中根据情况逐步扩充:

        public class Movie {
            private int id;
            private String title;
            // 此处省略若干行
        }
    

嗯,就是一个很普通的 JavaBean,实际项目中可以根据需要加上 @Entity 等注解。接下来看看如何编写 JAX-RS 服务。


一个 JAX-RS 服务就是一个使用了 JAX-RS 注解来将 HTTP 请求绑定到方法的 Java 类,一共支持两种类型:单请求对象或单例对象。单请求对象意味着每来一个请求,就创建一个服务对象,在请求结束时销毁。单例对象则意味着只有一个服务对象处理所有的请求,从而可以在多个请求间维持服务状态。JAX-RS 服务可通过继承 javax.ws.rs.core.Application 来定义,其中的 getClasses 方法返回单请求对象的类型,getSingletons 方法返回单例对象的类型。这两个方法是可选的。在 Java EE 6 环境中,如果这两个方法都返回 null 或者空集合,那么应用程序中的所有 JAX-RS 都将被部署。这时可以用 CDI 的 @javax.inject.Singleton 或者 EJB 的 @javax.ejb.Singleton 注解来指定单例对象。

如果电影服务的上下文根路径为 http://localhost/ms,而楼主希望将服务部署到 http://localhost/ms/rest 下面,只需要写一个类:

        @ApplicationPath("rest")
        public class RestApplication extends Application {
        }
    

@ApplicationPath 注解指定所有服务的相对基址,如果为空字符串,则直接使用上下文根路径。另一种配置方式是在 web.xml 文件中进行声明,那是为了使 JAX-RS 能在 Servlet 容器(例如 Tomcat)中运行,此处略过。这项配置必不可少,否则无法部署服务。


很好很强大,现在开始编写电影服务类 MovieService,先看看声明和初始化:

        @Singleton
        @Path("movie")
        public class MovieService {
            private AtomicInteger ai;
            private ConcurrentMap<Integer, Movie> movieMap;

            @PostConstruct
            private void init() {
                ai = new AtomicInteger();
                movieMap = new ConcurrentHashMap<>();
                int id = ai.getAndIncrement();
                movieMap.put(id, new Movie().setId(id).setTitle("Avatar"));
            }
    

因为楼主只需要一个“内存数据库”,所以用单例对象即可,此处使用 CDI 的 @javax.inject.Singleton 来声明单例。@Path 声明了一个服务,它指示 MovieService 负责处理发送到 http://localhost/ms/rest/movie 的请求。路径的拼接方式非常直观。init 方法带有 @PostConstruct 注解,因此将在 MovieService 构造完成后立即调用,它向 movieMap 中存入了一个 ID 为 0 的 Movie 对象。为简化代码,Movie 的设置方法都返回 this,有点伪造构建者模式的味道。


接下来看看如何处理 HTTP 请求。

GET

GET 请求用于获取一个或多个资源。在本例中用来获取一部电影的信息:

        @GET
        @Path("{id}")
        @Produces(MediaType.APPLICATION_JSON)
        public Movie find(@PathParam("id") int id) {
            Movie movie = movieMap.get(id);
            if (movie != null) {
                return movie;
            } else {
                throw new WebApplicationException(Response.Status.NOT_FOUND);
            }
        }
    

该方法标注了 @GET,表示用来处理向 http://localhost/ms/rest/movie/{id} 发送的 GET 请求。@Path 再次用来绑定路径,注意其参数 {id},它带有花括号,对应 URI 的最后一段,也正好和方法参数 id@PathParam 的值相对应。这种参数还有很多高级用法,以后再介绍。@Produces 注解指定输出格式为 JSON。JAX-RS 内置了很多格式,详见 MediaType 的文档。如果找到了相应 ID 的对象,则将其返回,JAX-RS 会自动加上响应码 200 OK;否则抛出异常,错误码为 404 Not Found。

例如,通过浏览器访问 http://localhost/ms/rest/movie/0,得到的结果为 {"@id":"0","@title":"Avatar"}。

POST

POST 请求用于创建一个资源。在本例中用来创建一部电影:

        @POST
        @Consumes(MediaType.APPLICATION_JSON)
        public Response create(Movie movie) {
            int id = ai.getAndIncrement();
            movieMap.put(id, movie.setId(id));
            return Response.created(URI.create(String.valueOf(id))).build();
        }
    

由于没有 @Path 注解,所以 POST 请求的目标就直接是 http://localhost/ms/rest/movie。Consumes@Produces 相反,表示接受的数据类型,此处 JAX-RS 会自动把 JSON 数据转换为 Movie 对象。返回的响应码为 201 Created,并且带有所创建资源的 URI。

例如,向 http://localhost/ms/rest/movie 发送 POST 请求,正文为 {"@title": "007"},则可以从 FireBug 的网络监控中看到返回的响应码,以及头部中 Location 的值为 http://localhost:8080/rest/service/movie/1。多次发送该 POST 请求,将会创建多个资源,以保证 POST 不是幂等的。

PUT

PUT 请求用于创建或更新一个资源。与 POST 不同,PUT 请求要指定某个特定资源的地址。在本例中用来更新一部电影的信息:

        @PUT
        @Path("{id}")
        @Consumes(MediaType.APPLICATION_JSON)
        public Response update(@PathParam("id") int id, Movie movie) {
            movie.setId(id);
            if (movieMap.replace(id, movie) != null) {
                return Response.ok().build();
            } else {
                throw new WebApplicationException(Response.Status.NOT_FOUND);
            }
        }
    

更新成功就返回 200 OK,否则返回 404 Not Found。这儿先把 movie 对象的 ID 强制改为 URI 所指定的,以免出现不一致。也可以根据需求,将不一致作为异常处理,给客户端返回一个错误码。

顺便啰嗦一句,反正代码在自己手中,楼主也可以把 PUT 搞成非幂等的,例如将 PUT 当成 POST 来处理,就像以前把 GET 和 POST 一视同仁那样。不过咱既然在搞 JAX-RS,就还是要沾染一点 REST 风格,严格遵守 HTTP 才是。

DELETE

DELETE 请求用于删除一个资源。在本例中用来删除一部电影:

        @DELETE
        @Path("{id}")
        public Response delete(@PathParam("id") int id) {
            if (movieMap.remove(id) != null) {
                return Response.ok().build();
            } else {
                throw new WebApplicationException(Response.Status.NOT_FOUND);
            }
        }
    

没什么特别的,该说的前面都说了。

HEAD 和 OPTIONS 请求就忽略吧,用得不太多,也同样挺简单的。


JAX-RS 服务的部署和部署常规 Web 程序一样,打包成 war 文件就可以了。最后赞一下 NetBeans 可以为 REST 风格的服务自动生成测试页面,很好用,虽然在 Firefox 下页面显示不正常(对此我已经提了一个 bug),但 IE 是可以的。

posted @ 2011-09-20 17:22 蜀山兆孨龘 阅读(9811) | 评论 (3)编辑 收藏

JAX-RS(JSR 311 - Java™ API for RESTful Web Services,用于 REST 风格的 Web 服务的 Java™ API)是 Java EE 6 规范的一部分,其目标在于简化和标准化用 Java 开发 REST 风格的 Web 服务。虽然 Java EE 6 刚出炉的时候,楼主也从头到尾看过这份规范,但苦于没有实际的项目练手,看过又忘了,现在最多算达到大成傻逼的境界。这次边看边写,期望完成后至少能破入小成牛逼。先从 REST 本身开始。


REST(REpresentational State Transfer,代表性状态传输)自称是一种风格而非标准,这在楼主看来有炒作的嫌疑。如果仅仅是一种风格,那么不同的框架如何兼容?所以才有 JAX-RS 的诞生。REST 最大的贡献是带来了 HTTP 协议的复兴。为什么叫复兴呢?本来 HTTP 的功能挺丰富的,可惜长期以来只被用作传输数据,大好青年被埋没了。楼主记得刚开始学 Servlet 的时候,一向是把 doGetdoPost 两个方法一视同仁的,因为书上这么教,很多 Web 框架也这么搞,以至于弄了很久才搞清楚 GETPOST 是两种不同的请求。现在 REST 拍砖说道,HTTP 早就定义好了一堆操作,以前一直被混淆使用,现在应该重新赋予它们本来的意义了,而且如果充分发挥 HTTP 的功能,完全能够胜任分布式应用的开发(传说中的 SOA)。


SOA 的理念在于将系统设计为一系列可重用的、解耦的、分布式的服务。这也不是新鲜玩意儿了,早期有 CORBA,稍晚有 SOAP 等等。REST 作为后起之秀,能够快速崛起,也必有其非同寻常的特色。下面一一列举。

可寻址性(Addressability)

系统中的每个资源都可以通过唯一标识符来访问。小插一句,“标识”的正确读音是 biāozhì。REST 使用 URI(统一资源标识符)管理资源的地址。URI 的概念不解释。一个 URI 可以指向一个或者多个资源。

统一的受限接口(The Uniform, Constrained Interface)

实际上强调了 HTTP 操作的原意。REST 主要使用了 GET、PUT、DELETE、POST、HEAD 和 OPTIONS 这 6 种操作。此处有两个曾经被忽略的 HTTP 概念:幂等(idempotent)和安全(safe)。幂等应该是 HTTP 从数学借来的一个术语(原始的数学意义楼主也不懂),意味着若干次请求的副作用与单次请求相同,或者根本没有副作用。GET、PUT、DELETE、HEAD 和 OPTIONS 都是幂等的:GET、HEAD 和 OPTIONS 都是读操作,容易理解;PUT 用于创建或更新已知 URI 的资源,多次创建或更新同一个资源显然和一次的效果相同;DELETE 删除资源,亦然。安全表示操作不会影响服务器的状态。GET、HEAD 和 OPTIONS 是安全的。POST 既不幂等又不安全,因为和 PUT 不同,POST 创建资源的时候并不知道资源的 URI,所以多个 POST 请求将会创建多个资源。

面向表象(Representation-Oriented)

表象这个词有点拗口,传闻在一个 REST 风格的系统中,服务端和客户端之间传输的咚咚就是表象……表象可以是纯文本、XML、JSON……或者自编的山寨格式。唉,不就是数据么?只不过可以用任意的格式来传输,因为 HTTP 正文里面啥都能放。Content-Type 头用来声明格式,一般是 MIME(多用途因特网邮件扩展),像 text/plaintext/htmlapplication/pdf 这些。MIME 可以带属性,例如 text/html; charset=UTF-8

无态通信(Communicate Statelessly)

REST 服务器只管理资源,而不会像 Web 服务器一样记录客户的会话状态,这些应该由客户端来管理,如此就能增强 REST 服务器的伸缩性。此处的客户端可以是客户端程序、浏览器,甚至一个 Web 应用。总之,REST 只负责库管啦!

HATEOAS

猛词砸来了!HATEOAS = Hypermedia As The Engine Of Application State,超媒体作为应用状态的引擎,怎么看起来比 SaaS(Software as a Service,软件作为服务)还要吓人呢?超文本不过是一只纸老虎,超媒体也瞒不过楼主的天眼:超媒体就是是由文字、图像、图形、视频、音频……链成一体的大杂烩!很简单的一个例子,有些坑爹的电影网站经常发布一些内嵌了广告的电影,播放的时候会弹出广告窗口,里面很多链接,你去点两下就中招了:这个电影文件就算是超媒体。

其实这个词最关键的地方是“状态引擎”。例如楼主在去网购,先选了几个东西,接下来可以干啥呢?可以继续选、可以把购物车清空、可以结账……楼主可以从现状“转换”到其他某些状态,而控制状态转换的那个咚咚就被冠名为状态引擎。多么聪明的词汇啊!楼主发现凡是高手都是造词砖家呀!用超媒体来控制状态转换,就是 HATEOAS:你是要继续看电影还是看广告?看哪个广告?自己慢慢考虑……


REST 相比 CORBA、SOAP、WS-* 之流确实独树一帜,但也难逃玩弄概念的嫌疑。记得大学里讲数据库的老师说过:“你们现在学了这么多理论,其实以后在工作中未必管用。在大街上随便找一个软件培训学校出来的小伙子,他哪儿懂什么第二第三范式啊?但却能把数据库玩儿得飞转!”

posted @ 2011-09-16 15:31 蜀山兆孨龘 阅读(5548) | 评论 (3)编辑 收藏

考虑两个具有一对一关联的实体类,受控方除了有一个自己的主键,还有一个引用主控方的外键。如果主控方和受控方是同生同灭的关系,换句话说,双方的一对一关联一旦确立就不可更改,就可以考虑让双方共享相同的主键,简化受控方的表结构。下面就让楼主通过实例来说明如何用 JPA 2.0 来实现这种映射。

盯着眼前的电脑,楼主想到了一个也许不太贴切的例子:员工和公司配的电脑的关系。员工的主键就是工号。虽然电脑可能被换掉,但电脑实体完全可以用工号做主键,只是把电脑配置的详细信息改掉而已。此处的难点就在与如何将电脑的主键字段同时映射一个员工,请看楼主是怎么一步步推导出来的。

一开始是最想当然的写法:

        public class Computer implements Serializable {
            @Id
            @OneToOne
            private Employee employee;
            // 此处省略若干行
        }
    

然而根据规范,只有这些类型可以作为主键:Java 原始类型(例如 int)、原始包装类型(例如 Integer)、Stringjava.util.Datejava.sql.Datejava.math.BigDecimaljava.math.BigInteger。所以直接拿 Employee 做主键是不行的。顺便提一下,也许某些 JPA 实现自己做了扩展,使得可以直接拿实体类做主键,这已经超出了 JPA 规范的范畴,此处不讨论。

直接映射是行不通了,那有什么间接的方式吗?这时楼主想起了一个特殊的注解:EmbeddedId。该注解的本意是用于联合主键,不过变通一下,是否可以将 Employee 包装进一个嵌入式主键,然后再将这个嵌入式主键作为 Computer 的主键以达到目的?带着这种想法,楼主有了下面的代码:

        @Embeddable
        public class ComputerId implements Serializable {
            @OneToOne
            private Employee employee;
            // 此处省略若干行
        }
    
        public class Computer implements Serializable {
            @EmbeddedId
            private ComputerId id;
            // 此处省略若干行
        }
    

现在又出现了新的问题:JPA 不支持定义在嵌入式主键类中的关联映射。好在天无绝人之路,EmbeddedId 的文档中直接指出,可以使用 MapsId 注解来间接指定嵌入式主键类中的关联映射,而且还附带了一个例子!于是最终的成品就出炉了:

        @Embeddable
        public class ComputerId implements Serializable {
            private String employeeId;
            // 此处省略若干行
        }
    
        public class Computer implements Serializable {
            @EmbeddedId
            private Employee employee;
            @MapsId("employeeId")
            @OneToOne
            private Employee employee;
            // 此处省略若干行
        }
    

唯一的遗憾是,逻辑上的主控方 Employee 现在不得不成为受控方了,好在可以定义级联操作来达到差不多的效果:

        public class Employee implements Serializable {
            @Id
            private String id;
            @OneToOne(mappedBy = "employee", cascade = CascadeType.ALL)
            private Computer computer;
            // 此处省略若干行
        }
    

虽然做到了,但确实挺绕的。希望未来版本的 JPA 能直接支持将实体类作为主键,楼主个人觉得不是一个技术问题。

posted @ 2011-09-13 11:27 蜀山兆孨龘 阅读(3942) | 评论 (0)编辑 收藏

JSF 都 2.0 了,尼玛居然还是无法识别 multipart/form-data(至少参考实现 Mojarra 如此),绑定的属性一个都读不出来,坑爹啊!!!既然官方不支持,咱就自己搞一个补丁,让它不从也得从。

说到底,JSF 的属性绑定功能不外乎是利用 FacesServlet 帮我们把参数进行转换和校验,然后拼装成受管 Bean。而 FacesServlet 必定是通过 HttpServletRequest 的相关方法来读取请求参数,因此只需要在 FacesServlet 之前增加一个过滤器,把文本类型的 Part 参数转换为普通参数就可以了。至于文件类型的 Part,则可以使用一些第三方工具来绑定,例如使用 PrimeFaces 将文件绑定到 File 对象。下图是这种思路的流程:

multipart/form-data 的处理流程

第二步的过滤器就是给 JSF 打的“补丁”:

        /**
         * 该过滤器帮助 {@link FacesServlet} 识别 {@code multipart/form-data} 格式的 POST 请求。
         */
        @WebFilter("*.xhtml")
        public class MultipartFilter implements Filter {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {
            }

            @Override
            public void doFilter(ServletRequest request, ServletResponse response,
                    FilterChain chain) throws IOException, ServletException {
                String contentType = request.getContentType();
                // 判断请求的格式是否为 multipart/form-data。
                if (contentType != null && contentType.startsWith("multipart/form-data")) {
                    MultipartRequest req = new MultipartRequest((HttpServletRequest) request);
                    for (Part part : req.getParts()) {
                        // 如果该 Part 的内容类型不为 null, 那它是一个文件,忽略。
                        if (part.getContentType() == null) {
                            req.addParameter(part.getName(), decode(part));
                        }
                    }
                    chain.doFilter(req, response);
                } else {
                    chain.doFilter(request, response);
                }
            }

            @Override
            public void destroy() {
            }

            /**
             * 将 {@link Part} 对象解码为字符串。
             */
            private String decode(Part part) throws IOException {
                try (InputStreamReader in = new InputStreamReader(
                        part.getInputStream(), StandardCharsets.UTF_8)) {
                    char[] buffer = new char[64];
                    int nread = 0;
                    StringBuilder sb = new StringBuilder();
                    while ((nread = in.read(buffer)) != -1) {
                        sb.append(buffer, 0, nread);
                    }
                    return sb.toString();
                }
            }

            /**
             * {@link HttpServletRequest} 中的请求参数映射是只读的,所以自己封装一个。
             */
            private static class MultipartRequest extends HttpServletRequestWrapper {
                private Map<String, String[]> parameters;

                public MultipartRequest(HttpServletRequest request) {
                    super(request);
                    parameters = new HashMap<>();
                }

                private void addParameter(String name, String value) {
                    String[] oldValues = parameters.get(name);
                    if (oldValues == null) {
                        parameters.put(name, new String[] {value});
                    } else {
                        int size = oldValues.length;
                        String[] values = new String[size + 1];
                        System.arraycopy(oldValues, 0, values, 0, size);
                        values[size] = value;
                        parameters.put(name, values);
                    }
                }

                @Override
                public String getParameter(String name) {
                    String[] values = getParameterValues(name);
                    return values == null ? null : values[0];
                }

                @Override
                public Map<String, String[]> getParameterMap() {
                    return parameters;
                }

                @Override
                public Enumeration<String> getParameterNames() {
                    final Iterator<String> it = parameters.keySet().iterator();
                    return new Enumeration<String>() {
                        @Override
                        public boolean hasMoreElements() {
                            return it.hasNext();
                        }

                        @Override
                        public String nextElement() {
                            return it.next();
                        }
                    };
                }

                @Override
                public String[] getParameterValues(String name) {
                    return parameters.get(name);
                }
            }
        }
    

这儿喷一下,为什么 HttpServletRequest 里面的请求参数映射是只读的,非得要通过继承 HttpServletRequestWrapper 这种蛋疼的弯路来黑?

posted @ 2011-09-09 11:23 蜀山兆孨龘 阅读(2001) | 评论 (0)编辑 收藏

最近闲来无事(楼主确实太懒了),重翻旧账,捣鼓了下 JPA 2.0,通过不断地写代码和谷歌,又有了一些旧瓶装新酒的发现和吐槽。楼主将在这一系列文章中慢慢道来。本次开篇带来的是两个模板类:用作实体类基础框架的 AbstractEntity, 以及实现了对实体的基本 CRUD 操作的 BasicEntityDao

一个实体类必须实现 java.io.Serializable 接口,必须有一个 ID 字段作为主键,且最好覆盖 equalshashCode 方法。因为实体类和数据表有对应关系,所以往往根据 ID 来实现 equalshashCode。这很自然地可以引出一个模板类,所有的实体类都可以从它继承:

        /**
         * 该类可作为实体类的模板,其 {@link #equals(Object)} 和 {@link hashCode()} 方法基于主键实现。
         * 子类只需要实现 {@link #getId()} 方法。
         */
        public abstract class AbstractEntity implements Serializable {
            /**
             * 返回主键。
             */
            public abstract Object getId();

            @Override
            public boolean equals(Object obj) {
                if (this == obj) {
                    return true;
                }
                if (obj == null || getClass() != obj.getClass()) {
                    return false;
                }
                return getId() == null ? false
                        : getId().equals(((AbstractEntity) obj).getId());
            }

            @Override
            public int hashCode() {
                return Objects.hashCode(getId());
            }
        }
    

针对主键的类型,AbstractEntity 可以进一步扩展。例如,可以扩展出一个 UuidEntity,它使用随机生成的 UUID 作为主键:

        @MappedSuperclass
        public class UuidEntity extends AbstractEntity {
            @Id
            private String id;

            @Override
            public String getId() {
                return id;
            }

            @PrePersist
            private void generateId() {
                // 仅在持久化前生成 ID,提升一点性能。
                id = UUID.randomUUID().toString();
            }
        }
    

继续发挥想象,让它支持乐观锁:

        @MappedSuperclass
        public class VersionedUuidEntity extends UuidEntity {
            @Version
            private int version;
        }
    

这儿顺便插嘴吐槽下主键的类型。用整数还是 UUID 好呢?这个问题在网上也是争论纷纷。在楼主看来,两者各有优劣:整数主键性能高,可读性也好,但会对数据迁移,例如合并两个数据库,造成不小的麻烦,因为可能出现一大堆重复的主键;UUID 性能差些,看起来晃眼,虽然据说有些数据库针对性地做了优化,想来也不大可能优于整数,不过好处就是理论上出现重复主键的概率比中彩票还小(福彩除外)。说这么一大堆,其实还是蛮纠结啊……楼主一般倾向于用 UUID,只要服务器的配置够劲,想来不会出现明显的性能问题。

接下来说说 BasicEntityDao,它提供了基本的 CRUD 实现,可以用来为会话 Bean 做模板:

        /**
         * 提供了对实体进行基本 CRUD 操作的实现,可作为会话 Bean 的模板。
         */
        public abstract class BasicEntityDao<T> {
            private Class<T> entityClass;
            private String entityClassName;
            private String findAllQuery;
            private String countQuery;

            protected BasicEntityDao(Class<T> entityClass) {
                this.entityClass = Objects.requireNonNull(entityClass);
                entityClassName = entityClass.getSimpleName();
                findAllQuery = "select e from " + entityClassName + " e";
                countQuery = "select count(e) from " + entityClassName + " e";
            }

            /**
             * 返回用于数据库操作的 {@link EntityManager} 实例。
             */
            protected abstract EntityManager getEntityManager();

            public void persist(T entity) {
                getEntityManager().persist(entity);
            }

            public T find(Object id) {
                return getEntityManager().find(entityClass, id);
            }

            public List<T> findAll() {
                return getEntityManager().createQuery(findAllQuery, entityClass).getResultList();
            }

            public List<T> findRange(int first, int max) {
                return getEntityManager().createQuery(findAllQuery, entityClass)
                        .setFirstResult(first).setMaxResults(max).getResultList();
            }

            public long count() {
                return (Long) getEntityManager().createQuery(countQuery).getSingleResult();
            }

            public T merge(T entity) {
                return getEntityManager().merge(entity);
            }

            public void remove(T entity) {
                getEntityManager().remove(merge(entity));
            }
        }
    

子类只需要提供 getEntityManager() 的实现即可。假设楼主要做一个养鸡场管理系统,对鸡圈进行操作的会话 Bean 就可以简单地写成:

        @Stateless
        public class CoopDao extends BasicEntityDao<Coop> {
            @Persistence
            private EntityManager em;

            public CoopDao() {
                super(Coop.class);
            }

            @Override
            protected EntityManager getEntityManager() {
                return em;
            }

            // 更多方法……
        }
    

posted @ 2011-09-07 17:40 蜀山兆孨龘 阅读(3612) | 评论 (8)编辑 收藏

     摘要: JSF 2.0 大量采用标注,从而使 web/WEB-INF/faces-config.xml 不再必需。本文介绍并比较了三种途径来定义可从页面上的 EL 表达式中引用的受管 Bean。  阅读全文

posted @ 2010-05-15 19:10 蜀山兆孨龘 阅读(4117) | 评论 (0)编辑 收藏

     摘要: Servlet 3.0 引入了 javax.servlet.http.Part 接口,从而提供了对 multipart/form-data 类型的 HTTP 请求的直接支持,我们从此就可以摆脱诸如 Apache Commons FileUpload 之类的第三方依赖。然而,该支持太过单纯,所以还要多做点事情,以便能更有效地进行工作。我将在本文中介绍两个辅助方法。  阅读全文

posted @ 2010-04-24 11:59 蜀山兆孨龘 阅读(4400) | 评论 (0)编辑 收藏

用接口实现回调 Implementing Callback with Interface
  C 语言里的函数指针,JavaScript 里的函数参数可以实现回调,从而完成很多动态功能。请看下面的 JavaScript 代码: C's function pointer and JavaScript's function parameter can implement callback, accomplishing lots of dynamic functionalities. Please look at the following JavaScript code:
  1. function add(a, b) {
  2.     return a + b;
  3. }
  4. function sub(a, b) {
  5.     return a - b;
  6. }
  7. function cal(a, b, callback) {
  8.     alert(callback(a, b));
  9. }
  10. cal(2, 1, add);
  11. cal(2, 1, sub);
  12. cal(2, 1, function (a, b) {
  13.     return a * b;
  14. });
  在对 cal 函数的三次调用中,变量 callback 分别指向三个函数(包括一个匿名函数),从而在运行时产生不同的逻辑。如果仔细研究网上各种开源的 JS 库,会发现大量此类回调。 In the three invokings to function cal, variable callback points to three different functions (including one anonymous function), which generates different logics at runtime. If you study various open source JS libraries on the Internet in depth, you will find many callbacks of this kind.
  Java 语言本身不支持指针,所以无法像 JavaScript 那样将方法名直接作为参数传递。但是利用接口,完全可以达到相同效果: Java language itself doesn't support pointer, so the method name can't be directly passed as a parameter like JavaScript. But with interface, the completely same effect can be achieved:
  1. public interface Cal {
  2.     public int cal(int a, int b);
  3. }
  4. public class Add implements Cal {
  5.     public int cal(int a, int b) {
  6.         return a + b;
  7.     }
  8. }
  9. public class Sub implements Cal {
  10.     public int cal(int a, int b) {
  11.         return a - b;
  12.     }
  13. }
  14. public class Test {
  15.     public static void main(String[] args) {
  16.         test(2, 1, new Add());
  17.         test(2, 1, new Sub());
  18.         test(2, 1, new Cal() {
  19.             public int cal(int a, int b) {
  20.                 return a * b;
  21.             }
  22.         });
  23.     }
  24.     private static void test(a, b, Cal c) {
  25.         System.out.println(c.cal(a, b));
  26.     }
  27. }

posted @ 2008-03-10 21:47 蜀山兆孨龘 阅读(412) | 评论 (0)编辑 收藏

C 库函数 feof(FILE*) 判断文件末尾的问题 A Problem on Using C Library Function feof(FILE*) to Judge The End of A File
  我用 C 写了一个程序读取 32768 字节大小的文件,每次读 16 个字节,应该是 2048 次读完。但结果读了 2049 次,并且最后两次的数据相同,似乎重复读取了最后 16 个字节。源代码如下:     I wrote a program with C, which read a file of 32768 bytes, 16 bytes each time, and it should finish reading after 2048 times. But the reault was it read 2049 times, and the data of last two times are the same, which seemed the last 16 bytes were read twice. Here is the code:
  1. int loop = 0;
  2. while (!feof(file)) {
  3.     loop++;
  4.     fread(buffer, 16, 1, file);
  5.     ......
  6. }
  7. printf("%d\n", loop);    // 2049
  我看了一阵,发现导致这个错误的原因是 feof(FILE*) 判断文件末尾的机制:文件指针在文件末尾的时候,除非再读一次导致发生 I/O 错误,feof(FILE*) 依然返回 0。因此用 feof(FILE*) 作为判断条件的 while 循环始终会多读一次,而最后一次的读取是失败的,buffer 也就没有改变,看起来就像是重复读了一次。     I reviewed it for a whil and found the reason that produced this error is the mechanism feof(FILE*) used to judge the end of a file: When the file pointer is at the end of a file, feof(FILE*) still returns 0 unless reads one more time to course a I/O error. Therefore, a while loop using feof(FILE*) as the judgment condition always reads one more time, and the last time of reading will fail, so buffer stayed unchanged which looked like it repeated reading once.
  用下面的代码就没问题了:     Use the code below to solve this problem:
  1. int loop = 0;
  2. while (fread(buffer, 16, 1, file) == 1) {
  3.     loop++;
  4.     ......
  5. }
  6. printf("%d\n", loop); // 2048

posted @ 2007-12-06 23:05 蜀山兆孨龘 阅读(8106) | 评论 (4)编辑 收藏

Java 中对象引用的类型 Object Reference Types in Java
  弱引用早有耳闻,但从来没去认真看过。前天改编陈维雷先生的下雪动画时,发现他使用了弱引用,于是趁机把 Java 的对象引用类型看了个究竟。     I've heard of weak reference for a long time, but have never study it seriously yet. The day before yesterday, when I was modifying Mr. William Chen's snowing animation, I saw weak reference was utilized, and then took the chance to read the details of Java's reference type.
  除了通常意义下的强引用,包 java.lang.ref 还定义了其他三种平时不太用到的引用:软引用、弱引用和虚引用,但 API 文档的解释比较含糊。我在网上搜到了一些资料,简单归纳一下。     Except the strong reference of common purpose, package java.lang.ref defines three other references which are less often used: soft reference, weak reference and phantom reference, but they have obscure explanations in the API documention. I searched online and got some stuffs and here are my summaries.
  强引用。当一个对象具有强引用时,Java 虚拟机宁愿抛出 OutOfMemeryError,也绝不让垃圾回收器回收它。     Strong Reference. When an object holds strong references, Java Virtue Machine would rather throw an OutOfMemeryError than let garbage collector (GC) collect it.
  软引用。当一个对象只具有软引用时,垃圾回收器只在内存不足的时候才回收它。     Soft Reference. When an object holds only soft references, GC collects it only if there is not enough memory.
  弱引用。当一个对象只具有弱引用时,一旦被垃圾回收器发现就会被回收。因为垃圾回收器是一个优先级很低的线程,所以弱引用对象也不一定会马上就会被回收。     Weak Reference. When an object holds only weak references, GC collects it as soon as finds it. GC is a thread of very low priority, so a weak reference object may not be collected immediately.
  虚引用。虚引用和对象的生命周期无关。虚引用必须和引用队列联合使用,对象将被回收前,其虚引用将被加入到引用队列。虚引用只是用来监视对象的回收。     Phantom Reference. Phantom reference has nothing to do with the life cycle of an object. Phantom reference must be used together with reference queue, and the object's phantom reference will be added into that reference queue right before collected. Phantom reference is only used to monitor object collecting.
  从以上是否能看出,一个对象不能同时具有软引用和弱引用?     From above shall we say that an object can't have a soft reference and a weak reference at the same time?

posted @ 2007-12-02 20:43 蜀山兆孨龘 阅读(1287) | 评论 (0)编辑 收藏

JDK 源代码中的搞笑之处 Funny Things in JDK Source
  虽然完整版的 JDK 源代码现已开放了,但安装在 Java\jdk[版本号] 目录下的公共 src.zip 仍然是我最经常参考的资源。每次我遇到一个 API 问题,都会刊这个公共源代码。解决问题之余,我还找到很多有趣的东西,有时还搞笑。这里距三个例子。     Though the full version of JDK source is available now, but the public src.zip installed under Java\jdk[version_number] directory is still my most frequent refered resource. Every time I encounter an API problem, this public source is read. And besides solving those problems, I've also found many interesting things which are sometimes also funny. Here are three exaples.
  大概从 JDK 5.0 开始,类 java.lang.Object 引入了一个叫 wait(long timeout, int nanos) 的方法。等等,nanos,纳秒?众所周知,即使在强大的 Windows 多媒体 API 里面,计时器的精度也只有一毫秒,也就是一兆纳秒。尽管 Java 非常棒,但不能处理纳秒。而源代码证明了这一点,纳秒被舍入到最接近的毫秒,0 或 1……精彩……     Maybe since JDK 5.0, a method called wait(long timeout, int nanos)is introduced into Class java.lang.Object.Object. Wait a minute, nanos, is it nanoseconds? It's no secret thst even in powerful Windows multimedia API, the precision of timer is only one millisecond, that is a million nanosecond. Though Java is pretty great, it can not deal with nanoseconds. And the source proves it, that nanoseconds are rounded to the nearest millisecond, 0 or 1... Amazing...
  今天我想得到一个 JDialog 的所有者,但却没有 getOwner() 方法。最后我才明白 JDialog 的所有者就是它的父组件,用 getParent() 就可以了。那现在所有者等同于父级了?     Today I wanted to get a JDialog's owner, but there's no method called getOwner(). Finally I was awear that the owner of a JDialog is exactly its parent component, and just using getParent() is okey. So owner is synonymous with parent now?
  最后,我想提下 JSpinner 的实现有错。一些安装在 JSpinner 上的侦听器丝毫不起作用。我在 JSpinner.java 里找到这段注释:“还是不对,我们没其他办法了,SpinnerModelJFormattedTextField 现已不同步了。”JDK 的开发者的诚实值得感谢。我的解决方法是直接操控复合式组件 JSpinner 中的 JFormattedTextField     At last, I wanna mention the JSpinner implementation is bugged. Some kinds of listener installed on a JSpinner take no effect at all. I found this comment in JSpinner.java: "Still bogus, nothing else we can do, the SpinnerModel and JFormattedTextField are now out of sync." The JDK developers deserve a thank for honesty. My solution is to directly manipulate the JFormattedTextField within JSpinner, a compound JComponent.

posted @ 2007-11-30 17:47 蜀山兆孨龘 阅读(1131) | 评论 (0)编辑 收藏