本文最早发布在IBM developerWorks。
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运行在JVM上,我们只需要准备好JDK和Java标配的Eclipse开发环境,就可以开始Clojure开发了!
我们的开发环境是:
Java 8 SDK:可以从Oracle官方网站下载最新64位版本;
Eclipse Luna SR1:可以从Eclipse官方网站下载Eclipse IDE for Java Developers最新64位版本。
安装完JDK后,通过命令java -version
确认JDK是否正确安装以及版本号:
$ java -version
java 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
安装:
安装完Counterclockwise后需要重启Eclipse,然后,我们就可以新建一个Clojure Project了!
选择菜单“File”-“New”-“Project...”,选择“Clojure”-“Clojure Project”,填入名称“cljweb”,创建一个新的Clojure Project:
找到project.clj
文件,把:dependencies
中的Clojure版本由1.5.1
改为最新版1.6.0
:
(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是Clojure的项目构建工具,类似于Maven。事实上,Leiningen底层完全使用Maven的包管理机制,只是Leiningen的构建脚本不是pom.xml
,而是project.clj
,它本身就是Clojure代码。
如果Leiningen没有自动运行,你可以点击菜单“Project”-“Build Automatically”,勾上后就会让Leiningen在源码改动后自动构建整个工程。
在src
目录下找到自动生成的core.clj
文件,注意到已经生成了如下代码:
(ns cljweb.core)
(defn foo
"I don't do a whole lot."
[x]
(println x "Hello, World!"))
只需要添加一行代码,调用foo
函数:
(println (foo "Clojure"))
然后,点击菜单“Run”-“Run”就可以直接运行了:
Leiningen会启动一个REPL,并设置好classpath。第一次REPL启动会比较慢,原因是JVM的启动速度慢。在REPL中可以看到运行结果。REPL窗口本身还支持直接运行Clojure代码,这样你可以直接在REPL中测试代码,能极大地提高开发效率。
Clojure和Java最大的区别在于Clojure的函数是头等公民,并完全支持函数式编程。Clojure自身提供了一系列内置函数,使得编写的代码简洁而高效。
我们随便写几个函数来看看:
;; 定义自然数序列
(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的“惰性计算”特性,我们可以极其简洁地构造一个无限序列,然后通过高阶函数做任意操作:
;; 打印前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.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
文件:
$ java -cp ~/.m2/repository/org/clojure/clojure/1.6.0/clojure-1.6.0.jar clojure.main cljweb/core.clj
Clojure: 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代码:
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”,填入名称db
,就可以创建一个db.clj
文件。
在编写代码前,我们首先要在project.clj
文件中添加依赖项:
[org.clojure/java.jdbc "0.3.6"]
[mysql/mysql-connector-java "5.1.25"]
[korma "0.3.0"]
使用Korma操作数据库十分简单,只需要先引用Korma:
(ns cljweb.db
(:use korma.db
korma.core))
定义数据库连接的配置信息:
(defdb korma-db (mysql {:db "test",
:host "localhost",
:port 3306,
:user "www",
:password "www"}))
然后定义一下要使用的entity,也就是表名:
(declare courses)
(defentity courses)
现在,就可以对数据库进行操作了。插入一条记录:
(insert courses
(values { :id "s-201", :name "SQL", :price 99.9, :online false, :days 30 })))
使用Clojure内置的map类型,十分直观。
查询语句通过select
宏实现了SQL DSL到Clojure代码的自然映射:
(select courses
(where {:online false})
(order :name :asc)))
这完全得益于Lisp的S表达式的威力,既不需要直接拼凑SQL,也不需要重新发明类似HQL的语法。
利用Korma的提供的sql-only
和dry-run
,可以打印出生成的SQL语句,但实际并不执行。
传统的JavaEE使用Servlet
接口来划分服务器和应用程序的界限,应用程序负责提供实现Servlet
接口的类,服务器负责处理HTTP连接并转换为Servlet
接口所需的HttpServletRequest
和HttpServletResponse
。Servlet
接口定义十分复杂,再加上Filter
,所需的XML配置复杂度很高,而且测试困难。
Clojure的Web实现最常用的是Ring。Ring的设计来自Python的WSGI和Ruby的Rack,以WSGI为例,其接口设计十分简单,仅一个函数:
def application(env, start_response)
其中env
是一个字典,start_response
是响应函数。由于WSGI接口本身是纯函数,因此无需Filter
接口就可以通过高阶函数对其包装,完成所有Filter
的功能。
Ring在内部把Java标准的Servlet
接口转换为简单的函数接口:
(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服务器即可:
;; hello.clj
(ns cljweb.hello
(:require [ring.adapter.jetty :as jetty]))
(defn handler [request]
{:status 200,
:headers {"Content-Type" "text/html"}
:body "Hello, world.
"})
(defn start-server []
(jetty/run-jetty handler {:host "localhost",
:port 3000}))
(start-server)
运行hello.clj
,将启动内置的Jetty服务器,然后,打开浏览器,在地址栏输入http://localhost:3000/
就可以看到响应:
handler函数传入的request
是一个map,如果你想查看request
的内容,可以简单地返回:
(defn handler [request]
{:status 200,
:headers {"Content-Type" "text/html"}
:body (str request)})
要处理不同的URL请求,我们就需要在handler函数内根据URL进行路由。Ring本身只负责处理底层的handler函数,更高级的URL路由功能由上层框架完成。
Compojure就是轻量级的URL路由框架,我们要首先添加Compojure的依赖项:
[compojure "1.2.1"]
Compojure提供了defroutes
宏来创建handler,它接收一系列URL映射,然后把它们组装到handler函数内部,并根据URL路由。一个简单的handler定义如下:
(ns cljweb.routes
(:use [compojure.core]
[compojure.route :only [not-found]]
[ring.adapter.jetty :as jetty]))
(defroutes app-routes
(GET "/" [] "Index page
")
(GET "/learn/:lang" [lang] (str "Learn " lang "
"))
(not-found "page not found!
"))
;; 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
,将得到如下响应:
not-found
处理任何未匹配到的URL,例如:
复杂的HTML通常不可能在程序中拼接字符串完成,而是通过模板来渲染出HTML。模板的作用是创建一个使用变量占位符和简单的控制语句的HTML,在程序运行过程中,根据传入的model——通常是一个map,替换掉变量,执行一些控制语句,最终得到HTML。
已经有好几种基于Clojure创建的模板引擎,但是基于Django模板设计思想的Selmer最适合HTML开发。
Selmer的使用十分简单。首先添加依赖:
[selmer "0.7.2"]
然后创建一个cljweb.templ
的namespace来测试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
下:
创建一个test.html
模板:
{{ title }} Welcome, {{ name }}
Time: {{ now|date:"yyyy-MM-dd HH:mm" }}
运行代码,可以看到REPL打印出了render-file
函数返回的结果:
Compojure可以方便地定义URL路由,但是,完整的Web应用程序还需要能解析URL参数、处理Cookie、返回JSON类型等,这些任务都可以通过Ring自带的middleware完成。
我们创建一个cljweb.web
的namespace作为入口,Ring自带的middleware都提供wrap
函数,可以用Clojure的->
宏把它们串联起来:
(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
,因此顺序很重要:
例如,cookies负责把request
的Cookie字符串解析为map并以关键字:cookies
存储到request
中,后续的处理程序可以直接从request
拿到:cookies
:
同时,如果在response
中找到了:cookies
,就把它转换为Cookie字符串并放入response
的:headers
中,服务器就会在HTTP响应中加上Set-Cookie
的头:
Ring没有内置能渲染Selmer模板的middleware,但是middleware不过是一个简单的函数,我们可以自己编写一个wrap-template-response
,它在response
中查找:body
以及:body
所包含的:model
和:template
,如果找到了,就通过Selmer渲染模板,并将渲染结果作为string放到response
的:body
中,服务器就可以读取response
的:body
并输出HTML:
(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"))))))))
绝大多数Web应用程序都会选择REST风格的API,使用JSON作为输入和输出。在Clojure中,JSON可以直接映射到Clojure的数据类型map,因此,只需添加处理JSON的相关middleware就能处理REST。首先添加依赖:
[ring/ring-json "0.3.1"]
在middleware中,添加wrap-json-response
和wrap-json-body
:
(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
:
(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 "page not found!
"))
把数据库操作、模板以及其他的URL处理函数都包含进来,我们就创建好了一个完整的基于Clojure的Web应用程序。
右键点击项目,在弹出菜单选择“Leiningen”,“Generate Leiningen Command Line”,在弹出的输入框里:
输入命令:
lein ring server
将启动Ring内置的Jetty服务器,并自动打开浏览器,定位到http://localhost:3000/
:
以这种方式启动服务器的好处是对代码做任何修改,无需重启服务器就可以直接生效,只要在project.clj
中加上:
:ring {:handler cljweb.web/app
:auto-reload? true
:auto-refresh? true}
要在服务器部署Clojure编写的Web应用程序,有好几种方法,一种是用Leiningen命令:
$ lein uberjar
把所有源码和依赖项编译并打包成一个独立的jar包(可能会很大),打包前需要先编写一个main
函数并在project.clj
中指定:
:main cljweb.web
把这个jar包上传到服务器上就可以直接通过Java命令运行:
$ java -jar cljweb-0.1.0-SNAPSHOT-standalone.jar start
需要加上参数start
是因为我们在main
函数中通过start
参数来判断是否启动Jetty服务器:
(defn -main [& args]
(if (= "start" (first args))
(start-server)))
要以传统的war
包形式部署,可以使用命令:
$ lein ring war
这将创建一个.war
文件,部署到标准的JavaEE服务器上即可。
Clojure作为一种运行在JVM平台上的Lisp方言,它既拥有Lisp强大的S表达式、宏、函数式编程等特性,又充分利用了JVM这种高度优化的虚拟机平台,和传统的JavaEE系统相比,Clojure不仅代码简洁,能极大地提升开发效率,还拥有一种与JavaEE所不同的开发模型。传统的Java开发人员需要转变固有思维,利用Clojure替代Java,完全可以编写出更简单,更易维护的代码。
源码下载:https://github.com/michaelliao/cljweb/
Clojure官方网站:了解并下载Clojure的最新版本;
Leiningen官方网站:了解并下载Leiningen的最新版本;
Korma官方网站:获取Korma源码并阅读在线文档;
Ring官方网站:获取Ring源码并阅读在线文档;
Compojure官方网站:获取Compojure源码并阅读在线文档。
廖雪峰,精通Java/Objective-C/Python/C#/Ruby/Lisp,独立iOS开发者,对开源框架有深入研究,著有《Spring 2.0核心技术与最佳实践》一书,其官方博客是http://www.liaoxuefeng.com/,官方微博是@廖雪峰。