Clojure 驱动的 Web 开发

      2020-03-11 03:16      JAVA开发

Clojure 是运行在 JVM 之上的 Lisp 方言,提供了强大的函数式编程支持。由于 Java 语言语法与固有范式存在的一些局限性,用 Java 编写大型应用程序时,代码往往十分臃肿。许多语言如 Groovy、Scala 等都把自身设计为一种可替代 Java 的、能直接编译为 JVM 字节码的语言,而 Clojure 则提供了 Lisp 在 JVM 的实现。

Clojure 经过几年的发展,其社区已经逐渐成熟,有许多活跃的开源项目,足以完成大型应用程序的开发。由 Twitter 开源的著名的分布式并行计算框架 Storm 就是用 Clojure 编写的。

Clojure 提供了对 Java 的互操作调用,对于那些必须在 JVM 上继续开发的项目,Clojure 可以利用 Java 遗留代码。对大多数基于 SSH(Spring Struts Hibernate)的 Java 项目来说,是时候扔掉它们,用 Clojure 以一种全新的模式来进行开发了。

本文将简要介绍使用 Clojure 构建 Web 应用程序的开发环境和技术栈。相比 SSH,相同的功能使用 Clojure 仅需极少的代码,并且无需在开发过程中不断重启服务器,可以极大地提升开发效率。

安装 Clojure 开发环境

由于 Clojure 运行在 JVM 上,我们只需要准备好 JDK 和 Java 标配的 Eclipse 开发环境就可以开始 Clojure 开发了!

