首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》13.7 我是不是一只水獭

关灯直达底部

互联网上似乎有两件事永远都不会让人厌烦:在线投票和可爱的动物图片。有个创业公司想把这两件事结合起来,让人们给水獭图片投票,然后靠广告回报赚钱,他们雇了你。勇敢面对吧,这毕竟还算不上是创业公司所尝试过的最傻的主意。

我们先想想这个水獭投票网站所需的基本页面和功能:

  • 网站首页应该展示两张水獭供用户选择;
  • 用户应该能给自己喜欢的那只水獭投票;
  • 应该有个单独的页面允许用户上传水獭的新照片;
  • 应该有个仪表板页面显示每张水獭图片的当前得票。

图13-7中展示了如何安排构成应用的页面和HTTP请求。

图13-7 “我是不是一只水獭?”的页面流

我们暂不考虑该应用的非功能性需求。

  • 该网站不做访问控制。

  • 对新上传的水獭图片文件不做安全检查。它们会以图片的形式在页面上显示,但上传对象的内容或安全性都没有经过检查。我们相信用户,他们不会上传任何不合适的东西。

  • 该网站没有持久化。如果Web容器崩溃了,所有投票数据就都没了。但在应用启动时,它会扫描硬盘,预先填充水獭图片的存储。

尽管我们会在这一章中介绍其中的重要文件,但github.com上就有这个项目,你可能会发现那个更好用。

13.7.1 项目设置

要开始这个Compojure项目,需要定义基本项目:它的依赖项、路由,还有一些页面函数。我们先来看看project.clj文件,如代码清单13-10所示。

代码清单13-10 项目project.clj

(defproject am-i-an-otter /"1.0.0-SNAPSHOT/"    :description /"Am I an Otter or Not?/"    :dependencies [[org.clojure/clojure /"1.2.0/"]                   [org.clojure/clojure-contrib /"1.2.0/"]                   [compojure /"0.6.2/"]                   [hiccup /"0.3.4/"]                   [log4j /"1.2.15/" :exclusions [javax.mail/mail                                                javax.jms/jms                                                com.sun.jdmk/jmxtools                                                com.sun.jmx/jmxri]]                   [org.slf4j/slf4j-api /"1.5.6/"]                   [org.slf4j/slf4j-log4j12 /"1.5.6/"]]    :dev-dependencies [[lein-ring /"0.4.0/"]]    :ring {:handler am-i-an-otter.core/app})  

这个文件中没什么新鲜玩意,除了log4j类库,其他在前面的例子里都有。

接下来我们看看core.clj文件里的连接和路由逻辑,如代码清单13-11所示。

代码清单13-11 core.clj的路由

