这是假期读的第二本书,以下是对本书的阅读的内容的一些总结:

就 Cloud native 这个词来说,已经被市场人员到处在用了,就跟 Microservice 一样,很难清晰地去定义到底什么是 Cloud native。在本书中,作者提到的 Cloud native 其实包含了两个部分的内容,一个是 Cloud native 的基础设施(Cloud native infrastructure),另一个是 Cloud native 的应用(Cloud native application)。作者对于 Cloud native infrastructure 的定义是:

Cloud native infrastructure is infrastructure that is hidden behind useful abstractions, controlled by APIs, managed by software, and has the purpose of running applications. Running infrastructure with these traits gives rise to a new pattern for managing that infrastructure in a scalable, efficient way.

所谓的 Cloud native 的基础设施就是当我们的基础设施(大量的 VM/Docker,复杂的网络,各种各样的储存等等)变的非常复杂和庞大的时候,通过人力是无法高效地进行运维的,在这样的大规模下,我们应该尽量地去避免人工介入,而是需要有一套软件来帮助我们更好地去管理我们的基础设施,可以让运行在这套基础设施上 Cloud native 的应用可以非常方便地被运维,部署,监控等等。那么怎么去设计管理 Cloud native 的基础设施的系统呢,作者提到了几个点,其中包括系统的自举,API 的设计等等,但是我觉得其中最重要的一点是 Reconciler Pattern,关于这个 Pattern,书中提到了四个原则:

  • Use a data structure for all inputs and outputs.
  • Ensure that the data structure is immutable.
  • Keep the resource map simple.
  • Make the actual state match the expected state.

其中最后一点和 Reconciler Pattern 的关系最大,这个也是 Kubernetes 中的方式,我们往 Kubernetes 中去提交一个 Spec,比如要求一个应用的实例的数量应该是 4,那么 Kubernetes 就会尽可能地去保证这个应用的实例的数量是 4,如果一个实例 Crash 了,它就马上新起一个实例。这就是所谓的 Reconciler Pattern,尽量让实际的状态可以和期望的状态匹配上。

Reconciler Pattern 在设计上非常大的一个优势就是它是声明式的,而不是反应式的。用户要做的是按照系统提供出的 API,直接告诉系统你想要什么,比如说,我想要一个应用的实例的数量保持在 4 个,系统就去考虑各种情况,让你的应用的数量保持在 4 个。而反应式的话,则是监听系统的事件,比如你的应用的某个实例 Crash 了,然后你监听到了这个事件之后,就调用系统的 API 去新创建一个实例。显然,对于用户来说,声明式的方式要简单地很多,不容易出错。而反应式则要求每一个应用都去监听系统的事件,不但侵入到了应用,也很容易出错。

在上面的 Reconciler Pattern 的四个原则中,第二点让数据结构是不可变的这一点也是非常重要的,但是我不是很明白这个点和 Reconciler Pattern 具体的关系在哪里?这个原则是说如果我们要修改一个数据结构,我们实际上不是修改它,而是新建一个新的,然后把原来的标记成过期,这样的好处是,我们可以保留数据结构中间的各种版本,当需要看下当前的 Infra 有哪些区别的,就可以把这些版本直接拿出来对比,非常方便地就可以回答线上环境到底发生了什么样的变更。

有了 Cloud native 的基础设施,我们还要有 Cloud native 的应用运行在上面,那么作为基础设施,我们还应该给应用到底提供什么样的能力呢,书中提到了八点:

  • Runtime and isolation(这里的 isolation 是指资源的 isolation,比如 CPU,Memory,Storage 等等)
  • Resource allocation and scheduling
  • Environment isolation(这里的 isolation 指的是环境的 isolation,比如 dev,test,staging,product)
  • Service discovery
  • State management(Readiness, Liveness)
  • Monitoring and logging
  • Metrics aggregation
  • Debugging and tracing

