本文将从创建一个 SpringBoot 的应用开始,详细讲述如何将一个 SpringBoot 的应用部署到本地的 Kubernetes 集群上面去。

创建 SpringBoot 应用

首先,我们需要创建一个 SpringBoot 的应用,可以到 http://start.spring.io/ 创建一个,在创建的时候,我们需要选择一个 Web 的依赖,以方便部署到 Kubernetes 之后可以看到效果。

创建完成之后,可以修改一下 SpringBoot 应用的 main 函数所在的类,让它成为一个 Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
@Controller
public class WebDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebDemoApplication.class, args);
    }

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "Hello, Kubernetes!";
    }
}

这样,在本地启动这个 SpringBoot 的应用之后,如果我们访问 http://localhost:8080/hello 的话,就可以看到 Hello, Kubernetes! 这句话。

创建一个 Dockerfile

为了能够将这个 SpringBoot 的应用部署到 Kubernetes 里面去,我们需要创建一个 Dockerfile,将它打成一个 Docker 镜像:

1
2
3
4
FROM openjdk:8-jdk-alpine
ARG JAR_FILE
ADD ${JAR_FILE} app.jar
ENTRYPOINT [ "java", "-jar", "/app.jar"]

上面的 Dockerfile 是一个非常简单的 Dockerfile,只是将 SpringBoot 应用打包后的 uber-jar 拷贝到容器里面去,然后运行这个 jar 包。有了这个 Dockerfile 之后,我们就可以在本地把 Docker 镜像打包出来了:

1
docker build --build-arg JAR_FILE=./target/web-demo-0.0.1-SNAPSHOT.jar . -t springboot-demo

然后需要注意的是,这样打出来的镜像是在本地的,没有办法被 minikube 找到,所以,要么将这个镜像放到一个中央的镜像仓库上,要么我们使用 minikube 的 docker daemon 来打镜像,这样 minikube 就可以找到这个镜像。

所以,你首先需要在本地将 minikube 安装上去,具体可以看官方的安装教程。安装完成之后,先运行:

1
minikube start

来将 minikube 启动起来,然后可以运行

1
eval $(minikube docker-env)

将 docker daemon 切换成 minikube 的。最后,我们再用上面的 docker build 来进行打包,minikube 就可以看到了。

将应用部署到 minikube 中去

Docker 镜像都准备好了,现在我们可以将应用部署到 minikube 中去了,首先我们需要创建一个 deployment 对象,这个可以用 yml 文件来描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: springboot-demo-deployment
  labels:
    app: springboot-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: springboot-demo
  template:
    metadata:
      labels:
        app: springboot-demo
    spec:
      containers:
        - name: springboot-demo
          image: springboot-demo
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080

上面的 yaml 文件没有什么特别的地方,除了 imagePullPolicy 需要指定成 IfNotPresent,这样 minikube 才会从本地去找镜像。

有了上面的 yaml 文件之后,我们就可以运行 kubectl apply -f springboot-demo.yml 来让 minikube 将我们的 SpringBoot 的应用的集群给创建出来。

访问 minikube 中的 SpringBoot 集群

现在我们已经将 SpringBoot 应用部署到了 minikube 中去,那么怎么访问这个集群呢,首先我们需要将端口暴露出来:

1
kubectl expose deployment springboot-demo-deployment --type=NodePort

然后运行:

1
minikube service springboot-demo-deployment --url

得到访问的 URL。再在得到的 URL 后面加上 /hello,就可以看到 Hello, Kubernetes! 了。

或者,我们可以直接运行 curl $(minikube service springboot-demo-deployment --url)/hello 来访问。

以上就是如何将一个 SpringBoot 的应用部署到 Kubernetes 里面去的全过程。

Oracle 昨天公布了 Java 10 的 GA 版本,Java 10 里面除了本地变量类型推断之外,还扩展了原来的 CDS 的能力为 AppCDS

什么是 CDS

CDS 的全称是 Class-Data Sharing,CDS 的作用是可以让类可以被预处理放到一个归档文件中,后续 Java 程序启动的时候可以直接带上这个归档文件,这样 JVM 可以直接将这个归档文件映射到内存中,以节约应用启动的时间。

这个特性其实 JDK 1.5 就开始引入了,但是 CDS 只能作用与 Boot Class Loader 加载的类,不能作用于 App Class Loader 或者自定义的 Class Loader 加载的类,其实有点鸡肋,而且这个是 Oracle JDK 的商业特性,在 OpenJDK 中似乎没有。

