Clojure w aplikacji webowej z Compojure oraz Ring i Hiccup

Z Jacek Laskowski - Wiki Projektanta Java EE

Nie ma co kryć, że tworzenie aplikacji webowej z Clojure i Java EE jak przedstawiłem w Clojure w aplikacji webowej Java EE 6 z Eclipse Helios, Apache Maven i Apache Tomcat nie należy do szczególnie zachęcających do skorzystania, a funkcjonalnie daleko jej do zaawansowanej. Innymi słowy, dużo pracy, aby ostatecznie zyskać niewiele (w tym konkretnym przypadku, aczkolwiek może to stanowić ciekawą bazę wypadową do dalszych doświadczeń).

Tym razem zaprezentuję alternatywne podejście do tworzenia aplikacji webowych z Clojure i rozwiązaniami wspierającymi opartymi na nim - Compojure, Ring oraz Hiccup. W skrócie, Compojure to szkielet webowy, Ring to mechanizm uruchamiający środowisko kontenera webowego (w naszym przypadku będzie to Jetty) oraz Hiccup do tworzenia szablonów stron webowych. Po więcej informacji zapraszam na odpowiednie strony domowe projektów (które nota bene są we wczesnej fazie rozwojowej i większość z nich to projekty na GitHubie).

Przestrzegam, że Java EE występuje jedynie w formie kontenera servletów Jetty, a wszystko, co zostanie zaprezentowane, to czyste programowanie w Clojure. Za wszelkie uszczerbki na zdrowiu autor niniejszego artykułu nie ponosi jakiejkolwiek odpowiedzialności :-)

Ach, chciałbym móc napisać, że ta treść jest wynikiem mojej niezwykłej zdolności tworzenia, ale cóż, prawda jest taka, że na artykuł złożyła się praca innych, wielkich, począwszy od Daniela Janusa, który zapoczątkował moje poznawanie Compojure po komentarzu do wpisu Clojure w aplikacji webowej Java EE 6 z Eclipse Helios, Apache Maven i Apache Tomcat z 19. lipca, a skończywszy na nieprzebranej skarbnicy wiedzy o Clojure i okolicach - grupie użytkowników Clojure. Warto również wskazać na zbiór przykładowych aplikacji webowych z Compojure, Ring i Hiccup pod adresem http://github.com/abedra/clojure-web.

Co jest niezwykle odświeżającym doświadczeniem, które pozostawia niezapomniane wrażenia po przeczytaniu tego artykułu, to fakt, że stworzenie aplikacji webowej w Clojure nie wymaga bardzo rozległej wiedzy. Szkielety webowe w Clojure składa się z interesujących i potrzebnych funkcjonalności, co stoi w przeciwieństwie do bardzo rozbudowanych (i wielokrotnie "spłycanych" do wykorzystania nielicznych funkcjonalności) szkieletów javowych. To może być cenione i ganione jednocześnie. Dla mnie, początkowo, taki sposób budowania aplikacji webowych przypominał programowanie w assemblerze, ale nie trwało długo, aż zdałem sobie sprawę, że może doświadczam niepotrzebnego zwątpienia, bo być może tylko tyle mi akurat potrzeba i szczęśliwie dobrałem właściwe narzędzie do problemu (a nie próbowałem dopasowywać problem do narzędzi). Ocenę pozostawiam Tobie.

Spis treści

Utworzenie projektu webowego z Leiningen - witaj-clojure-webapp

Do zarządzania projektami w Javie korzystamy z Apache Ant, Apache Maven, Apache Ivy i kilku innych, mniej lub bardziej zaawansowanych i rozpoznawalnych projektów. W Clojure mamy Leiningen. Instalacja narzędzia sprowadza się do pobrania pojedynczego pliku lein, któremu nadajemy prawa wykonywania i umieszczamy w ścieżce aplikacji PATH, aby ostatecznie wykonać polecenie inicjujące lein self-install.

Poprawna instalacja pozwala na wykonanie polecenia lein version.

devmac:sandbox jacek$ lein version
Leiningen 1.3.1 on Java 1.6.0_20 Java HotSpot(TM) 64-Bit Server VM

Kolejnym krokiem jest stworzenie projektu - struktury katalogowej dla naszej aplikacji webowej - poleceniem lein new pl.jaceklaskowski.clojure/witaj-clojure-webapp.

devmac:sandbox jacek$ lein new pl.jaceklaskowski.clojure/witaj-clojure-webapp
Created new project in: witaj-clojure-webapp

