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

安装完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是什么

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

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

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,将得到如下响应:

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函数返回的结果:

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

例如,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"))))))))

处理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”,在弹出的输入框里:

输入命令:

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,完全可以编写出更简单,更易维护的代码。

参考资料

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/,官方微博是@廖雪峰。

clojure java.jdbc_Clojure驱动的Web开发相关推荐

  1. .NET、JAVA和PHP在Web开发的优缺点

    .NET.JAVA和PHP在Web开发的优缺点 现在做Web开发,用哪个平台哪种语言其实本质上没有太大的区别,因为Web开发框架已经非常成熟,只要符合需求,能按时交付产品就ok了. 要选择哪个平台,是 ...

  2. java css路径_java web开发中CSS路径有问题吗,运行jsp文件为什么找不到css文件?...

    ---------------------------------------------------------------------------------------------------- ...

  3. java session原理_java web开发—session的工作原理总结

    session的工作原理总结 一.什么是session session是一次浏览器和服务器交互的会话,在jsp中,作为一个内置对象存在.我的理解,就是当用户打开网页时,程序会在浏览器中开辟一段空间来存 ...

  4. java 高性能web_高性能WEB开发 - BearRui(AK-47) 的Blog - BlogJava

    高性能WEB开发 摘要: 用了这么多年的CSS,现在才明白CSS的真正匹配原理,不知道你是否也跟我一样?看1个简单的CSS:DIV#divBox p span.red{color:red;},按习惯我 ...

  5. web开发的java语言步骤_java web开发入门一(servlet和jsp)基于eclispe

    servlet 用java语言开发动态资源网站的技术,在doGet方法中拼接显示html,在doPost方法中提交数据.类似于.net的ashx技术. servlet生成的class文件存放在tomc ...

  6. java中文乱码decode_Java WEB开发中的中文乱码问题解决

    在项目中总是遇到乱码问题,有时候在网上查找到了解决方案,但是没有记录下来为什么出现的乱码.因为出现乱码的方式有好几种,我简单总结一下吧,为以后留着用,也算总结学习一下. 一般来讲,为了处理乱码问题,在 ...

  7. 51CTO专访人人网黄晶:WEB开发需要随需应变(2)

    51CTO专访人人网黄晶:WEB开发需要随需应变(2) http://developer.51cto.com  2010-04-27 16:33  彭凡  51CTO  我要评论(0) 在2010年4 ...

  8. 初学Java Web(2)——搭建Java Web开发环境

    虽然说 html 和 css 等前端技术,是对于 Web 来说不可或缺的技术,但是毕竟更为简单一些,所以就不详细介绍了,没有基础的同学可以去菜鸟教程或者W3school进行自主学习,最好的方式还是做一 ...

  9. Web开发——PHP vs Java

    比较PHP和JSP这两个Web开发技术,在目前的情况是其实是比较PHP和Java的Web开发.以下是我就几个主要方面进行的比较: 一. 语言比较 PHP是解释执行的服务器脚本语言,首先php有简单容易 ...

最新文章

  1. 强悍!使用Flash和Silverlight制作控件
  2. Mol Plant | 中科院遗传与发育生物学研究所周俭民课题组报道了细菌效应蛋白在植物细胞内诱导免疫受体ZAR1寡聚的新发现...
  3. 实战:vue项目中导入swiper插件
  4. 微软 WinGet 抄袭 AppGet 始末,个人开源的困境该如何破?
  5. Nodejs--url模块
  6. 启明云端分享| IDO-SOM2D01-V1-2GW核心板SPI调试总结
  7. .NET 6 中的七个 System.Text.Json 特性
  8. oracle 事务未正常回滚,Spring事务没有回滚异常(Oracle JNDI数据源)
  9. SSH远程登录失败,提示“Password authentication failed”
  10. php request对象,PHP 中TP5 Request 请求对象的实例详解
  11. 计算机组成原理中移码怎么算,计算机组成原理中移码是怎么回事?
  12. Unity3D基础29:消息发送
  13. svn钩子自动化同步代码提交任务
  14. 清华姚班毕业生开发新特效编程语言,99行代码实现《冰雪奇缘》,网友:大神碉堡!创世的快乐
  15. 新hp设备无法连接到计算机,联想的台式机,用的win7系统,无法装惠普1108打印机驱动,一直显示新设备现已连接,然后无限循环!!!...
  16. Unity Navigation--自动寻路、分离路面导航、分层烘培、动态障碍
  17. adb连接手机工具_adb命令——连接手机
  18. oracle 登录失败次数,Oracle用户连续登录失败次数限制如何取消
  19. C++学习资料和视频
  20. Linux驱动调试之修改系统时钟中断定位系统僵死问题

热门文章

  1. Android编译工具Freeline的使用
  2. 领域模型命名规约【PO,VO,POJO,BO,DTO,DO,JavaBean】
  3. leetcode 640. Solve the Equation | 640. 求解方程(字符串处理)
  4. leetcode 115. Distinct Subsequences Hard | 115. 不同的子序列(动态规划)
  5. leetcode 492. 构造矩形(Java版,三种解法)
  6. 牛客网_PAT乙级_1026跟奥巴马一起编程(15)
  7. 小师妹学JavaIO之:try with和它的底层原理
  8. ActiveMQ的自定义安全插件(十)
  9. 分布式事务——TCC 原理
  10. 【测试点4】基础实验4-2.8 部落 (25 分)