我们可以看到,上面的八个点中其实下面非常多的都是原来传统的中间件在干的一些事情,而在 Cloud native 的基础设施中,这些中间件正在往下沉,直接成为基础设施的一部分,为应用提供能力。而且在 CNCF 中,上面的八个点基本上都有一个对应的产品对应:

  • Runtime and isolation: containers, rkt
  • Resource allocation and scheduling: kubernetes
  • Environment isolation: kubernetes
  • Service discovery: kubernetes
  • State management: kubernetes
  • Monitoring and logging: Prometheus, Fluentd
  • Metrics aggregation: Prometheus
  • Debugging and tracing: OpenTracing, Jaeger

除了上面的八个点之外,我个人认为应该还加上一个点,就是 Network Resilient,这可以极大程度地解决应用之间的网络通信的问题,这个正是 Service Mesh 所提供的能力,在 CNCF 里面对应的产品是 Envoy(奇怪 istio 怎么还没有进入 CNCF)。

总结来说,本书的作者基本上把设计一个 Cloud native infrastructure 所需要做的事情都讲了一遍,包括上面没有提到的测试等等,大部分的内容其实就是 Kubernetes 目前已经做到的一些事情,如果你对 Kubernetes 的设计已经非常清楚了,那么这本书对你的价值可能不大,如果对 Kubernetes 的设计并不熟悉,那么相信你可以从这本书里面学到不少东西。

浅浅出生快半个月了,这半个月的时间基本上没有睡好觉,刚好在豆瓣上看到一个友邻分享了看完这本书的一些看法,挺有意思的,于是花了几个小时的时间看了一下。

本书将的是一位美国妈妈移居法国后,在法国的各种育儿的经历。主要的内容是在法国,爸爸妈妈们是如何培养孩子的,其中不乏拿美国来进行对比(感觉和中国比较类似),法国的育儿理念受到卢梭多尔托的影响,主张孩子是一个独立的个体,即使在非常小的时候,孩子也有自己的认知,有自己的思想的,对待孩子应该是把他作为一个小大人来看待。

作为大人,应该善于倾听孩子们的想法,并且向他们解释这个世界,对他们的信任和尊重会赢得他们对自己的信任和尊重。在培养孩子的时候,应该给他们设定一定的界限,什么时候可以干,什么事情不能干,而在界限之类,孩子可以自由地做任何事情,以睡觉来举例子,可以要求孩子在 9 点钟就进自己的房间,但是具体她在里面做什么样的事情,我们可以不管,这样,孩子在里面玩耍,一会儿累了,就会自己上床睡觉。这样的好处是可以保证孩子的教养,安全,有素质的情况下,最大程度地让孩子的天性可以得到自由的发挥。

当然,除此之外,作为家长,千万不能以孩子为中心,家长应该有自己的空间和时间,也是独立的个体,不能一天到晚地追着小孩子跑,孩子是生活中非常重要的一个部分,但是生活中应该还有其他也非常重要的部分:工作,旅游,夫妻之间的约会等等。不能认为有了孩子,那么家长所有的时间就都是孩子的了。孩子长大了还是会独立,会离开,而你的配偶才是与你度过下半辈子的人。这方面,我们已经见到我们的长辈身上的一些悲剧,我不止听到一个人提起过,他的母亲有说过:「要不是为了你,我早就自杀了」这样的话,这样把孩子作为绝对重心的家庭往往会导致家庭的悲剧,我们这一代人,不能重蹈覆辙。

书中除了写了一些发过的育儿理念之外,也有一些比较实用的操作,比如如何让宝宝养成在几个月内就能够整晚睡觉的习惯,关键就是在孩子半夜哭起来的时候,先等一下仔细观察一下,看下孩子到底是不是真的需要换尿不湿了,或者真的是饿了,可以让她先哭上个几分钟,小孩子的睡眠周期大概是两个小时左右,哭一会,她会尝试控制自己,然后直接进入了下个睡眠周期了。作为家长,在这方面不能太急,需要有点耐心,有点狠(作为一个父亲,我发现女儿哭起来的时候,我就有一种冲过去抱抱她的冲动),控制一下抱她的冲动。这个技巧,我们也在尝试中,期望在浅浅几个月大的时候可以整晚地自己睡眠。