Przechodzimy do katalogu witaj-clojure-webapp, wykonujemy polecenie lein pom, aby umożliwić użycie NetBeans IDE jako zintegrowanego środowiska programistycznego z zainstalowaną wtyczką enclojure. W nim dokonamy odpowiednich modyfikacji. Można również skorzystać z Eclipse z wtyczką counterclockwise (CCW), jednakże tylko NetBeans i enclojure mają możliwość podłączenia się do zdalnej konsoli Clojure REPL, aby wprowadzać zmiany w działającej aplikacji (!) W zasadzie, był to jedyny powód, dla którego zdecydowałem się na NetBeans IDE z enclojure.

devmac:witaj-clojure-webapp jacek$ lein pom
Wrote pom.xml

UWAGA: Domyślnie lein tworzy bardzo podstawową konfigurację, która za moment zostanie zmieniona w NetBeans IDE i jedynie, aby nie korzystać z edytorów typu vi, już w tym momencie importujemy projekt. Zwykle (acz nie nazwałbym tego bardziej profesjonalnym podejściem) najpierw edytujemy główny plik konfiguracji projektu, a dopiero po tym importujemy projekt do IDE.

Jako ciekawostkę podam, że Leiningen nie pozwala na tworzenie projektów, których nazwa zawiera ciąg znaków "jure" - świadoma decyzja twórcy narzędzia.

Import projektu do NetBeans IDE

Importujemy projekt do NetBeans IDE jako projekt mavenowy - File > Open Project... (Shift+Jabłko+O) i wskazujemy na katalog projektu.

Plik:netbeans-compojure-open-project.png

NetBeans otworzy dwa pliki core.clj oraz project.clj. Na chwilę obecną interesuje nas ten drugi - project.clj - jest to odpowiednik pliku pom.xml w Apache Maven czy build.xml w Apache Ant. Na bazie tego pliku został stworzony plik pom.xml do zaimportowania projektu do NetBeans IDE.

UWAGA: Jeśli plik project.clj nie został automatycznie otworzony, przechodzimy do zakładki Files i odszukujemy go tam.

W pliku project.clj umieszczamy następującą treść, która definiuje zależności projektowe oraz informacje pomocnicze - opis (atrybut :description) i główną przestrzeń nazewniczą Clojure (atrybut :main).

(defproject pl.jaceklaskowski.clojure/witaj-clojure-webapp "1.0.0-SNAPSHOT"
  :description "Aplikacja webowa typu 'Witaj Świecie' w Clojure z Compojure"
  :main pl.jaceklaskowski.clojure.witaj-clojure-webapp.core
  :dependencies [[org.clojure/clojure "1.2.0"]
                 [org.clojure/clojure-contrib "1.2.0"]
                 [ring/ring-jetty-adapter "0.2.5"]
                 [hiccup "0.2.6"]
                 [compojure "0.4.1"]]
  :dev-dependencies [[swank-clojure "1.3.0-SNAPSHOT"]])

Z tak zmodyfikowanym plikiem project.clj, ponownie generujemy plik pom.xml, który jest zrozumiały dla NetBeans IDE i dzięki któremu poinformujemy środowisko o wymaganych zależnościach i informacjach dodatkowych.

Przechodzimy na poziom linii poleceń i ponownie wykonujemy polecenie lein pom.

devmac:witaj-clojure-webapp jacek$ lein pom
Wrote pom.xml

NetBeans powinien odświeżyć projekt i w węźle Libraries wyświetlić wszystkie wymagane zależności.

Plik:netbeans-compojure-libraries.png

Od tej pory korystanie z linii poleceń jest opcjonalne i traktowane jako...samookaleczanie się :)

Edycja głównego pliku aplikacji Compojure - pl.jaceklaskowski.clojure.witaj_clojure_webapp/core.clj

Modyfikujemy plik src/pl.jaceklaskowski.clojure.witaj_clojure_webapp/core.clj, tak aby zawierał następującą treść:

(ns pl.jaceklaskowski.clojure.witaj-clojure-webapp.core
  "Bardzo prosta(cka) aplikacja webowa w Compojure"
  (:use [compojure.core :only [defroutes GET]])
  (:use [compojure.route :as route])
  (:use [ring.adapter.jetty :only [run-jetty]])
  (:use [hiccup.core]))
 
(defn display []
  (html [:h1 "Witaj, tu Clojure z Compojure!"]))
 
(defroutes main-routes
 (GET "/" [] (display))
 (not-found "<b>Nie znaleziono</b>"))
 
(defonce server (run-jetty main-routes
                           {:join? false
                            :port 8080}))

