Mariusz Prowaźnik

o programowaniu w Javie, Scali i Clojure.


Aplikacja webowa w Springu bez konfiguracji w xml

W tym poście pokażę jak stworzyć aplikację webową w Springu, która poza plikiem pom.xml, nie zawiera, żadnej konfiguracji w xml'u. Aplikacją tą będzie prosta lista rzeczy do zrobienia:
Do zbudowania projektu będzie potrzebny Apache Maven 3. Przyda się jakieś IDE, wszystko jedno jakie.

Wygenerowanie projektu

Wygeneruj projekt z linii poleceń:
mvn archetype:generate
Zaimportuj projekt do swojego IDE. Dodaj zależności w pliku pom.xml, oraz zmień packaging z jar na war. Zaktualizuj projekt.
    <packaging>war</packaging>
       <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <scope>compile</scope>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <scope>compile</scope>
            <groupId>cglib</groupId>
            <artifactId>cglib-nodep</artifactId>
            <version>2.2.2</version>
        </dependency>

Konfiguracja kontekstu Spring'a

Zamiast tradycyjnych plików *-context.xml z konfiguracją beanów Springowych można stworzyć klasę pełniącą tę samą funkcję. Trzeba ją oznaczyć adnotacją @Configuration.
package pl.mpro;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("pl.mpro")
public class SpringContextConfiguration {
}
Nie zadeklarowałem żadnych bean'ów, ale określiłem jaka paczka ma być skanowana w poszukiwaniu klas oznaczonych adnotacjami @Service @Component @Repository, poprzez @ComponentScan("pl.mpro").

Logika aplikacji

Oto prosta klasa realizująca funkcjonalność dodawania nowych "todo", oraz pobierania istniejących.
package pl.mpro;

import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class TodoService {
    private ArrayList<String> todos = new ArrayList<String>();

    public List<String> getAll() {
        return new ArrayList<String>(todos);
    }

    public void addItem(String item) {
        todos.add(item);
    }
}

Kontroler

Kontroler będzie obsługiwać żądania HTTP, wywoływać odpowiednie metody wstrzykniętej klasy TodoService i decydować, który widok będzie wyrenderowany. W tym przypadku kontroler posiada dwa mapowania dla \index.s, oddzielne dla metody GET i POST - ta pierwsza będzie zwracać stronę z listą "todo", druga dodawać nowy.
package pl.mpro;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
public class TodoController {
    private TodoService todoService;

    @Autowired
    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    @RequestMapping(value = "index.s",method = RequestMethod.GET)
    public ModelAndView printAll() {
        List<String> all = todoService.getAll();
        ModelAndView mav = new ModelAndView("/WEB-INF/views/list.jsp");
        mav.addObject("todoList", all);
        return mav;
    }

    @RequestMapping(value = "index.s",method = RequestMethod.POST)
    public ModelAndView addItem(@RequestParam String item) {
        if (item != null) todoService.addItem(item);
        return new ModelAndView("redirect:index.s");
    }
}
Dlaczego metoda addItem nie zwraca ModelAndView ze ścieżką do strony jsp? Po to aby uchronić przed ponownym dodaniem elementu po odświeżeniu strony. Zrealizowane jest to w ten sposób, że po obsłużeniu POST /index.s zostanie zwrócona odpowiedź z kodem 302 i nagłówkiem Location z url, na który nastąpi przekierowanie. Gdy potem w przeglądarce odświeżymy stronę, powtórzone będzie ostatnie żądanie, czyli GET.

Strona jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
    <title>Lista rzeczy do zrobienia</title>
    <meta charset="utf-8">
</head>
<body>
    <form method="POST" accept-charset="utf-8">
        <input type="text" name="item" />
        <input type="submit" value="Dodaj"/>
    </form>
    <c:if test="${not empty todoList}">
         <ul>
             <c:forEach items="${todoList}" var="todo">
                 <li>${todo}</li>
             </c:forEach>
          </ul>
    </c:if>
</body>
</html>

Inicjalizacja kontenera serwletów

Tradycyjnie, informacje o servletach, filtrach itp znajdowały się w pliku web.xml. Obecnie nie jest on niezbędny i można zarejestrować servlet programatycznie. W tej aplikacji potrzebujemy DispatcherServlet. Będzie on przyjmował żądania http i rozdzielał je do odpowiednich metod kontrolerów. Mapowania określone zostały przez adnotacje @RequestMapping w klasie kontrolera.
package pl.mpro;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

