無標題文檔

Arch Linux 下安装 EM7345 4G 网卡小记

最近买了台二手的 ThinkPad X1 Carbon 用来当做开发用机,系统方面选择安装了 Arch Linux 。

由于硬件方面并不是算很新,所以系统安装硬件的过程很顺利。还注意到 X1 Carbon 支持 4G 网卡,手头上刚好有一张联通的流量卡,考虑能否用起来。但是打开了盖板以后发现只是预留了插槽和天线,并没有 4G 网卡模块。

https://friable.rocks/_/2018_09_20/1537424625.jpg

根据 ArchLiux 的文档),淘宝上找了家靠谱的卖家,名为  Sierra EM7345 的模块很便宜,原装拆机的只要一百多,而且对 Linux 的支持很好,于是下单。

还是要夸下 ThinkPad 的,硬件安装非常方便,拧开螺丝就可以安装上去。硬件安装好了以后,启动使用 # lsusb 就能看到硬件了。

然后安装对应的软件包:

# pacman -S usbutils usb_modeswitch modemmanager mobile-broadband-provider-info

但是启动 ModemManage 的时候发现了问题,有报错信息:

ModemManager[13625]: <info>  Couldn't check support for device '/sys/devices/pci0000:00/0000:00:14.0/usb2/2-1': not supported by any plugin
ModemManager[13625]: <info>  Couldn't check support for device '/sys/devices/pci0000:00/0000:00:19.0': not supported by any plugin
ModemManager[13625]: <info>  Couldn't check support for device '/sys/devices/pci0000:00/0000:00:1c.1/0000:04:00.0': not supported by any plugin

好在 ArchLinux 的 Wiki 上有对应的信息说明,对应的应该是 udev 的权限问题。于是增加相应的配置文件,发现并不成功。然后搜索了一圈,并没有对应的解决方案。

后来,在 rules.d 目录中发现了另外个文件 /lib/udev/rules.d/77-mm-sierra.rules 里面虽然没有对应的 Vendor, 但是从注释可以看出来这是 Sierra 相对应的 udev 权限配置文件,于是将配置加入到 /etc/udev/rules.d 其中。

注意,直接修改 /lib/udev/rules.d/ 目录的 rule 文件可能在下次更新的时候被覆盖掉。

ACTION!="add|change|move|bind", GOTO="mm_sierra_end"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1199", GOTO="mm_sierra"
GOTO="mm_sierra_end"

LABEL="mm_sierra"

# @see https://wiki.archlinux.org/index.php/USB_3G_Modem
# @see https://support.lenovo.com/us/en/solutions/pd031021
# @see https://www.freedesktop.org/software/ModemManager/api/1.8.0/ref-overview-modem-filter.html

ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{FILTER_RULE_TTY_ACM_INTERFACE}="1"
ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{MM_FILTER_RULE_CDC_WDM}="1"
ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{MM_FILTER_RULE_EXPLICIT_WHITELIST}="1"
ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{MM_FILTER_RULE_NET}="1"
ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{MM_FILTER_RULE_TTY_BLACKLIST}="1"
ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{MM_FILTER_RULE_TTY_DEFAULT_ALLOWED}="1"
ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{MM_FILTER_RULE_TTY_DRIVER}="1"
ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{MM_FILTER_RULE_TTY_MANUAL_SCAN_ONLY}="1"
ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{MM_FILTER_RULE_TTY_PLATFORM_DRIVER}="1"
ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{MM_FILTER_RULE_TTY_WITH_NET}="1"
ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{MM_FILTER_RULE_TTY}="1"
ATTRS{idVendor}=="1199", ATTRS{idProduct}=="a001", ENV{MM_FILTER_RULE_VIRTUAL}="1"

LABEL="mm_sierra_end"

然后,刷新 udev 的配置,并通知 kernel 启用新的权限:

# udevadm control -R && udevadm trigger

再看下对应设备文件文件的 rule 是否已经启用成功:

# udevadm info /dev/cdc-wdm0

对应的输出

