Pinpoint 插件开发

一、运行 Pinpoint

运行 Pinpoint 系统最简单的方法是使用 Docker。

二、编译环境搭建

编译 Pinpoint 1.5.2 的源代码需要 JDK 6、JDK 7+ 以及 Maven 3.2.x+ 的支持,符合以上要求的最新版本的编译工具列表如下:

要求 最新版本 备注
JDK 6 JDK 6u45 已经停止更新
JDK 7+ JDK 8u112
Maven 3.2.x+ Maven 3.2.9 之所以使用 Maven 3.2.x,猜想是因为只有 Maven 3.2.x 才支持 JDK 6

并且还要设置两个环境变量:JAVA_6_HOME 和 JAVA_7_HOME 分别指向对应的 JDK 安装目录。

然后运行以下命令完成编译:

使用 Docker 来编译会更加容易,免去了配置环境的必要。首先将 Pinpoint 的源代码下载到本地目录,例如 /projects/pinpoint,然后运行命令:

其中的两个 -v 参数是用来映射目录的。第一个 -v 将本地 /projects/pinpoint 目录映射到容器的 /pinpoint 目录;而第二个 -v 是将本地的 Maven Repository 映射到容器的 /root/.m2 下,否则每次编译 Maven 都会从网络上下载大量的依赖,非常缓慢,这样在本地共享一下,以后再编译就快了。

三、技术概述

3.1、架构组成

Pinpoint 架构

Pinpoint 主要由 3 个组件外加 Hbase 数据库组成,三个组件分别为:Agent、Collector 和 Web UI。

3.2、系统特色

  1. 分布式交易追踪,追踪分布式系统中穿梭的消息
  2. 自动侦测应用程序拓扑,以帮助指明应用程序的配置
  3. 横向扩展以支持大规模的服务器组
  4. 提供代码级别的可见性,以方便识别故障点和瓶颈
  5. 使用字节码注入技术,无需修改代码就可以添加新功能

3.3、分布式追踪系统如何工作

无论 Pinpoint 还是 Zipkin,其实现都是基于 Google Dapper 的论文。其核心思想就是在服务各节点彼此调用的时候,记录并传递一个应用级别的标记,这个标记可以用来关联各个服务节点之间的关系。比如两个节点之间使用 HTTP 作为请求协议的话,那么这些标记就会被加入到 HTTP 头中。因此如何传递这些标记是与节点之间使用的通讯协议有关的,有些协议就很容易加入这样的内容,但有些协议就相对困难甚至不可能,因此这一点就直接决定了实现分布式追踪系统的难度。

3.4、Pinpoint 的数据结构

Pinpoint 消息的数据结构主要包含三种类型 Span,Trace 和 TraceId。

Span 是最基本的调用追踪单元,当远程调用到达的时候,Span 指代处理该调用的作业,并且携带追踪数据。为了实现代码级别的可见性,Span 下面还包含一层 SpanEvent 的数据结构。每个 Span 都包含一个 SpanId。

Trace 是一组相互关联的 Span 集合,同一个 Trace 下的 Span 共享一个 TransactionId,而且会按照 SpanId 和 ParentSpanId 排列成一棵有层级关系的树形结构。

TraceId 是 TransactionId、SpanId 和 ParentSpanId 的组合。TransactionId(TxId)是一个交易下的横跨整个分布式系统收发消息的 ID,其必须在整个服务器组中是全局唯一的。也就是说 TransactionId 识别了整个调用链;SpanId(SpanId)是处理远程调用作业的 ID,当一个调用到达一个节点的时候随即产生;ParentSpanId(pSpanId)顾名思义,就是产生当前 Span 的调用方 Span 的 ID。如果一个节点是交易的最初发起方,其 ParentSpanId 是 -1,以标志其是整个交易的根 Span。下图能够比较直观的说明这些 ID 结构之间的关系。

Pinpoint ID 结构之间的关系

3.5、如何使用 Java Agent

Pinpoint 的优势就在于其使用 Java Agent 的方式向节点应用注入字节码,而不是直接修改源代码。因此部署一个节点就变得非常容易,只需要在程序启动的时候加入如下一些启动参数:

3.6、代码注入是如何工作的

Pinpoint 字节码注入

Pinpoint 对代码注入的封装非常类似 AOP,当一个类被加载的时候会通过 Interceptor 向指定的方法前后注入 before 和 after 逻辑,在这些逻辑中可以获取系统运行的状态,并通过 TraceContext 创建 Trace 消息,并发送给 Pinpoint 服务器。但与 AOP 不同的是,Pinpoint 在封装的时候考虑到了更多与目标代码的交互能力,因此用 Pinpoint 提供的 API 来编写代码会比 AOP 更加容易和专业。(这些内容后面会有更详细说明)

3.7、Pinpoint 的应用实例

下图展现了两个 Tomcat 服务器应用了 Pinpoint 之后,被收集到的追踪数据。

Pinpoint 数据收集

四、Agent 插件开发

开发 Pinpoint Agent 只需要关注两个接口:TraceMetadataProvider 和 ProfilerPlugin,实现类通过 Java 的服务发现机制进行加载。

4.1、ServiceLoader 配置

Pinpoint 的插件是以 jar 包的形式部署的,为了使得 Pinpoint Agent 能够定位到 TraceMetadataProvider 和 ProfilerPlugin 两个接口的实现,需要在 META-INF/services 目录下创建两个文件:

META-INF/services/com.navercorp.pinpoint.common.trace.TraceMetadataProvider
META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin

这两个文件中的每一行都写明对应实现类的全名称即可。

4.2、TraceMetadataProvider

TraceMetadataProvider 提供了对 ServiceType 和 AnnotationKey 的管理。

4.2.1、ServiceType

每个 Span 和 SpanEvent 都包含一个 ServiceType,用来标明他们属于哪一个库(Jetty、MySQL JDBC Client 或者 Apache HTTP Client 等),以及追踪此类型服务的 Span 和 SpanEvent 该如何被处理。ServiceType 的数据结构如下:

属性 描述
name ServiceType 的名称,必须唯一
code ServiceType 的编码,短整形,必须唯一
desc 描述
properties 附加属性

Pinpoint 为了尽量压缩 Agent 到 Collector 的数据包的大小,ServiceType 被设计成不是以序列化字符串的形式发送的,而是以整形数字发送的(code 字段),这就需要建立一个映射关系,将 code 转换成对应的 ServiceType 实例,这些映射机制就是由 TraceMetadataProvider 实现的。

ServiceType 的 code 必须全局唯一,为了避免冲突,Pinpoint 官方对这个映射表进行了严格的管理,如果所开发的插件想要声明新的映射关系,需要通知 Pinpoint 团队,以便对此映射表进行更新和发布。与私有 IP 地址段一样,Pinpoint 团队也保留了一段私有区域可供开发内部服务的时候使用。具体的 ID 范围参照表如下:

ServiceType Code 全部范围

类型 范围
Internal Use 0 ~ 999
Server 1000 ~ 1999
DB Client 2000 ~ 2999
Cache Client 8000 ~ 8999
RPC Client 9000 ~ 9999
Others 5000 ~ 7999

ServiceType Code 私有区域范围

类型 范围
Server 1900 ~ 1999
DB Client 2900 ~ 2999
Cache Client 8900 ~ 8999
RPC Client 9900 ~ 9999
Others 7500 ~ 7999

4.2.2、AnnotationKey

Annotation 是包含在 Span 和 SpanEvent 中的更详尽的数据,以键值对的形式存在,键就是 AnnotationKey,值可以是字符串或字节数组。Pinpoint 内置了很多的 AnnotationKey,如果不够用的话也可以通过 TraceMetadataProvider 来自定义。AnnotationKey 的数据结构如下:

属性 描述
name AnnotationKey 的名称
code AnnotationKey 的编码,整形,必须唯一
properties 附加属性

同 ServiceType 的 code 字段一样,AnnotationKey 的 code 也是全局唯一 的,Pinpoint 团队给出的私有区域范围是 900 到 999。

4.2.3、TraceMetadataProvider 接口

TraceMetadataProvider 接口只有一个 setup 方法,此方法接收一个 TraceMetadataSetupContext 类型的参数,该类型有三个方法:

方法 描述
addServiceType(ServiceType) 注册 ServiceType
addServiceType(ServiceType, AnnotationKeyMatcher) 注册 ServiceType,并将匹配 AnnotationKeyMatcher 的 AnnotationKey 作为此 ServiceType 的典型注解,这些典型注解会显示在瀑布视图的 Argument 列中
addAnnotationKey(AnnotationKey) 注册 AnnotationKey,这里注册的 AnnotationKey 会被标记为 VIEW_IN_RECORD_SET,显示在瀑布视图中是以单独一行显示的,且前面有一个蓝色的 i 图标

