無標題文檔

随谈 Java 的空指针(翻译和整理)

整理自: http://www.yegor256.com/2014/05/13/why-null-is-bad.html

在日常编码中,有个经典的 Java 空指针(NULL)调用如下:

public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return null;
  }
  return new Employee(id);
}

那么这段方法有什么问题?这个方法的最大问题就是有可能会返回 NULL 对象。空指针(NULL)问题在面向对象编程角度上说是个很严重的问题,所有面对对象编程的过程中都会碰到类似的问题。甚至 Tony Hoare 向全世界道歉,忏悔他曾经发明了「空指针」这个玩意

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement.

原来,在程序语言中加入空指针设计,其实并非是经过深思熟虑的结果,而仅仅是因为它很容易实现而已。这个设计是如此的影响深远,以至于后来的编程语言都不假思索的继承了这一设计,这个范围几乎包括了目前业界所有的流行的编程语言。

对许多程序员来说,早就已经习惯了空指针的存在,就像日常生活中的空气和水一样。那么,空指针究竟有什么问题?

额外的错处处理

当然,有经验的编码人员自然会时刻意识到空指针问题的存在,那么有可能在编码的过程中加入额外的判断,就会规避这个问题,例如下面的写法:

// this is a terrible design, don't reuse
Employee employee = dept.getByName("Jeffrey");
if (employee == null) {
  System.out.println("can't find an employee");
  System.exit(-1);
} else {
  employee.transferTo(dept2);
}

那么这样的话就会多了额外的流程去处理因为空指针判断而多加的判断。例如,上面的处理逻辑和流程中,如果按照面向对象的思维书写,不考虑空指针的情况,只是简单的:

dept.getByName("Jeffrey").transferTo(dept2);

就可以。加入空指针判断以后,程序往往会有额外以及复杂的逻辑判断。

语义不清晰

上面的问题自然可以规避,例如在 getByName() 这个方法中加入异常的判断,那么重新命名这个方法的名字为 getByNameOrNullIfNotFound() 。这样子从代码可读性的角度上说,从方法名字就可以判断这个方法会返回什么,
但是自然而然为了表意清晰就会让方法的名字变得越来越长(感觉有点在黑 Objective-C)。

为了表明这个歧义,除了正常的返回值以外,其他的空指(NULL Object)针情况都会返回一个异常。那么这里又会引申出另外一个问题,就是性能。从上面的代码考虑,如果返回了 NULL 以后再抛出一个异常,实际上这段代码是已经被执行了的,然后再加的判断。

例如,在示例中 getByName() 通常是个 Map 然后执行了 get 方法去搜索是否存在这个名字,如果不存在再抛异常,这样就会造成额外的执行时间:

Employee employee = employees.get("Jeffrey");
if (employee == null) {
  throw new EmployeeNotFoundException();
}
return employee;

当然,这个问题也是很好规避,例如只需要先搜索它的索引即可,然后再取值返回:

if (!employees.containsKey("Jeffrey")) { // first search
  throw new EmployeeNotFoundException();
}
return employees.get("Jeffrey"); // second search

那么,这部分操作判断只是针对空指针就多了一次搜索操作。我们考虑再优化下,直接使用迭代器处理:

Iterator found = Map.search("Jeffrey");
if (!found.hasNext()) {
  throw new EmployeeNotFoundException();
}
return found.next();

这样子似乎很好的解决了问题。

但是,我们编写这段代码的业务初衷似乎越来越远,然后方法的名字也变得越来越长,自然而然代码也变得越来越复杂。

如何最大程度得避免空指针?

本篇开头的这个问题一样,空指针的问题可以追溯到计算法发展史时期,同时空指针异常的情况也很多,甚至在程序运行阶段也无法避免空指针的情况。那么,在编码层面,我们需要注意哪些呢?

确认调用的的每个变量都已经被初始化

这点说起来很简单,但事实上随着业务的发展项目代码也会越来越庞大。这时候方法之间调用的关系也会越来越复杂,很难避免使用到的方法都已经明确被初始化。

所以这块单独放在这里,需要我们在编码的实话重点考虑变量存在的可能性,这其实大体上基于自己的实际编码经验。

尽量使用明确的值调用

如果已经明确某个变量(常量)的值,那么是可以安全调用它的方法的。例如对比下面的几行代码:

String a = null;
a.equal("b");   // 会产生空指针异常
"b".equal(a);   // 推荐的写法

很明显使用常量去做调用这代码会更健壮一些。

尽量避免在函数中返回 NULL

