Guice 中的协助式注入

Guice 的常规用法是把构造函数标记(Annotate)为 @Inject,这样 Guice 就会自动注入(Inject)并创建这个类的实例。例子如下,一个 Worker 依赖于一个 Connection。当有一个全局 Connection 的时候,Guice 就可以自动创建出一个 Worker 的实例出来。

但现实情况往往不是这样,Worker 可能还需要一个名字(Name)成员,这个成员也需要在构造的时候就确定下来。于是,构造函数就需要接受一个额外的参数,并且这个参数是不受 Guice 控制的。代码大致是这样:

注意第 5 行的 @Assisted 标记,这也是一个 Guice 的特性。它告诉 Guice,这个参数你不知道,每次使用的时候需要我来告诉你。那这个“告诉”的方法,是通过一个工厂方法。这种方法称为协助式注入(Assisted Injection),类似如下的代码:

在使用的时候,需要事先安装(Install)WorkerFactoryModule,然后就可以注入 WorkerFactory 了,获得 WorkerFactory 的实例之后,通过调用 create 方法就可以轻松创建出不同名字的 Worker 来。

在 Java 单元测试中使用 Truth

Truth 是 Google 的一个开源 Java 库,用于解决 JUnit 测试中不明确的断言(Assertion)问题。

JUnit 的断言通常是这么写的:

上述代码断言获得的 List 为空,当断言失败后,JUnit 抛出如下异常:

看到这种错误信息,一般都得回去看代码,才能知道到底出了什么错,然后再分析原因。而 Truth 的目标就是改变这种状况,让错误信息更有意义。比如在 Truth 中,可以这么写:

当断言失败后,会得到如下错误信息:

这样我们看到错误信息的时候,对出现的问题大致有个印象,比起看到一个 AssertionError 一头雾水的情况要好太多了。

而 Truth 不仅提供了常见类型的断言支持,还可以自定义断言,比如有一个二叉树节点类 Node:

我们要针对它写一个 Truth 的断言支持,可以这样:

然后我们就可以做类似如下的断言:

如果 root 没有左子树,则会显示如下错误:

这样无论是代码,还是错误信息都非常直观,一看就能明白,维护起来也方便。

Docker 的应用场景

一段时间以来,Docker 是一项很热门的技术,受到了一些大公司的关注。不过热归热,Docker 对多数开发人员来说,并没有什么实际的作用。

Docker 是什么?Docker 是一个沙盒(Sandbox)机制。Docker 的术语中,一个 Sandbox 是一个 Docker 容器(container)。在一个 Docker 容器中,用户可以自由地安装各种软件,而不同的 Docker 容器互相之前不影响。

Docker 最直接的应用场景是取代虚拟机。比如一个博客运营商,它的物理主机配置肯定异常得好,比如 16 核 CPU、32G 内存、10T 硬盘等,但它卖给用户的时候,每个用户差不多单核 CPU、1G 内存、5G 硬盘就够用了。这个时候就需要用到虚拟机技术,把一台物理主机划分成若干个虚拟机,每个虚拟机之间互不影响,也就是用户 A 的博客,不会被同主机的用户 B 修改。这样用户可以按自己的需要来购买合适的虚拟机,而主机商把整台主机切开来卖出去,不会浪费,是一个双赢的局面。

但虚拟机技术有一个缺陷,就是它太庞大了。一台虚拟机就是一个完整的操作系统,包含了所有操作系统的必要功能,比如进程调度、通知、DNS 解析等应有尽有,而这些基本功能,不是每个用户都需要自己用自己的。大多数用户,只是在虚拟机上运行自己的博客程序,而其底层依赖的系统功能,大多是可以共享的。但在虚拟机的限制下,系统功能也被复制成很多份,不能共享,实际上造成了一些浪费。

这些浪费是主机商不想看到的,浪费系统资源就等于减少了收入。这也是为什么各种云计算厂商,在看到 Docker 技术的时候两眼放光的原因。Docker 的本质不是虚拟机,它只是一个轻量级的容器。在 Docker 环境是,系统功能可以共享,只有用户的应用程序被隔离开来,从而使得一台主机上可以卖给更多的用户,赚更多的钱。