另外一些有用的操作就是如何养成孩子在吃东西上的一些习惯。一个是养成孩子定时吃饭的习惯,方法是让孩子能够等一等,不能孩子想吃的时候就想吃,慢慢习惯之后,孩子会在吃饭的时间尽量吃饱,然后即使饿了,也会自己耐心等一等。另一个是如何给孩子尝试各种食物,就是如果孩子不吃某种食物,可以几天之后再尝试给他吃同一种食物(可以用不同的烹饪方式),经过几次之后,孩子就会吃的。

当然,本书也有不少瑕疵,一个观点抛出来,和这个观点无关的废话有点多。有几个章节,作者主要就是在讲自己的经历(比如,双胞胎出生的那一章节),而没有谈育儿的内容,比较水。总的来讲,书中的一些方法和观点对于新爸爸妈妈们还是有一定的参考意义的,建议有兴趣的可以阅读一下。

虽然今天遇到的不是一个有什么技术含量的问题,但是国内做这块的人太少了,觉得还是需要多分享分享,至少可以帮助新人走一些弯路,^_^

前几天手贱升级了 macOS Sierra,本来看到 Intellij IDEA 在 macOS Sierra 下面只有一个触摸板异常灵活的问题,觉得自己触摸板用地比较少就直接升级了,哪知道升级以后,在开发我们自己的 Intellij IDEA 插件的时候,启动的 IDEA 一会儿就出现了 Crash 的问题(Crash 的是通过 Intellij IDEA 启动起来的用来测试插件的 Intellij IDEA)。具体的 Crash 截图如下:

intellij_idea_crash.png

本来想是不是要直接回到 OS X Yosemite,但是不甘心啊,觉得既然都已经升级了,那遇到问题就解决吧,幸好在 Jetbrains 官方的问题跟踪平台上看到了有了提了类似的问题:https://youtrack.jetbrains.com/oauth?state=%2Fissue%2FJRE-3

看这个帖子的意思是这个问题是 OpenJDK 的 Bug,于是顺藤摸瓜找到了 Jetbrains 在自己维护的 JDK 上对这个问题 Fix 的 Commit:

https://github.com/JetBrains/jdk8u_jdk/commit/02f9a5fbb4924ff67c8a04c15e490acfcc750003

如果把运行插件 SDK 的 JDK 换成 Jetbrains 自己的 JDK,应该就可以解决问题。当然,要使用 Jetbrains 自家的 JDK,不用拿着源代码自己 Build,可以直接从这里下载对应的 Build:https://bintray.com/jetbrains/intellij-jdk

下载过来以后在插件工程的「Project Structure」配置界面进行如下配置即可:

  1. 将下载过来的 JDK 增加到 SDK 里面。
  2. 找到当前正在用来运行插件的插件 SDK,将其以依赖的 JDK 修改成刚刚增加的 JDK。

至此问题就已经解决,在这里建议大家如果要开发 Intellij IDEA 的插件的话,还是用 Jetbrains 自家的 JDK 比较好,毕竟 Jetbrains 已经在 OpenJDK 的基础上 Fix 了不少的问题,特别是很多和 Swing 相关的问题,使用他们的 JDK 可以帮助我们少走不少弯路。


PS:如果有人对开发工具、插件感兴趣,欢迎留言联系我,蚂蚁金服需要最优秀的工程师来做研发工具,提升工程师的效率。

问题