public class TodoWebInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        registerDispatcherServlet(servletContext);
    }

    private void registerDispatcherServlet(ServletContext servletContext) {
        WebApplicationContext springContext = createContext(SpringContextConfiguration.class);
        DispatcherServlet dispatcherServlet = new DispatcherServlet(springContext);
        ServletRegistration.Dynamic dispatcher = servletContext.
                                      addServlet("dispatcherServlet", dispatcherServlet);
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("*.s");
    }

    private WebApplicationContext createContext(Class<?>... contextClasses) {
        AnnotationConfigWebApplicationContext springContext = 
                                            new AnnotationConfigWebApplicationContext();
        springContext.register(contextClasses);
        return springContext;
    }
}

Uruchomienie aplikacji

Do pom.xml trzeba dodać jeszcze konfigurację dwóch wtyczek. Maven nie zbuduje projektu webowego bez web.xml, jeśli nie dodamy
<failOnMissingWebXml>false</failOnMissingWebXml>
Wtyczka jetty-maven-plugin umożliwi wygodne uruchomienie aplikacji przy użycia silnika servletów Jetty.
    <build>
        <!-- ... -->
        <plugins>
           <!-- ... -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>8.1.5.v20120716</version>
            </plugin>
        </plugins>
    </build>
Teraz wystarczy:
mvn clean install jetty:run
i można przetestować aplikację:
http://localhost:8080/index.s


Emacs jako IDE dla Clojure

Dla kogoś, kto chce zaznajomić się z programowaniem w Clojure, najprostszym wyborem jest zainstalowanie odpowiedniej wtyczki do IDE, którego używa na co dzień do Javy. W moim przypadku jest to IntelliJ IDEA. Niestety z moją wersją wtyczka La Clojure nie chciała działać. Za to CounterClockwise do Eclipse zadziała jak należy i jej używałem do pisania drobnych programów. Do wyboru jest jeszcze Enclojure dla NetBeansa. Jest to dobre podejście na początek, bo można się skupić na nauce nowego języka, a nie na konfigurowaniu środowiska.

Z drugiej strony, po co uruchamiać dosyć spore IDE, żeby poeksperymentować trochę z Clojure. Są dwa "lżejsze" rozwiązania: Sublime Text i Emacs. SublimeText zdobywa ostatnio sporą popularność, czemu się nie dziwię. Jednak pod Windowsem są problemy z REPL'em., dlatego postanowiłem wypróbować Emacsa.

Emacs ma swoje wady i zalety. Trzeba poświęcić czas na naukę skrótów klawiaturowych, na grzebanie w plikach konfiguracyjnych i instalowanie potrzebnych paczek. Ale jak już się go skonfiguruje, to można wrzucić pliki konfiguracyjne na jakieś repozytorium kodu i potem mamy gotowca.

Jest dużo tutoriali w internecie na temat wykorzystania Emacsa do programowania w Clojure i można się przy nich pogubić, ponieważ często dotyczą starszego zestawu: Slime i Swank, zamiast nowszego nRepl. Często też można znaleźć tutoriale ze StarterKit'ami, przy których instaluje się duża ilość paczek. Jednak moim zdaniem lepiej stopniowo je sobie doinstalowywać, żeby mieć czas, by się z nimi zaznajomić. Bo co z tego, że od razu będzie dużo paczek zainstalowanych, jak nie będzie się umiało z nich skorzystać.

Moje wskazówki odnośnie pierwszych kroków z Emacsem i Clojure:

  1. Zainstalować Leiningen.

  2. Zainstalować Emacs korzystając z instalki na stronie:
    http://vgoulet.act.ulaval.ca/en/emacs/windows/

  3. Dodać archiwum paczek i zainstalować nrepl, tak jak w poście:
    http://ravingbytes.blogspot.com/2013/02/clojure-i-emacs-z-wtyczna-nrepl-na.html

  4. Zainstalować dodatkowe pakiety, np takie jak opisane w:
    http://blog.worldcognition.com/2012/07/setting-up-emacs-for-clojure-programming.html