“轻量”所带来的另一个好处是高效地部署,一台虚拟机从开始部署到可以使用,大概需要 10 分钟时间,而 Docker 可能不到一分钟就完成了。多数在线系统都有负载均衡机制,就是使用大量的后端服务器来响应用户请求,假设有 10 台服务器,每台服务器只需处理 1 / 10 的请求。在应对突发高访问量的时候,比如春节的微博红包和双十一的抢购潮,系统可以通过增加服务器数量的形式来减轻每台服务器的负载。在动态增加服务器时候,服务器的部署时间就成了瓶颈。这时候如果系统使用了 Docker,则能更流畅地增加吞吐量。

上述是 Docker 最主要的应用场景。如果你的应用没有上述需求,鲁莽地使用 Docker 只要造成不必要地浪费。

使用 Node.js 下载文件

最近在写一个爬虫,选择了 Node.js 做了工具,因为 Javascript 写多了很熟练。为什么不选 Python,因为基本不会 :P

不过 Node.js 是一个比较坑的东西,它的 API 是我见到过最奇怪的。拿 HttpClient 来说吧,大多数语言都自带了 HttpClient 的库,基本用法是发起一个 Http 请求,可以指定各种参数,比如 Http 方法(Method)、URL、发送过去的 Http 头和内容,然后设置返回内容的处理等等。比如下面是 jQuery 的 ajax 请求示例代码:

Node.js 中也差不多,比如这样:

但是 Node.js 有一个小小的问题,当 URL 是 Https 的时候,它会报错。为什么呢?因为 Http 库只支持 Http,如果需要下载 Https 的内容,需要使用 Https 库。看到这里我整个人就不好了。Node.js 作为一个服务器程序,对 Http 和 Https 服务器端有不同的支持,这无可厚非;但是对于 HttpClient 也要区别对待,这没有任何的道理,只能认为 API 的设计人员水平太差。

另外,Node.js 中回调函数(Callback)和事件(Event)混用,有些 API 使用回调函数,有些 API 使用事件,还有一些则两者都用。从上面的代码中就可以看出,HttpClient.get() 第二个参数是一个回调函数,而它的返回值又支持事件模式,什么时候要用哪种方式,对初学者而言,相当不友好。

而 Node.js 的官方文档相当不全,比如 ReadableStream 的 pipe() 方法,可以把当前的 Readable Stream 传递到一个 Writable Stream 中。当 pipe 读完之后,自动把 Writable Stream 给关闭。文档中是这么写的:

它只说明了,如果 end 参数为 true,则自动把 Writable Stream 关闭,而完全没有提到这个过程是同步的还是异步的。由于 Readable Stream 有一个 end 事件,我一开始很自然就把后续的操作挂在了 end 事件上,既然读完了,Stream 也关闭了,自然可以进行后续操作了。但万万没想到,那个关闭方法是异步的,要等 Writable Stream 把文件内容写完才可以,也就是需要等待 Writable Stream 的 finish 事件。这一细节在文档和示例代码中,完全没有体现出来。

好吧,写一个下载工具也有这么多坑,我也算是服了。以下是一个简单版本的代码,按 Node.js 的传统实现了回调机制:

更加完善的爬虫代码稍后公开,敬请期待。

在RHEL 7中安装mod_pagespeed

Redhat 在它的 Linux 发行版 RHEL(Redhat Enterprise Linux)中引入了一个新功能:Software Collections,简称 SCL。SCL 的工作原理和其它的软件部署工具不太一样,SCL 不会把软件装到系统目录中,而是做了一个虚拟的目录结构;而安装的软件默认不启用,需要使用特定命令开启。

拿 PHP 5.5 举例,安装完成之后,PHP 5.5 会出现在 /opt/rh/php55/ 目录下,使用 scl enable php55 可以把它开启。比如查看 PHP 的版本:

使用 PHP 貌似没什么太问题,但是 Apache 和 mod_pagespeed 就有点不和谐了。Apache 2.4 装好之后,在 /opt/rh/httpd24 中。但是安装 mod_pagespeed 的时候,RPM 会抱怨说:

httpd >= 2.2 is needed by mod-pagespeed-beta

而使用了 scl enable httpd24 之后,问题依旧。我还没有仔细看 scl enable 的原理是什么,但基本上它和 RPM 的沟通不太正常。

不过不要紧,RPM 有一个 --nodeps 选项,即不检查所需的依赖库。把原先的命令改成这样即可:

但是呢,还是有一点点的小问题。mod_pagespeed 默认的安装目录是 /etc/httpd,但真正的 httpd 还在 /opt/rh/httpd24 下面,于是我们要么把装好的文件复制过去,要么在安装之前做一个符号链接(Symbolic Link)。我倾向于后者,因为这样可以解决后续的升级问题。制作符号链接的命令如下:

之后再安装 mod_pagespeed 就没有问题了。

计算网页中某个元素的位置

由于项目的需要,测试中需要对网页元素进行截图,以确保它看上去没有问题。之前我写过一篇文章介绍过一种方法,先使用 WebDriver 进行全屏截图,然后根据目标元素(DOM Element)所在的位置,再对截下来的图片进行剪裁,保留我们需要的位置即可。

那段代码一直都工作得很好,直到我知道了一个东西:iframe。iframe(普通的 frame 也是一样的,不过 frame 现在不太常见,这里只用 iframe 举例)中的内容被视为一个独立的网页,连 Window 对象也是和它的父级网页分开的。而 WebDriver 中的 WebElement.getLocation()方法只能返回这个 WebElement 和它所在的 Window 的位置关系,它的实现没什么问题,但全屏截图不仅包含了 iframe 的内容,可能也包含了它的父级页面的内容,剪裁的时候需要知道目标元素在截图中的位置。那么问题来了,挖掘机技术哪家强?如何计算一个元素相对于截图的位置?

这个问题还要分类讨论,原因是:Chrome 和 Firefox 中截图的行为是不一样的。Chrome 的截图是当前可见(viewport)的网页内容,比方说,当网页的实际大小超过 Chrome 窗口大小时,根据滚动条的位置不同,窗口中显示的内容不同,Chrome 的截图就是显示出来的内容。于是我们要计算目标元素相对于当前可见内容的位置。而 Firefox 用了一个方法,可以截到整个网页的内容,无视当前窗口大小。于是对于 Firefox 我们要计算元素的绝对位置(Absolute Position)。

获得一个元素的位置,需要用到一个方法:Element.getBoundingClientRect()。这个方法返回这个元素相对于它所处的 Windows 在当前可见内容的位置,用 top、left、right、bottom 四个值来表示。我们只关心其中的 top 和 left,至于剪裁的尺寸,我们可以通过元素本身的长和宽来得到,不需要计算。要计算目标元素对于顶级 Window的位置,我们只需要依次加上它的父级 Window的 top 和 left 即可。代码如下:

以上代码适用于 Chrome ,而在 Firefox 中,我们还需要计算元素的绝对位置。这里需要用到 Window.pageXOffset。pageXOffset,或者 scrollX,表示当前 Window 的横向滚动条滚动的位置,把这个值和上述的 left 相加,即可得到目标元素的横向绝对位置。当然,iframe 也可以特殊处理的:

由于 IE8 不支持 pageXOffset 和 scrollX,于是在 IE8 中需要一些特殊处理,即代码中标注“IE8”的部分。把这两段 Javascript 代码,替换之前文中的 WebElement.getLocation(),即可实现在 iframe 中对特定元素截图。

使用 Pagespeed 做反向代理

之前有提到过我把博客的图片都移到 App Engine 了,这样做的好处是 Google 的 CDN 很快,并且价格便宜,但坏处也很明显,就是由于某些“不可抗力”的影响,图片时常打不开。没办法,只能再绕回来。

反向代理(Reverse Proxy)是这么一个东西,它可以把远程服务器的资源收回本机,然后再向客户端发出去。简单来说,现在博客图片放在 leonax.storage.googleapis.com 上面,访问不到。设置了一个反向代理之后,图片回到了 leonax.net/images,这样就可以正常访问了。之所以称它为“代理”,是因为源图片依然存放在 googleapis.com,只是用户访问时候,动态抓取过来而已。

一开始我想用 Apache 做反向代理,但貌似它对 CDN 的支持不太好,只好放弃。后来发现 pagespeed 也有类似的功能,尝试下来效果还不错。只需要两行代码:

其中 ModPagespeedMapRewriteDomain 是把页面中所有指向 googleapis.com 的引用都改写成 leonax.net/images,这样我就不用一个一个页面去改了。然后用 ModPagespeedMapProxyDomain 把 leonax.net/images 反向代理到 https://leonax.storage.googleapis.com。这样一来,用户访问 leonax.net/images 的时候,就自动获得后者的内容了。

另外,开启 SSL 支持还需要:

这样做的好处是避免了 googleapis.com 时常访问不了的情况,但坏处是增加了主站的流量,AWS 的小机器可能会承受不了。