当如果在编写方法中考虑返回 NULL,这个时候则需要冷静下是否真的需要这样子做。因为,通常来说会有比返回 NULL 更好的处理方式。

自动装箱需谨慎

自动装箱确实为编写程序带来很多方便,但我们在编程时候也不能滥用自动装箱。

比如,下面这个程序依然存在空指针异常隐患:

Person jack = new Person("jack");
int weight = jack.getWeight();

这种异常在我们使用一些 ORM 框架中会碰到,如果数据库对应的对象并不存在该值,而我们又在类中使用了一个基本类型与之对应,依然就会抛出空指针异常。在这种情况下就尽量使用包装类来对应,并且在使用该值时候先判断是否为空。

遍历谨防集合为空

for (int num : list) {
    // for each num in list
}

及时验证外部数据

在代码运行的过程中,尤其在解析外部数据的时候可能会引发影响不到的问题。例如下面的 Json 数据

{"name": null, age: 28}

如果不处理完善,虽然这可能是外部原因造成,但直接使用 name 属性也会导致空指针的问题。

使用第三方库加强验证

很多第三方的 Common 库都会有验证空指针的方法,例如 Guava 中针对空指针的判断有个单独的包去处理。

Optional<Integer> possible = Optional.of(5);
possible.isPresent(); // returns true
possible.get(); // returns 5

或者过滤 NULL 也会更加的方便

Joiner joiner = Joiner.on("; ").skipNulls();
return joiner.join("Harry", null, "Ron", "Hermione");

使用 @NotNull 或 @Nullable 注解

强烈建议多使用注解来增加代码的可读性,例如多增加 @NotNull 或 @Nullable 注解,也可以加强代码静态检查方面可能会造成空指针的可能性,具体可以参见这里

使用 Java8 的 Optional

很多「现代」语言都会有针对变量为空的可选链式判断,例如

Groovy 语言有一个 ?. 的操作符,可以安全地处理潜在可能的空引用(据说 Java7 曾被建议引入这个但是并没有发布)。它是这么用的:

 String version = computer?.getSoundcard()?.getUSB()?.getVersion();

虽然 Java 看起来非常的保守,但好在 Java8 中增加了 Option[T] 这个对象包来代表类型 T 的某一个值存在或者没有。那么上面的代码可以写成:

String name = computer.flatMap(Computer::getSoundcard)
                      .flatMap(Soundcard::getUSB)
                      .map(USB::getVersion)
                      .orElse("UNKNOWN");

看起来似乎有点麻烦,但相信我你会爱上这样的写法,具体可以参见这里

好了,针对空指针的总结和整理先到这里,如果你有更好的意见和建议,欢迎不吝提出。

参考资源

-- eof --

使用 Docker 部署 VPS 备忘

本 Blog 的代码也有很多的年头,当时使用的技术过了几年在目前看来有些的陈旧。同时,在 Vultr 的服务器空间也即将到期,所以考虑干脆做个迁移顺便将目前 VPS 上的服务也一起整理了下。

这一整理,发现了不少的巨坑,这在这里记录下。先说说在迁移前 VPS 目前的主要问题

  1. 部分代码老旧,同时又有新的代码,运行环境就有好几套;
  2. 尝试使用 Node 的工具进行开发,然而 Nginx 以及 php-fpm 独占了 80 端口,配置文件越来越复杂;
  3. 权限分配不合理,有部分的权限使用 root 运行有安全上的风险;
  4. 备份机制不合理,没有做到企业级的多点备份(虽然个人 VPS 没必要,但图个心安)。

其他情况不多说,重新整理以后的 VPS 应用架构图如下,相对来说还是不是很复杂:

https://friable.rocks/_/2017_10_12/1507794332@800.png

综合价格以及习惯的因素(可能是情怀更多?),大部分选择了阿里云的服务。同时针对以上的问题,这边的解决方案是:

  1. 使用不同的 Docker 集群将 php5 和 php7 的应用分别调用,相互不受影响。老旧的代码就让它们在 php5 环境中「颐养天年」;
  2. 最前端使用 Tengine 分发请求到每个对应的 Docker,方便管理;
  3. 使用 Docker 以后,权限这块就相对来说好控制很多,宿主机不受任何的影响;
  4. 配置文件使用 git 管理起来,并托管到 Bitbucket,方便部署;
  5. 备份使用阿里云的 OSS 存档服务,价格很便宜而且也是安心很多。