这次在 Java 10 中,则将 CDS 扩展为 AppCDS,顾名思义,AppCDS 不止能够作用于 Boot Class Loader,App Class Loader 和自定义的 Class Loader 也都能够起作用,大大加大了 CDS 的适用范围。有了 AppCDS,可以给 Java 的应用程序带来两个方面的好处:

  • 可以提升一些大型的 Java 应用的启动速度。
  • 可以提升 Serverless 的应用程序的启动速度。我觉得这个点可能是 Java 10 提供 AppCDS 的主要原因,Serverless 极可能成为未来的应用的一种非常常见的形态,而把 Java 应用在 Serverless 上,相比于其他的语言来说,一个很大的劣势就是 JVM 的启动速度太慢了,虽然像 AWS 的 Lambda,会给 Java 的 Serverless 应用加上 -client 来用 Client 模式跑加快启动速度,但是实际上效果甚微。有了 AppCDS,可以大大加快 Serverless 应用的启动速度,按照 AppCDS 的 JEP 的说明,对于一个 JEdit 来说,AppCDS 可以为 JEdit 提升 20% 到 30% 的启动速度。

尝试 AppCDS

作者写了一个简单的 Java 应用,来测试 AppCDS 的效果。程序的代码已经放到了 Github 上面,大家可以直接去看,这里只给出大概的操作步骤和最后的测试效果。

1. 决定要 Dump 哪些 Class

一般来说,一个 Java 应用程序会包含很多的 Class 文件,但是在运行中,并不是所有的 Class 文件都会被用到,所以,第一步我们需要来决定具体需要使用哪些 Class 文件,你需要给你的运行命令上加上如下的 JVM 参数:

1
-Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=hello.lst

几个参数的意思分别是: * -Xshare:off:这个参数的意思是不做任何内存的共享,也就是不利用 AppCDS 产生的文件来做内存映射。因为是要决定 Dump 哪些类的内存到归档文件中,所以这个参数需要关掉。 * -XX:+UserAppCDS:默认的情况下 AppCDS 不会开启,所以我们需要加上这个参数来开启 AppCDS。 * -XX:DumpLoadedClassList:表示需要把需要做 Dump 的类名写入到哪个文件中。

这个命令执行之后,会出现一个 hello.lst 的文件,里面就是一个个的类名,下面是一部分内容的截图:

2. 将类的内存 Dump 到归档文件中

有一个需要 Dump 的类的列表之后,第二步,我们就可以将类的内存 Dump 到归档文件中了,在这一步中,我们需要将以下的参数加入到 JVM 参数中去:

1
-Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=hello.lst -XX:SharedArchiveFile=hello.jsa

几个参数的含义分别如下:

  • -Xshare:dump:表示现在要进行类的内存的 Dump。
  • -XX:SharedClassListFile:用来指定需要 Dump 的类的列表。
  • -XX:SharedArchiveFile:表示需要将类的内存 Dump 到哪个归档文件中。

运行上面的命令之后,我们会得到一个 hello.jsa 的文件,包含需要的内存信息的 Dump。

3. 使用 Dump 出来的归档文件加快应用启动速度

有了前面的归档文件之后,我们就可以来加速应用的启动速度了,为了使用上述的归档文件,我们需要在 JVM 中加上如下的参数:

1
-Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa

几个参数的含义分别如下:

  • -Xshare:on:表示打开内存映射。
  • -XX:SharedArchiveFile=hello.jsa:表示用来做内存映射的归档文件是 hello.jsa

4. 测试效果

在我的本机上,不使用 AppCDS 和使用 AppCDS 的效果如下:

不使用 AppCDS

使用 AppCDS

可以看到,对于这样一个简单的应用,AppCDS 还是有 20% 左右的启动速度提升的,当然这个应用的很多的启动时间都花在了类加载上,其他的耗时不多,所以效果挺好。如果其他的应用程序的启动时间花在类上的加载时间比较少的话,可能效果就没有这么明显。

看起来 AppCDS 很美好,但是目前我使用下来有几个坑:

  • 虽然 AppCDS 号称可以支持自定义的 ClassLoader,但是我试了一个 SpringBoot 的应用,发现对于没有在 -classpath 中指定的 JAR 包中的类,并不会有效果。
  • 如果你下载 Oracle 的 JDK,需要加上 -XX:+UnlockCommercialFeature 来开启 AppCDS,但是 OpenJDK 却不用,也是很奇怪,😅

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

就 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。