我们的开发环境是:

    Java 8 SDK:可以从 Oracle 官方网站下载最新 64 位版本; Eclipse Luna SR1:可以从 Eclipse 官方网站下载 Eclipse IDE for Java Developers 最新 64 位版本。

    安装完 JDK 后,通过命令 java -version 确认 JDK 是否正确安装以及版本号:

    清单 1. 验证 JDK 版本
    $ java -versionjava version "1.8.0_20"Java(TM) SE Runtime Environment (build 1.8.0_20-b26)Java HotSpot(TM) 64-Bit Server VM (build 25.20-b23, mixed mode)

    Clojure 开发环境可以通过 Eclipse 插件形式获得,Counterclockwise 提供了非常完善的 Clojure 开发支持。

    首先运行 Eclipse,通过菜单“Help”-“Eclipse Marketplace...”打开 Eclipse Marketplace,搜索关键字 counterclockwise ,点击 Install 安装。

    图 1. 安装 counterclockwise

    安装完 Counterclockwise 后需要重启 Eclipse,然后,我们就可以新建一个 Clojure Project 了!

    选择菜单“File”-“New”-“Project...”,选择“Clojure”-“Clojure Project”,填入名称“cljweb”,创建一个新的 Clojure Project。

    图 2. 新建 Clojure Project

    找到 project.clj 文件,把 :dependencies 中的 Clojure 版本由 1.5.1 改为最新版 1.6.0。

    清单 2. 修改 project.clj
    (defproject cljweb "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.6.0"]])

    保存,然后您会注意到 Leiningen 会自动编译整个工程。

    Leiningen 是什么

    Leiningen 是 Clojure 的项目构建工具,类似于 Maven。事实上,Leiningen 底层完全使用 Maven 的包管理机制,只是 Leiningen 的构建脚本不是 pom.xml,而是 project.clj,它本身就是 Clojure 代码。

    如果 Leiningen 没有自动运行,您可以点击菜单“Project”-“Build Automatically”,勾上后就会让 Leiningen 在源码改动后自动构建整个工程。

    第一个 Clojure 版 Hello World

    在 src 目录下找到自动生成的 core.clj 文件,注意到已经生成了如下代码:

    清单 3. Hello World
    (ns cljweb.core)(defn foo "I don't do a whole lot." [x] (println x "Hello, World!"))

    只需要添加一行代码,调用 foo 函数:

    清单 4. 调用 foo 函数
    (println (foo "Clojure"))

    然后,点击菜单“Run”-“Run”就可以直接运行了。

    图 3. 运行 clj 文件

    Leiningen 会启动一个 REPL,并设置好 classpath。第一次 REPL 启动会比较慢,原因是 JVM 的启动速度慢。在 REPL 中可以看到运行结果。REPL 窗口本身还支持直接运行 Clojure 代码,这样您可以直接在 REPL 中测试代码,能极大地提高开发效率。

    Clojure 函数式编程

    Clojure 和 Java 最大的区别在于 Clojure 的函数是头等公民,并完全支持函数式编程。Clojure 自身提供了一系列内置函数,使得编写的代码简洁而高效。

    我们随便写几个函数来看看:

    清单 5. 序列
    ;; 定义自然数序列(defn natuals [] (iterate inc 1));; 定义奇数序列(defn odds [] (filter odd? (natuals)));; 定义偶数序列(defn evens [] (filter even? (natuals)));; 定义斐波那契数列(defn fib [] (defn fib-iter [a b] (lazy-seq (cons a (fib-iter b (+ a b))))) (fib-iter 0 1))

    这些函数的特点是拥有 Clojure 的“惰性计算”特性,我们可以极其简洁地构造一个无限序列,然后通过高阶函数做任意操作。

    清单 6. 操作序列
    ;; 打印前 10 个数(println (take 10 (natuals)))(println (take 10 (odds)))(println (take 10 (evens)))(println (take 10 (fib)));; 打印 1x2, 2x3, 3x4...(println (take 10 (map * (natuals) (drop 1 (natuals)))))

    再识 Clojure

    Clojure 自身到底是什么?Clojure 自身只是一个 clojure.jar 文件,它负责把 Clojure 代码编译成 JVM 可以运行的 .class 文件。如果预先把 Clojure 代码编译为 .class,那么运行时也不需要 clojure.jar 了。

    Clojure 自身也作为 Maven 的一个包,您应该可以在用户目录下找到 Maven 管理的 clojure-1.6.0.jar 以及源码:.m2/repository/org/clojure/clojure/1.6.0/。

    如果要在命令行运行 Clojure 代码,需要自己把 classpath 设置好,入口函数是 clojure.main,参数是要运行的 .clj 文件。

    清单 7. 运行 clj
    $ java -cp ~/.m2/repository/org/clojure/clojure/1.6.0/clojure-1.6.0.jar clojure.main \cljweb/core.cljClojure: Hello, World!nil(1 2 3 4 5 6 7 8 9 10)(1 3 5 7 9 11 13 15 17 19)(2 4 6 8 10 12 14 16 18 20)(0 1 1 2 3 5 8 13 21 34)(2 6 12 20 30 42 56 72 90 110)

    在 Eclipse 环境中,Leiningen 已经帮您设置好了一切。

    访问数据库

    Java 提供了标准的 JDBC 接口访问数据库,Clojure 的数据库接口 clojure.java.jdbc 是对 Java JDBC 的封装。我们只需要引用 clojure.java.jdbc 以及对应的数据库驱动,就可以在 Clojure 代码中访问数据库。

    clojure.java.jdbc 是一个比较底层的接口。如果要使用 DSL 的模式来编写数据库代码,类似 Java 的 Hibernate,则可以考虑几个 DSL 库。我们选择 Korma 来编写访问数据库的代码。

    由于 Clojure 是 Lisp 方言,它继承了 Lisp 强大的“代码即数据”的功能,在 Clojure 代码中,编写 SQL 语句对应的 DSL 十分自然,完全无需 Hibernate 复杂的映射配置。

    我们先配置好 MySQL 数据库,然后创建一个表来测试 Clojure 代码:

    清单 8. 创建表
    create table courses ( id varchar(32) not null primary key, name varchar(50) not null, price real not null, online bool not null, days bigint not null);

    新建一个 db.clj 文件,选择菜单“File”-“New”-“Other...”,选择“Clojure”-“Clojure Namespace”,填入名称 cljweb.db,就可以创建一个 db.clj 文件。

    在编写代码前,我们首先要在 project.clj 文件中添加依赖项。

    清单 9. 添加依赖项
    [org.clojure/java.jdbc "0.3.6"][mysql/mysql-connector-java "5.1.25"][korma "0.3.0"]

    使用 Korma 操作数据库十分简单,只需要先引用 Korma。

    清单 10. 引用 Korma
    (ns cljweb.db (:use korma.db korma.core))

    定义数据库连接的配置信息。

    清单 11. 定义数据库连接
    (defdb korma-db (mysql {:db "test", :host "localhost", :port 3306, :user "www", :password "www"}))

    然后定义一下要使用的 entity,也就是表名。

    清单 12. 定义 entity
    (declare courses)(defentity courses)

    现在,就可以对数据库进行操作了。插入一条记录。

    清单 13. 执行 insert
    (insert courses (values { :id "s-201", :name "SQL", :price 99.9, :online false, :days 30 })))

    使用 Clojure 内置的 map 类型,十分直观。

    查询语句通过 select 宏实现了 SQL DSL 到 Clojure 代码的自然映射。

    清单 14. 执行 select
    (select courses (where {:online false}) (order :name :asc)))

    这完全得益于 Lisp 的 S 表达式的威力,既不需要直接拼凑 SQL,也不需要重新发明类似 HQL 的语法。

    利用 Korma 提供的 sql-only 和 dry-run,可以打印出生成的 SQL 语句,但实际并不执行。

    Web 接口

    传统的 Java EE 使用 Servlet 接口来划分服务器和应用程序的界限,应用程序负责提供实现 Servlet 接口的类,服务器负责处理 HTTP 连接并转换为 Servlet 接口所需的 HttpServletRequest 和 HttpServletResponse。Servlet 接口定义十分复杂,再加上 Filter,所需的 XML 配置复杂度很高,而且测试困难。

    Clojure 的 Web 实现最常用的是 Ring。Ring 的设计来自 Python 的 WSGI 和 Ruby 的 Rack,以 WSGI 为例,其接口设计十分简单,仅一个函数。

    清单 15. WSGI 函数
    def application(env, start_response): pass

    其中 env 是一个字典,start_response 是响应函数。由于 WSGI 接口本身是纯函数,因此无需 Filter 接口就可以通过高阶函数对其包装,完成所有 Filter 的功能。

    Ring 在内部把 Java 标准的 Servlet 接口转换为简单的函数接口。

    清单 16. Ring Handler 函数
    (defn handler [request] {:status 200 :headers {"Content-Type" "text/html"} :body "Hello World"})

    上述函数就完成了 Servlet 实现类的功能。其中 request 是一个 map,返回值也是一个 map,由 :status 、 :headers 和 :body 关键字指定 HTTP 的返回码、头和内容。

    把一系列 handler 函数串起来就形成了一个处理链,每个链都可以对输入和输出进行处理,链的最后一个处理函数负责根据 URL 进行路由,这样,完整的 Web 处理栈就可以构造出来。

    Ring 把 handler 称为 middleware,middleware 基于 Clojure 的函数式编程模型,利用 Clojure 自带的 -> 宏就可以直接串起来。

    一个完整的 Web 程序只需要定义一个 handler 函数,并启动 Ring 内置的 Jetty 服务器即可。

    清单 17. Ring Web App
    ;; hello.clj(ns cljweb.hello (:require [ring.adapter.jetty :as jetty]))(defn handler [request] {:status 200, :headers {"Content-Type" "text/html"} :body "<h1>Hello, world.</h1>"})(defn start-server [] (jetty/run-jetty handler {:host "localhost", :port 3000}))(start-server)

    运行 hello.clj,将启动内置的 Jetty 服务器,然后,打开浏览器,在地址栏输入 http://localhost:3000/ 就可以看到响应。

    图 4. Web App

    handler 函数传入的 request 是一个 map,如果您想查看 request 的内容,可以简单地用 str

    函数返回 request。

    清单 18. 返回 request 信息
    (defn handler [request] {:status 200, :headers {"Content-Type" "text/html"} :body (str request)})

    URL 路由

    要处理不同的 URL 请求,我们就需要在 handler 函数内根据 URL 进行路由。Ring 本身只负责处理底层的 handler 函数,更高级的 URL 路由功能由上层框架完成。

    Compojure 就是轻量级的 URL 路由框架,我们要首先添加 Compojure 的依赖项。

    清单 19. 添加依赖项
    [compojure "1.2.1"]

    Compojure 提供了 defroutes 宏来创建 handler,它接收一系列 URL 映射,然后把它们组装到 handler 函数内部,并根据 URL 路由。一个简单的 handler 定义如下:

    清单 20. 定义 route
    (ns cljweb.routes (:use [compojure.core] [compojure.route :only [not-found]] [ring.adapter.jetty :as jetty]))(defroutes app-routes (GET "/" [] "<h1>Index page</h1>") (GET "/learn/:lang" [lang] (str "<h1>Learn " lang "</h1>")) (not-found "<h1>page not found!</h1>"));; start web server(defn start-server [] (jetty/run-jetty app-routes {:host "localhost", :port 3000}))(start-server)

    该 defroutes 创建了 3 个 URL 映射:

    GET / 处理首页的 URL 请求,它仅仅简单地返回一个字符串;

    GET /learn/:lang 处理符合 /learn/:lang 这种构造的 URL,并且将 URL 中的参数自动作为参数传递进来,如果我们输入 http://localhost:3000/learn/clojure,将得到如下响应:

    图 5. URL 路由

    not-found 处理任何未匹配到的 URL,例如:

    图 6. not-found 路由

    使用模板

    复杂的 HTML 通常不可能在程序中拼接字符串完成,而是通过模板来渲染出 HTML。模板的作用是创建一个使用变量占位符和简单的控制语句的 HTML,在程序运行过程中,根据传入的 model——通常是一个 map,替换掉变量,执行一些控制语句,最终得到 HTML。

    已经有好几种基于 Clojure 创建的模板引擎,但是基于 Django 模板设计思想的 Selmer 最适合 HTML 开发。

    Selmer 的使用十分简单,首先添加依赖。

    清单 21. 添加依赖项
    [selmer "0.7.2"]

    然后创建一个 cljweb.templ 的 namespace 来测试 Selmer。

    清单 22. 使用 Selmer
    (ns cljweb.templ)(use 'selmer.parser)(selmer.parser/cache-off!)(selmer.parser/set-resource-path! (clojure.java.io/resource "templates"))(render-file "test.html" {:title "Selmer Template", :name "Michael", :now (new java.util.Date)})

    在开发阶段,用 cache-off! 关掉缓存,以便使得模板的改动可以立刻更新。

    使用 set-resource-path! 设定模板的查找路径。我们把模板的根目录设置为 clojure.java.io/resource "templates",因此,模板文件的存放位置必须在目录 resources/templates 下。

    图 7. 模板位置

    创建一个 test.html 模板:

    清单 23. 编写 html 模板
    <html><head> <title>{{ title }}</title></head><body> <h1>Welcome, {{ name }}</h1> <p>Time: {{ now|date:"yyyy-MM-dd HH:mm" }}</p></body></html>

    运行代码,可以看到 REPL 打印出了 render-file 函数返回的结果。

    图 8. 模板运行结果

    配置 middleware

    Compojure 可以方便地定义 URL 路由,但是,完整的 Web 应用程序还需要能解析 URL 参数、处理 Cookie、返回 JSON 类型等,这些任务都可以通过 Ring 自带的 middleware 完成。

    我们创建一个 cljweb.web 的 namespace 作为入口,Ring 自带的 middleware 都提供 wrap 函数,可以用Clojure 的 -> 宏把它们串联起来。

    清单 24. 配置 middleware
    (ns cljweb.web (:require [ring.adapter.jetty :as jetty] [ring.middleware.cookies :as cookies] [ring.middleware.params :as params] [ring.middleware.keyword-params :as keyword-params] [ring.middleware.json :as json] [ring.middleware.resource :as resource] [ring.middleware.stacktrace :as stacktrace] [cljweb.templating :as templating] [cljweb.urlhandlers :as urlhandlers]))(def app (-> urlhandlers/app-routes (resource/wrap-resource (clojure.java.io/resource "resources")) ;; static resource templating/wrap-template-response ;; render template json/wrap-json-response ;; render json json/wrap-json-body ;; request json stacktrace/wrap-stacktrace-web ;; wrap-stacktrace-log keyword-params/wrap-keyword-params ;; convert parameter name to keyword cookies/wrap-cookies ;; get / set cookies params/wrap-params ;; query string and url-encoded form ))

    每个 middleware 只负责一个任务,每个 middleware 接受 request,返回 response,它们都有机会修改 request 和 response,因此顺序很重要。

    图 9. middleware 链

    例如,cookies 负责把 request 的 Cookie 字符串解析为 map 并以关键字 :cookies 存储到 request 中,后续的处理程序可以直接从 request 拿到 :cookies。

    图 10. cookie middleware 处理 request

    同时,如果在 response 中找到了 :cookies,就把它转换为 Cookie 字符串并放入 response 的 :headers 中,服务器就会在 HTTP 响应中加上 Set-Cookie 的头。

    图 11. cookie middleware 处理 response

    Ring 没有内置能渲染 Selmer 模板的 middleware,但是 middleware 不过是一个简单的函数,我们可以自己编写一个 wrap-template-response,它在 response 中查找 :body 以及 :body 所包含的 :model 和 :template,如果找到了,就通过 Selmer 渲染模板,并将渲染结果作为 string 放到 response 的 :body 中,服务器就可以读取 response 的 :body 并输出 HTML。

    清单 25. 编写 middleware
    (ns cljweb.templating (:use ring.util.response [selmer.parser :as parser]))(parser/cache-off!)(parser/set-resource-path! (clojure.java.io/resource "templates"))(defn- try-render [response] (let [body (:body response)] (if (map? body) (let [[model template] [(:model body) (:template body)]] (if (and (map? model) (string? template)) (parser/render-file template model))))))(defn wrap-template-response [handler] (fn [request] (let [response (handler request)] (let [render-result (try-render response)] (if (nil? render-result) response (let [templ-response (assoc response :body render-result)] (if (contains? (:headers response) "Content-Type") templ-response (content-type templ-response "text/html;charset=utf-8"))))))))

    处理 REST API

    绝大多数 Web 应用程序都会选择 REST 风格的 API,使用 JSON 作为输入和输出。在 Clojure 中,JSON 可以直接映射到 Clojure 的数据类型 map,因此,只需添加处理 JSON 的相关 middleware 就能处理 REST。首先添加依赖。

    清单 26. 添加依赖项
    [ring/ring-json "0.3.1"]

    在 middleware 中,添加 wrap-json-response 和 wrap-json-body。

    清单 27. 添加 middleware
    (def app (-> urlhandlers/app-routes (resource/wrap-resource (clojure.java.io/resource "resources")) ;; static resource templating/wrap-template-response ;; render template json/wrap-json-response ;; render json json/wrap-json-body ;; request json stacktrace/wrap-stacktrace-web ;; wrap-stacktrace-log keyword-params/wrap-keyword-params ;; convert parameter name to keyword cookies/wrap-cookies ;; get / set cookies params/wrap-params ;; query string and url-encoded form ))

    wrap-json-body 如果读到 Content-Type 是 application/json,就会把 :body 从字符串变为解析后的数据格式; wrap-json-response 如果读到 :body 是一个 map 或者 vector,就会把 :body 序列化为 JSON 字符串,并重置 :body 为字符串,同时添加 Content-Type 为 application/json。

    因此,在 URL 处理函数中,如果要返回 JSON,只需要返回 map,如果要读取 JSON,只需要读取 :body。

    清单 28. 添加 routes
    (defroutes app-routes (GET "/rest/courses" [] (response { :courses (get-courses) })) (POST "/rest/courses" [] (fn [request] (let [c (:body request) id (str "c-" (System/currentTimeMillis))] (create-course! (assoc c :id id, :online true,)) (response (get-course id))))) (not-found "<h1>page not found!</h1>"))

    把数据库操作、模板以及其他的 URL 处理函数都包含进来,我们就创建好了一个完整的基于 Clojure 的 Web 应用程序。

    右键点击项目,在弹出菜单选择“Leiningen”,“Generate Leiningen Command Line”,在弹出的输入框里输入命令:

    图 12. 运行 leiningen 命令
    清单 29. 运行 server
    lein ring server

    将启动 Ring 内置的 Jetty 服务器,并自动打开浏览器,定位到 http://localhost:3000/。

    图 13. 首页

    以这种方式启动服务器的好处是对代码做任何修改,无需重启服务器就可以直接生效,只要在 project.clj 中添加如下内容:

    清单 30. 修改 project.clj
    :ring {:handler cljweb.web/app :auto-reload? true :auto-refresh? true}

    部署

    要在服务器部署 Clojure 编写的 Web 应用程序,有好几种方法,如果用 Leiningen 命令,需要

    把所有源码和依赖项编译并打包成一个独立的 jar 包(可能会很大),打包前需要先编写一个 main 函数并在 project.clj 中指定。

    清单 31. 修改 project.clj
    :main cljweb.web
    清单 32. 打包为 jar
    $ lein uberjar

    把这个 jar 包上传到服务器就可以直接通过 Java 命令运行。

    清单 33. 运行 jar
    $ java -jar cljweb-0.1.0-SNAPSHOT-standalone.jar start

    需要加上参数 start ,因为我们在 main 函数中通过 start 参数来判断是否启动 Jetty 服务器。

    清单 34. 编写 main 函数
    (defn -main [& args] (if (= "start" (first args)) (start-server)))

    如果是以传统的 war 包形式部署,创建一个 .war 文件,部署到标准的 JavaEE 服务器上即可。

    清单 35. 打包 war
    $ lein ring war

    结束语

    Clojure 作为一种运行在 JVM 平台上的 Lisp 方言,它既拥有 Lisp 强大的 S 表达式、宏、函数式编程等特性,又充分利用了 JVM 这种高度优化的虚拟机平台,和传统的 JavaEE 系统相比,Clojure 不仅代码简洁,能极大地提升开发效率,还拥有一种与 JavaEE 所不同的开发模型。传统的 Java 开发人员需要转变固有思维,利用 Clojure 替代 Java,完全可以编写出更简单、更易维护的代码。