同时,在这里汇总下碰到的几个坑和心得,供参考

  1. 官方 Docker 的 php7-fpm 是没有带 gd 库的,所以要自己 build 一个版本(生产环境顺便把 debug 这些都去掉);
  2. 因为只是在最外面 Router 应用服务使用 https,所以 PHP 应用内部需要增加判断下,防止判断失败,详见这里。简单的说,就是加个判断
/**
 *  Handle SSL reverse proxy
 *  @see https://www.variantweb.net/blog/wordpress-behind-an-nginx-ssl-reverse-proxy/
 */
if ($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
    $_SERVER['HTTPS']='on';
}

if (isset($_SERVER['HTTP_X_FORWARDED_HOST'])) {
    $_SERVER['HTTP_HOST'] = $_SERVER['HTTP_X_FORWARDED_HOST'];
}
  1. Alpine Linux 的 apk 源有时候会不稳定,Docker build 的时候会有出错的几率,多重试就好。

--

补充下,有关 HTTPS 转发这块,khsing 提出了很好的建议,非常感谢:

刚才看了你的使用 Docker 那篇文,里面处理 WP 处理 https protocol
的方法还挺硬的,这事儿我之前在阿里云部署的适合也遇到了,不过我是在 nginx 上做了处理,这样就不用动 PHP 代码了。

map $http_x_forwarded_proto $forwarded_https { default off; https on; }
fastcgi_param HTTPS $forwarded_https;

-- eof --

再见,丁香园

已经正式从丁香园离开两个月,时间过得很快。

说起来离职的过程并不算是十分得愉快,然而两个月过去了心态也平静了很多,应该可以从初心上去写点文字回忆在丁香园的这段时期。

初心

2010 年的时候,@Fenng 来找我问有个项目你或许可以试试,这家公司的名字叫做丁香园。那时候个人个人在阿里的时候正处在转型期(瓶颈期?),所以抱着聊聊看的心态和创始人 李天天 一起吃了个吃饭。

印象很深,这顿饭吃完了以后,我自己就有感觉是应该为自己的理想和初心去实践些什么。后面的一来二回持续沟通了以后,就做下了这个决定:离开阿里去当时这个没任何名气的小公司见证团队从小到大的过程。

我当时在博客里说明了我离开阿里的想法,私底下很多朋友和同事很不解我的决定。毕竟,在阿里(淘宝)四年的时间不算短,无论个人发展方面还是待遇方面,当时的丁香园是没有任何优势的。

然而,时至今日我还是坚持我这个决定,并不后悔。

技术

在淘宝做了四年的前端,来到丁香园的时候整个公司的技术团队不足十人。技术方面,由于丁香园在前端方面是没有任何的框架以及代码规范可言,业务的高速发展对于优化和重构这块是势在必行的。

现在回过头来想,开始我可能过于理想化以及出于技术方面的思维惯性(说白了就是没经验),还是沿用了淘宝的技术架构。

在前端框架的选型上,使用了淘宝前端团队自己编写的 KISSY,这个后来是个大坑。KISSY 无论在框架的接受程度上还是可维护性、扩展性方面,相比当时业内的通用知名框架没有任何的优势可言。

甚至在一段时间内,我还在和团队的其他前端逐个在说明 KISSY 框架如何使用,乃至至一段时间内都影响到了业务的发展。

悬崖勒马,经过考虑团队还是重新选择到了 jQuery 框架。好在当时的业务高速发展,冗余代码迅速被迭代才没有被后来的绝大多数前端们发现竟然还有这段黑历史 :^)

然而由此带来的影响,再后来 node.js 等推出和成熟,在技术方面的由于前一次的“吃亏”,团队并没有坚持选择非常新的技术。

PHP 作为前端和后台的“粘合剂”,在丁香园的服务器中存在了很久一段时间(至今历史代码还在)。

疲于应付业务和需求技术方面在很长的一段时期是没有任何的规范可言,这可能是小团队的通病。

客户端的兴起,业务驱动个人逐渐技术方面转向了客户端这边。因此,比较遗憾的是前端这边并没有推动太多新的技术和流程,然后又要去挖移动端这个“坑”。

在丁香园的六年多,可以说有 70% 的时间都给了移动端这边:团队、技术、架构、甚至采购。这一切的内容对于我而言都是从零开始,而我也是乐在其中。

期间产品两个 iOS 和 Android 平台的版本分别获得了 App Store 以及豌豆荚的推荐和奖励,这在我个人职业生涯中也是莫大的鼓舞很肯定。