Zrozumienie jej nie powinno nastręczać trudności, zważywszy fakt, że głównym jego elementem jest wywołanie makra defroutes i uruchomienie serwera w defonce z run-jetty. W defroutes określamy konfigurację "tablicy żądań", w której akcja GET obsługiwana jest przez funkcję display, a pozostałe przypadki są obsłużone przez funkcję not-found (oferowaną bezpośrednio przez Compojure). Wykonanie makra defonce służy uruchomieniu Jetty (z możliwością przeładowywania przestrzeni nazewniczej bez ponownego uruchamiania serwera).

Uruchomienie aplikacji webowej

Uruchamiamy aplikację za pomocą menu Start Project REPL i wykonaniu polecenia (require 'pl.jaceklaskowski.clojure.witaj-clojure-webapp.core).

UWAGA: Może się zdarzyć, że próba uruchomienia REPL z poziomu NetBeans zakończy się komunikatem błędu o upłynięciu czasu oczekiwania na REPL. Nie udało mi się rozwiązać, co powoduje wystąpienie tego błędu.

W oknie Clojure REPL (witaj-clojure-webapp) wykonujemy polecenie (require 'pl.jaceklaskowski.clojure.witaj-clojure-webapp.core).

Plik:netbeans-compojure-clojure-repl-repl.png

W zakładce *err* możemy przekonać się, że tym samym uruchomiliśmy serwer Jetty na porcie 8080.

Plik:compojure-netbeans-repl-err.png

Uruchamiamy przeglądarkę i wskazujemy na adres http://localhost:8080/.

Plik:compojure-chrome-strona-startowa.png

Wskazanie na jakąkolwiek inną stronę w aplikacji, np. http://localhost:8080/cokolwiek, zakończy się błędem 404 (w Google Chrome będzie to strona z możliwością wyszukania).

Plik:compojure-chrome-404.png

Przegląd treści strony - View Page Source - upewni nas jednak, że strona faktycznie zawiera nasz komunikat błędu (pewnie projesjonalniej byłoby stworzyć odpowiednią stronę w ramach aplikacji, co pozostawiam jako zadanie domowe).

Plik:compojure-chrome-view-page-source.png

Powodzenia!

Dla dociekliwych: użycie lein z linii poleceń

Uruchomienie aplikacji z poziomu lein jest możliwe z linii poleceń (na chwilę obecną nie jest mi znana możliwość uruchomienia lein z poziomu NetBeans bezpośrednio, np. przez menu Run).

Po stworzeniu projektu poleceniem lein new, kolejnym krokiem jest uruchomienie lein deps, którego celem jest pobranie zależności do katalogu lib w projekcie. Wszystkie pliki jar i zip w tym katalogu są umieszczone na CLASSPATH dla lein.

devmac:witaj-clojure-webapp jacek$ lein deps
Copying 15 files to /Users/jacek/sandbox/witaj-clojure-webapp/lib
[INFO] snapshot swank-clojure:swank-clojure:1.3.0-SNAPSHOT: checking for updates from central
[INFO] snapshot swank-clojure:swank-clojure:1.3.0-SNAPSHOT: checking for updates from clojure
[INFO] snapshot swank-clojure:swank-clojure:1.3.0-SNAPSHOT: checking for updates from clojure-snapshots
[INFO] snapshot swank-clojure:swank-clojure:1.3.0-SNAPSHOT: checking for updates from clojars
Overriding previous definition of reference to witaj-clojure-webapp
Overriding previous definition of reference to dependency.fileset
Copying 2 files to /Users/jacek/sandbox/witaj-clojure-webapp/lib/dev

Z tak przygotowaną strukturą projektową i zależnościami możemy uruchomić aplikację poleceniem lein repl, czyli nic innego jak uruchomieniu Clojure REPL. Należy wcześniej zatrzymać REPL uruchomione w NetBeans - Stop Project REPL, gdyż w przeciwnym przypadku nastąpi konflikt portów dla Jetty.

devmac:witaj-clojure-webapp jacek$ lein repl
"REPL started; server listening on localhost:61978."
2010-09-17 00:21:24.488::INFO:  Logging to STDERR via org.mortbay.log.StdErrLog
2010-09-17 00:21:24.491::INFO:  jetty-6.1.14
2010-09-17 00:21:24.551::INFO:  Started SocketConnector@0.0.0.0:8080
pl.jaceklaskowski.clojure.witaj-clojure-webapp.core=>

Uruchamiamy przeglądarkę i wchodzimy na główną stronę http://localhost:8080/. Fajrant!

Osobiste