详细使用方法可以参考官方提供的样例文件 SampleTraceMetadataProvider

4.3、ProfilerPlugin

ProfilerPlugin 通过字节码注入的方式拦截目标代码以实现跟踪数据的收集。

4.3.1、插件的工作原理

  1. Pinpoint Agent 随 JVM 一起启动
  2. Agent 加载所有 plugin 目录下的插件
  3. Agent 调用每个已经加载的插件的 ProfilerPlugin.setup(ProfilerPluginSetupContext) 方法
  4. 在 setup 方法中,插件定义那些需要被转换的类,并注册 TransformerCallback
  5. 目标应用启动
  6. 当类被加载的时候,Pinpoint Agent 会寻找注册到该类的 TransformerCallback
  7. 如果 TransformerCallback 被注册,Agent 就调用它的 doInTransform 方法
  8. TransformerCallback 修改目标类的字节码(例如添加拦截器、添加字段等)
  9. 修改后的代码返回到 JVM,类型加载的时候就使用修改后的字节码
  10. 应用程序继续
  11. 当调用到被修改的方法的时候,注入的拦截器的 before 和 after 方法被调用
  12. 拦截器记录追踪数据

Pinpoint 插件的工作原理看似跟 AOP 非常相似,但还是有一些区别和自身的特色:

  1. 因为 Pinpoint 需要处理的注入场景比较单一,因此他提供的注入 API 相对简单;而 AOP 为了要处理各种可能的切面情况,Pointcut 被设计得非常复杂。
  2. Pinpoint 插件拦截是通过拦截器的 before 和 after 方法实现的,很像 around 切面,如果不想执行其中一个方法,可以通过 @IgnoreMethod 注解来忽略。
  3. Pinpoint 的拦截器可以任意拦截方法,因此被拦截的方法之间可能会有调用关系,这会导致追踪数据被重复收集,因此 Pinpoint 提供了 Scope 和 ExecutionPolicy 功能。在一个 Scope 内,可以定义拦截器的执行策略:是每次都执行(ExecutionPolicy.ALWAYS),还是在没有更外层的拦截器存在的时候执行(ExecutionPolicy.BOUNDARY),或者必须在有外层拦截器存在的时候执行(ExecutionPolicy.INTERNAL)。具体请参考这个样例
  4. 在一个 Scope 内的拦截器彼此还可以传递数据。同一个 Scope 内的拦截器共享一个 InterceptorScopeInvocation 对象,可以使用他来交换数据。参考样例
  5. 除了拦截方法以外,Pinpoint 还可以向目标类中注入字段以及 getter 和 setter 方法,可以使用它们来保存一些上下文的数据。

通过上述内容可以了解,如果要编写一个 Pinpoint 的插件,除了要对目标代码的调用逻辑有较深入的理解,还必须得设计好上下文数据如何存储、如何传递,以及如何通过 Scope 避免信息被重复收集等问题。这些问题在 AOP 的场景下也会存在,只是 Pinpoint 提供了更加一致和便捷的解决方案,而 AOP 的就要自己去考虑这些问题了。

4.3.2、方法拦截

如前文所述,Pinpoint 插件需要实现 ProfilerPlugin 接口,该接口只有一个 setup(ProfilerPluginSetupContext) 方法。为了更容易的操作 Pinpoint 的代码注入 API,还需要实现一个 TransformTemplateAware 的接口,该接口会注入 TransformTemplate 类。

ProfilerPluginSetupContext 有两个方法:getConfig() 和 addApplicationTypeDetector(ApplicationTypeDetector…)。第一个方法用来获取 ProfilerConfig 对象,该对象保存了所有插件的配置信息,而第二个方法用来添加 ApplicationTypeDetector。ApplicationTypeDetector 是用来自动检测节点所运行服务的类型的。例如在 pinpoint-tomcat-plugin 项目中,有 TomcatDetector 类,这个类的作用是通过如下检测来确定当前服务为 Tomcat 的:

  1. 检查 main class 是不是 org.apache.catalina.startup.Bootstrap
  2. 检查是否有系统变量 catalina.home
  3. 检查是否存在某个指定的类,这里也是 org.apache.catalina.startup.Bootstrap

如果这三个条件都满足,就把当前节点的 ServiceType 设置为 Tomcat。

TransformTemplate 只有一个方法 transform(String, TransformCallback),第一个参数是需要被转换的类的全名称,而第二个参数就是 4.3.1 章节中提到的 TransformCallback 接口,这个接口也只有一个方法叫 doInTransform,所有的注入逻辑都在这里完成。

  1. 注入过程是从获取 InstrumentClass 类开始的。
  2. 如果想要拦截一个方法,或者是添加字段以及 getter、setter 方法,就可以调用 InstrumentClass 对应的 API 来实现,这里是获取了一个签名为 targetMethod(String) 的方法,返回的对象是 InstrumentMethod 类型。
  3. 调用 InstrumentMethod 的 addInterceptor 方法注入拦截器,所有跟踪信息的收集行为都是在拦截器中实现的,这里添加的拦截器是 com.navercorp.pinpoint.bootstrap.interceptor.BasicMethodInterceptor,这是 Pinpoint 框架默认提供的一个现成的拦截器,里面收集了一些 targetMethod 的调用信息。后面的 va 是一个静态方法,即可变参数列表,va 中给出的参数会传递到 BasicMethodInterceptor 的构造方法中。
  4. 调用 InstrumentClass.toBytecode() 方法即可返回注入后的字节码,剩下的事情就是 Pinpoint Agent 自己来完成的了。

BasicMethodInterceptor 类仅提供了对方法调用信息的简单收集,只收集方法的名称、参数、返回值以及是否产生异常等等。在某些复杂的场景下,我们会需要收集更多的信息,如当前登录用户、线程池、数据库查询语句以及任何跟中间件功能有关的信息,这就需要我们定义自己的 Interceptor 类。

以上内容请参考该样例

Interceptor 是一个标记接口,真正有意义的是 AroundInterceptor 接口,该接口定义了如下两个方法:

为了应对被拦截方法的不同个数的参数列表,AroundInterceptor 还有若干子接口:AroundInterceptor0, AroundInterceptor1,…,AroundInterceptor5,分别对应没有参数,一个参数,到 5 个参数的方法。实现 Interceptor 接口的时候要提供一个如下的构造方法:

TraceContext 和 MethodDescriptor 会被 Pinpoint Agent 运行时注入进来,当然也可以添加额外的参数,这些额外的参数,需要在 addInterceptor 的时候指定,就像上文中关于 va 的描述那样。

有了 TraceContext 对象,就可以开始收集信息了。调用 traceContext.getCurrentTraceObject() 方法可以获取当前的 Trace,再调用 trace.traceBlockBegin() 就开始记录一个新的 Trace 块了(这里我理解应该就是 Span 了)。在 traceBlockBegin 以后,可以 调用 currentSpanEventRecorder 获取 SpanEventRecorder 对象,这个对象提供了诸如 recordServiceType、recordApi、recordException 和 recordAttribute 等方法,可以记录方法的有关信息。但是 SpanEventRecorder 并没有提供 recordReturnValue 这样的方法,只能通过 recordAttribute 来记录。所有自己扩展的信息也是通过 recordAttribute 来记录的。最后所有信息记录完成就调用 traceBlockEnd() 方法关闭区块。

以上内容请参考该样例

五、总结

其实 Pinpint 的插件开发 API 还提供了非常丰富的能力,如拦截异步方法、调用链跟踪、拦截器之间共享数据等等,但原理都是基于上述这些内容,只是调用了更复杂的 API 而已。具体代码可以参考官方提供的样例项目,里面有非常详尽的代码及注释,相信理解了上面的内容,再看这个代码就不会有任何困难了。

使用 Zipkin 和 Brave 实现分布式系统追踪(基础篇)

Zipkin

一、Zipkin

1.1、简介

Zipkin 是一款开源的分布式实时数据追踪系统(Distributed Tracking System),基于 Google Dapper 的论文设计而来,由 Twitter 公司开发贡献。其主要功能是聚集来自各个异构系统的实时监控数据,用来追踪微服务架构下的系统延时问题。

应用系统需要进行装备(instrument)以向 Zipkin 报告数据。Zipkin 的用户界面可以呈现一幅关联图表,以显示有多少被追踪的请求通过了每一层应用。

瀑布图

Zipkin 以 Trace 结构表示对一次请求的追踪,又把每个 Trace 拆分为若干个有依赖关系的 Span。在微服务架构中,一次用户请求可能会由后台若干个服务负责处理,那么每个处理请求的服务就可以理解为一个 Span(可以包括 API 服务,缓存服务,数据库服务以及报表服务等)。当然这个服务也可能继续请求其他的服务,因此 Span 是一个树形结构,以体现服务之间的调用关系。