Dodatkowo warto zainstalować lub uruchomić:

  • expand-region - rozszerza zaznaczenie o jeden poziom wyżej (podobnie jak Ctr+W w IntelliJ albo Ctr+Alt+Góra w Eclipse
  • inną skórkę np. color-theme-sanityinc-tomorrow
  • recentf - ostatnio otwierane pliki
  • przesuwanie linii do góry i do dołu (tu wystarczy parę linijek w pliku konfiguracyjnym, zamieszczę cały na końcu posta)

(require 'package)
(add-to-list 'package-archives
             '("marmalade" . "http://marmalade-repo.org/packages/"))
(package-initialize)



(defvar my-packages '(
                      clojure-mode
                      clojure-test-mode
                      ac-nrepl paredit popup
        nrepl
                      expand-region
                      jump 
        color-theme-sanityinc-tomorrow))


(dolist (p my-packages)
  (when (not (package-installed-p p))
    (package-install p)))

;; Set color-theme

(load-theme 'sanityinc-tomorrow-eighties t) 


;; Paredit
(add-hook 'clojure-mode-hook 'paredit-mode)
(add-hook 'nrepl-mode-hook 'paredit-mode)

;; odpowiednik Ctrl+W w IntelliJ IDEA
(require 'expand-region)
(global-set-key (kbd "C-=") 'er/expand-region)

(put 'upcase-region 'disabled nil)
;; Auto complete
(require 'auto-complete-config)
(ac-config-default)

;; ac-nrepl
(require 'ac-nrepl)
(add-hook 'nrepl-mode-hook 'ac-nrepl-setup)
(add-hook 'nrepl-interaction-mode-hook 'ac-nrepl-setup)
(eval-after-load "auto-complete" '(add-to-list 'ac-modes 'nrepl-mode))

(menu-bar-mode 1)
(tool-bar-mode 0)
(desktop-save-mode)
(ido-mode)
(recentf-mode)
(show-paren-mode)

(require 'recentf)

(recentf-mode 1)
(global-set-key "\C-xf" 'recentf-open-files)
(setq recentf-auto-cleanup 'never)


(defun move-line-up ()
  "Move up the current line."
  (interactive)
  (transpose-lines 1)
  (forward-line -2)
  (indent-according-to-mode))

(defun move-line-down ()
  "Move down the current line."
  (interactive)
  (forward-line 1)
  (transpose-lines 1)
  (forward-line -1)
  (indent-according-to-mode))
  
  
(global-set-key [(control shift up)]  'move-line-up)
(global-set-key [(control shift down)]  'move-line-down)


Data Science na Courserze

Od dzisiaj dostępny jest kurs "Introduction to Data Science" prowadzony przez prof. Billa Howe z University of Washington za pośrednictwem serwisu coursera.org

W programie znajdziemy bardzo ciekawe rzeczy, np:

  • MapReduce, Hadoop, NoSQL
  • modelowanie statystyczne, planowanie doświadczeń, wprowadzenie do uczenia maszynowego
  • algorytm najbliższych sąsiadów, k-uśrednień, drzewa decyzyjne
A także możliwość wzięcia udziału w rzeczywistym projekcie z zakresu Data Science.


Clojure i baza danych

W ramach nauki Clojure napisałem krótki programik do zapisywania i odczytywania danych przy użyciu clojure.java.jdbc. Zamieszczam jako szybki przykład.

Przechowywanymi danymi będą notowania giełdowe, a jako bazy użyję HSQLDB. Tabela stock będzie zawierać symbol papieru wartościowego, jego opis, oraz nazwę tabeli z notowaniami (notowania poszczególnych papierów w oddzielnych tabelach).

stock
(pk) ticker
table_name
description
quot_eod_kghm
(pk) date
open
close
high
low
volume
quot_eod_alma
(pk) date
open
close
high
low
volume
quot_eod_pekao
(pk) date
open
close
high
low
volume
itd...

Funkcjonalności, które można by sobie życzyć od takiego kodu:

  1. Zainicjowanie tabel
  2. Dodanie papieru wartościowego
  3. Pobranie symboli wszystkich papierów wartościowych
  4. Usunięcie papieru wartościowego z notowaniami
  5. Dodanie notowania
  6. Pobranie notowań

Realizacja:

Do budowania projektu użyję Maven'a, fragment pom.xml:

<dependencies>
 <dependency>
  <groupId>org.clojure</groupId>
  <artifactId>clojure</artifactId>
  <version>1.4.0</version>
 </dependency>
 <dependency>
  <groupId>hsqldb</groupId>
  <artifactId>hsqldb</artifactId>
  <version>1.8.0.10</version>
 </dependency>
 <dependency>
  <groupId>clj-time</groupId>
  <artifactId>clj-time</artifactId>
  <version>0.4.4</version>
 </dependency>
 <dependency>
  <groupId>org.clojure</groupId>
  <artifactId>java.jdbc</artifactId>
  <version>0.2.3</version>
 </dependency>
</dependencies>
<repositories>
 <repository>
  <id>clojars.org</id>
  <url>http://clojars.org/repo</url>
 </repository>
</repositories>

Zależność clj-time dodałem, żeby ułatwić sobie parsowanie i formatowanie dat.

sqlexample.clj:

(ns sqlexample
  (:require [clojure.java.jdbc :as sql]
            [clj-time.core :as jt]
            [clj-time.format :as cf]))

;;Mapa z parametrami połączenia do bazy danych. 
;;HSQLDB będzie zapisywać stan bazy w pliku db/stocks.script
;;Plik ten można podejrzeć w notatniku, zawiera SQL.
(def db { 
         :subprotocol "hsqldb" 
         :user "SA"
         :password ""
         :subname "db/stocks;create=true"})

(def table-prefix "quot_eod_") ;prefix nazwy tabeli z notowaniem
(def date-formatter (cf/formatter "yyyyMMdd")) ;do formatowania i parsowania dat

(defn init-stock-table 
"Tworzy tabelę stock. Wykorzystuje makro with-connection do otwarcia połączenia do bazy danych, 
oraz makro create-table."
  []
  (sql/with-connection db
    (sql/create-table :stock
       [:ticker "VARCHAR(32)" "PRIMARY KEY"]
       [:table_name "VARCHAR(64)"]
       [:description "VARCHAR(256)"])))

(defn- init-quot-table
"Tworzy tabelę z notowaniami. Nie otwiera połączenia, będzie wywoływana wewnątrz transakcji."
  [ticker]
  (sql/create-table (str table-prefix ticker)
                    [:date "DATE" "PRIMARY KEY"]
                    [:open "DOUBLE"]
                    [:close "DOUBLE"]
                    [:high "DOUBLE"]
                    [:low "DOUBLE"]
                    [:volume "DOUBLE"]))

(defn add-ticker 
"Dodanie papieru wartościowego. W tym celu musi być dodany rekord to tabeli stock, 
oraz stworzona tabela z notowaniami. Jeśli któraś z operacji się nie powiedzie, 
druga musi zostać wycofana, dlatego opakowane są w transakcję "
  [ticker] 
  (sql/with-connection 
    db
    (sql/transaction
      (sql/insert-record
        :stock
        {:ticker ticker :table_name (str table-prefix ticker)}
        )
      (init-quot-table ticker)
      )))

(defn get-tickers 
  "Pobiera symbole wszystkich papierów wartościowych"
  []
  (sql/with-connection
    db
    (sql/with-query-results 
      res
      ["select ticker from stock"]
      (map :ticker (into [] res)))))

(defn add-quotation
 "Dodaje notowanie."
 [ticker quotation]
  (sql/with-connection
    db
    (sql/insert-record
      (str table-prefix ticker)
      quotation)))
    

(defn get-quotations
 "Pobiera notowania."
 [ticker]
  (sql/with-connection
    db
    (sql/with-query-results 
      res
      [(str "select * from " table-prefix ticker " order by date asc")]
      (into [] res))))

(defn delete-ticker
 "Usuwa dane papieru wartościowego."
 [ticker]
  (sql/with-connection
    db
    (sql/transaction
      (sql/drop-table 
        (str table-prefix ticker))
      (sql/delete-rows
        :stock
        ["ticker=?" ticker])
      )))

Użycie

(init-stock-table)
;(0)

(get-tickers)
;()

(add-ticker "kghm")
;(0)

(get-tickers)
;("kghm")

(get-quotations "kghm")
;[]

(add-quotation "kghm" 
{:open 1.0 :close 2.0 :high 3.0 :low 0.50 :volume 100 :date (jt/date-time 2012 12 22)})
;1

(get-quotations "kghm")
;[{:volume 100.0, :low 0.5, :high 3.0, :close 2.0, :open 1.0, 
;:date #inst "2012-12-21T23:00:00.000-00:00"}]

Co dalej?

Warto zapoznać się z dwoma językami dziedzinowymi (ang. domain specyfic language) na bazie Clojure: