讀古今文學網 > Java程序員修煉之道 > 13.7 我是不是一隻水獺 >

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 org.mortbay.jetty.HttpParser$Input@206bc833>}
  

從這個請求映射中能看到容器已經把上傳的文件內容放到了/var/tmp的臨時文件中。可以通過(:tempfile (get (:multipart-params req) \"file\"))訪問相應的File對象。然後簡單地用clojure.contrib.duck-streams中的(copy)函數把它保存到文件系統中。

水獺投票不大,但它是一個完整的應用程序。在本節開頭提出的功能性和非功能性需求的限定下,它的表現符合我們的預期。我們對Compojure及一些相關類庫的探索就到此為止了。