Zipkin 的用户界面除了可以查看 Span 的依赖关系之外,还以瀑布图的形式显示了每个 Span 的耗时情况,可以一目了然的看到各个服务的性能状况。打开每个 Span,还有更详细的数据以键值对的形式呈现,而且这些数据可以在装备应用的时候自行添加。

Span 详细信息

从图中可以看出如下的调用关系:整个调用链中有两个微服务 service1 和 service2,在 10ms(相对时间点)的时候,service1 作为客户端向 service2 发送了一个请求(Client Send),之后 service2 服务于 19ms 的时候收到请求(Server Receive),并用了 12ms 的时间来处理,并于 31ms 时刻将数据返回(Server Send),最后 service1 服务于 1ms 以后接收到此数据(Client Receive),因此整个过程共耗时 22ms。图中还给出了 service1 访问 service2 服务前后 Http Client 连接池的状态信息。

1.2、架构

Zipkin 架构

如图所示,Zipkin 主要由四部分构成:收集器、数据存储、查询以及 Web 界面。Zipkin 的收集器负责将各系统报告过来的追踪数据进行接收;而数据存储默认使用 Cassandra,也可以替换为 MySQL;查询服务用来向其他服务提供数据查询的能力,而 Web 服务是官方默认提供的一个图形用户界面。

而各个异构的系统服务向 Zipkin 报告数据的架构如下图。

服务向 Zipkin 报告数据

1.3、运行

使用 Docker 运行 Zipkin 最为简单,其过程如下:

这样启动,默认会使用 Cassandra 数据库,如果想改用 MySQL,可以换做以下命令启动:

启动成功以后,可以通过 http://:8080 来访问。具体获取 IP 地址的方法请参阅 Docker 的相关文档。

二、Brave

2.1、简介

Brave 是用来装备 Java 程序的类库,提供了面向 Standard Servlet、Spring MVC、Http Client、JAX RS、Jersey、Resteasy 和 MySQL 等接口的装备能力,可以通过编写简单的配置和代码,让基于这些框架构建的应用可以向 Zipkin 报告数据。同时 Brave 也提供了非常简单且标准化的接口,在以上封装无法满足要求的时候可以方便扩展与定制。

2.2、初始化

Brave 的初始化就是要构建 Brave 类的实例,该库提供了 Builder 类用来完成这件事情。

注:下文中约定,大写的 Brave 指该 Java 类库,而 Brave 类指 com.github.kristofa.brave.Brave 类型,而小写的 brave 指该类型的实例。

其中的 serviceName 是当前服务的名称,这个名称会出现在所有跟该服务有关的 Span 中。默认情况下,Brave 不会将收集到的监控数据发送给 Zipkin 服务器,而是会以日志的形式打印到控制台。如果需要将数据发送给服务器,就需要引入 HttpSpanCollector 类。当前版本(3.8.0)将这个类命名为 Collector,这个概念容易跟 Zipkin 自身的 Collector 相混淆,因此在 Issue #173 中官方建议将其更名为 Reporter,也就是说这个类是用来向 Zipkin 的 Collector 报告数据的。

Zipkin Reporter

使用 HttpSpanCollector 的方法如下:

使用 HttpSpanCollector.create 方法可以创建该类的一个对象,第一个参数就是 Zipkin 服务的地址(默认部署时的端口为 9411)。

如果使用 Spring 的话,为了方便扩展,建议添加一个名为 ZipkinBraveFactoryBean 的类,其内容大致如下:

然后只需要在 application-context.xml 配置文件中使用该 FactoryBean 就可以了:

2.3、装备标准的 Servlet 应用

Brave 提供了 brave-web-servlet-filter 模块,可以为标准的 Servlet 应用添加向 Zipkin 服务器报告数据的能力,需要做的就是在 web.xml 文件增加一个 BraveServletFilter。

不过这个 Filter 在初始化的时候需要传入几个参数,这些参数可以通过 brave 对象的对应方法获得,但是注入这些构造参数,最简单的办法还是使用 Spring 提供的 DelegatingFilterProxy。

在 web.xml 中添加如下内容(最好配置为第一个 Filter,以便从请求最开始就记录数据):

然后在配置文件中添加以下内容(创建 brave Bean 的有关代码请参考上文):

最后一个类 com.github.kristofa.brave.http.DefaultSpanNameProvider 存在于 brave-http 模块中。当使用 Maven 或 Gradle 来管理项目的话,brave-http 会随着 brave-web-servlet-filter 的引入被自动关联进来。

一切无误的话就可以启动服务。如果给定了 zipkinHost 参数,数据就会被发送到指定的 Zipkin 服务器上,然后可以在其 Web 界面上看到相关内容;否则会有类似如下的信息打印到系统控制台(做了格式美化):

2.3、装备 Spring MVC 应用

Brave 自带了 brave-spring-web-servlet-interceptor 模块,因此装备 Spring MVC 项目变得非常容易,只需要在配置文件中添加一些 interceptor 就好了:

2.4、装备 MySQL 服务

brave-mysql 模块在 JDBC 驱动层面添加了一些拦截器,可以对 MySQL 的查询进行监控。在使用之前也需要通过 Spring 进行一下配置。

该配置的目的是要给 MySQLStatementInterceptorManagementBean 类注入一个 ClientTracer 实例,这个实例会在后来的 MySQL JDBC 驱动的拦截器中被使用。初始化完成以后只需要在连接字符串中添加如下参数就可以了:

其中的 zipkinServiceName 用来指定该 MySQL 服务的名称,如果省略的话,会默认以 mysql-${databaseName} 的形式来呈现。

这里需要特别说明一点,因为 MySQL 服务是跟 Java 服务分离的,因此上文初始化 brave 对象时提供的服务名称,并不适用于 MySQL 服务,所以才需要在这里另外指定。

MySQL 服务的监控数据

可以看出,添加了 statement interceptor 之后,可以看到 service2 请求 MySQL 查询的起止时间,以及执行的 SQL 语句等信息。

三、总结

本文主要介绍了 Zipkin 服务和其 Java 库 Brave 的一些基本概念及原理,并且针对 Brave 开箱提供的一些装备组件进行了详细的使用说明。在后面进阶篇的文章中,会对如何扩展 Brave 以实现自定义监控信息的内容进行介绍,敬请期待!

企业级 IM 的差异化发展之路

钉钉与微信

钉钉 X 微信

2016 年 4 月 18 日,微信团队发布了继微信和微信支付后的第三个重量级产品企业微信,正式吹响了与阿里钉钉交锋的号角,并且还扬言:要像干掉来往一样的干掉钉钉。一时间,企业级 IM 这个既陌生又熟悉的产品,成为了互联网界热议的话题。

那么企业级 IM 到底是什么?它又跟移动办公产品有什么区别?当钉钉和企业微信展开厮杀的时候,企业级 IM 的差异化发展之路到底在何方?

既然是叫“企业级 IM”,那么不难看出其产品特性的两个着眼点:企业应用和即时通讯。企业应用一方面是应用于企业内部,另一方面也在于能够整合企业内部的各种管理流程与制度。小了来说是各种轻量级应用,大了来说可以汇聚成管理信息系统。而即时通讯就更不言而喻了,曾经当 QQ 几乎成为国内即时通讯代名词的时候,有多少企业早已不由自主的经由 QQ 群完成了许多内部的沟通事宜,可见企业还是非常需要这样一类产品的。无奈的是,无论 QQ 还是微信,其社交成分要远远大于协同办公的成分,无法成为组织内部沟通协作的首选。

当来往完败给微信的时候,阿里仍不死心的想要切入即时通讯这个腾讯最核心的业务领域,于是就此诞生了钉钉。马云更是豪赌 10 亿,另辟蹊径的从企业内用户寻找突破口,让钉钉迅速站稳了脚跟。腹地被偷袭的腾讯自然不会善罢甘休,企业级 IM 市场顿时风生水起、剑拔弩张。企业级 IM 毕竟不是什么新鲜玩意,国外的 Slack 和 HipChat 早已是行业中的佼佼者,但显然钉钉与企业微信跟 Slack 和 HipChat 看上去并不怎么相似,不只是 Copy to China 这么简单。其两者都在通讯功能的基础上增加了类似签到、审批、任务管理和文件共享等企业内常用的功能,而且还能够开放给第三方合作伙伴进行二次开发,俨然一个企业管理流程互联网化的开放平台,让很多中小企业似乎找到了协同办公的感觉,从而忘记了这些东西原本在移动办公领域已经是必备的功能了。

然而企业内部的系统,必然不同于互联网化的系统。一个组织并不会因为“系出名门”、“免费”或“更加易用”等特性就放弃正在使用的古老系统;而且企业内部的管理流程也远非轻量级应用所能覆盖,尤其是在以流程驱动管理的企业内部,动辄十几步甚至几十步的流程都大有所在,而且流程信息量也绝非任务管理这等规模的应用所能比拟的;最后无论钉钉抑或企业微信,都还仅仅是将即时通讯和轻量级应用做了一个简单的叠加,流程数据还存在流程中,而碎片化的聊天信息依旧隐含着大量的珍贵知识,企业级 IM 是否应该走得更深入一些。

文章 An Inside Look at a Flat Organization That Serves Millions 的作者 Nate Lee 在介绍如何管理一个扁平化组织的时候,提出了一个命题: Assume that new hires won’t be able to talk to any of their co-workers for 48 hours. This is of course never the case, but if it was, they should have enough information at their fingertips to do their job. They should be able to read up on any problem or feature or question they have.(假设新来的员工在 48 小时内不能和其他同事讲话。在这种假定下,他们应该动动手指就能得到足够的信息去开展工作。如果在工作中遇到了问题,他们也应该能够通过阅读自己弄明白。)结合 Nate Lee 在文章中给出的解决方案,外加我个人的一些理解,我觉得企业级 IM 应该可以往如下一些方向发展。

一、流程与消息的整合

与其在企业级 IM 内部实现轻量级应用,不如让其与现有系统做更好的集成。在这一点上不能不提 HipChat,她与 Bitbucket、Jira 以及 Github 都有非常完美的集成,能够在上述系统发生变化的时候以即时消息的形式推送给用户,而且开放的 API 也使得越来越多的第三方系统能够与其整合,使其功能日益强大。如果真的要在企业级 IM 内部嵌入流程应用,那么就该想想如何实现融合。

想象一个场景,开发人员在写程序的时候通常都要关注变更管理系统,而当一个问题并没有被很好描述的时候,开发人员要么像回复论坛中的帖子一样提出问题,并等待答复(就像 Github 那样),要么就更直接拉几个相关的人进入一个讨论组开始聊天,直到把问题讨论清楚。这两种方式显然都不是最优的。第一种方式就好像在使用邮件,邮件虽然更正式、更严谨,但是受到排斥的根本原因就是时效性,不能够“即时通讯”,只能书写然后等待。但是无疑所有的交流痕迹都会被保存下来,当日后需要回顾的时候,查阅就变得异常方便了;第二种方式解决了时效性的问题,但最严重的缺陷就是有可能丢失所有针对该问题的讨论细节,仅仅会把一些结论性质的东西更新到问题的描述中,更有可能连这些结论性质的东西都会随着懒惰与监管的不到位逐渐流失。

或许想象中的系统应该是这样工作的。当开发人员需要针对某个问题展开讨论的时候,可以直接针对这个问题发起一个话题,工具应该有能力自动将相关人员拉入这个话题讨论组,并定位到当前的流程状态。所有接下来的讨论均会被这个话题记录下来,并关联到当前状态上。某些核心的讨论结果还可以直接更新回或添加到问题的描述信息,一次交流就解决了历史留存和沟通时效的问题,将来再回过头来查看的时候,每个状态的每个时间点上都有若干聊天记录呈现在旁边,辅佐查看问题的人了解达成最终结论的演变过程。

不仅如此,即时通讯除了可以解决沟通上的时效性以外,结合流程的时候还能够提高流程的工作效率。就拿最简单的报销流程来讲,倘若一个报销事宜经由申请人填报、主管经理审批、部门总监审批、财务人员审批最后到公司负责人审批这样的流程。若其中任何一个审批环节出现问题,都会导致流程被退回。虽然这本身正是流程存在的意义,但是显然被退回的流程一定程度上影响了流程的运作效率。如果填报人员或某一级审批人员在处理信息的过程中发现有疑问,那么针对当前环节上的流程进行一番讨论,消除分歧,可以促使流程走到终点的可能性大大提高。

二、话题

其实上面的能力都离不开一个功能就是话题,可以有针对流程的话题,也可以有自由创建的话题。话题无非限定了群组里面讨论的内容,当一个话题讨论完成以后,就应该将其关闭,避免发生偏离。在需要的时候还可以将话题之间进行关联或者是让话题与流程、文档抑或某个用户进行关联。话题将碎片化的消息进行了归类与划分,多维度的话题关联让话题有了更多的延展,同时还能加快话题的定位速度,久而久之一个由话题构建成的知识库就慢慢诞生了,无需再特意去整理这些交流过程中显现出来的隐性知识,后来者可以非常方便的经由话题逐渐深入了解组织正在发生的事情。

三、个人助理

个人助理的想法是由微软小冰带来的灵感,她就像是一个逗逼的邻桌,只要你跟她说话,她就总能出乎意料的给你反馈。既然企业级 IM 定位在了聊天,那么为什么我还要去应用中开始某个八竿子打不着的流程能?如果每个人都有一个像微软小冰那样的助手,你可以跟她说需要走一个流程、需要记录一份文档、需要展开一个话题甚至需要进行一次电话会议,她会应该帮你把余下的事情都处理妥当,你需要的就等待最后的结果。Tony Aube 在 No UI Is The New UI 中对未来的人机交互给出了大胆的假设,想必以后需要做什么事情之前都最好找你的机器人助手帮个忙。而且时间久了她还会更了解你,你无需再跟她说复杂的指令来让他做一个事情,可能仅仅一个字,甚至一个动作她就知道了你的初衷。

还是以报销作为容易理解的场景,你直接跟你助手说你要发起一个报销,她会告诉你报销的注意事项,以及最近是否有制度上的更新。然后她会根据权限和组织关系把此次报销的相关人员都拉入一个话题,你直接说出你要报销的条目,她会帮你将其整理成一份标准格式的表单,并提交到流程系统,同时知会相关人员。如果需要讨论就地就可以进行,而且信息会随着讨论的进行逐渐更新,直到最后你确认提交此次申请,其他人可以同时就完成审批。

四、状态更新

我们平时都喜欢更新 QQ 或微信的状态以及签名档,好让朋友们知道我们现在的心情或者正在做的一些事情,这个功能拿到工作中也同样适用。哪个领导不想知道自己的下属在做什么,难道要每天去问问看吗?当你在处理某项工作、打开某个文档抑或参与某个话题的时候,所有的状态都会自动进行更新,主管领导就可以看到每个人的当前工作内容,不会有人再不停的追问你在做什么,你的工作上的一切(包括发呆)都会被清楚的记录下来(可能会有种被窥视的感觉,不过汇报工作内容本来就是需要的,把控好力度就 OK 了)。

最后,所有这些设想中的功能都需要即使通讯与企业应用做到有机的结合,这本来也该是企业级 IM 天生具有的能力。这些异想天开的功能,嘴上说说容易,但实现起来还需要一段漫长的路,甚至是颠覆用户体验的变革。企业级 IM 产品必然会迎来一次集中爆发,让我们拭目以待。

Java 栈上的 JavaScript

注:以上视频在 youtube 上,你懂得该怎么做。

这是我在 ShenJS 2015 上面所做的一个 lighting talk,当时的题目叫做 Enterprise JavaScript。

其实那天准备了很多演示和代码片段,无奈时间太短,只把 slides 简单过了一下就没有时间了,于是在这里做一个完整的介绍。

Enterprise JavaScript 是一个非常大的话题,会涉及到跟各种 Java 中间件的互操作问题,这里仅就如何在一个完整的 Java 栈上面以 JavaScript 作为首要编程语言来提高 Java 系统的开发效率展开讨论。

作为一门常年把持 TIOBE 排行榜冠军的静态编译类语言,Java 固然有数不胜数的优点,以至于其在企业级应用开发领域拥有无可替代的地位。但是,相信每一个 Java 开发人员都会在日常工作中忍受其种种弊端带来的烦恼,其中最令人发指的也正是其静态编译类的特性。静态类型在减少开发人员出错可能性的同时降低了语言的动态扩展能力;而编译执行除了能保证代码的编译期校验以外,更多的就只剩下无尽的等待。尽管现在大多数的工具链和集成开发环境都可以做到增量编译,但是增量编译后可执行代码的热部署能力却是差强人意。尽管有一些商业的开源的解决方案,但总归不是天生特性,使用很不方便。

其实针对 Java 缺陷的改良,社区始终也没有停止过尝试。著名的成果包括 ScalaGroovyJythonJRuby 以及我们今天要讨论的 JavaScript。社区通常都有这么一个共识:Java 虚拟机是个好东西,但 Java 语言本身不一定。因此改良的重点全部集中在语言层面,通过引入具有不同特性的新语言,经过编译器或解析器翻译成 Java 字节码,再交给虚拟机来运行。