产品的迭代用户数量的攀升,带来了团队的壮大以及需求的不断增长,移动团队在公司十分被重视,各种资源都能够得到满足,有段时间内移动端团队相对于其他技术团队独立,这是好事也是坏事就不详细说。

说回到了技术方面,由于没有 QA 团队,所以在质量保证方面产品团员、开发、甚至运营都需要承担一部分的测试和质量保证工作。

“吃自己的狗粮”

这是团队的传统也是为什么不设 QA 这个职位的初衷。现在看来,这个话题还是颇有争议的,或许可以单独开一篇文章去讨论的。

业务的洗礼、QA 角色的“平均”,所以丁香园在技术方面的选型以及步进是偏向于保守的。

不过期间也做出过尝试,令人感到自豪和欣慰的是 iOS 端(2015年)开始逐渐的迁移到 Swift2,在一段时间内由于这块新技术调整造成产品迭代缓慢,产品和需求方都表示理解。

再后面,Android 也想考虑尝试下例如 Kotlin 等技术,比较遗憾我无法参与其中了,这是后话。

业务

写了一半,可能会招惹非议,略过吧。

团队

回到团队本身,如果说总分有十分,我完全毫无保留得给我们的团队打分九分,留一分让我剩下用岁月慢慢给到你们。

我爱这个团队,我爱团队的每个成员,我爱你们。

很多人崇尚的扁平化、足够的自由度,以及“漠视 KPI”,等看起来很美好的关键词,都或许在这个团队都得以实现。

同时,管理层面的 Sense 就是十分关注组员的成长,无论是技术还是个人方面。团队的信息透明也是我所推崇的,所有的信息都不会被二次咀嚼以后,再分别给到团队分别不同的人。

技术方面,能够给予足够的自由度以及尽可能的尝试,这里回过头来思考可能会抛出好几个问题。例如,如何保证团队的自我驱动能力?新技术的尝试如何保证项目本身的质量和进度?等等。

这些我问题坦白讲,我至今也在继续摸索和求证。

很是感谢在丁香园的几年,在管理方面的很多想法都能得到实践以及总结,团队从小到大的过程并不是所有管理者所能亲身经历。

Fenng 曾经和我说或许如果当时留在阿里待遇方面可能会更加可观,我可以很如实的回答,丁香园给予我的这些经历可能留在阿里这些年都无法换回,感谢这六年。

--

2016 年丁香园发生了些不愉快的事情,管理层的变更、团队的调动以及后续管理者的价值观、技术均无法认同,这是我离开团队的根本原因。

从丁香园离职了以后,我也有过抱着巨大的怨念去回忆以及诉说我的离职过程,现在回过头来想其实十分没必要以及幼稚。然而时至今日,还能听到很多或许有关于我以及团队的刺耳言论,个人有时还是无法控制自己的情感。

爱的深,可能就会更在乎它的每个细节。我现在已经可以自我调节和理智面对,对于那些信息的源头的始作俑者,我只想对你们说,你们是永远都不会 GET 和理解我对这个团队的情感以及初心。它对于我而言不仅仅是我一份工作的证明,怎么可能去伤害。

夏虫不可语冰

再见丁香园,曾经和现在都爱过的。

-- EOF --

upyun-cli.py,又拍云的命令行工具

截图

其实这是个填坑的项目,两年了我终于把当时留下的坑给填上了,但愿这迟来的不会太迟。

事情是两年前 又拍云 做了个活动,大概是为又拍云开发第三方应用就可以获得不定的流量。由于我的博客一直用的是又拍云的资源服务,自然这个活动对于我而言是很有诱惑力的。

又拍云 其实已经是非常老的老朋友了,往前可以追溯到又拍在线图片服务时期。凭借多年的图片处理以及运维经验,我个人从 12 年使用到现在没有出现过任何的问题,对于又拍云的服务自然是非常的信赖。

对于老用户也是相当的信任,那个活动我其实并没有开始针对又拍云的 SDK 开发任何的小项目,只是报了个名,他们就将不菲的流量资源(T 级别,多到个人博客根本用不完)已经给到我了。

其实我是个极其懒惰的人,自然这件事情就因为各种琐事给忘在脑后了。直到近期我翻阅 github 项目的时,发现 Star 的项目中,为数不多的项目还在持续的更新,这里就有又拍云的 Python SDK

有些触动,不仅仅是因为 又拍云 那种默默实打实做实事的态度,同时也是为了我自己的懒惰感到非常的惭愧。

言归正传,我自己也荒废了 Python 语言多年,刚好可以拿这个 SDK 练练手,所以就有了这个小项目。