P: /devices/pci0000:00/0000:00:14.0/usb2/2-4/2-4:1.0/usbmisc/cdc-wdm0
N: cdc-wdm0
E: DEVNAME=/dev/cdc-wdm0
E: DEVPATH=/devices/pci0000:00/0000:00:14.0/usb2/2-4/2-4:1.0/usbmisc/cdc-wdm0
E: FILTER_RULE_TTY_ACM_INTERFACE=1
E: ID_MM_CANDIDATE=1
E: ID_MM_DEVICE_PROCESS=1
E: MAJOR=180
E: MINOR=0
E: MM_FILTER_RULE_CDC_WDM=1
E: MM_FILTER_RULE_EXPLICIT_WHITELIST=1
E: MM_FILTER_RULE_NET=1
E: MM_FILTER_RULE_TTY=1
E: MM_FILTER_RULE_TTY_BLACKLIST=1
E: MM_FILTER_RULE_TTY_DEFAULT_ALLOWED=1
E: MM_FILTER_RULE_TTY_DRIVER=1
E: MM_FILTER_RULE_TTY_MANUAL_SCAN_ONLY=1
E: MM_FILTER_RULE_TTY_PLATFORM_DRIVER=1
E: MM_FILTER_RULE_TTY_WITH_NET=1
E: MM_FILTER_RULE_VIRTUAL=1
E: SUBSYSTEM=usbmisc
E: USEC_INITIALIZED=10423134

配置生效,kernel 的配置看起来一切正常。

这时候启动 ModemManage 还是发现有错误信息,但是过了一段时间以后对应的 Modem 已经是 enable 可用状态(这点百思不得解,可能是 4G 网络连接需要初始化比较慢)。

Sep 20 14:25:44 x1-carbon ModemManager[2047]: opening device...
Sep 20 14:25:44 x1-carbon ModemManager[2047]: [/dev/cdc-wdm0] Read max control message size from descriptors file: 512
Sep 20 14:25:44 x1-carbon ModemManager[2047]: <info>  Couldn't check support for device '/sys/devices/pci0000:00/0000:00:14.0/usb2/2-1': not supported by any plugin
Sep 20 14:25:50 x1-carbon ModemManager[2047]: <info>  Modem: state changed (unknown -> disabled)
Sep 20 14:25:50 x1-carbon ModemManager[2047]: <info>  Modem /org/freedesktop/ModemManager1/Modem/0: state changed (disabled -> enabling)
Sep 20 14:25:54 x1-carbon ModemManager[2047]: <info>  Modem /org/freedesktop/ModemManager1/Modem/0: state changed (enabling -> enabled)
Sep 20 14:25:54 x1-carbon ModemManager[2047]: <info>  Modem /org/freedesktop/ModemManager1/Modem/0: 3GPP Registration state changed (unknown -> registering)
Sep 20 14:25:54 x1-carbon ModemManager[2047]: <info>  Modem /org/freedesktop/ModemManager1/Modem/0: 3GPP Registration state changed (registering -> home)
Sep 20 14:25:54 x1-carbon ModemManager[2047]: <info>  Modem /org/freedesktop/ModemManager1/Modem/0: state changed (enabled -> registered)

然后在控制台下使用 mmcli 获取相关的信息,具体的 mmcli 可以参考这里的文档。发现了一个不大不小的问题,就是 sim 卡的日期还是 20040101 ,很明显没有和网络同步(后来发现这块并不重要)。

$ mmcli -m 0 --simple-status

/org/freedesktop/ModemManager1/Modem/0
  -------------------------
  Status |          state: 'connected'
         | signal quality: '67' (recent)
         |          bands: 'unknown'
         |    access tech: 'lte'
  -------------------------
  3GPP   |   registration: 'home'
         |  operator code: '46001'
         |  operator name: 'CHN-UNICOM'
         |   subscription: 'unknown'

再尝试启动 modem-manage-gui 看其信息,过一会发现已经注册了网络并能获取对应的信息。

https://friable.rocks/_/2018_09_20/1537427550.png

硬件方面的配置没有问题,那么软件方面的设置就简单多了。然后使用 Gnome 的网络配置,配置 APN 已经相关的网络参数,过一会就能看到久违的信号了。

https://friable.rocks/_/2018_09_20/1537427570.png

总结下,这个问题困扰了我一个晚上的时间,然后也走了很多的弯路。

主要还是在硬件资源的权限方面,这块还是要多看 Arch 的 Wiki 以及对应软件的 Manual(开源社区的锅都扔给用户了)。

相关的参考链接:

PS,有什么靠谱的联通流量卡推荐下?

- eof -

集成 Dubbo Spring Boot 时的 ZooKeeper 版本问题

微服务节点使用 Spring Boot 会方便很多,在搭建 Spring Boot 的时候碰到了个不大不小的问题,在这里记录下。

主要情况是配置好了 Dubbo Spring Boot 启动 Provider 节点的时候发现异常,抛出了两个错误:

Caused by: org.apache.zookeeper.KeeperException$UnimplementedException: KeeperErrorCode = Unimplemented for ...
Caused by: java.lang.IllegalStateException: KeeperErrorCode = Unimplemented for ...

检查堆栈发现是 Dubbo 建立 ZooKeeper 链接的时候,就直接抛出了异常:

// from com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry.doRegister(ZookeeperRegistry.java:116) ~[dubbo-2.6.2.jar:2.6.2]

protected void doRegister(URL url) {
    try {
        this.zkClient.create(this.toUrlPath(url), url.getParameter("dynamic", true));
    } catch (Throwable var3) {
        throw new RpcException("Failed to register " + url + " to zookeeper " + this.getUrl() + ", cause: " + var3.getMessage(), var3);
    }
}

但是 ZooKeeper 的服务器配置是正常的,百思不得:

$ echo stat | nc localhost 2181
Zookeeper version: 3.4.13, built on 06/29/2018 04:05 GMT

然后继续查看 KeeperException$UnimplementedException 异常的定义,

public static class UnimplementedException extends KeeperException {
    public UnimplementedException() {
        super(Code.UNIMPLEMENTED);
    }
}

对应的调用:

// @from org.apache.zookeeper.ZooKeeper.create
public String create(final String path, byte data[], List<ACL> acl,
        CreateMode createMode, Stat stat, long ttl)
        throws KeeperException, InterruptedException {
    final String clientPath = path;
    PathUtils.validatePath(clientPath, createMode.isSequential());
    EphemeralType.validateTTL(createMode, ttl);

    final String serverPath = prependChroot(clientPath);

    RequestHeader h = new RequestHeader();
    setCreateHeader(createMode, h);
    Create2Response response = new Create2Response();
    if (acl != null && acl.size() == 0) {
        throw new KeeperException.InvalidACLException();
    }
    Record record = makeCreateRecord(createMode, serverPath, data, acl, ttl);
    ReplyHeader r = cnxn.submitRequest(h, record, response, null);
    if (r.getErr() != 0) { // 这里抛出的异常
        throw KeeperException.create(KeeperException.Code.get(r.getErr()),
                clientPath);
    }
    if (stat != null) {
        DataTree.copyStat(response.getStat(), stat);
    }
    if (cnxn.chrootPath == null) {
        return response.getPath();
    } else {
        return response.getPath().substring(cnxn.chrootPath.length());
    }
}

那么基本上可以判定 1、是 ZooKeeper 本身链接的问题,和 Dubbo 没有关系;2、实际上 ZooKeeper 本身的服务已经连接上,但 makeCreateRecord 方法调用出现了异常。

那么,可以得出结论是 Java 端和 ZooKeeper 服务端出现了通讯问题。然后发现日志上 jar 包的版本是 zookeeper-3.5.3-beta.jar ,同时查看了下 Manifest 内容如下:

Implementation-Title: org.apache.zookeeper
Implementation-Version: 3.5.3-beta
Implementation-Vendor: The Apache Software Foundation

然后再运行客户端查看了下服务器的版本,两者版本不一致,突然觉得应该是版本的问题。

$ echo stat | nc localhost 2181
Zookeeper version: 3.4.13, built on 06/29/2018 04:05 GMT

很明显 Java 端的版本比服务端的版本要新,那么考虑使用和服务端同个版本的 jar 包试试。

修改对应 build.gradle 的 dependencies 如下,不要纳入 dubbo-spring-boot-starter 提供的 ZooKeeper 的 jar 包

dependencies {
    // ...
    compile('com.alibaba.boot:dubbo-spring-boot-starter:0.2.0') {
        exclude(module: 'org.apache.zookeeper')
    }
    compile 'org.apache.zookeeper:zookeeper:3.4.13'
}

然后再运行 gradle clean bootRun -x test 发现 Dubbo 正常启动,问题解决。

这个问题有点坑,同时很难发现,我又查了下对应的资料。官方其实已经有对应说明,简单的说就是 ZooKeeper 3.5.x 和 ZooKeeper 3.4.x 有不兼容的情况。

而回过头来看 dubbo-spring-boot-starter 包的 pom.xml 定义,对应的 ZooKeeper 这块的引用是这样子的:

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
</dependency>

没有指定版本,也就是默认是取 Maven 库的最新版本,目前是 3.5.4-beta 自然对应本地版本 3.4.13 就有冲突了(竟然不向下兼容,坑)。

相关讨论,看来被坑的不止我: https://stackoverflow.com/questions/35734590/apache-curator-unimplemented-errors-when-trying-to-create-znodes

最后顺便说一句,如果有用到 Spring Cloud 相关的 Zookeeper 组件,也要留个心眼:

dependencies {
    // ...
    compile('org.springframework.cloud:spring-cloud-starter-zookeeper-config') {
        exclude group: 'org.apache.zookeeper', module: 'zookeeper'
    }
    compile('org.springframework.cloud:spring-cloud-starter-zookeeper-discovery') {
        exclude group: 'org.apache.zookeeper', module: 'zookeeper'
    }
    compile 'org.apache.zookeeper:zookeeper:3.4.13'
}

这样子就能保证统一引用的是指定版本的 ZooKeeper 的 jar 包了。

总结下,dubbo-spring-boot-starter 项目目前相对来说还是比较新,相关的文档还是没跟上,但是已经能够日常和生产环境使用了,还是推荐使用简化配置提高些开发效率。

- eof -

Mermaid,一个生成结构图的工具

以前在设计软件架构的时候免不了使用 Visio 、OmniGraffle 等这样的工具来生成结构图。

它们普遍有个缺点,「成也 GUI、败也 GUI」。就是无法将自己想法比较直观的直接使用文本来输出,有时候看似鼠标点点拖拖所见即所得,然而思路就在这时候被一闪而过。

说到这里,有可能你会想到使用 Graphviz 等文本图形描述语言来生成结构图。它的确很好用,但是无法直接嵌入到 Web 中。通常做法就是使用 Graphviz 生成图片以后,上传到 Web 然后再插入到页面。

文档和图其实都是内容,我们不应该因为排版的问题耗费太多的时间,精力应该更加专注到内容本身。

说了那么多,这就是我推荐 Mermaid 初衷

Generation of diagram and flowchart from text in a similar manner as markdown.

多种类型的图表

目前 Mermaid 成熟的结构图模块可以生成 流程图、序列图以及甘特图。它们生成的语法很简单,例如以下的例子:

流程图

使用源代码

graph TD;
    A-->B;
    A-->C;
    B-->D;
    C-->D;

就可以生成以下的流程图

flowchart

序列图

使用以下的序列图定义

sequenceDiagram
    participant Alice
    participant Bob
    Alice->John: Hello John, how are you?
    loop Healthcheck
        John->John: Fight against hypochondria
    end
    Note right of John: Rational thoughts <br/>prevail...
    John-->Alice: Great!
    John->Bob: How about you?
    Bob-->John: Jolly good!

就可以生成以下的序列图

sequence

甘特图

使用以下的甘特图定义:

gantt
        dateFormat  YYYY-MM-DD
        title Adding GANTT diagram functionality to mermaid
        section A section
        Completed task            :done,    des1, 2014-01-06,2014-01-08
        Active task               :active,  des2, 2014-01-09, 3d
        Future task               :         des3, after des2, 5d
        Future task2               :         des4, after des3, 5d
        section Critical tasks
        Completed task in the critical line :crit, done, 2014-01-06,24h
        Implement parser and jison          :crit, done, after des1, 2d
        Create tests for parser             :crit, active, 3d
        Future task in critical line        :crit, 5d
        Create tests for renderer           :2d
        Add to mermaid                      :1d

就可以生成如下的甘特图。

gantt

如果你还是感觉压力,可以尝试使用在线编辑器去感受下

Markdown 集成

Mermaid 很方便和 Markdown 集成使用,甚至类似的代码段以后的嵌入即可以使用:

graph TD;
    A-->B;
    A-->C;
    B-->D;
    C-->D;

Markdown 加载了 Mermaid 插件以后,解析器会自动将代码块中的代码渲染为 SVG 格式的结构图。

gitbook 集成

如果您习惯使用 gitbook 编写技术文档,那么 Mermaid 会让你事半功倍。只需要在 package.json 中加入对应的 devDependencies 就可以直接使用。

  "devDependencies": {
    "gitbook-cli": "^2.3.2",
    "mermaid": "^8.0.0-rc.6",
    "..."
  }

使用 CSS 自定义样式