另一方面,JavaScript 近两年人气爆棚,有逐渐发展成为全栈开发语言的趋势。尽管在 Node 平台上使用 JavaScript 开发后端应用早已是习以为常的事情,但是在 Java 平台上,这件事情就显得不那么容易了。说到在 Java 开发栈上运行 JavaScript 就不能不提大名鼎鼎的 Rhino。这是早在 1997 年就已经出现的 JavaScript 引擎,并完全由 Java 代码编写而成。后来被 JDK 1.6 收纳为官方默认的 JavaScript 引擎。这也就意味着在整个 Java 发展历史中,都是一直可以支持运行 JavaScript 的。不过由于大环境(在 Java 平台上面跑 JavaScript 毕竟接受度不高)以及 Java 虚拟机对动态语言支持能力欠缺等问题,使得这件事情并没有流行起来。

另外一个导致 Rhino 不够流行的原因,就是对其性能的诟病。在早期的时候 Rhino 是将 JavaScript 代码直接编译为 Java Class,运行期性能非常出色,往往可以击败很多用 C 实现的基于 JIT 技术的引擎。不过生成 Java 二进制代码和装载这些生成的类是个非常重量级的操作,需要耗费大量时间,而且编译过程产生的许多无用中间类和字符串无法被虚拟机垃圾回收掉。不过后来引入了解析模式以后这些问题就得到了解决。一些 性能 测试 报告 显示出 Rhino 的性能依旧非常良好。

后来 Java 官方也意识到虚拟机对动态语言欠缺支持以后,就从 JDK 1.7 开始引入了 invokedynamic 的技术,可以有效提升动态语言的运行性能,而且 Oracle 也在 JDK 1.8 中自带了一个基于此技术实现的新一代的 JavaScript 引擎 Nashorn,终于让性能不再是什么大问题了。

javascript-in-jvm-2

针对在不同版本的 JDK 上运行 JavaScript,可以有不同的解决方案(如上图)。如果是在老版本的 1.6 上面,只能使用 Rhino,以及在 Rhino 之上封装的开发框架 Ringo,这个组合里面没有对 invokedynamic 的支持。当然由于向后兼容性的存在,其实这个解决方案是可以适用于后续 1.7 和 1.8 版本的。如果是在 1.7 版本上面尝试使用 JavaScript,那么可以考虑 DynJS 引擎,这是另外一个独立的 JavaScript 引擎实现,支持了 invokedynamic 技术,与此配套的还有一个 Nodyn 项目,是对 Node 的一个移植。不过很可惜,这套开发栈没等流行起来,就被后来的技术给超越了,因此除非你必须要坚守在 1.7 上面,否则并不建议采用。最后,也是这个话题的终极解决方案,就自然是升级到 JDK 1.8 并使用官方的 Nashorn 引擎,与其对应的框架可以选用 Vert.x。Vert.x 是由 Eclipse 开发的,实现了良好的事件驱动和异步非阻塞机制,这些都是 Ringo 所没有的。再有 Vert.x 还是 reactive 的。不过 Oracle 官方有一个基于 Nashorn 引擎的 Node 实现,叫做 avatar.js,不过已经有一段时间没有更新过了。

下面展示一些使用 JavaScript 开发 Java 应用的示例。

注:在尝试运行这些样例代码之前,请确认已经正确安装了 JDK 1.8u51 或以上版本、Vert.x 3.x 和 Maven 3.x。

一、操作 ArrayList

arraylist

二、Filter and Reduce

filter-and-reduce

三、Web 服务

webserver

四、Spring、Stick 和 Hibernate(SSH)

这是一个比较复杂的例子,首先项目使用 Maven 来管理,并通过 jetty-maven-plugin 来运行。项目内部使用 Spring 作为整个集成的核心,用 Hibernate 来实现数据访问。Stick 是基于 Ringo 的一个组件,实现了类似 express 一样的功能。下面的这段代码,主要演示了如何通过 JavaScript 实现一个 router,并在 router 内部调用 Spring 和 Hibernate 的 API 实现数据查询,并返回 JSON 结果。此项目的完整源代码请在 Github 上获取(这里面还包含了会上的演示文稿,所有的样例都在 demo 目录下)。

运行该项目前,需要首先安装一个 MySQL 数据库,并运行在默认的 3306 端口,root 用户的口令为 mysecretpassword。创建一个名为 ringo-ssh 的数据库,字符集为 UTF-8。然后使用如下的 SQL 语句创建一个数据库表 T_TASK,并插入一些数据:

另外,该项目依赖了我们公司的两个开源项目,分别是 origincdeio-runtime。请从 Github 上 clone 下来以后,进入项目目录,运行 mvn clean install 来编译和安装。注意执行的顺序为先 origin 再 cdeio-runtime。

最后,可以通过运行 mvn jetty:run 命令来启动这个示例。

注:如果是第一次运行该示例,Maven 会下载很多依赖包,这个过程根据网速快慢会长短不一,请耐心等待。

以下是系统运行后的控制台截图和 curl 的返回结果。

ringo-ssh-maven

ringo-ssh-curl

总之,在 Java 平台上使用 JavaScript 作为首要开发语言是完全可行的,在上面这个示例中,除了 Hibernate 必须的领域实体等是使用 Java 来编写的之外,其他的大部分逻辑都是可以用 JavaScript 完成的,而且可以跟各种 Java 中间件完美集成。有关这方面的深入介绍远不止一篇博客所能言尽,有兴趣了解更多的可以参考我公司的 CDEIO 平台。

ShenJS 2015 那些年我们一起追过的盛典

这还是我第一次听说过有追会的,当我第一天来到 ShenJS 的现场。

深圳的天气一如既往的好,也一如既往的热。

对 JSConf 其实早有耳闻,放眼全世界都算是最顶尖 JS 开发者的盛会。JSConf CN 三年前悄然兴起,我就在望眼欲穿的盼。沪——京——杭——终于,就跟 Linkin Park 要光临深圳开演唱会一样,我听说了传说中的 ShenJS。

虽然在经济上总跟“北上广”齐名,但是在技术峰会方面,深圳永远无法望“京沪”项背。或许是我所接触的行业不同,国内的互联网圈子或许会更活跃,更喜欢组织活动,进行分享。可惜我们是搞企业信息系统集成的——对,就是那些成天面对需求变更,被外行指导软件开发,永远延期,没有最烂只有更烂的项目外包企业——在这里,Oracle 是我们的天空,Java 是我们的空气,Apache 是我们的信仰。

这似乎跟 JS 没有太大关系,除了我个人的 JS 情节。

从我接触 JS 的那天起就被她吸引了,虽然当时根本不知道那是一种什么样的语言。那个时候 Web 开发很纯净,纯净到只有 IE 6,Firefox 还是 1.0,Chrome 还没有出生,什么 jQuery 还不流行,prototype.js 在争议与无奈中被一遍又一遍的使用,Crockford 刚刚发明 JSON,即时函数调用还在他脑子里,至于 Node.JS 嘛——孩子,你说的是用来处理 DOM 的函数库吗?就这样在懵懂中我完成了职业生涯的第一个像模像样的项目,整个前端重度依赖 JavaScript。也是那个时候才知道,原来一种语言可以有那么多种风格截然不同的写法,浏览器原来有那么大的差异,IE 6 原来是那么的慢。或许正是这次项目的成功让我对 JS 产生了好感和依赖。“任何能够碰巧用 JS 编写的程序最后就一定会被用 JS 编写”,这个碰巧在我这里成为了必然。

转眼间十年已逝,Java 和 JavaScript 都迎来了 20 周年的庆典,我走入了 ShenJS 的会场。

一进门就被热烈气愤所惊骇:这是技术人员的聚会吗?程序猿会有那么嗨吗?竟然还有人是从外地专门赶来的?有人遭遇台风到不了的?竟然有神人参加过全部四届的 JSConf 大会?Oh my god,这是我想要的感觉,外加舞台上还有个这么逗逼的主持人,Oh yeah,程序猿的高潮瞬间就一泻千里了。

JSConf 无愧为国内最高水平的 JS 会议,总共邀请了来自 11 个国家和地区的 21 位讲师,为大家奉上两天不间断的饕餮盛宴。先来看看菜单吧:

JS中国开发者大会   ShenJS

主持人 Goddy Zhao 必须要提一下,他的开场和串场也真是醉了,还让单身狗明白,“找不到对象”的原因是因为写了太多的空指针。

言归正传,这毕竟还是一次技术的交流会议,总得有点干货吧。

一、React + Flux

这个一点都不稀奇,这半年除了 ES6,估计就属 React 最风光无限了。Web 开发就是这样,为了解决 DOM 性能瓶颈,八仙过海,各显神通。不过 Facebook 给出的解决方案还是让社区为之一振。Virtual DOM(就是在内存中构建一棵虚拟的 DOM 树,在界面需要更新的时候,先在内存中做一次 diff,只将发生变更了的 DOM 节点应用到界面上)的引入让社区瞬间沸腾(Herman 说 React 在阿里很火),看上去多此一举的计算,却能显著提高渲染的性能。在符合要求的前提下,两棵 DOM 树的 diff 复杂度能控制在 O(n) 水平,充分体现了 Facebook “牛逼闪闪”的技术实力。就连不怎么好看的 JSX 也逐渐被爱屋及乌了,况且 React 的组件特性还能顺带解决 CSS 的一些固有问题

Flux 更是简单得一塌糊涂,简单到竟然不一定需要第三方类库来实现(当然还是会有的)。比起 MVC 和 MVVM 这样的重量级框架,React + Flux 就可以解决从数据流动到界面渲染的全套 UI 过程,而且强制要求的单项数据流,也使得架构设计变得简单许多。

因为 Virtual DOM 需要先期做一次 diff,那么很明显的性能瓶颈就有可能会出现在这个地方。Facebook 何许人也,immutable.js 就好像是 React 的孪生姐妹一样,早就在那里等待大家的临幸了。这次会议上听说的唯一一个新概念就是 Persistence Data Structure,其实也可以完美的和 React 集成来实现更多炫酷的能力。我之所以对这个概念非常感冒是因为在曾经的一个项目中,客户就要求实现这样一个需求:可以将组织机构变更的历史记录下来,并随时可以回滚。这正是 PDS 的应用场景呀,当时我懂得少,把这需求给搪塞回去了。

Web 开发还经常前前后后的变化。曾经的服务器比终端性能好,大部分计算在服务器上完成,以实现所谓的瘦客户端;后来终端性能提升了,为了释放服务器的计算能力,降低网络传输负载,将一部分的运算(尤其是界面渲染)转交给终端,以实现所谓的富客户端(或者那个时候的流行词 Web 2.0);现在 React 的来临再一次提出服务器端渲染的能力,而且 isomorphic/universal 的类库还使得前后端的代码一致而通用。

不过 React + Flux 也不是完全没有问题。Flux 官方对异常处理没有一个明确的方案指引,Yahoo Fluxible 的实现也通篇没提到过关于异常数据该如何流动的问题,这些还需要时间和更多的社区实践来总结跟积累了。测试方面,Jest 的性能是个严重的问题,coding.net 的同学们在单元测试的选型中就遭遇了这个问题,最终投向 Karma 的怀抱。另外我觉得 Jest 的侵入性太强,不如 Karma 有那么好的框架整合能力,对于混合使用各种技术的单元测试应该不是个明智之选。

二、异步流程控制

这个话题应该是 JS 社区永远无法停止的。JS 异步单线程的特性,刺激了各种最强大脑对于此问题的思考。就目前状况来看,解决方案无疑有如下几种:

1、Promise:ES6 先天支持的特性,ES5 也有各种第三方库可以使用,算是最容易获得的解决方案。
2、co + generator:这是最接近 ES7 async/await 写法的解决方案,需要 JS 运行时支持 ES6 的 generator 特性,或通过 Babel 等转译。
3、thunks:这是在严老师的《Koa 和 Toa 的框架原理及开发实践》课程中提到过的一种解决方案,能够友好的支持 promise,generator 和回调等各种异步返回方式。
4、async/await:这个应该算是异步流程控制的终极解决方案了,可惜是 ES7 中才会出现的特性,但是依靠 Babel 尝尝鲜是不成问题的,但谁能保证最终的规范不会发生变化呢?

当然,重要的事情最后再说,庄恒飞同学给出的 fibjs 是以消灭这个问题的思路去解决问题的,有兴趣的大大们可以多多关注。

三、架构实战

说实在,因为不从事互联网这种高并发、重架构的日常工作,所以对其中的一些设计没有太多感觉。而且因为没有真正遇到过问题,对其解决方案的精妙之处也就无从感知了。只是在演示中看到各种性能的提升。外行就不在这里评论了,免得被笑掉大牙。

四、炫酷演示

我把 90 后(尼玛,那么年轻)沈毅同学的 ECharts 课程放到炫酷演示里面应该毫不为过,顺便也刺激一下身为东道主的企鹅公司,就算不开 session,也好歹赞助几个公仔吧。百度在前端领域为社区做出的贡献是有目共睹的,我们公司在日常开发中使用的唯一一个国产开源产品就是百度 FEX 团队出品的 F.I.S。当然这个课程并不是纯粹的演示,还是深入介绍了一些难点问题的解决思路和实现原理,本人对 canvas 及相关内容不甚熟悉,也不妄加评论了。

真正的炫酷演示还是来自于三个老外。感觉国内大牛们分享的大多是跟工作有关的内容,或者或多或少会在工作中碰到或用到的;而国外的牛牛们似乎更偏重娱乐与兴趣,不知是环境使然还是文化使然。

三个炫酷演示都是跟硬件有关,也再一次证明了 JS 作为全栈语言的强大实力。曾几何时,硬件的开发都永远是阳春白雪,束之高阁的。什么汇编还有 C,都不是普通码农所能靠近的。然而 JS 正在悄悄的改变着这一切。Arduino 作为标准的开源硬件,JS 作为使用最广泛使用的编程语言,他们的结合剩下的就是想象力的无限放飞。使用 Johnny Five 就可以通过电脑连接 Arduino 而实现用 JS 控制硬件的能力,Andrew 的 Nodebot 就是这么一个原理。Tim 的 NERD DISCO 更是使用了浏览器的 Web Audio API,在浏览器上实现了声音可视化的效果。其实技术都不是问题,音乐 + 灯光 + DJ 调音器,呈献给现场的就是一段炫爆了的电子 DISCO 秀,真是”不会写 JS 的 DJ 不是好讲师呀”。

有了声音自然还要有图像,平面的图像电影院都不怎么放了,3D 的东西也见太多了,VR 是近些年红的发紫的新鲜事物,ShenJS 也真是给力,Martin 关于 WebGL 和 WebVR 的介绍也算是让我们这些无法接触到像 Google Cardboard 这类玩意儿的天朝子民能开开眼界。而且他现场演示使用的 IDE 真是与众不同,实时写代码,还能及时看效果,关键是现场写程序调 3D 图形建模,可见功底之扎实呀,曾几何时,我们学过的这些知识都还给大学老师了。

Martias 的 p2p pipes 现场写代码,佩服佩服,讲什么都不重要了,但是老外们的 JS 都不写分号的吗?

五、其他

剩下的这些就不知道怎么分类了。

PM2 和 Keymatrix.io 的介绍,说实在的下午第一节,我基本上半睡半醒,只知道功能确实强大,还给出了 ShenJS2015 一个七折的优惠码。当然最令人兴奋的还是作为讲师的 Alexandre 竟然是 PM2 的作者,“敬仰犹如滔滔江水连绵不绝,又如黄河泛滥一发不可收拾”。

509.pic

Hax 的 JavaScript: The World’s Best Programming Language,我不得不先上图了,因为群里已经有人将此作为表情了。我在这里想评论的一点就是,这明显是一群 JSer 在一起意淫的结果嘛,但是那又怎样,JavaScript 专治各种不服。

Lighting Talk 是个有意思的环节,每个程序猿都有分享的意愿,这里就能满足你“好为人师”的心理。两天的小讲座内容着实不少:有介绍安全漏洞的,有介绍自己小产品的,有诉说开发经验的等等,太多了没都记下来。让我印象深刻的是华大基因的介绍,因为以前在 Amazon Awsome Day 的活动中,华大基因对 AWS 的应用也是作为典型案例被介绍的(这算广告吗,能给点赞助费吗)。当然还有我自己分享的关于 Enterprise JavaScript 的内容,可惜时间太短了,又是个大话题,准备的一些 Demo 都没来得及演示。后面抽空写个博客详细展开一下。

最后的最后,以 Herman 老师的课程作为结束吧,虽然全篇没太注意他在讲什么,但就是觉得印象深刻。Herman 老师的前端开发摩尔定律更是令人俯首称臣呀:每 18 到 24 个月,前端开发的难度就会增加一倍!干!

因为住的地方离南山比较远,所以两天的 Afterparty 都没有参加,尤其是 Nodebot 的分组对抗,尤其遗憾。后面又听群里说周日的派对上面很多人聊得非常投机,也没有机会参与。这是算是我第一次参加这种规模的技术峰会,收益颇深。难怪会有这么多忠实粉丝不远万里、不怕险阻、不惧台风、不辞艰辛的来执着追随着这个会议。或许所有 JSer 的内心都跟 JS 本身一样无比复杂,又无比单纯。回归生活我们是被人揶揄的程序猿,在这里我们是崇尚自由的乐天派。不论下一届的 JSConf 开在哪里,相信永远会是“那些年我们一起追过的盛典”。