有些惭愧没使用 Python 做过实际项目多年,发现 Python 这门语言的发展还是很快的,非常适合短平快的些项目的开发。

现在的 Python 已经过了阵痛期,如果你还在问是选择 Python2 还是 Python3 这些月经性的问题,这里我建议如果没有历史包袱那么就直接使用 Python3 开始吧。

回到 upyun-cli.py ,除了使用 Upyun SDK,还使用了些目前很流行的模块,例如 clickcoloramapyaml 等。

当然这个小脚本还没有大到需要分模块的地步,因此我将所有的代码都集中在一个文件,也同时是为了方便部署使用。

最后废话不多说,还是抛砖引玉直接看代码吧。

作为 Python 新手,我很期待有经验的同学能够给我些更多的建议 :^)

网易云音乐接口的简单分析

注意,此文档有时效性。

由于公司网络的关系,在线听音乐经常会被断断续续,于是考虑批量将音乐(高清)下载到本地。同时,由于熊孩子的缘故,在自己的音乐榜单上「小苹果」等歌曲一直在前排,显得很没有「格调」,于是就萌生了折腾云音乐接口的想法。

网易云音乐的接口相对而言还是比较简单,尤其是大部分前端与服务器端通信的逻辑都已经在 core.js 里面,虽然加了压缩但是没有混淆因此还是很好去处理和理解。

访问云音乐接口每次都需要带个名为 csrf_token 的 GET 参数,这个参数很好找就在 Cookie 里面,而且还是持久化的。

剩下的两个参数大部分都是 POST 过去,分别名为 params 和 encSecKey,里面的字符串经过 encodeURIComponent 处理过。

云音乐请求的加密方式

加密和解密的方法还是在 core.js 这个文件里面能够找到,AES CBC 加密,客户端自己生成 Pair 。两个加密解密的方法都暴露在全局作用域下,分别名为 window.asrsea 以及 window.ecnonasr

部分的接口,例如 feedback/weblog 是没有做任何重复请求处理的,换句话说可以刷。上面说到 csrf_token 其实是持久化的,也没有来源判断等,因此拿到 Cookie 以后就可以自己构造请求。

不过话说回来网易的平台设置做得的确财大气粗,乃至多点并发去做请求都能撑得下来(估计是我的小水管还不够人家的零头)。

顺便提一句,再深入研究下其实可以发现非会员也可以下载收费资源,这里不铺开讲了。

总结下一些看起来是「大道理」的心得:

  1. csrf_token 这些参数应该和时间和请求次数挂钩,更不应该加入到 Cookie 里;
  2. 客户端和服务器端的加密一直是安全方面讨论的主要课题,我的倾向是轻客户端重服务器端,不要把所有的逻辑都写在客户端中;
  3. 前端脚本代码的打包方面尽可能的不要污染全局空间,尤其是框架方面的代码;
  4. 服务器端应该对用户异常行为作出判断,例如业务方面用户频繁下载以及标记打星是否合理;
  5. PS,网易云音乐考虑下 WebSocket
  6. 查找和 Hook 网络接口这块可以从请求方法入手,通常现在端架方面请求都会放在一个方法中处理;
  7. 有点废话,抓取的时候使用 HTTP Keep-Alive 头能够明显的加快请求速度;
  8. 可以考虑多个 IP (代理)去抓取数据,以免实际地址被服务器 Block;
  9. 实际情况中需要考虑的外部因素有很多,例如尽可能的在预算范围内购买足量的存储空间…;
  10. Charles 很好用,这钱花得值…

最后,顺便刷个榜开个玩笑捣蛋下。

云音乐刷榜

-- EOF --

我的照片

嗨!我叫「明城」,八零后、码农、宁波佬,现居杭州。除了这里,同时也欢迎您关注我的 GitHub (2) 、 TwitterInstagram 等。

这个 Blog 原先的名字叫 Gracecode.com 、现在叫 「無標題文檔」 。 其实无所谓叫什么名字,作为码农知道取名是件很难的事情。最后想到的这个名字,其实都没啥特别的含义,系统默认的文件名而已。

作为八零后,自认为还仅存点傲娇式的幽默感,以及对平淡生活的追求和向往。 为了免得对号入座和不必要的麻烦,声明本站点所持观点仅代表个人意见,不代表自己所服务公司的立场。

如果您想联系我,可以发我邮件 `echo bWluZ2NoZW5nQG91dGxvb2suY29tCg== | base64 -d`

文章

项目