IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Clojure驱动的Web开发

    廖雪峰发表于 2014-12-21 04:21:00
    love 0

    本文最早发布在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开发环境

    由于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安装:

    find-counterclockwise

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

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

    new-clj-proj

    找到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是什么

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

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

    第一个Clojure版Hello World

    在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”就可以直接运行了:

    run-clj-code

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

    Clojure函数式编程

    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自身只是一个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语句,但实际并不执行。

    Web接口

    传统的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/就可以看到响应:

    7-hello

    handler函数传入的request是一个map,如果你想查看request的内容,可以简单地返回:

    (defn handler [request]
          {:status 200,
           :headers {"Content-Type" "text/html"}
           :body (str request)})
    

    URL路由

    要处理不同的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,将得到如下响应:

    cljweb-url-learn

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

    cljweb-url-notfound

    使用模板

    复杂的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下:

    selmer-template

    创建一个test.html模板:

    
    
        {{ title }}
    
    
        

    Welcome, {{ name }}

    Time: {{ now|date:"yyyy-MM-dd HH:mm" }}

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

    clj-repl-test-selmer

    配置middleware

    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,因此顺序很重要:

    middlewares-chain

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

    cookie-request

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

    cookie-response

    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"))))))))
    

    处理REST API

    绝大多数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”,在弹出的输入框里:

    run-lein-cmd

    输入命令:

    lein ring server
    

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

    cljweb-index

    以这种方式启动服务器的好处是对代码做任何修改,无需重启服务器就可以直接生效,只要在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/,官方微博是@廖雪峰。



沪ICP备19023445号-2号
友情链接