历经二十多年的发展,Java 已经成为最成熟和发达的软件开发体系,有着丰富的技术资源和活跃的社区。但是时间也带给了 Java 的沧桑感,那么如何有效提高 Java 项目的开发效率,改善 Java 语言过于朴素所带来的笨拙感。
语言、框架与技术栈关于软件开发,特别是服务端软件开发的技术栈的探索,几乎贯穿了我整个的职业生涯。几年前我就想写这样一个专题,但是开了个头就又很快搁笔。反复思考,感觉自己仍然太过浅薄。
近几年随着技术领域的整体进步,和自己的学习积累,感觉现在是个比较合适的时机,建立一个围绕 Java 体系的工程技术栈,将它整理成文字,与同行分享。
一方面,Java8.0 之后,语言的进步使它在生产力上与旧版本已有很大提高。虽然 Java 与新时代的编程语言相比,仍显得比较笨拙,但是做为一个技术体系的基础,已经足够可用。另一方面,基于 JVM 环境的编程语言的发展,使得 JVM 可以成为一个通用的运行时环境,在其上利用若干互补的编程语言构建工程。
JVM 平台上最著名的新贵莫过于 Scala,特别是 Spark 的兴起为 Scala 争取了很多爱好者。但是就我亲身体验而言,Scala 也有一些问题:
首先是 Scala 本身是一门复杂语言,要用好这门语言,需要大量的领域知识。当然将其作为一个“更好的 Java”来用,未尝不是一件好事,这也是我向普通用户推荐的用法。要发挥 Scala 的全部优势,需要一个优秀的团队,成员有对代码风格和质量的追求和共识,愿意为驾驭工具,提高生产力付出精力。其次 Scala 的构建工具 sbt 实在不能说令人满意另外就是,Scala 实在跟 java“太像”了,它是一个自成一体的复杂体系,如果用 Scala,很难划清一个边界,找到其它语言的切入点。这倒不能说是 Scala 的缺点,更准确的说,其它技术需要 Scala 的多,Scala 需要其它技术的少。
我尝试了 Java+Clojure 的体系,发现是一个非常好的组合。Java 是静态编译型语言,Clojure 是动态类型,虽然它本质上仍然是编译型的,但是可以在 repl 中方便的交互,也可以以脚本形式运行。两者可以在工程上建立非常清晰的功能边界,各司其职。Clojure 的构建工具 leiningen 是 maven 的高级封装,可以充分利用 Clojure 的语法和 maven 的资源。Clojure 的语言风格和内置库,都强调了与 Java 的互通,而表达式内在的数据抽象能力,以及大量依赖 Clojure 语法的功能支持,可以将 Java 项目变得更敏捷和干净。在 Java 项目中引入 Clojure ,可以有效提高生产力。
或许 Java 语言层面的笨拙,本身也促进了其工具库和框架的蓬勃发展,现代 Java 生态中,负责组建项目架构的 Spring/Guice,提供并行 / 并发抽象的 Java Concurrent 和 Akka、Clojure.core.async 、提供数据库访问的 Hibernate 等,都是很好的作品。在具体的项目中,找到一个互补的工具集很有意义。
一个复杂工程,往往不是单一架构和技术栈能够覆盖的,技术组合能够互补就很重要了。例如异步框架中 Vertx 曾经是我期待很高的一环,它为多种语言提供了 SDK,包括 Java、Scala、NodeJs 等。但是实践中这个东西完全无法让人满意,在高性能压力下,出现大量无法管理的错误。更重要的是它非常的排外,一旦在项目中使用 Vertx,就要整个在编程风格上遵循它的需要,大量的回调并没有节省开发人员的思考时间,相反还要削足适履,不断思考自己的代码逻辑是否会阻塞框架。而它的异步安全,依赖全局的单一 Vertical 对象,在原生支持多核并行的 JVM 环境下,强制开发人员依赖 GIL,是一个非常愚蠢的退步。
相反,我在尝试 Akka 的过程中,体验非常好,Akka 不会强制用户在一个单一的 materializer 下运行逻辑,跨节点扩展非常容易,而进程内的 actor 运行负担也非常小,接入 Akka 的过程很友善,并不会污染 actor 之外的代码风格,在 Spring MVC 中使用 Akka 也不会有任何问题,我还尝试在 Google Cloud 的 App Engine 实例(war 环境)中用 Akka 管理爬虫逻辑,整个过程没有任何问题。这也印证了前面所说的 " 其它 Jvm 技术需要 Scala" 的场景。感谢 Akka 提供了完整的 Java dsl,虽然它只能用在 JVM 项目中,但是对于服务端开发并不是很大问题。我编写了很多在 Clojure/Java 中使用 Akka 的代码,组合使用了各种各样的框架和库,都没有遇到风格上的冲突。后续会和大家分享一些 Java+Clojure+Leiningen+AKKA 的开发体验。
源码地址:
Hello LeiningenMaven 相信 绝大部分 Java 程序员都不陌生,但是当前市面上关于 Maven 的书籍却几乎没有,毕竟构建工具这件事太不直接了,也不像软件工程领域的那些流行词那么吸引人。但是构建工具还是会影响项目的质量甚至企业的协作方式,可以说是实际生产中非常重要的一环,因此 Java 程序员需要了解 Clojure ,也必须要从 Clojure 的构建工具开始。
主流的 Clojure 构建工具是 Leiningen 和 Boot,个人惯用第一个,前提是需要确认你已经安装了 JDK,接下来进入代码时间。
接下来就默认大家开发环境中已经有了 JDK 10+。Leiningen 的安装很简单。在官网 脚本,保存后在自己的终端里就可以执行下载安装,特意提到这个东西是因为,呃……它经常下载失败。
有时候需要按照脚本里面给到的 URL(随着版本升级会不一样,所以建议用的时候去脚本里找)手工下载,然后放到 ~/.lein/self-installs 里,然后就可以顺利安装了。可喜的是 Leiningen 并不依赖 Clojure 环境,Clojure 的 runtime 太小了(或者说 JVM 已经足够完备)。得益于此,Leiningen 只需要一个不大的 standalone.jar,下面是平常惯用的一个 Leiningen 项目模板示例:
(defproject one-by-one "0.1.0-SNAPSHOT" :description "FIXME: write description" :url " 功能的演示脚本可以在官网上找到,这里就不赘述了,与我自己的个人习惯有以下几个方面的区别:
将 Clojure 代码放在 src/main/clojure 下,Java 代码则是 src/main/java ,如果有其它语言,依次类推。测试代码位于 test/main/clojure 和 test/main/java 。main 同级的目录会用来管理可能存在的不同 profile 的目录,例如以前用 Clojure 写 Spark 任务时我会需要 src/dev/clojure 和 src/release/clojure 这样的目录,以及 test/local/clojure 和 test/clusters/clojure 等。所以一般来说我不会用默认的项目模板,如果需要搭建一个新项目,我的项目目录大概是下图的结构:
第一步,先定个小目标,Hello Leiningen !
类似于 maven 的 pom 文件,Leiningen 的代码文件放在项目根目录下,名为 project.clj ,一个最简单的 project.clj 大概是这么一个样子:
内容很容易理解,基本的项目信息,依赖库、main 函数的入口类和代码目录。对应的 app.clj 内容也很简单:
运行结果没有什么关子可卖:
需要说明的是依赖里那个 Clojure 是用来运行这个 println 的,如果我们写个 Java 版的 Hello World,连这个依赖都可以去掉,project.clj 如下:
对应的 App.java:
然而此时如果运行 Lein run 会得到一条错误:
原因很简单,因为我们要编译:
得到的 jar 是可直接运行的:
运行 lein jar 可以得到项目 jar ,而 lein uberjar 可以同时构建 standalone 版本的 jar 。说实话,在发现 maven 和 gradle 里需要 shadow 插件之前,我并没意识到 Leiningen 内置的功能还挺可爱的……
一个比较有意思的事情是,如果我们在不包含 project.clj 的目录下执行 lein repl ,它会用自己的 Clojure 库启动一个 repl 环境
但是如果我们在上一个纯 java 版的 hello world 项目目录里执行 lain repl ,它会按照项目配置加载依赖,所以会给出一个错误提示:
当然在项目里加入一个仅供开发时测试运行 repl 同时并不会引入到发布版中的 Clojure 并不难,在特定 profile 里加入依赖就好了,只要看一下官方示例代码,相信熟悉 Maven 的 Java 工程师都能搞定。这里不跑题太远。现在我们有了一个比 Maven 更友好,更容易书写的构建工具,有了 repl 环境和基本的项目结构,就可以做更丰富的测试案例了。