今天在排查一个线上的问题,线上的一个应用在初始化一个类的静态字段的时候出现了 NoClassDefFoundError,并且在导致 NoClassDefFoundError 出现的根本原因消失后,后续再次尝试初始化这个类的时候,持续出现了 NoClassDefFoundError

于是怀疑 JVM 是不是对一个类的 NoClassDefFoundError 做了缓存,在第一次加载这个类出现 NoClassDefFoundError 以后,后续再尝试加载就直接抛出 NoClassDefFoundError

实验

为了证实自己的猜想,尝试设计了一个简单的实验,一个涉及三个类

1
2
3
public class Test1 {
    static Test2 test2 = new Test2();
}
1
2
public class Test2 {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
    public static void main(String... args) throws Exception {
        while(true) {
            System.out.println("================================");
            try {
                new Test1(); // 尝试实例化 Test1,触发 NoClassDefFoundError
            } catch (Throwable e) {
                e.printStackTrace();

                try {
                    Test.class.getClassLoader().loadClass("Test2"); // 尝试加载 Test2,用于证实当将 
                                                                    // Test2.class 拷贝到 ClassPath 下的时候,
                                                                    // Test2 就可以加载到了。
                } catch (Throwable ex) {
                    ex.printStackTrace();
                }
            }
            Thread.sleep(3000);
        }
    }
}

上述类的的作用是:Test2 是一个空的类,Test1 里面有一个 Test2 的静态成员。Test 是程序的主入口,在一个无限循环内部,不断地尝试去实例化 Test1,并且在加载 Test1 出现异常的时候,尝试加载一下 Test2。

实验的步骤是:

  1. 编译以上类,运行 javac Test.java
  2. 将生成出的 Test2.class 重命名成 Test2.class.bak
  3. 运行 java Test,这个时候程序去加载 Test1 的时候,就会出现 NoClassDefFoundError,并且在尝试加载 Test2 的时候,会出现 ClassNotFoundException
  4. 将第二步重命名的 Test2.class.bak 该回成 Test2.class,这个时候程序去加载 Test1 的时候,就会出现 NoClassDefFoundError,在加载 Test2 的时候,不会出现 ClassNotFoundException

实验的第二步的目的是为了程序在加载 Test1 的时候因为找不到 Test2 出现 NoClassDefFoundError,第四步是为了和第二步做对照,说明在后续程序可以加载到 Test2 的时候,在实例化 Test1 的时候,依旧出现 NoClassDefFoundError

在我的机器上,按照上面的方式去操作,结果如下:

NoClassDefFoundError

结果正如预期,即使在后面 Test2 在 ClassPath 下的时候,NoClassDefFoundError 依旧出现,所以 JVM 里面肯定有地方对 NoClassDefFoundError 做了缓存。

JVM 里面的实现

带着这个疑问,请教了部门里面的 JVM 专家,这个猜测得到了证实,并且他给出了 JVM 内部具体处理这段逻辑的代码,处理的代码在 JDK 的 instanceKlass.cpp 这个文件里面:

1
2
3
4
5
6
7
8
9
10
11
12
bool instanceKlass::link_class_impl(
     instanceKlassHandle this_oop, bool throw_verifyerror, TRAPS) {
   // check for error state
   if (this_oop->is_in_error_state()) {
     ResourceMark rm(THREAD);
     THROW_MSG_(vmSymbols::java_lang_NoClassDefFoundError(),
                this_oop->external_name(), false);
   }
   // return if already verified
   if (this_oop->is_linked()) {
     return true;
   }

并且在 instanceClass.hpp 这个文件中,定义了类的 _init_state,其中,is_in_error_state 这个方法的定义如下:

1
bool is_in_error_state() const           { return _init_state == initialization_error; }

^M,神奇的字符!相信很多人写 Shell 脚本的时候都被这个字符坑过,我自己也至少被坑过两次。最近周围的好几个小伙伴又被 ^M 坑,花了好几个小时检查脚本的错误,结果发现是 ^M 导致的。所以写了这篇文章讲一下什么是 ^M,当 ^M 出现的时候一般会伴随着什么样的现象,出现了我们可以用什么手段去解决。

^M 是何方神圣

这个得先从 Windows 和 Unix 下的换行符开始说起,在我的 Intellij IDEA 的右下方的状态栏上,有一块是展示当前文件的换行符的:

Windows 和 Unix 下的文件换行符

可以看到在 Windows 下,换行符是 \r\n,在 Unix 下换行符是 \n。如果我们用把一个文件的换行符换成 Windows 的换行符,那么当我们用 cat -v 来看的时候,就可以看到:

cat -v 查看文件是否含有 ^M

实际上 ^M 就是 Windows 下的换行符中的 \r 部分。因为 Unix 下的换行符是 \n,所以当一个用 Windows 下的换行符的文件放在 Unix 下的时候,单行的最后一个字符就变成了 \r\r 在 ASCII 码中是 0xD,而 0xD 在 VIM 和 cat -v 则刚好被显示为 ^M

刚才之所以用 cat -v 而不用普通的 cat 是因为 ^M 是不可见的字符,如果仅仅用 cat,是看不到这个字符的。cat-v 参数的作用就是显示不可打印的字符。

^M 会导致什么样的问题?

我们已经知道了 ^M 实际上就是 \r,而 \r 是回车符(Carriage Return),回车符的作用是将设备的位置重置到当前行的开头

知道了 \r 的作用时候,我们来看一个现象:

1
2
3
4
5
6
7
### 有一个普通文件,存放了一个路径,当前行的最后以 ^M 结尾
$ cat -v Main
/home/admin/khotyn.huangt/test/^M

### Echo 一下,神奇了!
$ echo "`cat Main`/where am i"
/where am i/khotyn.huangt/test/

看到后面那个 echo 命令,它将 Main 文件中的内容提取出来,再在后面加上 /where am i 这个字符串,结果我们看到,/where am i 在打印的结果中跑到最前面去了,这正是 \r 这个字符的作用,因为 cat Main 的执行的结果的最后一个字符是 \r,所以一遇到这个字符,设备指针就直接回到了当前行的开头,所以 \r 后面的 /where am i 就直接显示在了最前面。

所以,当你看到什么奇怪的路径,这个路径中莫名其妙地少了一些字符,出现了一些莫名其妙的字符串的,很可能就是 ^M 导致的。

如何逃离 ^M 的魔掌

当你发现了 ^M 导致的问题的时候,最直截了当的方式就是将 ^M 从文件中去掉。

一、临时解决的几个方法

如果的机器上安装有 dos2unix,那么恭喜你,直接运行

1
dos2unix /path/to/file

就可以将一个文件的换行符从 Windows 的转换成 Unix 的。

但是,如果机器上没有装 dos2unix,而你又没法装上去(在一家公司工作总是会有各种各样的让你感觉很丧气的权限控制),那么你可以用 sed 来替换:

1
sed --in-place='' 's/^M//g' /path/to/file

注意:上面的 ^M 只是显示的效果,输入的时候需要用组合键输入,先 Ctrl + V,然后马上 Ctrl + M 就可以在终端中输入 ^M 了。

当然,用 tr 之类的命令也可以,不过我一般用 sed 的原因是 sed 加上 --in-place 参数可以做到直接替换原文件,而不用产生临时的文件(危险而高效的操作)。

二、预防此问题

不过,前面说的只是当出现问题的时候如何解决,那么如何预防这个问题呢?

第一个方法当然是直接放大招,换个 Mac 啥的,或者把你的机器上的 Windows 格了,装个 Ubuntu 也好啊。真心觉得 Windows 对于程序员来说真的没有啥好处(我好想听说连微软都开源 .Net 了,并且会提供多平台的支持)。

第二个方法嘛,当在 Windows 下使用各种编辑器的时候,尽量将换行符设置成 Unix 的换行符。不要偷懒用 Windows 的换行符,出现了问题就是好几个小时的排查时间。(目前没有发现有什么场景下有必须用到 Windows 下的换行符的,如果有同学知道有这样的场景的话,不吝赐教)。

三、防止被别人坑

虽然我个人觉得不应该用 Windows,不过还是有同学的确是喜欢用,或者因为不可抗拒的因素而暂时在使用,为了防止出现这个问题,可以在版本管理软件上做控制,比如 Git 就可以设置换行符,当你提交文件的时候,可以将你的所有文本的换行符替换成你设定的换行符,详细可以看 https://help.github.com/articles/dealing-with-line-endings/

所有的这些功能都是在 Intellij IDEA 14 中测试的,其他的版本不一定适用

打开类的直接定位到某一行

在 Mac 下,IDEA 默认的打开类的快捷键是 Command+O,不过这个快捷键也有一些技巧。

第一个是可以在打开类的时候直接跳到某一行,比如下面这样:

打开 String 这个类的同时直接跳转到 String 的第 40 行。

到某个类的某个方法

IDEA 的 Open Symbol 功能可以直接定位到某一个类的某一个方法,默认的快捷键是 Option+Command+O,如下:

像 Sublime 那样多行编辑

以前要做多行编辑,总是现在 Sublime 里面先做好,然后再拷贝回到 IDEA 里面,现在知道了 IDEA 本身就自带这个功能,快捷键是 Option+Shift+鼠标,直接来看一个 gif 动画看来这个功能吧:

Smart Code Completion

除了普通的代码补全的功能之外,IDEA 还提供了智能的不全功能,我们看下对比:

下面是基本的补全功能:

这个是智能的补全功能:

可以看到智能补全可以直接推断类型,把不符合类型的提示直接全部过滤,让我们可以更加高效地编写代码。

草稿

工作的时候我们经常会创建一些临时文件,在 IDEA 14 中,加入了一个非常有用的创建草稿的功能,Mac 下的快捷键是 Command+Shift+N,你可以在一个工程里面随意创建任意数量的草稿。

上面的这些是前几天参加 QCon 的时候听 IDEA 的一个 Session 知道的一些技巧。个人认为这个 Session 比很多其他的 Session 都更加有料

「多看阅读」一直是我最喜欢的阅读器,没有之一,在多看阅读上买的书也不少了,绝佳的用户体验,「多看阅读」甩出豆瓣阅读、唐茶等 N 条街。

不过,今天在多看阅读上看书的时候,却弹出了这样的东西:

多看阅读弹窗

多看在读者阅读书籍的中间弹出了这样的东西,并且不止弹出了一次,过个几分钟又弹出一次。我觉得多看这种极度不尊重读者的做法与被小米收购有着莫大的关系,小米一直以来给我的印象就是一个营销的公司,而不是一个认真做产品的公司,多看到了小米手里已经沦落成这份模样了,靠这样的弹窗来增加用户量,我决定在看完目前的这本书以后,就卸载多看阅读,以后看书还是买个 Kindle 省事。

Guice 的初学者在使用 Guice 往一个类中注入一个集合注入的时候,肯定有感觉到非常地不自然(这里的不自然我觉得一定程度上是不符合 Guice 给人的初印象),由于最近在项目中也在使用 Guice,所以在这里对 Guice 的集合注入做一个记录。

一、使用 Guice 的扩展 guice-multibindings

Guice 的文档上关于 Guice 注入的最简单的例子应该就是:

1
bind(Interface.java).to(Implementation.java);

我们希望在使用 Guice 做集合注入的时候肯定也是希望使用类似的 API 做注入,不过可惜的是,Guice 的核心里面并没有提供类似的 API 让我们可以使用来注入集合。

所幸的是,Guice 提供了一个扩展的包 guice-multibindings 使用和 Guice 最原始的 API 类似的方式来做注入。

需要使用这个扩展的包,使用 Maven 的话,可以在项目中加入如下的依赖:

1
2
3
4
5
<dependency>
    <groupId>com.google.inject.extensions</groupId>
    <artifactId>guice-multibindings</artifactId>
    <version>3.0</version>
</dependency>

guice-multibindings 主要使用了两种方式来注入,一种是注入一个 Set:

1
2
Multibinder<CheckHandler> checkAdapter = Multibinder.newSetBinder(binder(), CheckHandler.class);
checkAdapter.addBinding().to(InstalledCheckHandler.class);

首先创建一个 checkAdapter,然后往这个 Multibinder 中,我们可以添加任意多的 CheckHandler 的实现。

另一种方式是注入一个 Map:

1
2
MapBinder<String, CheckHandler> mapBinder = MapBinder.newMapBinder(binder(), String.class, CheckHandler.class);
mapBinder.addBinding("Hello").to(InstalledCheckHandler.class);

Map 的注入方式和 Set 的注入方式非常类似。不过奇怪的一点是,Guice 并没有提供注入 List 的方法,这是值得思考的一点

二、使用 @Provides 来注入

看了第一种方法,我们可以看到,上面的这种方法并不能注入一个 List,不过,我们还是有办法来注入一个 List 的,就是使用一个 @Provides 注解,比如在我们的 Guice Module 的类里面加入一下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Provides
List<BindingAdapter> provideBindingAdapter() {
    List<BindingAdapter> bindingAdapters = new ArrayList<BindingAdapter>();
    List<OsgiServiceHolder<BindingAdapter>> bindingAdapterHolders = OsgiFrameworkUtils
        .getServices(bundleContext, BindingAdapter.class);

    for (OsgiServiceHolder<BindingAdapter> bindingAdapterHolder : bindingAdapterHolders) {
        final BindingAdapter bindingAdapter = bindingAdapterHolder.getService();

        if (bindingAdapter != null) {
            bindingAdapters.add(bindingAdapter);
        }
    }

    return bindingAdapters;

}

这样,我们就可以从其他的地方拿到对应的类的实例,然后放到一个 List 中,通过 Guice 注入给其他的类了。

上面的这两种方式其实各有优劣。一般情况下,我觉得选择第一种就可以了,毕竟,第一种方法的类的实例是由 Guice 来生成的。选择第二种方式的场景我觉得可能有:

  • 类的实例是从其他的地方来的,比如上面的例子中,是从 OSGi 来的。
  • 简单类型的类,比如一个 String 的 List。

最近在写框架的测试代码的时候,有需求要对 Log4j 的输出进行测试(依赖 Log4j 的输出来进行测试,这一点本身可能得深思一下),之前也有对 stdout 和 stderr 进行测试,用了一个叫做 system-rule 的包:

1
2
3
4
5
<dependency>
    <groupId>com.github.stefanbirkner</groupId>
    <artifactId>system-rules</artifactId>
    <version>1.5.0</version>
</dependency>

利用这个包中类,只需要在测试用例中加上一个 JUnit Rule,就可以获取到 stdout 和 stderr 中的内容,然后对其进行测试。现在我也想对 Log4j 的输出采用类似的方式进行测试,于是扩展了 JUnit 的 Rule,就有了以下这一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
 * 此 Rule 用于对 Log4j 进行测试
 *
 * @author khotyn 8/14/14 9:18 PM
 */
public class Log4jRule extends ExternalResource {
    private String       logName;
    private List<String> loggerMessages = new ArrayList<String>();

    public Log4jRule(String loggerName) {
        this.logName = loggerName;
    }

    public Log4jRule(Class className) {
        this.logName = className.getName();
    }

    public List<String> getLoggerMessages() {
        return loggerMessages;
    }

    public String getLogMessageAsString() {
        String result = "";

        for (String loggerMessage : loggerMessages) {
            result += loggerMessage;
            result += "\n";
        }

        return result;
    }

    @Override
    protected void before() throws Throwable {
        Logger logger = LogManager.getLogger(logName);
        logger.addAppender(new AppenderSkeleton() {
            @Override
            protected void append(LoggingEvent event) {
                loggerMessages.add(event.getMessage().toString());
            }

            @Override
            public void close() {

            }

            @Override
            public boolean requiresLayout() {
                return false;
            }
        });
    }
}

整段代码非常简单,继承了 JUnit 的 ExternalResource 类,然后在 before 方法中,给对应的 Logger 加上了一个 Appender,在 Appender 中,将日志内容收集到一个 List 中,然后拿到这个 List 就可以拿到日志的输出了。

使用的时候非常简单:

1
2
3
4
5
6
7
8
9
10
11
public class SampleTest {
    @Rule
    public Log4jRule log4jRule = new Log4jRule(SampleTest.class);

    @Test
    public void test() {
      	// .........
        Assert.assertTrue(log4jRule.getLogMessageAsString().contains(
            "Hello, world"));
    }
}

通过

1
log4jRule.getLogMessageAsString()

可以拿到一个 String 格式的日志输出,或者通过:

1
log4jRule.getLoggerMessages()

来获得一个日志输出的内容的 List,List 中的没一行就是日志中的一行

最近在开发公司的集成测试框架,有一些关于单元测试的体会,写一个博客记录一下想法。

为什么要写单元测试?

这个问题,已经有无数的关于技术的书、文章去阐述了,不断地强调单元测试的重要性。比如单元测试可以让你在软件开发的早期阶段发现 Bug,而不必到集成测试的时候才发现等等。不过,对于我来说,在切实地戳到我的痛点之前,我一直都没有去重视这些关于单元测试的忠告(虽然在心中记着,但是实际上并不是很在意)。

在写公司的集成测试框架的时候,有那么好几次,在调整了现有的功能,或者修复了某个 Bug 之后,因为懒惰,也因为跑一次完整的单元测试所需要的耗时较长,我侥幸地认为这些修改应该没有问题,直接打包交付。结果是,墨菲定律出现了,果然,没有经过测试的修改引发了新的 Bug,我不得不重新修改代码,然后厚着脸皮让用户重新试一次。

人总是不靠谱的,我们懒惰,我们存在侥幸心理,坏事儿总是在我们最不希望发生的时候发生。写单元测试不能防止我们懒惰,防止我们存在侥幸心理。但是一次成本低廉的单元测试会让我们觉得:“反正运行一次但单元测试不会耗费很多时间,不如跑一次吧”,它在一定程度上降低我们犯错的几率。

单元测试对于重构的意义也非常重大。很多有意思的程序员都有洁癖,会想着去修改某一段「恶心」的代码。我有过这样几次经历,在把一段「恶心」的代码修改地赏心悦目后,最后上线后发现引入了一个 Bug,心里暗骂一句 WTF,然后一脸黑线地把 Bug 修改了,想想如果当时有单元测试,那么会给我多大的勇气,让我可以肆无忌惮地去重构代码,这是多么爽的一件事情。

好的单元测试应该是怎样的?

一个好的单元测试,我觉得最重要的一点就是运行成本得低,也就是说一个单元测试越快越好。运行一次单元测试的成本越低,你才会越愿意去运行单元测试。如果运行一次单元测试得 10 分钟 20 分钟,那么我想很多人的侥幸心理又会出来了。

我是怎样写单元测试的?

我一般是这样写单元测试的,先想清楚模块的边界,有哪几种可能的输入,这些输入对应的可能输出是什么,然后以最快的速度堆积代码把功能先实现出来,接着写单元测试,把测试用例全部跑过。接下来马上着手重构之前写的代码,不断重构,不断地跑单元测试,知道重构后的代码让自己满意为止。