版权所有,欢迎转载!

当起点变成终点

我昨天和同事聊天,说搞一场忽悠人的演讲也是挺费心思的:既要高端、大气、上档次,又不能太虚无缥缈,还得能够吸引人,最后要是能再跟“我的中国梦”扯上点关系,那就完美了。显然这些我都做不到,所以我不是来忽悠人的,不知道这个因果关系能否成立。

先来介绍一下我自己。本人唐睿,显然我爸就不是李刚了,没有高智商,没有高学历,更不是什么海归和高富帅之类的!这是我在 Twitter 上的签名档。

签名档

我没有什么天资,但自认为在我人生最重要的时间点上有点不错的运气!首先从高考开始,我们那一年是第一次三加综合试点,这理科综合刚好是我的强项,于是就考上了对我来说还算非常不错的大学,专业呢就是计算机科学与技术!在大学期间参加了一个由微软资助的社团,在那里遇到了一个师兄,在他的带领下我在大二的时候就开始做项目挣钱,毕业以后去了北京的一家公司,从事软件研发工作,从初级程序员一直做到项目经理,历时四年。到了08年公司有个项目派我来到深圳,于是就遇到了我现在的领导兼合作伙伴,之后我们就一起创办了这家公司,并一同经营至今。

我的故事没有《中国好声音》那么煽情,但这里关系到了人生重要的三个里程碑:高考,就业和创业!这三个人生阶段性的总结,恰好是一个时期的终点!

我们先来说说高考,对任何人估计都是“往事不堪回首”。高考固然有这样那样的问题,但迄今仍然是唯一可行的全民选举制度!问问我们坐在这里的同学有多少是从大山里走出来的就知道高考对他们是多么的不同寻常!但是我这里要讲的是考试结束以后!这是个令人兴奋的话题,我记得当年高考结束以后,我做的第一件事情就是去书店买了本《三个火枪手》,把它一口气读完,之后发现一个问题:为什么小说里的人物都喜欢有夫之妇,其它的现在都忘了!但是自从高考结束以后,我发现我很难再像高中时代那样夜以继日地拼命去学习了,因为我怕了,怕回到以前披星戴月读书的日子!虽然我现在依然在学习,但是原来的状态再也没有了!不知道大家是否有这种感觉?

那让我们来分析一下原因。你当然可以说是高考把你战斗的热情都耗光了,或者是中国高进低出的大学生活没有了挑战,但是我觉得最重要的是我们心态的变化。所谓“当起点变成终点”。

起跑线?终点线?

高中生活固然非常充实,而大学生活同样可圈可点,只是通常我们都要多年以后才能意识得到!我不敢说这次分享之后大家能对大学生活的态度有多大转变,而是希望能够珍惜!学生时代是穷了一点,但换来的是学习的时间,我总喜欢说:为什么穷人的孩子早当家,因为他们把别人花钱来玩的时间都用来学习了!

大学是一个接受分专业、系统化的高等教育的地方,但是这显然不是市场所需要的!打个比方,我前两天跟个朋友聊天说到,你不觉得现在计算机系毕业的学生除了不干 IT 的,一大部分都在做程序员!我上学的那个年代,程序员还算一个“三高”职业,而如今程序员满大街都是。不是说程序员没有用,而是本来我们可以做得更好!

举个例子,大家是否都听说过自动驾驶汽车?首先必须说这里面少不了程序员,因为所有这些功能都必须一行一行代码的写出来。但是程序员绝对没办法独自搞出这样一辆车来的。我们还需要什么?当然一部汽车,然后还有会改装它的人,再有熟悉一些人工智能理论的人,外加信号处理与控制理论专业相关的人才!当然你还得有良好的数学基础,否则面对这样的公式,我们只能瞠目结舌了!

卡尔曼过滤器

大学里面既可以修炼你的专业技能,又能满足你涉猎其它学科的需求,遇到你不明白的可以花大量的时间,查阅大量的资料,或者与很多小伙伴去探讨,或最终找教授去获得一些点拨!这些将是我们毕业以后所很难奢求的,尤其是大把的时间!在学校如果能参加一些社团,或者企业举办的比赛,将会比在考试中拿到好成绩更有意义。比如挑战杯,数学建模之类的。理论结合实际远远好于纸上谈兵。不要仅仅局限于本专业这个小圈子,市场需要的是跨界人才。搞软件的能够懂得硬件,学计算机的再去了解一些自动化,学电子的男生去找一个学服装设计的女朋友等等,扩大你的交际范围,并从其他专业的视角审视你的专业,直到觉得自己变得无知而渺小的时候,说明你已经看到了另一个境界。要记住:离你近的,离所有人都近。

转眼间就毕业了,然后必须就业!说到找工作,就不得不提市场与企业对人才的供需矛盾!一方面毕业生找不到工作,而另一方面企业也苦于招不到合适的人才。矛头直指我们的教育!大学毕业生无法立即开始工作,但是未来的潜力是不错的;相反从培训学校走出来的人倒是动手能力还不错,但是从长远来看,非科班出身将是极大的瓶颈。在大环境无法改变的前提下我们就该去想想如何改变自我。我仅从我们公司对人员的要求上对大家的就业做一点指引。

首先,不要相信“临时抱佛脚”的威力,这顶多能让你通过笔试阶段,当与面试官聊天的时候,你永远都无法掩盖自己的不足。因此从现在开始,学好专业课,打好基础。关于专业课方面,现在的你们幸福多了。以前上课听不明白了,就只能去图书馆找资料看,现在有大把的名校的开放课程,无论国内的还是国外的,上网的时候多去看一看这些内容,对课堂时间是个有益的补充。

第二,一定要有过硬的英语水平。我本不想提及这一点,因为从小学就开设英语课的中国教育好歹能培养出来看得懂英文文章的人吧。可是我错了,我发现我周围会说英语的人越来越少。英语除了日常会话(出国旅游时用),更重要的是专业部分。我经常跟我们公司的小孩们说,如果你用 Google 搜索出来一篇英文的文档你应该感到庆幸,因为当你搜出一篇德文文档的时候,用 Google 翻译弄出来的英文版就没那么容易读懂了。

第三,如果面试的时候你真的什么都不知道,要表现出来你的诚实,如果你用直觉去猜的话,是个好事情。但是告诉面试官这是你猜的,如果你猜得正确说不定还能加分,这说明你的第六感很不错,有时候工作需要这样的灵感。还有就是表现出来你的学习能力,没有谁能够一上来就融入公司,并开始工作。一个人的学习能力越强,公司可能为其付出的培训成本就越少。

第四,第一份工作的薪水并不能代表什么,因为校园招聘的运气成分远大于实力本身。像腾讯这样的超级公司聊聊无几,大部分毕业生都将进入中小微企业工作。这本没有优劣之分。大公司或许薪水高,能学到更多的东西,但是体制庞大、结构完整,所谓一个萝卜一个坑,新人要经历漫长的成长和攀升过程,才能达到一个有吸引力的目标岗位。而中小微企业,各方面都不完善,所谓浑水才能摸鱼。如果在座的各位是喜欢折腾的人,大可以在这里一显身手。小微企业有的是机会和位置,只是苦于没有合适的人。过了四五年以后(一定要这么久,才能了解公司是否真的不再适合你的发展),做到了经理职位,你的眼界的提升和对职场的认识将会登山一个新的台阶,这时如果有机会选择跳槽,或者创业的话,那么你的机会才刚刚开始。

回想很久以前,跟朋友吹水的时候说,北京的那家公司将是我为其打工的第一家也是最后一家公司。这句话现在看来真的是这样。我没有选择跳槽,而是直接去创业。《中国合伙人》这部电影大家应该都看过,我不知道各位的心情是如何的,我基本上是感动着看完的。里面有太多创业的真实场景。随着就业环境越来越差,很多毕业生被迫走上了创业之路。如果你真的打算创业,毕业之后再想就太晚了,无论何时付诸实施,现在就得开始准备。无论将来是做互联网,游戏,企业级应用,传统产业,甚至在淘宝开网店,我希望以下一些我的想法能够给大家带来一些启发。

首先,找到一个好的团队。没有什么比这更重要了,你一生的合作伙伴有可能现在就坐在这间教室里。团队首先需要的是诚信,是互信,这是团队长久的根本。信,国之本也。立国如此,置业也当如此。讲一个我经历过的小故事。那一年我在北京,还是一个安分守己的好员工。某天公司安排我来深圳支持几个客户,傻傻的我就过去了。在那边接待我的的是公司负责华南区的售前,这些客户也基本上都是他在维护。到了客户现场才发现情况远没有支持一下那么简单。原来是他在这边接了一个私活,想让我帮他完成,那些什么所谓的要支持的客户根本就没出问题。私活有钱赚,我也十分乐意,呆在深圳一个星期,拉上我的一个小伙伴做了个七七八八。于是问题就来了。当时客户方面主管项目的领导被双规了,就跟现在很多领导被双规是一样的,你懂的。那么项目自然就搁浅了,钱也没有了。回到北京还得编我在深圳都干了哪些事情,很是郁闷。但是后来我查银行卡发现他给转来了 5000 块钱,说是项目的首款,只有这么多了,我也没多计较,分了一半给帮忙的朋友,自己拿了剩下的一半。这件事情本来不值一提,但后来随着交往的深入,我才知道这根本不是什么首付款,而是他自己掏腰包为我的工作买单。就这样我们的合作逐步加深,直到有一天我们一起出来单干了。团队除了信,成员的角色要求能够互补。我们当时一起创业的有五个人:有负责市场的,有负责技术的,有负责管理的,有负责售前售后的和负责财务的,这样的一个黄金组合风风雨雨四年多,至今仍是我们认为最宝贵的财富。

第二,先小人后君子。没有哪个创业不是为了挣钱。没有钱的时候大家可以一起吃苦,但问题是当大家分钱的时候,心态还能否一样的平静。事先明确自己的权力和义务,划分好股份,掰扯的清清楚楚,明明白白。需要争夺的时候就分文不如,锱铢必争;需要给予的时候就慷慨大方,毅然决然。当这些“君子协定”都拟定清楚之后,大家便可以放开手脚,无所顾忌的去拼搏一番。

第三,害人之心不可有,防人之心不可无。任何时候我们都得做最坏的打算,你再亲信的人,都有可能在时机成熟的时候捅你一刀,这时你的底线在哪里,你如何应对以不至于被动。还说回刚才的那个故事。有人可能会问,他就不拍你拒绝,然后把事情报告给公司吗?的确会有这种可能,而且他也做好了预防。这件事情之前他曾去过北京,我们的关系当时并不很熟,他找我要了几百块钱的发票,出差费用用发票报销也很正常,就给他了。没过两天他拿来几百块钱给我,说是用一些名义把那些发票替我报销了,让我留着这些钱。我当时还傻傻的,以为只是多报了一些钱而已。其实也是后来才知道,如果真的事情如前面所说,那么这件事情便成了他手中的把柄。

第四,选对主题,找好切入点。如今的创业比拼的是 idea,这是一个 idea 为先的时代。这句话不假,但是在茫茫人海之中,你能确信的想法是独一无二的吗?所以很可能在你有这个念头的时候,人家的产品都已经上线了。那么我们该怎么办,岂不是就不要做了。书上都说要做蓝海,而不是红海,真的找到一片广阔的蓝海谈何容易呀。纵观被百度、阿里巴巴和腾讯把持的中国互联网;再看被苹果、三星瓜分的智能手机市场,360 和小米是如何从中脱颖而出,与其平起平坐的。

以前的中国互联网是新浪、网易和搜狐,现在则变成了 3BAT,这个 3 就是 360。这个初创的互联网公司竟然能够逼迫小马哥的 QQ 在某些机器上停止运行,虽不光彩,但不得不说真牛逼。周鸿祎从 3721 开始做流氓软件,被阿里巴巴收购之后转投 360,开始搞安全。有谁会相信流氓能够从良,于是他开始与卡巴斯基合作,装 360 免费提供半年的卡巴斯基授权。卡巴斯基借此机会迅速打开国内市场,360 也借机完成了装机量的提升。接着他开始以雄厚的资金推免费杀毒服务,早已把老大哥卡巴不放在眼里了,从而进一步巩固了装机量。然后爆发的时刻来到了。360 推出了浏览器,并强行推广 360 导航。曾经有一个笑话说:当某一天 360 把 IE 消灭了,元芳你怎么看。至此 360 完成了他在互联网行业的布局,真正迈进中国互联网王国,与百度、阿里巴巴和腾讯相提并论。

再说小米。为发烧而生,有多少人是看到了这句话才去买的呢。但用过的人似乎并不这么认为。为发烧而生,是用来迷惑竞争对手的。因为发烧友是小众,个别分子,为发烧而生就是为小众人准备的设备,潜台词就是不会与大品牌形成竞争,使其在初期放松对小米的警惕。加之其期货的销售模式,也很难在一瞬间霸占市场。但发烧友的热情似乎超越了所有人的想象。就拿刚刚结束的双十一来说,这是小米的成绩单。

小米双十一

天猫商城首个销售破亿的商家,全天总销售额 5.5 亿。小米能够成功我觉得归功于几个方面:第一,小米有其固有的基础,就是 MIUI 的深度定制系统,这让其在技术上不落后于竞争对手;第二,期货模式,也有人说是饥饿营销。但我觉得对于一个刚刚起步的手机企业,占领市场是关键,饥饿营销能够一时激发人们的渴望,但长久会对用户造成伤害。但是小米为什么还持续搞定时抢购,我认为更多的是期货。因为小米的售价非常低,甚至接近成本。庞大的产量需要对供应链有良好的控制,需要库存,营销等各个环节的通力配合,任何一个方面出现问题,将导致成本的增加,意味着利润的减少。反之如果几十万部手机在两分钟之内销售一空,剩下的事情交给快递公司,这个不是就简单了很多。小米在今年 8 月份完成了新一轮的融资,估值达 100 亿美元!

这两家公司有一个共同的特点,都是在竞争激烈,甚至垄断的红海中,杀出一片天地。360 是从杀软开始,这是其他互联网公司都不碰的东西,又是每个 Windows 用户都必须的东西;小米从低端市场切入,并以完美的市场营销来积累用户。红米手机更是通过与中移动的合作,在农村等二、三线城市推广其产品。因此小米是典型的互联网公司,而不是产品公司。

第五,执行力。虽然 idea 很重要,但是能实现 idea 的执行力更重要。这就是为什么我们经常能看到一些不以为奇,却融资几百万美元的产品的原因了,关键就是人家做出来了,而我们还在想。在我们这些人当中,真的没有谁努力到需要靠天赋来比拼的地步。因此执行力不光在于做,还在于要做好,做完、按照想法做和持续的做。

一个好的团队,有一定的想法,找好切入点,有顽强的执行力,这样的创业才能够成功,如果某一天被风投看上,这些也是他们最关注的,因为这是能够为其赚钱的前提。

唠唠叨叨半天,做一个总结。我开始提到了人生开始阶段的三个里程碑:高考、就业和创业。不同人的里程碑或许并不一样,有人是想出国,有人是想做公务员,有人想当兵。无论是什么,别把起点当成终点。融资,上市,销售额突破 350 亿,员工超过 2000 人,不要被这些设定的阶段性目标所束缚,更不能仅仅为了达成而努力。相信只要你做了就一定会有结果,有了结果却不能松懈,在新的高度上继续前进!

最后,以我们公司的座右铭结束本次分享。谢谢!

终其而为,欲达则至

如何去掉 OSX 系统中打开方式里的重复项

在 Terminal 里面执行如下命令:

之后重启 Finder 就可以了。

提升 Jetty 服务器的启动速度

最近在使用 Jetty 调试一个系统的时候发现,控制台会长时间停在某个地方,致使系统无法正常启动。经过调整日志的输出级别后发现,Jetty 有一个 AnnotationParser 的类,会去扫描 classpath 中所有的类文件,而碰巧这个项目有一个 20M 大小的 jar 包,就导致这个扫描过程异常缓慢,使得 Jetty 无法在短时间内里面完成启动。

要说明 AnnotationParser 类的作用,需要先从 Servlet 2.5 的规范说起。2.5 版的 Servlet 规范最重要的是引入了对 Java EE Annoation 的支持,也就是可以用注解来声明 servlet、filter 等对象。这件事前以前是在 web.xml 文件中完成的,现在可以把 web.xml 中的定义全部移植到 Java 类里面,而不引入任何配置文件。这就导致一个问题:作为 Servlet 容器的 Jetty 怎么能够知道哪些类使用了这些注解?很显然,并没有非常有效的办法,因此 Jetty 才实现了一个 AnnotationParser 的类型,通过遍历的方法来寻找所有使用了注解的类型。

如果我们开发的程序没有使用注解,这个过程就是纯粹在浪费时间,Servlet 规范也提供了一种办法禁用此种类型的扫描,从而提升启动速度。方法就是在 web.xml 文件中增加 metadata-complete 配置。

需要注意的是 web.xml 中声明使用的 version 属性一定要大于 2.5 版,才有这个配置。

参考资料:

背井离乡

一路向南的脚印一步一步通向了夏天
冰雪撒在回忆上面梦想全都结了霜
天空总是灰蒙蒙 心里总是茫茫然
我该怎么办 看见路边独自弹唱的少年