(ns am-i-an-otter.core  (:use compojure.core)  (:require [compojure.route :as route]            [compojure.handler :as handler]            [ring.middleware.multipart-params :as mp]))(load /"imports/") ﹃导入函数 (load /"otters-db/")(load /"otters/") ﹄导入函数 (defroutes main-routes //主路由  (GET /"//"  (page-compare-otters))  (GET [/"/upvote/:id/", :id #/"[0-9]+/" ] [id] (page-upvote-otter id))  (GET /"/upload/"  (page-start-upload-otter))  (GET /"/votes/"  (page-otter-votes))  (mp/wrap-multipart-params //文件上传处理程序     (POST /"/add_otter/" req (str (upload-otter req)(page-start-upload-otter))))  (route/resources /"//")  (route/not-found /"Page not found/"))(def app  (handler/site main-routes))  

文件上传处理程序展示了一种新的参数处理方式。我们在下一小节还会展开来讲,但现在,可以把它看做“将整个HTTP请求传给页面函数处理”。

core.clj中的关联关系让你可以看清哪个页面函数跟哪个URL相关。所有页面函数都以page打头——这只是函数命名的惯例。

代码清单13-12给出了该应用程序的页面函数。

代码清单13-12 项目的页面函数

(ns am-i-an-otter.core  (:use compojure.core)  (:use hiccup.core))(defn page-compare-otters   //水獭比较页面  (let [otter1 (random-otter), otter2 (random-otter)]    (.info (get-logger) (str /"Otter1 = /" otter1 /" ; Otter2 = /"otter2 /" ; /" otter-pics))   (html [:h1 /"Otters say /'Hello Compojure!/'/"]         [:p [:a {:href (str /"/upvote//" otter1)}                 [:img {:src (str /"/img//"(get otter-pics otter1))} ]]]         [:p [:a {:href (str /"/upvote//" otter2)}                 [:img {:src (str /"/img//"(get otter-pics otter2))} ]]]         [:p /"Click /" [:a {:href /"/votes/"} /"here/"]             /" to see the votes for each otter/"]         [:p /"Click /" [:a {:href /"/upload/"} /"here/"]              /" to upload a brand new otter/"])))(defn page-upvote-otter [id] //处理投票    (let [my-id id]    (upvote-otter id)    (str (html [:h1 /"Upvoted otterUpload a new otter/"]        [:p [:form {:action /"/add_otter/" :method /"POST/" :enctype /"multipart/form-data/"} //设置表单             [:input {:name /"file/" :type /"file/" :size /"20/"}]            [:input {:name /"submit/" :type /"submit/" :value /"submit/"}]]]        [:p /"Or click /" [:a {:href /"//"} /"here/" ] /" to vote on some otters/"]))(defn page-otter-votes   //显示投票结果  (let   (.debug (get-logger) (str /"Otters: /" @otter-votes-r))  (html [:h1 /"Otter Votes/" ]        [:p#votes.otter-votes         (for [x (keys @otter-votes-r)]            [:p [:img {:src (str /"/img//" (get otter-pics x))} ](get @otter-votes-r x)])]))) 

代码中还有两个Hiccup特性。第一个可以对一组元素进行循环,在这儿是刚上传的水獭图片。Hiccup在下面的代码片段中表现得非常像简单的模板语言(带有嵌入的(for)形态):

[:p#votes.otter-votes  (for [x (keys @otter-votes-r)]    [:p [:img {:src (str /"/img//" (get otter-pics x))} ]  (get @otter-votes-r x)])]  

第二个特性是:p#votes.otter-votes语法。这是指明某一标签的idclass属性的快捷办法。它会变成HTML标签<p>。开发人员可以借此把最可能由CSS使用的属性分离出来,不会让HTML结构变得太乱。

CSS和其他代码(比如JavaScript源文件)通常会放在静态内容目录中等待读取。在Compojure项目中默认是在resources/public目录下。

HTTP方法的选择

水獭投票这个例子在架构上有缺陷。我们为投票页面指定的路由规则是GET规则。这是错误的。应用程序绝不应该用GET请求修改服务器端的状态(比如水獭的投票数)。因为Web浏览器在觉得服务器没有响应时是可以重发GET请求的(比如当请求进来时它正因为垃圾收集而暂停呢)。这一重发请求的行为可能会导致同一水獭收到重复投票,可实际上用户只点了一次。对于电子商务应用来说,这会引发灾难!记住这条原则:有意义的服务器端状态绝不能用GET请求修改。

我们已经看过了关联起来的应用和它的路由,以及页面函数。我们再来看一些处理水獭投票的后台函数,继续讨论这个应用。

13.7.2 核心函数

在讨论应用的核心功能时,我们提到应用应该扫描图片目录找出磁盘里已有的水獭图片。代码清单13-13是扫描目录并进行预填充的代码。

代码清单13-13 目录扫描函数

(def otter-img-dir /"resources/public/img//")(def otter-img-dir-fq  (str (.getAbsolutePath (File. /"./")) /"//" otter-img-dir))(defn make-matcher [pattern]  (.getPathMatcher (FileSystems/getDefault) (str /"glob:/" pattern)))(defn file-find [file matcher] //如果匹配,返回去掉两边空格的文件名  (let [fname (.getName file (- (.getNameCount file) 1))]    (if (and (not (nil? fname)) (.matches matcher fname))      (.toString fname) //用 (toString)启用:img标签      nil)))(defn next-map-id [map-with-id] //取下一个水獭的ID  (+ 1 (nth (max (let [map-ids (keys map-with-id)]    (if (nil? map-ids) [0] map-ids))) 0 )))(defn alter-file-map [file-map fname]   (assoc file-map (next-map-id file-map) fname)) //修改函数并将文件名加到映射中(defn make-scanner [pattern file-map-r] //返回扫描器   (let [matcher (make-matcher pattern)]    (proxy [SimpleFileVisitor]       (visitFile [file attribs] //在所有文件上执行的回调函数        (let [my-file file,              my-attrs attribs,              file-name (file-find my-file matcher)]          (.debug (get-logger) (str /"Return from file-find /" file-name))          (if (not (nil? file-name))             (dosync (alter file-map-r alter-file-map file-name) file-map-r)             nil)          (.debug (get-logger) (str /"After return from file-find /" @file-map-r))          FileVisitResult/CONTINUE))        (visitFileFailed [file exc] (let [my-file file my-ex exc]          (.info (get-logger)            (str /"Failed to access file /" my-file /" ; Exception: /" my-ex))                  FileVisitResult/CONTINUE)))))(defn scan-for-otters [file-map-r]  (let [my-map-r file-map-r]    (Files/walkFileTree (Paths/get otter-img-dir-fq (into-array String )) (make-scanner /"*.jpg" my-map-r))     my-map-r))(def otter-pics (deref (scan-for-otters (ref {})))) //设置水獭图片  

这段代码的入口是(scan-for-otters)。它用Java 7中的Files类从otter-img-dir-fq开始遍历文件系统,并返回一个映射。这里用了一个简单的惯例,以-r结束的标记名称表示这是对某个结构的引用。

遍历文件的代码是SimpleFileVisitor类(在java.nio.file包中)的Clojure代理,这个类在第2章就出现过。我们自行实现了其中两个方法:(visitFile)(visitFileFailed),对这个例子来说足够了。

其他有趣的函数是实现投票功能的那些,如代码清单13-14所示。

代码清单13-14 水獭投票函数

(def otter-votes-r (ref {}))(defn otter-exists [id] (contains? (set (keys otter-pics)) id))(defn alter-otter-upvote [vote-map id]  (assoc vote-map id (+ 1 (let [cur-votes (get vote-map id)]    (if (nil? cur-votes) 0 cur-votes)))))(defn upvote-otter [id]  (if (otter-exists id)    (let [my-id id]      (.info (get-logger) (str /"Upvoted Otter /" my-id))      (dosync (alter otter-votes-r alter-otter-upvote my-id)otter-votes-r))      (.info (get-logger) (str /"Otter /" id /" Not Found /" otter-pics))))(defn random-otter  (rand-nth (keys otter-pics)))(defn upload-otter [req]  (let [new-id (next-map-id otter-pics),        new-name (str (java.util.UUID/randomUUID) /".jpg"), //赋予随机文件名        tmp-file (:tempfile➥ (get (:multipart-params req) /"file/"))] //提取临时文件   (.debug (get-logger) (str (.toString req) /" ; New name = /"➥ new-name /" ; New id = /" new-id))   (ds/copy tmp-file (ds/file-str ➥ (str otter-img-dir new-name))) //复制到文件系统中(def otter-pics (assoc otter-pics new-id new-name))(html [:h1 /"Otter Uploaded!/"]))) 

(upload-otter)函数中处理的是完整的HTTP请求映射。其中有很多信息可供Web开发人员使用,不过有些可能是你已经熟悉的了:

{:remote-addr /"127.0.0.1/", :scheme :http, :query-params {}, :session {}, :form-params {}, :multipart-params {/"submit/" /"submit/", /"file/" {:filename /"otter_kids.jpg",    :size 122017, :content-type /"image/jpeg/", :tempfile #<File /var/tmp/    upload_646a7df3_12f5f51ff33__8000_00000000.tmp>}}, :request-method :post, :query-string nil, :route-params {}, :content-type /"multipart/form-data; boundary=----    WebKitFormBoundaryvKKZehApamWrVFt0/", :cookies {}, :uri /"/add_otter/", :server-name /"127.0.0.1/", :params {:file {:filename /"otter_kids.jpg", :size 122017, :content-type    /"image/jpeg/", :tempfile #<File /var/tmp/    upload_646a7df3_12f5f51ff33__8000_00000000.tmp>}, :submit /"submit/"}, :headers {/"user-agent/" /"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6;    en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.205    Safari/534.16/", /"origin/" /"http://127.0.0.1:3000/", /"accept-charset/" /"ISO-    8859-1,utf-8;q=0.7,*;q=0.3/", /"accept/" /"application/xml,application/    xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5/", /"host/"    /"127.0.0.1:3000/", /"referer/" /"http://127.0.0.1:3000/upload/", /"contenttype/"     /"multipart/form-data; boundary=----    WebKitFormBoundaryvKKZehApamWrVFt0/", /"cache-control/" /"max-age=0/",    /"accept-encoding/" /"gzip,deflate,sdch/", /"content-length/" /"122304/",    /"accept-language/" /"en-US,en;q=0.8/", /"connection/" /"keep-alive/"}, :content-length 122304, :server-port 3000, :character-encoding nil, :body #<Input [email protected]>}  

从这个请求映射中能看到容器已经把上传的文件内容放到了/var/tmp的临时文件中。可以通过(:tempfile (get (:multipart-params req) /"file/"))访问相应的File对象。然后简单地用clojure.contrib.duck-streams中的(copy)函数把它保存到文件系统中。

水獭投票不大,但它是一个完整的应用程序。在本节开头提出的功能性和非功能性需求的限定下,它的表现符合我们的预期。我们对Compojure及一些相关类库的探索就到此为止了。