Mermaid 使用 JavaScript 编写并使用 SVG 输出,因此天生自带了 Web 相关的属性。可以很方便的使用 CSS 定义对应的样式类(Style CSS)来自定义样式。

架构简单便于扩展

Mermaid 只是个结构图的生成引擎,如果你想让它生成其他的结构图,那么可以花点时间来扩展它。简单的说,扩展 Mermaid 生成结构图只需要两个因素:语法定义、以及图形绘制逻辑。

在这里有个比较简单的介绍如何扩展 Mermaid,当然你也可以参考现成的结构图源代码来对比输出。

新项目

Mermaid 是个比较新的工具,功能和文档都在不断的完善中。我发起了个 Mermaid 中文文档翻译计划,目前基本的文档都已经翻译完成,可以在这里阅读。

如果您有任何的意见或者建议,也欢迎您不吝提出。

- eof -

使用 Nginx 反向代理阿里云 OSS

上次将 Blog 的主机重新整理下了以后,这次抽空将站点的静态资源也整理了下。

虽然使用又拍云有一段时间了,也非常的稳定,但毕竟要「居安思危」,又拍云同时又提供了「融合云」的第三方存储的同步方案,于是就选择了阿里云的 OSS 作为存储的备份方案。

Aliyun OSS

上图是我目前的方案,客户端这边通过脚本或者其他工具上传到又拍云上,然后又拍云自动将增量数据同步到阿里云的 OSS,最后我再使用 Nginx 反向代理去访问两块同步的资源。

为什么使用反向代理,是因为由于「众所周知」的原因,在国内绑定 HTTP(S) 服务的域名是需要备案的,我觉得麻烦同时也不想浪费个域名做这样的事情。

同时使用反向代理的方式,后面自己还可以控制缓存、负载均衡等,可操作性更大一些。长话短说,在配置阿里云 OSS 的反向代理的时候碰到了些坑,在这里记录下。

首先,在阿里云 OSS 后台务必设置好权限,在 Bucket 中设置读写权限为「公共读」,以及在 Refer 设置为自己实际的情况(很重要)。

下面是目前的针对阿里云 OSS 的 Nginx 反向代理的具体配置,供参考:

location /<base_url>/ {
    proxy_pass http://<bucket>.oss-cn-shanghai.aliyuncs.com/;

    proxy_redirect off;
    proxy_set_header Host '<bucket>.oss-cn-shanghai.aliyuncs.com';
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header User-Agent $http_user_agent;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    proxy_cache_valid 200 302 3600m;

    proxy_buffer_size 256k;
    proxy_buffers 4 256k;
    proxy_read_timeout 600s;
    proxy_send_timeout 300s;
    proxy_temp_file_write_size 256k;

    expires max;
    etag off;
}

proxy_set_headerHost 必须要设定自己对应的主机名,例如我的阿里云 OSS 对应的主机在华东2(也就是上海机房)阿里云 OSS 是根据 Host 确定 Bucket 的名称的。顺便提一句,如果 Nginx 主机也是和 OSS 同个机房,那么可以使用内部 VPC 内网节省流量。

OSS Endpoint

由于反向代理的是静态资源,所以我考虑尽可能的多缓存时间以加快速度和减少请求流量,所以设置了 proxy_cache_valid 200 302 3600m 以及 expires max 强制缓存。

然后 reload Nginx 以后就可以看到效果了。由于目前 又拍云 和 阿里云 OSS 的内容是同步的,所以后面使用 Nginx 做负载均衡也是比较方便的事情,这里就不重复贴配置了。

顺便在这里吐槽下,个人不是很推荐买阿里云的香港服务器!因为又是那个「众所周知」的原因,阿里云的香港节点 HTTPS 流量是会被部分 ISP 给污染的

这块个人也是困扰了很久,然而不打算处理了,毕竟现在写 Blog 的人不多看 Blog 的人更少,被墙了反而能够更加自在些。

- EOF -

随谈 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

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

Grovvy 语言有一个 ?. 的操作符,可以安全地处理潜在可能的空引用(据说 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 --

我的照片

嗨!我叫「明城」,八零后、孩她爸、码农、真谷粉、伪果粉、微软无脑黑、宁波佬,现居杭州。 除了这里,同时也欢迎您关注我的 GitHub (2) 、 TwitterInstagram 等,谢谢。

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

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

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

文章

项目

微信公众号