środa, 20 maja 2015

Spring Boot 3 - deployment na zewnętrzny serwer

Embedded container nie taki doskonały

Kilka dni temu zachwalałem framework Spring Boot za to, że nie wymaga dodatkowego serwera do tego, aby nasza aplikacja się uruchomiła; że radzi sobie doskonale z serwerem wbudowanym w aplikację. Czy zamierzam się z tego wycofać? Nie. Dalej uważam, że embedded container w Spring Boot ułatwia development aplikacji uwalniając nas od konieczności utrzymywania i konfiguracji dodatkowego serwera.
Takie rozwiązanie jednak ma również pewne ograniczenia. Nie postawimy np. na jednym serwerze więcej niż jednej aplikacji (chyba że nasłuchiwałyby na różnych portach, ale to dość toporne rozwiązanie). Możliwości konfiguracyjne takiego serwera będą ubogie. Różnego rodzaju usługi w chmurze nie zawsze dają możliwość swobodnego uruchamiania plików .jar, więc tu też mógłby się pojawić problem.
Wygląda na to, że mogą wystąpić również problemy ze stabilnością. Taki przykład z życia: podczas pierwszej naszej próby uruchomienia naszej aplikacji na serwerze OpenShifta, odpalaliśmy standardowo jara, jednak aplikacja w nieznanych okolicznościach przestawała działać po jakimś czasie. Po wdrożeniu tej samej aplikacji na serwer WildFly również na OpenShifcie, w takiej samej konfiguracji sprzętowej, działa stabilnie już od jakiegoś czasu.
Z czasem może przyjść zatem czas, kiedy wbudowany serwer (w szczególności na środowisku produkcyjnym) nie będzie już odpowiednim rozwiązaniem. Dzisiaj więc zobaczymy, jak przygotować naszą aplikację (znów posłużymy się aplikacją z poprzednich postów) do deploymentu na zewnętrzny serwer

Co trzeba zrobić?

Nie jest to trudne, ale jest parę rzeczy, które musimy wziąć pod uwagę, jeśli chcemy uniknąć problemów:
  • aplikacja musi być zapakowana w plik .war
  • musimy pozbyć się wbudowanego serwera. W przeciwnym razie aplikacja na zewnętrznym serwerze próbuje robić jakieś dziwactwa związane z serwerem wbudowanym, co oczywiście kończy się katastrofą
  • wszelka dodatkowa inicjalizacja zawarta w metodzie main() musi zostać przeniesiona w inne miejsce (zaraz napiszę gdzie). Metoda main() nie zostanie wywołana na zewnętrznym serwerze

Pakowanie aplikacji do .war

To najprostszy krok. Wystarczy do naszego pliku pom.xml dodać linijkę <packaging>war</packaging>. Wtedy plik ten powinien wyglądać tak:

         
    war
         
    4.0.0
    com.example
    SpringBootFirstApp
    0.0.1-SNAPSHOT
         
    
        org.springframework.boot
        spring-boot-starter-parent
        1.2.3.RELEASE
    
    
    
         
            
                config
            
        
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        
    

Usunięcie wbudowanego serwera

Kwestia prawie tak samo prosta, gdyż znów ogranicza się do edycji pliku pom.xml (tym razem jednakże, będzie trzeba dodać więcej linijek :) ). Robi się to przez dodanie zależności serwera tomcat i ustawienie jej zakresu (scope'u) jako provided - dzięki temu Maven nie zapakuje nam bibliotek odpowiedzialnych za serwer. Wynikowy pom.xml będzie wyglądał następująco:

         
    war
         
    4.0.0
    com.example
    SpringBootFirstApp
    0.0.1-SNAPSHOT
         
    
        org.springframework.boot
        spring-boot-starter-parent
        1.2.3.RELEASE
    
    
    
         
            
                config
            
        
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            org.springframework.boot
            spring-boot-starter-tomcat
            provided
        
    

I tyle!

Inicjalizacja aplikacji

Tutaj już nie jest tak banalnie, ale nadal nienajgorzej. Aby nasza aplikacja działała prawidłowo na zewnętrznym serwerze, również musi mieć jakiś punkt wejścia (entry-point). Wcześniej była to metoda main(). Jak będzie teraz? Otóż przy uruchamianiu Spring Boot wyszuka klasy typu SpringBootServletInitializer i wywoła jej metodę protected SpringApplicationBuilder configure(SpringApplicationBuilder application). Typowo chcemy mieć klasę, która dziedziczy z klasy SpringBootServletInitializer i przesłania (override) metodę oryginalną. Taka metoda jest właśnie miejscem na wszelkie działania inicjalizacyjne, które mają mieć miejsce tuż po uruchomieniu aplikacji.
W naszym przykładzie zmofyfikujemy klasę MainClass, aby tak jak wcześniej, od niej rozpoczynało się działanie aplikacji. Po wspomnianych zmianach będzie wyglądać tak:
package com.example;

import java.lang.Override;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.web.SpringBootServletInitializer;
import org.springframework.boot.builder.SpringApplicationBuilder;

@EnableConfigurationProperties
@SpringBootApplication
public class MainClass extends SpringBootServletInitializer {
    
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        //inicjalizacja aplikacji
        System.out.println("Inicjalizuję się...");
        return application.sources(MainClass.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(MainClass.class, args);
    }

}

Co właściwie możemy inicjalizować? Np. jeśli nasza aplikacja korzysta z bazy danych po JDBC, konieczne pewnie będzie zarejestrowanie sterownika bazy. Wbudowany w aplikację Tomcat nie ma problemu, jeśli taki sterownik nie zostanie zarejestrowany, natomiast zewnętrzny sypał błędami, jeśli nie zostało to zrobione. Taką rejestrację można wykonać następującym kawałkiem kodu w metodzie configure:
try {
    DriverManager.registerDriver(new Driver());
} catch(SQLException e) {
    throw new RuntimeException(e);
}

Budowa aplikacji

Ze zmian to tyle, czas zbudować naszą aplikację. W tym celu oczywiście wchodzimy do katalogu z plikiem pom.xml i wykonujemy komendę mvn clean install. Budowanie powinno się udać i powinniśmy zauważyć, że w katalogu target utworzone zostały dwa pliki:
  • SpringBootFirstApp-0.0.1-SNAPSHOT.war
  • SpringBootFirstApp-0.0.1-SNAPSHOT.war.original
Uwaga! Teraz ważna kwestia:
Zawsze należy do deploymentu używać pliku .original. Ten drugi posiada też spakowane biblioteki wbudowanego Tomcata.
Tak przygotowany plik .war możemy wdrożyć na nasz ulubiony serwer.

Angular - routing i komunikacja z serwerem

Agenda

W tym artykule postaram się przedstawić jeden z najważniejszych aspektów pracy z Angularem, czyli w jaki sposób możemy łączyć się z naszym serwerem, które wystawia API w stylu REST, wspierając nas dodatkowym mechanizmem routingu. Nie mamy się czego obawiać - Angular wprowadza bardzo przyjazny sposób realizacji tych zadań, a co więcej, większość kwestii możemy sami dostosować do własnych potrzeb.
Jak już wspominaliśmy we wcześniej artykułach, Angular najlepiej współpracuje z serwerem, który wystawia statyczne html'e interpretowane przez niego samego oraz API REST, który w najprostszym i najlepszym przypadku ma postać JSONa, mającego bezpośrednią reprezentację w JS-ie. Tym sposobem tworzymy aplikację typu SPA (Single Page Application). Taki styl aplikacji internetowej skuteczenie odciąża serwer od generowania dynamicznych stron, a zarazem pozwala na bardzo elastyczne zmiany, nie obciążające deweloperów w dalszej konserwacji i utrzymaniu. Podsumowując mamy nowoczesną architekturą aplikacji internetowych, którą łatwo możemy skalować i tworzyć w odpowiedzi na szybko zmieniające się standardy technologii web'owych.

Pierwsze kroki z routingiem

Nie owijając w bawełnę przejdźmy do sedna sprawy. Co jest do przewidzenia, powinniśmy już mieć działający serwer, gotowy do rozpoczęcia realizowania zadania (jak go postawić, możemy zobaczyć w poprzednich artykułach na tym samym blogu). Nie jest dla mnie istotne, czy wolisz korzystać z technologii pokroju Javy EE czy też ze Springa, a w szczególności Spring Boota, który docelowo został stworzony do szybkiego dewelopmentu takich aplikacji. Dodatkowo zakładam, że macie ściągnięty moduł ng-route, dostępny na stronie Angular'a : angular-route.js

Routing w Angularze

Rozczajanie rozpoczniemy od podstawowej funkcji jaką pełni serwis routingu w Angularze. Trudno jest nie skorzystać z takich możliwości, jakie zapewnianiają:
  • uniezależnienie ścieżek - urli jakie widzi użytkownik w swojej przeglądarce od ich reprezentacji na serwerze, który typowo ma wystawiać REST, którego użytkownik nie powinien "przez przypadek" widzieć;
  • zachowanie stanu aplikacji Javascript, która w normalnych warunkach przeładowuje się w przypadku przejścia do nowego adresu URL;
  • działanie standardowych przycisków prev/next w oknie przeglądarki internetowej;
  • przekierowanie użytkownika, gdy ten wejdzie na nieistniejącą stronę;
  • obsługę parametrów żądania;
Każda z powyżej wymienionych kwestii jest istotna dla tworzenia strony SPA, gdzie pomimo braku przeładowania strony, dajemy użytkownikowi poczucie używania zwykłych serwisów, dodatkowo oferując dynamizm.
Niestety, przez to, że JS jest nadal wrażliwy na przeładowanie zachodzące w przeglądarce (np. gdy użytkownik odświeży stronę za pomocą F5) to programista musi dbać o dobrą inicjalizację strony. Przydatne jest w tym to, że routing w Angularze oferuje przypisanie kontrolera do ścieżki w URLu i jest automatycznie uruchamiany w przypadku przejścia do powiązanego kontekstu. Warto, żeby ścieżka zawierała wszystkie parametry niezbędne do poprawnej inicjalizacji w kontrolerze, dzięki czemu użytkownik może przesłać swoim znajomym linka do naszej wspaniałej strony i jesteśmy pewni, że oni zobaczą identyczną treść.

Moduł ng-route

Moduł jest oferowany przez oddzielny plik Javascript. Dołączamy go do naszego głównego szablonu, typu index.html, który już wcześniej tworzyliśmy:

<html ng-app="searchApplication">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>Search Application</title>
    <script src="/lib/angularjs/angular.js"></script>
    <script src="/lib/angularjs/angular-route.js"></script>
    <script src="/scripts/app.js"></script>
</head>

W pliku app.js do modułu tworzymy moduł searchApp:
var searchApp = angular.module("searchApplication", ['ngRoute']);

Teraz musimy zdecydować się jak wyglądać będzie hierarchia naszego serwisu. W moim przykładzie zakładamy, że:
  • na głównej stronie będzie wyświetlona wyszukiwarka zawierające możliwe wynikami poszukiwań użytkownika - http://localhost:8080/#/;
  • po wejściu w szczegóły wybranego artykułu przejdziemy pod link - http://localhost:8080/#/article/{articleId}. 
Wasze URL'e mogą się oczywiście różnić. Jak taka definicja będzie wyglądać po stronie Angular'a? W zdefiniowanym przez nas module korzystamy z funkcji config, której argumentem jest funkcję realizująca konfigurację modułu aplikacji. My konfigurujemy routing, pozostawiamy więc Angularowi wstrzyknięcie nam obiektu $routeProvider.
searchApp.config(function($routeProvider) {
    $routeProvider
        .when('/', {
            templateUrl : '/searcher.html',
            controller : 'searchController'
        })
        .when('/article/:articleId', {
            templateUrl : '/article.html',
            controller : 'articleController'
        })
        .otherwise({
            templateUrl : '/searcher.html',
            redirectTo: '/'
        });
});

W powyższym kodzie definiujemy - kiedy (when) jesteśmy w podanym kontekście ('/' lub '/article/:articleId') to ma zostać wyświetlony wybrany szablon oraz uruchomiony określony kontroler, w innym przypadku (otherwise) zawsze przekierowujemy pod kontekst '/' i wyświetlamy podany szablon. W przypadku przejścia pod ścieżkę article wymagamy identyfikatora artykułu, co pozwoli na poprawną inicjalizację stanu. Brak tego parametru powoduje wykonanie klauzuli 'otherwise'. Nic nie stoi na przeszkodzie, aby zbudować artykuły, które są opcjonalne - definiuje to znak zapytania '?' po nazwie parametru (np. '/article/:articleId?').
Żeby przetestować, że to co napisaliśmy rzeczywiście działa, musimy zdefiniować miejsce, gdzie nasz szablon ma się pojawić, gdy użytkownik przejdzie po dany adres. Tag <body> w pliku index.html będzie miał następującą zawartość:

<body ng-controller="mainController">
    {{hello}}
    <div ng-view></div>
</body>

Możemy tu zawuażyć charakterystyczną deklarację div-a zawierającego dyrektywę ng-view. Dyrektywa ta przypisana do jakiegoś tagu spowoduje wypełnienie jego zawartości szablonami określonym wcześniej w konfiguracji routingu. Teraz dodatkowo musimy stworzyć dwa pliki szablonów, które będą ładowane w przypadku odwiedzenia odpowiadającego adresu. searcher.html - jego zawartość i sposób działania była omawiana we wcześniejszych artykułach.

<body>
    <span ng-bind="yourSearch"></span>
    <div>
        <span>Search:</span>
        <input type="text" ng-model="userInput" ng-change="changeSearch()"/>
    </div>
</body>

article.html - jak na razie poglądowy szablon:
<body>
    {{articleInformation}}
</body>

Dla naszych szablonów potrzebujemy jeszcze kontrolerów. Do app.js załączamy:
var mainController = searchApp.controller('mainController', ['$scope', function($scope) {
    $scope.hello = "What are you looking for?";
}]);

var searchController = searchApp.controller('searchController', ['$scope', '$http', function($scope, $http) {

    $scope.yourSearch;

    $scope.userInput;

    $scope.changeSearch = function() {
        if ($scope.userInput !== undefined && $scope.userInput.length !== 0) {
            $scope.yourSearch = "You are looking for " + $scope.userInput + "!";
        } else {
            $scope.yourSearch = "You seek: nothing?";
        }
        if ($scope.userInput.length > 2) {

        }
    }
}]);

var articleController = searchApp.controller('articleController', ['$scope', '$http', '$routeParams', function($scope, $http, $routeParams) {
    $scope.articleId = $routeParams.articleId;

    $scope.articleInformation = "You are looking at invisible article with id = " + $scope.articleId;
}]);

Kontroler articleController pokazuje, jak prosto możemy wybrać parametry przekazywane przez url - odnosimy się do nich przez obiekt $routeParams. Budujemy i uruchamiamy ponownie aplikację. Jak możemy zaobserwować odwiedzając wyspecifikowane przez nas szablony, na razie nie dzieje się nic wielkiego, natomiast możemy zobaczyć mechanizm działania routingu. Bardzo dobrze radzi sobie w przypadku, gdy użytkownik stara się wejść na nieistniejący kontekst aplikacji. Przycisk back w przeglądarce także działa poprawnie. Podając różne identyfikatory artykułów, angular automatycznie stan kontroler dzięki czemu wyświetla różne napisy. Czego nam tu teraz potrzeba, to  komunikacji z serwerem!

Komunikacja z serwerem

Większość komunikacji z serwerem będziemy realizować przy użyciu serwisu udostępnianego nam przez Angular'a - $http. Istotne do zrozumienia jest to, że zapytanie do serwera odbywa się w sposób asynchroniczny. Najczęściej używane funkcje tego serwisu, czyli post i get, zwracają obiekt typu 'promise', co oznacza, że w przyszłości obiekt będzie zawierał odpowiedź na zapytanie. Na obiekcie zwracanym przez funkcje naszego serwisu możemy więc wywołać główne dwie metody: success lub error, tym samym przekazując im funkcję jako argument, która zostanie wywołana po otrzymaniu odpowiedzi z serwera (lub nie).

Protokół komunikacji

Przed przystąpieniem do realizacji API dobrze jest określić protokół komunikacji, czyli w jaki sposób i jakim stylem będzie odpowiadał nasz serwer. Dobrze jest zgeneralizować sposób odpowiedzi, czyli stworzyć uniwersalny sposób tworzenia odpowiedzi na zapytanie, dzięki czemu utrzymamy jednolitą konwencję nazewnictwa, porządek i tym samym mniej problemów dla innych aplikacji korzystających z naszego API. Przecież w przyszłości z naszego API mogą korzystać programiści dla których serwer będzie 'czarną skrzynką' i sposób odpowiedzi poznają na podstawie samych odpowiedzi!
Tym samym najprościej jest stworzyć klasę realizującą protokół komunikacyjny. W Javie możemy posłużyć się typem wyliczeniowym, który dodatkowo dostarczy funkcji użytkowych:

/**
 * Standard kodów odpowiedzi wysyłanych przez aplikację.
 *
 * @author Mateusz Kamiński
 */
public enum ResponseCodes {
    OK("200", "OK"),
    BAD_REQUEST("400", "Bad Request");

    private String code;
    private String value;
    private String json;

    ResponseCodes(String code, String value) {
        this.code = code;
        this.value = value;
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("{ \"code\" : ")
                .append(code)
                .append(", \"value\" : \"")
                .append(value)
                .append("\" }");
        this.json = stringBuilder.toString();
    }

    public String getJson() {
        return json;
    }

    public String getJson(String additionalJsonAttribute) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("{ \"code\" : ")
                .append(code)
                .append(", \"value\" : \"")
                .append(value)
                .append("\", \"attribute\" : \"")
                .append(additionalJsonAttribute)
                .append("\"}");
        return stringBuilder.toString();
    };

    public String getJsonWithData(String additionalJsonData) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("{ \"code\" : ")
                .append(code)
                .append(", \"value\" : \"")
                .append(value)
                .append("\", \"data\" : ")
                .append(additionalJsonData)
                .append("}");
        return stringBuilder.toString();
    }
}

Można się zastanawiać, dlaczego zwracamy typowe kody standardu protokołu HTTP - ma to jednak zastosowanie utrzymaniu porządku samej aplikacji i informacji dlaczego ten kod występuje w naszym kontekście. Jeżeli uważamy to za niepotrzebną nadmiarowość, nie musimy tego implementować akurat w taki sposób, pomoże to jednak ludziom, którzy skorzystają z tego API, bez możliwości debugowania jego zawartości.

REST API w Springu

W poniższym kodzie dołączony jest kontroler, który jest kontrolerem restowym - co zapewnia adnotacja @RestController. Implementując publiczne metody określamy, aby zwracały String, który będzie naszym JSON-em. Metodę oznaczamy adnotacją @RequestMapping, przy czym deklarujemy przy jakiej metodzie HTTP będzie wywoływana (u nas GET), pod jakim kontekstem ('/list/document') i jakiego typu odpowiedź zwraca ('application/json').

@RestController
public class DocumentController {

    @RequestMapping(value = "/list/document", method = RequestMethod.GET, produces = "application/json")
    public String getDocument(@RequestParam(value = "documentId") String documentId) {
        Principal userPrincipal = httpServletRequest.getUserPrincipal();

        if ( /** documentId nie jest liczbą *?) {
            return ResponseCodes.BAD_REQUEST.getJson("documentId parameter is not a digit!");
        } else {
            Gson gson = new Gson();
            Document document = new Document();
            return ResponseCodes.OK.getJsonWithData(gson.toJson(document));
        }
    }

    // klasa reprezentująca dokument na potrzeby tutoriala
    public static class DocumentBean {

        private Integer documentId;

        private String title;

        private String content;

        // settery i gettery
    }

}
Powyższy przykład wam nie zadziała, ponieważ sami musicie określić skąd pobieracie obiekt typu Document, a to wykracza poza zakres tego tutoriala (możecie się z takimi aspektami zapoznać w artykule o bibliotece jooq na naszym blogu). W obsłudze żądania używamy biblioteki Gson, stworzonej przez Google, która w prosty sposób serializuje klasy Javowe na napisy odpowiadające JSON-owe. Jeżeli budujemy nasz projekt przy użyciu mavena lub gradle'a przyda się dodać tam zależność do tej biblioteki.
Warto wspomnieć, że dla każdej odpowiedzi wystawianej przez API powinniśmy zazwyczaj stwrozyć inną klasę, która zwróci pola istotne dla żądania, a nie bezpośrednie obiekty z bazy danych. Oddzieli to logikę biznesową jaką pełni API od logiki modelu danych obiektów trzymanych w bazie.
W zależności od poprawności zapytania wysłanego do API odpowiadamy typem BAD_REQUEST lub OK. Opis, zawarty w pierwszym z nich, pozwoli łatwo zdiagnozować problem po stronie klienta API.

Angular

Teraz przyszła kolej na klienta naszego API. W naszym kontrolerze article dodajemy dodatkową inicjalizację polegające na pobranie artykułu z serwera. Jeżeli realizacja zapytania trwałaby zbyt długo, warto wyświetlić użytkownikowi "ładowaczkę", aby wiedział, że coś się dzieje asynchronicznie. W przypadku błędu odpowiedni należy wyświetlić komunikat z błędem (i przeprosinami!).
$http({
    method: 'GET',
    url: '/list/document?documentId' + $scope.documentId,
    headers: {'Content-Type': 'application/x-www-form-urlencoded'}
    })
     .success(function(data, status, headers, config) {
       if (data.code === 200) {
          $scope.title = data.data.title;
          $scope.content = data.data.content;
       } else (data.code === 400) {
          $scope.message = "documentId was not a digit - please do not hack our REST API!";
       }
    })
     .error(function(data, status, headers, config) {
       $scope.message = "Sorry, there was an error while getting document!";
    });

Sposób wyświetlenia pól $scope.title i $scope.content w szablonie html pozostawiam czytelnikowi. Powyższy kod jest na tyle przejrzysty, że chyba nie muszę go opisywać. Bardzo podobnie wygląda obsługa innych metod HTTP - post, delete itd. Nie musimy definiować obsługi na odpowiedź z serwera, lecz są to bardzo rzadkie przypadki.
Inna postać odwoływania się do obiektu $http korzysta z wbudowanych funkcji:

$http.get('/list/document?documentId' + $scope.documentId
                {headers: {'Content-Type': 'application/x-www-form-urlencoded'}})

I co dalej?

Ten artykuł przedstawił jedynie podstawy podstaw. Zbudowanie dobrego API nie jest proste, pewne regulacje zawiera 'standard 'HATEOAS' o którym warto poczytać - odsyłam na https://spring.io/understanding/HATEOAS. Budowanie architektury opartej o REST to realizacja wzorca mikroserwisów - http://en.wikipedia.org/wiki/Microservices. Angular staje się standardem, dzięki naturalnej współpracy z aplikacjami w takiej architekturze.

O tym jak boty uprzykrzają życie i gotowych rozwiązaniach - angular + recaptcha.

Będąc szczęśliwym człowiekiem w projekcie który realizujemy zrobiłem zaczyn rejestracji, który na moje nieszczęście nie posiadał żadnej weryfikacji antybotowej. Po deploymencie na nasz serwer symulujący serwer produkcyjny po dniach dwóch napisał do mnie kolega, że aplikacja wali 500. Szybka analiza wykazała, że w bazie jest milion użytkowników (ach ten sukces komercyjny), a ponieważ w aktualnej wersji żadnego stronicowania nie było, to oczywiście mieliśmy zawieche na bazie. Rozwiązanie jest zapewne wszystkim dobrze znane - mechanizm captchy tzn. obrazka, który jest dla komputera nieanalizowalny (w rozsądnym czasie), więc aby go przejść trzeba być człowiekiem. Oczywiście gotowe machanizmy istnieją i jednym z nich jest reCaptcha od wujka google, która w wersjach tradycyjnych stron sprowadza się do wygenerowania pary kluczy u googla, dodania jednego diva zawierającego informacje o kluczu publicznym i dołączenia odpowiedniego java-skryptu, który przy wysyłaniu formularza "dołączy" status captchy, gdzie później w naszej aplikacji sprawdzimy status łącząc się do serwera googla (na wejściu dostarczamy wartość formularza, na wyjściu otrzymujemy status weryfikacji). Realizacja wg. koncepcji angulara oczywiście w tak prosty sposób działać nie będzie i trzeba to jakoś połączyć (oczywiście realizacja serwerowa jest taka sama), ponaglany czasem, strachem przed światem oraz wychodząc z założenia, że wyważać otwartych drzwi nie będę, skorzystałem z gotowego rozwiązania jakim jest https://github.com/VividCortex/angular-recaptcha, które to rozwiązanie dostępne jest również w pakietach Bower oraz npm. Sam sposób użycia jest dokładnie opisany w przykładzie pod podanych linkiem (nie polecam wymyślać tego na nowo, tylko zrobić tak jak opisał to autor), natomiast implementacje połączenia z serwerami googla, łatwo znaleźć w dokumentacji recaptchy. A na koniec obrazek efektu:

Wstęp do jOOQ

Wstęp

W dobie powszechności bibliotek służących do mapowania klas na bazę lub bazę na klasy (tak zwanych ORMappingów) powstało wiele rozwiązań implementujących jedno lub drugie rozwiązanie. Jednym z takich rozwiązań jest biblioteka jOOQ, która realizuje drugi z tych typów, tzn. na podstawie stworzonej bazy danych tworzy klasy javowe za pomocą których w łatwy sposób możemy zarządzać danymi. Warto wspomnieć o polityce licencyjnej przyjętej przez twórców biblioteki, mianowicie w przypadku korzystania z darmowych baz danych używanie owej jest darmowe. Natomiast w przypadku baz komercyjnych licencja jest już płatna (jest to całkiem logiczne, skoro stać nas na baze to zapewne też stać nas na bibliotekę). Każdy kto myśli o migracji na baze komercyjną, a chce używać jOOQ powinien wzięć to pod uwagę przy jej wyborze.

Co tak właściwie będzie prezentował artykuł?

Celem artykułu jest pokazanie podstawowych możliwości jOOQa (i oczywiście zachęcenie do wypróbowania) na przykładzie akademickim jakim jest realizacja książki telefonicznej umożliwiającej dodawanie/usuwanie kontaktów wraz z telefonami. Książka ta będzie dostępna z poziomu przeglądarki internetowej. Do realizacji tego zadania będę używał frameworka SpringBoot (zakładam czytelniku, że znasz owy framework lub jemu podobny) który spina cała warstwę aplikacyjną. Za warstwę danych będzie odpowiedzialna dobra darmowa baza jaką jest PostgreSQL (w przykładam korzystam z wersji 9.3). Do budowania projektu będę używać oprogramowania Maven. A w końcu do "zaprojektowania" bazy wraz z wygenerowaniem zapytań SQL będę używać świetnej aplikacji chmurowej jaką jest Vertabelo.

Instalacja środowiska

Przestawię poniżej przykładową konfigurację dla dystrybucji Ubuntu, myślę, że w przypadku innych dystrybucji oraz systemu Windows realizacja będzie podobna.
Instalacja PostgreSql:

sudo apt-get install postgresql postgresql-contrib
Konfiguracja PostgreSql:
W pliku
/etc/postgresql/<numer_wersji>/main/pg_hba.conf
zamienic wszystkie metody na trust.
Instalacja Maven:
sudo apt-get install maven
Konfiguracja środowiska niezbędną do realizacji projektu:
W katalogu gdzie chcemy przechowywać źródłą naszego projektu tworzymy następujący plik
pom.xml

    4.0.0

    pl.mostua
    jooq-tutorial
    0.0.1

    
        org.springframework.boot
        spring-boot-starter-parent
        1.2.3.RELEASE
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        
    

    
        1.8
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

służący programowi maven do budowania środowiska.
Należy też utworzyć odpowiednie pliki java:
src/main/java/jooq_example/TelephoneBookController.java
package jooq_example;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class TelephoneBookController {

    @RequestMapping("/")
    public String myPhones() {
        return "Ksiazka kontaktowa";
    }

}

src/main/java/jooq_example/Application.java
package jooq_example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;

import java.sql.Connection;
import java.sql.DriverManager;

@SpringBootApplication
@Configuration
public class Application {

    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(Application.class, args);
    }
}

Aby uruchimić to co udało nam się stworzyć należy w katalogu głównym projektu wykonąć polecenie
mvn package && java -jar target/jooq-tutorial-0.0.1.jar 
W przeglądarce pod adresem
localhost:8000
powinniśmy ujrzeć naszą stronę internetową.
Teraz możemy przejść do meritum artykułu.

Warstwa danych

Projekt bazy danych zamodelowany w aplikacji Vertabelo wraz z sekwencjami zaczynającymi się od 500, aby można było dodać dane testowe:
Oraz skrypty wygenerowane przez nań.
sql/jooq_tutorial_create.sql:
-- Created by Vertabelo (http://vertabelo.com)
-- Last modification date: 2015-05-17 18:24:18.697

-- tables
-- Table: friend
CREATE TABLE friend (
    id int  NOT NULL,
    name varchar(255)  NOT NULL,
    surname varchar(255)  NOT NULL,
    CONSTRAINT friend_pk PRIMARY KEY (id)
);

-- Table: phone_number
CREATE TABLE phone_number (
    id int  NOT NULL,
    phone varchar(13)  NOT NULL,
    friend_Id int  NOT NULL,
    CONSTRAINT phone_number_pk PRIMARY KEY (id)
);

-- foreign keys
-- Reference:  phone_number_friend (table: phone_number)

ALTER TABLE phone_number ADD CONSTRAINT phone_number_friend 
    FOREIGN KEY (friend_Id)
    REFERENCES friend (id)
    NOT DEFERRABLE 
    INITIALLY IMMEDIATE 
;

-- sequences
-- Sequence: friend_id_seq
CREATE SEQUENCE friend_id_seq
      INCREMENT BY 1
      NO MINVALUE
      NO MAXVALUE
      START WITH 500 
      
      NO CYCLE
      
;

-- Sequence: phone_number_id_seq
CREATE SEQUENCE phone_number_id_seq
      INCREMENT BY 1
      NO MINVALUE
      NO MAXVALUE
      START WITH 500 
      
      NO CYCLE
      
;
-- End of file.
Ponadto warto dodać kilka danych testowych.
sql/jooq_tutorial_create.sql:
INSERT INTO friend VALUES (1, 'Adam', 'Kowalski');
INSERT INTO friend VALUES (2, 'Michał', 'Nowak');

INSERT INTO phone_number VALUES(1, '333222111', 1);
INSERT INTO phone_number VALUES(2, '222333111', 1);
Aby stworzyć bazę która będzie przechowywać nasze dane oraz wykonać powyższe skrypty należy wykonać poniższe polecenie w katalogu głównym projektu
createdb jooq_tutorial -U postgres
psql jooq_tutorial postgres -f sql/jooq_tutorial_create.sql
psql jooq_tutorial postgres -f  sql/jooq_tutorial_add.sql
Żeby sprawdzić czy wszystko się udało możemy się zalogować do bazy:
psql jooq_tutorial postgres
oraz wpisać
SELECT * FROM friends; 
Co pokaże czy z sukcesem udało nam się dodać dane testowe.

Konfiguracja jOOQa

Należy zmodyfkować plik pom.xml w następujący spsób:
<?xml version="1.0" encoding="UTF-8"?>


    4.0.0

    pl.mostua
    jooq-tutorial
    0.0.1

    
        org.springframework.boot
        spring-boot-starter-parent
        1.2.3.RELEASE
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.jooq
            jooq
            3.4.5
        

        
            org.jooq
            jooq-meta
            3.4.5
        

        
            org.jooq
            jooq-codegen
            3.4.5
        

        
            org.postgresql
            postgresql
            9.3-1102-jdbc41
        
    

    
        1.8
    


    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
            

              
              org.jooq
              jooq-codegen-maven
              3.4.5

              
              
                
                  
                    generate
                  
                
              

              
              
                
                  org.postgresql
                  postgresql
                  9.3-1102-jdbc41
                
              

              
              

                
                
                  org.postgresql.Driver
                  jdbc:postgresql://localhost:5432/jooq_tutorial
                  postgres
                  test
                

                
                
                  org.jooq.util.DefaultGenerator
                  
                    org.jooq.util.postgres.PostgresDatabase
                    .*
                    
                    public
                  
                  
                    jooq_sources
                    src/main/java
                  
                  
                    true
                  
                
              
               
        
    

Wyjaśnienie: Do zależności dodajemy wszystkie zależności komponentów jOOQa oraz sterownik bazy (sekcja <dependencies>). Natomiast w sekcji <plugins> definiujemy plugin jooq-codegen-maven, który wygeneruje nam klasy na podstawie bazy. Zleży on od sterownika bazy (sekcja <dependency>), tę należy zmienić gdy korzystamy z innej. W sekcji <configuration> określamy parametry połączenia z których będzie korzystał plugin (jeżeli ustawimy metodę uwierzytelnienia na trust to hasło nie ma znaczenia, warto o tym pamiętać). W sekcji <generator> określamy typ bazy, to gdzie kod ma być wygenerowany (<target>) oraz, że chcemy wygenerować obiekty DAO (o owych jeszcze wspomnę).
Teraz po wykonaniu polecenia
mvn package
w katalogu głównym projektu powinien pojawić się folder
src/main/java/jooq_sources
w którym plugin jooqa wygenerował odpowiednie klasy.

Implementacja

Zacznijmy od modyfikacji pliku Application tak, żeby dostarczał on odpowiednich beanów.
package jooq_example;

import org.jooq.SQLDialect;
import org.jooq.impl.DefaultConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.sql.Connection;
import java.sql.DriverManager;

@SpringBootApplication
@Configuration
public class Application {

    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(Application.class, args);
    }

    @Autowired
    private org.jooq.Configuration configuration;

    @Bean(name = "dbConfiguration")
    public org.jooq.Configuration getConfiguration() {
        return new DefaultConfiguration().set(connection).set(SQLDialect.POSTGRES);
    }

    @Bean
    public TelephoneBookManager getTelephoneBookManager() {
        return new TelephoneBookManagerImpl(configuration);
    }

    @Bean
    public Connection getConnection() {
        String userName = "postgres";
        String password = "test";
        String url = "jdbc:postgresql://localhost:5432/jooq_tutorial";
        try {
            return DriverManager.getConnection(url, userName, password);
        } catch (Exception e) {
            throw new RuntimeException("Cannot connect to database");
        }
    }

}
Powyższe beany to
  • Configuration wymagany przez jOOQ, jest to informacja o bazie jak i połączeniu.
  • Connection slużący do połaczenia się z bazą.
  • Implementacje interfejsu TelephoneBookManager realizującego logikę biznesową aplikacji.
Teraz możemy przystąpić do zdefinjowaniu interfejsu TelephoneBookManager realizującego logikę biznesową:
package jooq_example;

import jooq_sources.tables.pojos.Friend;
import jooq_sources.tables.pojos.PhoneNumber;

import java.util.List;

/**
 * Realizuje logike biznesową związana z dodawniem telefonów
 *
 * @author Adam Mościcki
 */


public interface TelephoneBookManager {
    /**
     * Pobiera listę znajomych
     * @return
     */
    List&ltFriend> getFriends();

    /**
     * Dodanie znajmego
     * @return
     */
    void addFriend(Friend friend);

    /**
     * Dodanie znajmego w raz z numerem telefonu
     * @return
     */
    void addFriendWithPhoneNumber(Friend friend, String phoneNumber);

    /**
     * Pobiera listę znajomych
     * @return
     */
    void removeFriend(Integer friendId);

    /**
     * Zwraca listę telefonów danego użytkownika
     * @param friendId
     * @return lista telefonów
     */
    List<PhoneNumber> getFriendPhones(Integer friendId);

    /**
     * Dodanie numeru telefonu dla danego użytkownika
     */
    void addPhoneToFriend(Integer friendId, String phoneNumber);

    /**
     * Usuniecie numeru telefonu
     */
    void removeNumber(Integer phoneId);
}

Oraz implementacje TelephoneBookManagerImpl (tutaj właśnie jOOQ lśnić będzie):
package jooq_example;

import jooq_sources.Sequences;
import jooq_sources.Tables;
import jooq_sources.tables.daos.FriendDao;
import jooq_sources.tables.daos.PhoneNumberDao;
import jooq_sources.tables.pojos.Friend;
import jooq_sources.tables.pojos.PhoneNumber;
import org.jooq.Configuration;
import org.jooq.DSLContext;
import org.jooq.TransactionalRunnable;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

/**
 * @author Adam Mościcki
 */
public class TelephoneBookManagerImpl implements TelephoneBookManager{

    private final Configuration configuration;
    private final FriendDao friendDao;
    private final PhoneNumberDao phoneNumberDao;

    @Autowired
    public TelephoneBookManagerImpl(Configuration configuration) {
        this.configuration = configuration;
        this.friendDao = new FriendDao(configuration);
        this.phoneNumberDao = new PhoneNumberDao(configuration);
    }

    @Override
    public List<Friend> getFriends() {
        return friendDao.findAll();
    }

    @Override
    public void addFriend(Friend friend) {
        //addFriendByDirectlySet(friend);
        //addFriendByDao(friend);
        addFriendDirectlySimple(friend);
    }

    private void addFriendByDao(Friend friend) {
        DSLContext create = DSL.using(configuration);
        Long nextId = create.nextval(Sequences.FRIEND_ID_SEQ);
        friend.setId(nextId.intValue());
        friendDao.insert(friend);
    }

    private void addFriendByDirectlySet(Friend friend) {
        DSLContext create = DSL.using(configuration);
        create.insertInto(Tables.FRIEND)
                .set(Tables.FRIEND.ID, Sequences.FRIEND_ID_SEQ.nextval().cast(Tables.FRIEND.ID))
                .set(Tables.FRIEND.NAME, friend.getName())
                .set(Tables.FRIEND.SURNAME, friend.getSurname())
                .execute();
    }

    private void addFriendDirectlySimple(Friend friend) {
        DSLContext create = DSL.using(configuration);
        create.insertInto(Tables.FRIEND)
                .values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
                .execute();
    }

    @Override
    public void addFriendWithPhoneNumber(Friend friend, String phoneNumber) {
        addFriendWithPhoneNumberClassic(friend, phoneNumber);
        //addFriendWithPhoneNumberLambda(friend, phoneNumber);
    }

    private void addFriendWithPhoneNumberLambda(Friend friend, String phoneNumber) {
        DSLContext create = DSL.using(configuration);
        create.transaction(configuration -> {
                DSLContext createInner = DSL.using(configuration);
                Integer id = createInner.insertInto(Tables.FRIEND)
                        .values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
                        .returning(Tables.FRIEND.ID).fetchOne().getId();
                createInner.insertInto(Tables.PHONE_NUMBER)
                        .values(Sequences.PHONE_NUMBER_ID_SEQ.nextval(), phoneNumber).execute();
        });
    }

    private void addFriendWithPhoneNumberClassic(final Friend friend, final String phoneNumber) {
        DSLContext create = DSL.using(configuration);
        create.transaction(new TransactionalRunnable() {
            @Override
            public void run(Configuration configuration) throws Exception {
                DSLContext create = DSL.using(configuration);
                Integer id = create.insertInto(Tables.FRIEND)
                        .values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
                        .returning(Tables.FRIEND.ID).fetchOne().getId();
                create.insertInto(Tables.PHONE_NUMBER)
                        .values(Sequences.PHONE_NUMBER_ID_SEQ.nextval(), phoneNumber, id).execute();
            }
        });
    }

    @Override
    public void removeFriend(Integer friendId) {
        DSLContext create = DSL.using(configuration);
        create.transaction(configuration -> {
            DSLContext createInner = DSL.using(configuration);
            createInner.delete(Tables.PHONE_NUMBER)
                .where(Tables.PHONE_NUMBER.FRIEND_ID.equal(friendId))
                .execute();
            friendDao.deleteById(friendId);
        });
    }

    @Override
    public List<PhoneNumber> getFriendPhones(Integer friendId) {
        return phoneNumberDao.fetchByFriendId(friendId);
    }

    @Override
    public void addPhoneToFriend(Integer friendId, String phoneNumber) {
        phoneNumberDao.insert(new PhoneNumber(DSL.using(configuration).nextval(Sequences.PHONE_NUMBER_ID_SEQ).intValue(),
                                                phoneNumber, friendId));
    }

    @Override
    public void removeNumber(Integer phoneId) {
        phoneNumberDao.deleteById(phoneId);
    }
}

Wyjaśnienia

Powyższy kod będę stopniowo analizował zaczynając od rzeczy bardzo podstawowych skończywszy na tych bardzo przydatnych.

Interfejsy DSLContext, Configuration oraz obiekt DSL

Obiekt implementujący interfejs Configuration przechowuje w sobie informacje o połączeniu z bazą oraz dialekcie - typie bazy. Obiekt implementujący interfejs DSLContext przechowuje dowiązany kontekst z konkretną konfiguracją, za pomocą tego obiektu możemy już tworzyć konkretne zapytania. Obiekt DSL m.in. zwraca implementacje poszczególnych interfejsów np. DSLContext na podstawie Configuration z czego będziemy często korzystać. Mam nadzieje, że na poniższych przykładach trochę to się rozjaśni.

Dodanie danych

 @Override
    public void addFriend(Friend friend) {
        //addFriendByDirectlySet(friend);
        //addFriendByDao(friend);
        addFriendDirectlySimple(friend);
    }

    private void addFriendByDao(Friend friend) {
        DSLContext create = DSL.using(configuration);
        Long nextId = create.nextval(Sequences.FRIEND_ID_SEQ);
        friend.setId(nextId.intValue());
        friendDao.insert(friend);
    }

    private void addFriendByDirectlySet(Friend friend) {
        DSLContext create = DSL.using(configuration);
        create.insertInto(Tables.FRIEND)
                .set(Tables.FRIEND.ID, Sequences.FRIEND_ID_SEQ.nextval().cast(Tables.FRIEND.ID))
                .set(Tables.FRIEND.NAME, friend.getName())
                .set(Tables.FRIEND.SURNAME, friend.getSurname())
                .execute();
    }

    private void addFriendDirectlySimple(Friend friend) {
        DSLContext create = DSL.using(configuration);
        create.insertInto(Tables.FRIEND)
                .values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
                .execute();
    }
Zaimplementowałem metodę
addFriend
na 3 równoważne sposóby:
  • addFriendDirectlySet
  • Za pomocą metody insertInto na obiekcie kontekstowym przyjmującej "identyfikator" tabeli wygenerowany przez jOOQa mówimy, że do tabeli friend dla atrybutu 'id' ustawiamy id pobierane z sekwencji, imię użytkownika na te z obiektu friend, nazwisko na te z obiektu friend, po czym dla tych ustawień wykonujemy zbudowane zapytanie. Jest to równoważne z zapytaniem:
    INSERT INTO friend (id,name, surname) VALUES (friend_id_seq.NEXTVAL,'imie','nazwisko');
    Pobranie z sekwencji następnej wartości realizowane jest poprzez odniesienie do obiektu Sequences.FRIEND_ID_SEQ i jego metody nextval(), co dalej jest to rzutowane to typu atrybutu id. Ponadto jeżeli chcielibyśmy pobrać id najpierw, a później je ustawić to wycięcie tej części na żywca nie zadziała, bo obiekt Sequences.FRIEND_ID_SEQ nie wiedziałby o kontekście w jakim jest realizowany, jak to zrobić pokaże poniżej.
  • addFriendDirectlySimple
  • To rozwiązanie robi dokładnie to samo tylko odpowiada sqlowi następującemu:
    INSERT INTO friend VALUES (friend_id_seq.NEXTVAL,'imie','nazwisko');
    Co przy wielu atrybutach staje sie mało czytelne
  • addFriendByDao
  • Ostatnia metoda ustawia zmiennej friend id i poprzez obiekt DAO (Data Access Object), wstawia go do bazy. Obiekty DAO udostępniają podstawowe metody typu wstawianie, usuwanie, wyszukiwanie przez co upraszczają cała logikę.
    Wyciągniecie następnej wartości z sekwencji realizujemy poprzez wywołanie metody nextval oczkującej obiektu sekwencji na obiekcie kontekstowym.

Tranzakcja, returning

    @Override
    public void addFriendWithPhoneNumber(Friend friend, String phoneNumber) {
        addFriendWithPhoneNumberClassic(friend, phoneNumber);
        //addFriendWithPhoneNumberLambda(friend, phoneNumber);
    }

    private void addFriendWithPhoneNumberLambda(Friend friend, String phoneNumber) {
        DSLContext create = DSL.using(configuration);
        create.transaction(configuration -> {
                DSLContext createInner = DSL.using(configuration);
                Integer id = createInner.insertInto(Tables.FRIEND)
                        .values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
                        .returning(Tables.FRIEND.ID).fetchOne().getId();
                createInner.insertInto(Tables.PHONE_NUMBER)
                        .values(Sequences.PHONE_NUMBER_ID_SEQ.nextval(), phoneNumber).execute();
        });
    }

    private void addFriendWithPhoneNumberClassic(final Friend friend, final String phoneNumber) {
        DSLContext create = DSL.using(configuration);
        create.transaction(new TransactionalRunnable() {
            @Override
            public void run(Configuration configuration) throws Exception {
                DSLContext create = DSL.using(configuration);
                Integer id = create.insertInto(Tables.FRIEND)
                        .values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
                        .returning(Tables.FRIEND.ID).fetchOne().getId();
                create.insertInto(Tables.PHONE_NUMBER)
                        .values(Sequences.PHONE_NUMBER_ID_SEQ.nextval(), phoneNumber, id).execute();
            }
        });
    }
Powyższa metoda addFriendWithPhoneNumber realizuje dodanie użytkownika wraz z numerem telefonów co jest równoważne operacji na dwóch tabelach, stąd uzasadnione jest robienie obu wstawień w obrębie transakcji, ponadto to wymaganie umożliwia pokazanie funkcjonalności Postgresa (oraz jej obsługi przez jOOQ) jaką jest komenda returning umożliwiająca zwrócenie wartości (zbioru wartości) wstawianej (w naszym przypadku id użytkownika, które jest kluczem obcym dla numeru telefenu). Powyższa metoda addFriendWithPhoneNumber , posiada 2 równoważne implementacje, jedna wykorzystująca nowość w javie 8, tzn. wyrażenia lambda (addFriendWithPhoneNumberLambda) operującego na obiekcie implementującym interfejs Configuration, druga wykorzystująca stary mechanizm tzn. anonimową implementacje interfejsu(addFriendWithPhoneNumberClassic) przeciązająca metodę run(Configuration). W jednym jak i drugim przypadku w obrębie metody transaction wykonujemy naszą transakcje. Aby skorzystać z komendy RETURNING, wystarczy że wykonamy metodę .returning(Tables.FRIEND.ID).fetchOne().getId(); tzn. określimy atrybut, a następnie go pobierzemy, co w prosty sposób załatwia nam pobranie danego atrybutu (w tym przypadku id).

Pełna implementacja

Aby nie zaciemniać tematu nie będę wstawiał tutaj implementacji Springowej, która po prostu wywołuje odpowiednie metody menadżera, natomiast drogi Czytelniku jeżeli chcesz to zachęcam do pobrania gotowego kodu oraz jego uruchamianie (ostrzegam, że jest on kodem przykładowym, a więc nie waliduje mnóstwa rzeczy, które powinny być sprawdzona w kodzie produkcyjnym). Implementacja

Podsumowanie

Starałem się pokazać jaka moc drzemie w ORMapingu jakim jest jOOQ jednocześnie prezentując prosty poradnik dla kogoś kto chciałby jej użyć "na już". Warto jednak pamiętać, że jego możliwości daleko wykraczają poza ten wpis, a więc wymagają pewnego wkładu aby je poznać (do czego gorąco zachęcam). A na koniec dla wszystkich zainteresowanych bazami danych polecam odwiedzenie twitera twórcy biblioteki https://twitter.com/lukaseder.

Bibliografia

http://www.jooq.org/
http:/vertabelo.com/
http://spring.io/guides/gs/spring-boot/

wtorek, 19 maja 2015

Spring Boot 2 - zmienne środowiskowe

Zmienne środowiskowe? A komu to potrzebne?

Pisząc poważną aplikację szybko zdamy sobie sprawę, że "hard-codowanie" pewnych stałych nie jest najlepszym pomysłem. Dane dostępowe do baz danych, czy innych aplikacji - są to rzeczy, które mogą się generalnie zmieniać w czasie. Nie chcemy być zmuszeni do grzebania w kodzie np. dlatego, że baza jest przenoszona na inny serwer. Jest jeszcze jeden problem: można się domyśleć, że na naszym lokalnym środowisku developerskim będziemy używać innej bazy danych niż na serwerze docelowym (produkcyjnym). W dodatku prawdopodobnie po drodze pojawi się jeszcze jakieś środowisko testowe z jeszcze inną bazą; co gorsza może być tych środowisk jeszcze więcej.
Zapisywanie więc takich rzeczy w kodzie jest kompletnie bez sensu. Jak inaczej? Można użyć zmiennych środowiskowych! Jednym ze sposobów na definiowane zmiennych środowiskowych jest wprowadzenie ich jako parametrów wywołania aplikacji, np. tak:
java -jar mojaAplikacja.jar --my.property=HELLO
Przekazywanie jednak wielu zmiennych w ten sposób, jeszcze inaczej dla każdego środowiska może być dość karkołomne. Zapewne można lepiej. A jak? Najlepiej w jakimś zewnętrznym pliku.

Co na to Spring Boot?

Spring Boot daje bardzo przyjemne metody pracy ze zmiennymi środowiskowymi. Jest ogólnie kilka sposobów na osiągnięcie celu. Skupię się jednak na jednym, który moim zdaniem jest najwygodniejszy - pliki .yml. Jak taki plik wygląda?:
spring:
  profiles: default
example:
  testProperty: testowa_właściwość_default
database:
  port: 5432
  userName: postgres
  host: localhost
  password: haslo
  databaseName: devdb
---
spring:
  profiles: test
example:
  testProperty: testowa_właściwość_test
database:
  host: 10.23.29.1
  password: GR5H2#$$^@@
  databaseName: testMyAppDb
Jak widać, plik .yml ma strukturę drzewiastą. Jest to niezwykle wygodne do określania struktury naszych zmiennych. Zastanawiacie się, w jaki sposób wstrzyknąć taką konfigurację do naszej aplikacji? Mniej więcej tak łatwo:
@Component
@ConfigurationProperties(prefix = "database")
public class DatabasePropertiesGetter {

    private String port;
    private String host;
    private String userName;
    private String databaseName;
    private String password;

    //setters!
}
Wystarczy więc stworzyć klasę odpowiadającą strukturą danej gałęzi drzewa i oznaczyć ją adnotacją @ConfigurationProperties z pasującym prefiksem. Zadziała to tylko na beanach zarządzanych przez Spring, stąd adnotacja @Component.
W pliku powyższym określiliśmy zmienne domyślne oraz te charakterystyczne dla środowiska testowego. Skąd Spring Boot będzie wiedział, które z nich wstrzyknąć do naszej aplikacji? To również trzeba podać przez odpowiednią zmienną konfiguracyjną: spring.profiles.active. Aby nie zaciemniać głównego pliku .yml, polecam ustawić tę zmienną w inny sposób: przez parametr w linii poleceń, albo w innym pliku.

Zastosowanie w aplikacji

Właśnie, to był wstęp. Teraz zapewne warto byłoby wyjaśnić krok po kroku jak zastosować pliki .yml do konfiguracji naszej aplikacji. Żeby nie zaczynać od zera, posłużmy się aplikacją, którą stworzyliśmy poprzednio (tutaj).
Załóżmy prosty scenariusz: użytkownik chce mieć możliwość sprawdzenia, na jakim środowisku jest aplikacja (może przecież zapomnieć :) ). W tym celu rozbudujmy strukturę plików naszej aplikacji do takiej postaci:
-MyFirstSpringBootApp
--config
---application.properties
---application.yml
--src
---main
----java
-----com
------example
-------Environment.java
-------HelloController.java
-------MainClass.java
--pom.xml
Zobaczmy teraz, co się zmieniło.
Folder config - tutaj znajdują się nasze pliki konfiguracyjne. Dlaczego akurat tutaj? Generalnie nie ma to znaczenia, gdzie się znajdą, o ile powiemy Mavenowi, że ma zawartość takiego folderu dołączyć do budowanego jara.

Plik application.properties - jego zawartość jest następująca:
spring.profiles.active=dev
Służy on do ustawienia środowiska, na którym działa aplikacja. Pliki .properties są natomiast alternatywnym do plików .yml sposobem ustawiania zmiennych środowiskowych. Póki co, ustawiamy w tym pliku aktywne środowisku developerskie.

Plik application.yml - definiuje zmienne środowiskowe z podziałem na środowiska. Dla naszego przypadku może wyglądać tak:
spring:
  profiles: default
environment:
  name: nieznane
---
spring:
  profiles: dev
environment:
  name: dev
---
spring:
  profiles: test
environment:
  name: test
---
spring:
  profiles: prod
environment:
  name: produkcja
OK, czas wprowadzić te dane do aplikacji.

Plik Environment.java - nowa klasa, która będzie odzwierciedlać właściwości środowiska (czyli jak na razie tylko jego nazwę :) ):
package com.example;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "environment")
public class Environment {
    
    private String name;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
}
Zwykła klasa z jednym polem oraz getterem i setterem dla niego. Dodatkowo mamy adnotację @Component, która tworzy beana springowego, do którego dzięki adnotacji @ConfigurationProperties wstrzykiwana jest wartość zmiennej name. Uwaga: setter, chociaż nie będzie jawnie wywoływany, jest tutaj konieczny. Inaczej pole name nie będzie zainicjalizowane.


Plik HelloController.java - co się zmieniło w naszym kontrolerze?:

package com.example;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.beans.factory.annotation.Autowired;

@RestController
public class HelloController {
    
    @Autowired
    private Environment env;
    
    @RequestMapping(value = "/")
    public String helloWorld() {
        return "

Hello World

" + "Moja pierwsza aplikacja w Spring Boot"; } @RequestMapping(value = "/whatEnvironmentIsThis") public String whatEnvironment() { return "Aplikacja działa na środowisku " + env.getName(); } }
Doszło nam więc pole typu Environment, w które zostanie wstrzyknięty nasz bean oraz metoda mapująca żądania HTTP pod adres /whatEnvironmentIsThis, która realizuje nasze założenie: zwraca nazwę środowiska.

Plik pom.xml - musimy mavena poinstruować, żeby dodał zawartość katalogu config do generowanego jara. Dzieje się to przez dodanie następującego kodu:

    
        config
    

Zaś cały plik wygląda tak:

    4.0.0
    com.example
    SpringBootFirstApp
    0.0.1-SNAPSHOT
         
    
        org.springframework.boot
        spring-boot-starter-parent
        1.2.3.RELEASE
    
    
    
         
            
                config
            
        
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        
    

Efekty

Czas sprawdzić, czy to wszystko działa. Budujemy więc aplikację komendą mvn clean build i uruchamiamy powstałego jara. Po wejściu w przeglądarce na:
http://localhost:8080/whatEnvironmentIsThis
otrzymujemy następującą odpowiedź:

Aplikacja działa na środowisku dev

Jest to zgodne z prawdą. Spróbujmy teraz to zmienić.
Uruchommy teraz ponownie aplikację, tym razem dodając parametr, który ustawi zmienną spring.profiles.active na wartość "test":
java -jar target/SpringBootFirstApp-0.0.1-SNAPSHOT.jar --spring.profiles.active=test
Strona, jaką zobaczymy pod adresem http://localhost:8080/whatEnvironmentIsThis wygląda tak:

Aplikacja działa na środowisku test

Zmienne podane jako parametry wywołania mają najwyższy priorytet - dlatego to, co wpisaliśmy w konsoli nadpisuje nam wartość zdefiniowaną w pliku application.properties. Gdyby zmodyfikować plik application.properties następująco:
spring.profiles.active=test
i odpalili jara bez dodatkowych parametrów, efekt byłby ten sam.
Na koniec uruchomimy aplikację z nazwą środowiska, które nie zostało określone w pliku .yml. Powinno to skutkować załadowaniem domyślnych wartości, a więc tych zdefiniowanych pod profilem default. Uruchamiamy jara następująco:
java -jar target/SpringBootFirstApp-0.0.1-SNAPSHOT.jar --spring.profiles.active=dontknow
Skutek tego jest zgodny z oczekiwaniem - teraz nasza strona wygląda tak:

Aplikacja działa na środowisku nieznane

Pola listowe

W pliku .yml możemy również definiować pola listowe. Listy można definiować następująco:
myProperty:
  servers:
    - serv1.abb.com
    - serv2.abb.com
Taką listę serwerów możemy przypiąć do naszej aplikacji tworząc klasę tego typu:
@Component
@ConfigurationProperties(prefix = "myProperty")
public class MyClass {
    
    private List< String > servers;

    public List< String > getServers() {
        return servers;
    }

    public void setServers(List< String > servers) {
        this.servers = servers;
    }
}

Edycja zmiennych po zbudowaniu aplikacji

Nasze pliki konfiguracyjne - application.properties i application.yml są pakowane do wynikowego jara - nie ma problemu, żeby w archiwum otworzyć taki plik i wyedytować go. Po ponownym uruchomieniu, aplikacja będzie działać z uaktualnionymi zmiennymi środowiskowymi.

Podsumowanie

Temat zmiennych środowiskowych przewija się w każdej poważniejszej aplikacji. Mam nadzieję, że ten post przyda się, aby w Waszych aplikacjach do utrzymania porządków w konfiguracji.

poniedziałek, 18 maja 2015

UI Bootstrap - AngularUI

Bootstrap i AngularJs - czy to może razem działać?

Framework Bootstrap posiada wiele aktywnych komponentów, które reagują na działania użytkownika (np. okna modalne, rozwijane listy, opisane w sekcji javascript). Przykładowo, aby uruchomić okno modalne, należy zdefiniować taki przycisk:

<button type="button" class="btn btn-primary btn-lg" data-toggle="modal" data-target="#myModal">
  Launch demo modal
</button>
 

Aby kontrolować akcje musielibyśmy odwoływać się do globalnych funkcji javascriptowych. Na szczęście problem został rozwiązany przez stworzenie biblioteki UI Bootstrap - Angular UI, która pozwala na łatwiejsze integrowanie komponentów reagujących Bootstrapa z Angularem.

W tym tutorialu stworzymy proste okno modalne zarządzane przez AngularJs.

Okno modalne

Na początek tworzymy pliki index.html oraz app.js, ktróre będą odpowiednio zawartością naszej strony oraz skryptem AngularJS. Nasze okno modalne tworzymy przy pomocy kodu:

<div ng-controller="ModalDemoCtrl">
    <script type="text/ng-template" id="content.html">
      <div class = "modal-header" > <h3 class = "modal-title" > I 'm a modal!</h3>
        </div>
        <div class="modal-body">
          Okno modalne!
        </div>
        <div class="modal-footer">
            <button class="btn btn-primary" ng-click="ok()">OK</button>
            <button class="btn btn-warning" ng-click="cancel()">Cancel</button>
        </div>
    </script>

    <button class="btn btn-default" ng-click="open()">Open me!</button>
    <div>Otworzono {{ opened }} razy</div>
  </div> 

Na początku definiujemy kontroler angularowy o nazwie ModalDemoCtrl. Następnie ukrywamy content naszego okna poprzez umieszczenie go pomiędzy znacznikami <script
type="text/ng-template"></script>. Nadajemy mu identyfikator, aby później odwołać się do niego w skrypcie.

Okno ma dwa przyciski wywołujące funkcje ok() oraz cancel(). Funkcje te zostaną zdefiniowane wewnątrz kontrolera obsługującego okno.

Widzimy również kod odpowiedzialny za przycisk otwierający okno z funkcją open() zdefiniowaną w ModalDemoCtrl. Tworzymy również prosty licznik otwarć okna.

Teraz możemy przejść do pliku app.js. Definiujemy naszą aplikację:

angular.module('demo', ['ui.bootstrap']);

W nawiasach kwadratowych musi znaleźć się nazwa frameworka UI Bootstrap - ui.bootstrap, aby Angular poprawnie współgrał z Bootstrapem.

Definiujemy główny kontroler okna:

angular.module('demo').controller('ModalDemoCtrl', function ($scope, $modal) {
    
    $scope.opened=0;
    
    $scope.open = function () {
        var modalInstance = $modal.open({
            animation: true,
            templateUrl: 'content.html',
            controller: 'contentController',
            resolve: {
              opened: function() {
                return $scope.opened;
              }
            }
        });
    
      modalInstance.result.then(function(opened) {
        $scope.opened = opened;
      });
    };

});

Jako jeden z parametrów funkcji kontrolera przyjmujemy zmienną $modal. To na niej będziemy definiowali wszystkie operacje. Funkcja open odpowiedzialna za otwarcie okna tworzy zmienną lokalną modalInstance, która korzysta z dostarczonej funkcji $modal.open. Możemy wykorzystać różne parametry, aby właściwie określić zachowanie okna. My określamy rodzaj animacji, content naszego okna, kontroler odpowiedzialny za akcje wywoływane wewnątrz otwartego okna oraz wartość przekazywaną do powyższego kontrolera (contentController). O wiele więcej parametrów można znaleźć na oficjalnej stronie UI Bootstrap. W parametrze resolve wysyłamy zmienną licznikową.

Określamy również co zrobić z wartością zwracaną z kontrolera contentController. U nas będzie to zaktualizowana zmienna licznikowa (aktualizuje się tylko po naciśnięciu "OK").

A oto kod naszego contentController:

angular.module('demo').controller('contentController', ['$scope', '$modalInstance','opened', function($scope, $modalInstance,opened) {
    $scope.opened=opened;
    $scope.ok = function () {
        $scope.opened++;
        $modalInstance.close($scope.opened);
    };
    $scope.cancel = function () {
        $modalInstance.dismiss('cancel');
    };

}]);

Jak widać jest bardzo prosty. W parametrach funkcji przekazujemy $modalInstance, czyli instancję naszego okna oraz zmienną licznikową opened. Wewnątrz kontrolera definiujemy dwie funkcje wywoływane po naciśnięciu przycisków. Wewnątrz korzystamy z wbudowanych funkcji. Funkcja close(obj) zamyka okno i zwraca podany w argumencie obiekt. Funkcja dismiss('cancel') po prostu zamyka okno.

Całość można zobaczyć w akcji tutaj.

Możliwości

To zaledwie ułamek możliwości tego frameworka. Szereg przykładów można znaleźć na głównej stronie UI Bootstrap. Definiując proste funkcje w Angularze, jesteśmy w stanie zapanować nad zachowaniem reaktywnych elementów na stronie nie musząc męczyć się w odwoływanie się do elementów DOM przez goły JavaScript.

Podsumowując, jeśli chcemy stworzyć aplikację biznesową, która nie mieć zbyt spersonalizowanego UI, połączenie Bootstrap + UI Bootstrap - Angular UI będzie bardzo dobrym wyborem - zaoszczędzimy czas i wiele potencjalnych problemów.

Framework Bootstrap

Odwieczny problem programisty... i nie tylko

Ileż to razy mamy głowę pełną pomysłów a brak nam odpowiednich narzędzi, aby wprowadzić je w życie? Programiści również często są w tej sytuacji. Mają wizję świetnej aplikacji zapowiadającej się na miarę Facebooka, wszystkie funkcjonalności spisane na kartce, architektura przygotowana. Wydaje się, że nic tylko siadać i wdrażać pomysły w życie. Niestety, gdy przychodzi do zaprojektowania interakcji z użytkownikiem oraz związanego z nimi interfejsu użytkownika rodzi się problem. Cała twórczość wizualna programisty na studiach ogranicza się zazwyczaj do kilku godzin na laboratoriach z grafiki komputerowej oraz do rysowania diagramów encji. Owszem, zdarzają się wyjątki, ale ciężko spotkać dobrego backendowca i śledzącego zmieniające się z zawrotną prędkością trendy UI/UX frontendowca w jednym.

Jednak nie tylko programiści mogą mieć problem z UI. Stworzenie ładnego, funkcjonalnego i reagującego(!) na akcje użytkownika interfejsu może być skomplikowane i dość uciążliwe dla początkujących (magiczne sztuczki i niezrozumiałe zachowania CSS) oraz czasochłonne dla profesjonalistów. Sam wiem, że stworzenie nawet prostej strony w standardowych technologiach (HTML,CSS,JavaScript) od podstaw zabiera sporo czasu.

Jest problem? To trzeba go rozwiązać :)

Na pierwszy ogień idą gotowe szablony. Są dziesiątki stron oferujących takie usługi. Jednak ładniejsze szablony są zazwyczaj płatne. Po drugie trendy w internecie zmieniają się bardzo szybko i musielibyśmy dość często wyrzucać do kosza stare i zastępować nowymi. Poza tym jakakolwiek modyfikacja funkcjonalności bez znajomości frontendowych technologii mogłaby być frustrująca dla backendowca.

Bootstrap, czyli jak prosto i szybko mieć coś ładnego

Z pomocą przychodzi framework Bootstrap. Zakłada on podstawową znajomość HTMLa oraz CSS. Oferuje szeroki wybór gotowych komponentów, które pozwalają na realizację zarówno podstawowych jak i zaawansowanych funkcjonalności. UI powstaje w nim bardzo szybko - wystarczy skopiować kawałek kodu z przykładu z oficjalnej strony, pozamieniać treść i mamy już działający komponent. Dzięki systemowi kolumn nie musimy martwić się o "rozejechanie elementów na stronie" czy inne nieciekawe efekty obecne przy pracy z CSS.

Zaczynamy

Wchodzimy na stronę:
http://getbootstrap.com/getting-started/#download
i wybieramy Download Bootstrap. Kopiujemy pliki do folderu naszej web-apki i tworzymy plik tekstowy style.css oraz index.html z poniższą zawartością (zmieniamy lokalizacje plików na odpowiednie):

<!DOCTYPE html>
<html lang="pl">
<head>
  <meta charset="utf-8" />
  <title>Nasza strona</title>
  <link rel="stylesheet" href="style.css" />
  <link rel="stylesheet" href="bootstrap/3.3.4 /css/bootstrap.css">
  <link rel="stylesheet"  href="bootstrap/3.3.4/css/bootstrap-theme.css"> 
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
</head>
<body>
</body>
</html>

Układ elementów i responsywność Bootstrap oferuje system podziału zawartości strony na 12 równych kolumn, które charakteryzują się pełną responsywnością. Naszym zadaniem jest jedynie określenie jak dużo kolumn będzie zajmował dany element strony w różnych rozdzielczościach.
Wyróżniamy 4 różne rozdzielczości urządzeń: extra-small (xs), small (sm), medium (md) i large (lg).
Aby zdefiniować jeden rząd kolumn na stronie, piszemy np:

<div class="row">
  <div class="col-md-8">.col-md-8</div>
  <div class="col-md-4">.col-md-4</div>
</div>


Jak widać, nie musimy definiować szerokości dla wszystkich czterech powyższych rozdzielczości urządzeń. Wystarczy, że zdefiniujemy szerokość dla jednej. Inny przykład:

<div class="row">
  <div class="col-xs-12 col-md-8">.col-xs-12 .col-md-8</div>
  <div class="col-xs-6 col-md-4">.col-xs-6 .col-md-4</div>
</div>

Tutaj widzimy, że pierwszy div dla urządzeń o rozdzielczości medium będzie zajmował 2/3 szerokości strony, a drugi 1/3. Dla rozdzielczości extra-small pierwszy będzie zajmował całą szerokość strony. Wynika z tego więc, że drugi zostanie wyświetlony pod nim.
Możemy teraz powyższy kod umieścić w divie z klasą container, dzięki czemu treść naszej strony będzie wyśrodkowana i nie będzie zajmowała 100% szerokości strony. Jeśli chcemy jednak zawartość na całej szerokośći okna przeglądarki używamy klasy container-fluid.

Więcej nt kolumn można znaleźć tutaj:
http://getbootstrap.com/css/#grid

Komponenty

Przetestujemy teraz kilka komponentów Bootstrapa. Stworzymy przykładową stronę korzystając tylko z nich.

Zaczynamy od nagłówka. Korzystając z gotowego kodu, wstawiamy go na naszą stronę i dopasowujemy wg potrzeb. Posiada on tytuł strony oraz linki, które mogą być rozwijane. Przykładowy kod tutaj:



Następnie wykorzystamy prosty slider, któremu w Bootstrapie odpowiada komponent carousel. Przykładowy kod poniżej:


Następnie wstawiamy trzy kolumny korzystając z siatki Bootstrapa oraz wykorzystując buttony:



Możemy również dodać panele i paginację:



 Ostatecznie cała strona wraz z kodem wygląda tak:




Kod kompilowany Less i Sass

Bootstrap jes zbudowany na preprocesorze Less, który umożliwia tworzenie zmiennych wewnątrz definicji CSS. Przydatne jest to np. w definiowaniu palety kolorów, czy innych stałych na stronie parametrów. Bootstrap wspiera również preprocesor Sass. Aby skompilować i wygenerować swój kod css Bootstrapa przechodzimy na stronę getbootstrap.com/customize i wypełniamy formularz zgodnie z własnymi preferencjami.

Jak widać dzięki Bootstrapowi można łatwo i szybko tworzyć przyjemne dla oka i responsywne aplikacje www. Jedyną wadą mogą być ograniczenia i powtarzalność, ale przy odrobinie wysiłku włożonego w modyfikację kodu css, można spersonalizować każdy komponent.

niedziela, 17 maja 2015

Spring Boot 1 - tworzymy prostą aplikację

Cześć,
w tym i w kilku kolejnych postach chciałbym przybliżyć Wam kilka rzeczy związanych z tworzeniem aplikacji we frameworku Spring Boot. Dzisiaj zbudujemy prostą aplikację opartą na Spring Boot. Ale najpierw...

A co to ten Spring Boot?

Spring był i jest bardzo często wykorzystywanym frameworkiem do tworzenia aplikacji webowych. Aplikacje webowe mają to do siebie, że muszą działać w środowisku jakiegoś serwera (kontenera) - a więc chociażby takiego tomcata. Dlatego, jeśli stworzymy aplikację w Springu, aby ją uruchomić, musimy wdrożyć ją na takowy serwer. Konieczność uzależniania się od dodatkowej aplikacji może niektórym się nie spodobać. Powstała więc alternatywa: Spring Boot. Co to robi? Pozwala stworzyć aplikację springową, która zachowuje się jak samodzielna (standalone) aplikacja - można spakować ją do pliku .jar i uruchomić na dowolnym komputerze z JRE (Java Runtime Environment). Uruchomienie naszej aplikacji jest więc tak proste:
java -jar mojaAplikacja.jar
Po uruchomieniu jara nasza po chwili będziemy mogli odwiedzić jej stronę główną wpisując w przeglądarce adres, który w domyślnej konfiguracji będzie taki:
http://localhost:8080/
No dobrze - pisałem, że aplikacje springowe wymagają serwera. Gdzie jest więc serwer? To proste - taka aplikacja korzysta z serwera (kontenera) wbudowanego (embedded container). Przy generacji pliku .jar dodane zostają biblioteki serwera, który jest natomiast startowany w wyniku wywołania odpowiedniej metody. Całkiem fajne, nie?
Spring Boot daje jeszcze trochę innych przydatnych funkcjonalności upraszczających życie. Generalnie jego ideą jest maksymalne uproszczenie konfiguracji (która przy Springu potrafi dać w kość), tak, aby dało się bez bólu i trudu zrobić aplikację, która działa.

Jak zacząć?

Dobrze, koniec gadania. Czas zająć się rzeczami praktycznymi. Do budowania aplikacji użyjemy Mavena - zajmie się on za nas pobraniem zależności, kompilacją oraz generacją prawidłowego jara.
Utwórzmy sobie gdzieś następującą strukturę katalogów/plików (zgodnie z konwencją Mavena):

-MyFirstSpringBootApp
--src
---main
----java
-----com
------example
-------MainClass.java
--pom.xml

MainClass.java będzie naszą główną klasą, a pom.xml będzie zawierał informacje dla Mavena.

pom.xml

Otwórzmy teraz plik pom.xml. Jego zawartość niech będzie następująca (za chwilę postaram się wyjaśnić o co chodzi):

    4.0.0
    com.example
    SpringBootFirstApp
    0.0.1-SNAPSHOT
         
    
        org.springframework.boot
        spring-boot-starter-parent
        1.2.3.RELEASE
    
    
    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        
    

W środku tagu <parent> definiujemy odwołanie do mavenowego projektu rodzica. Dzięki temu nie musimy specyfikować ogromnej ilości zależności, pluginów itd., ponieważ to wszystko dziedziczymy od naszego rodzica, czyli spring-boot-starter-parent. Dalej włączamy plugin do budowania aplikacji używającej Spring Boot - dzięki niemu maven będzie generował nam właściwe, wykonywalne jary. Niżej mamy zdefiniowaną tylko jedną zależność - dla aplikacji webowych będzie zawsze tutaj występować. Przyznacie, że nieskomplikowane? To też jest siła Spring Boota. Co dalej? Teraz możemy już napisać naszą klasę główną.

MainClass.java

Otwórzmy naszą klasę główną i umieśćmy tam następujący kod. Jest to absolutne minimum, aby wystartować wbudowany serwer i uchronić aplikację przed natychmiastową awarią:
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MainClass {

    public static void main(String[] args) {
        SpringApplication.run(MainClass.class, args);
    }

}

Adnotacja @SpringBootApplication jest niezbędna - jak czytamy w dokumentacji, równoważna jest ona trzem adnotacjom @Configuration, @EnableAutoConfiguration oraz @ComponentScan z domyślnymi parametrami
Funkcja main() zaś inicjuje całą maszynerię przez wywołanie funkcji run() klasy SpringApplication.

Pierwsze uruchomienie

Tak! Mamy wystarczająco kodu, aby naszą aplikację zbudować i uruchomić. Aby dokonać tego pierwszego, wchodzimy przez terminal do folderu MyFirstSpringBootApp i wpisujemy:
mvn install
Jeśli wszystko pójdzie zgodnie z planem, otrzymujemy pod koniec komunikat:
[INFO] BUILD SUCCESS
Teraz jesteśmy gotowi do uruchomienia aplikacji. Wpisujemy:
java -jar target/SpringBootFirstApp-0.0.1-SNAPSHOT.jar
Aplikacja uruchomi się, o czym świadczą m.in. te komunikaty:
2015-05-17 18:06:13.280  INFO 10832 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-05-17 18:06:13.282  INFO 10832 --- [           main] MainClass                                : Started MainClass in 3.313 seconds (JVM running for 3.722)
Tak więc, uruchomiony został tomcat, który domyślnie pełni rolę serwera wbudowanego - domyślnie na porcie 8080. Wpisując w przeglądarce adres:
http://localhost:8080/
zobaczymy stronę Whitelabel Error Page - nie ma się co dziwić - nasza aplikacja póki co nie została skonfigurowana do obsługi jakichkolwiek żądań. Czas to naprawić!

Klasa kontrolera

Napiszmy więc na szybko klasę, która posłuży jako banalny kontroler - dzięki temu nasza aplikacja da jakiś znak życia. Utwórzmy w katalogu MyFirstSpringBootApp/src/java/main/com/example plik HelloController.java i wprowadźmy następujący kod:
package com.example;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class HelloController {
    
    @RequestMapping(value = "/")
    public String helloWorld() {
        return "

Hello World

" + "Moja pierwsza aplikacja w Spring Boot"; } }

W klasie tej mapujemy wszelkie żądania do korzenia naszej strony do metody helloWorld(), która zwraca to, co widać. Dzięki adnotacji @RestController serwer zwraca do użytkownika w odpowiedzi dokładnie to, co zwraca nasza metoda.
Ponownie budujemy aplikację (mvn clean install) i uruchamiamy utworzonego jara.
Jeśli teraz w przeglądarce ponownie wpiszemy adres:
http://localhost:8080/
Zobaczymy coś takiego:

Hello World

Moja pierwsza aplikacja w Spring Boot

Dokładnie o to nam chodziło!

Podsumowanie

To by było na tyle - udało nam się zrobić prostą aplikację używając Spring Boot. Gratulacje :) .

AngularJS - gruby klient

Od czasu wydania biblioteki AngularJS przez firmę Google, programiści decydujący się na jej użycie zauważają, że struktura i sposób tworzenia aplikacji internetowych gwałtownie się zmienia.  Sukces tej biblioteki (a w zasadzie frameworka) można przypisać funkcjom, których braki w zwykłym JavaScripcie powodowały nieefektywne pisanie ciężko-utrzymywalnych aplikacji. Ważną cechą Angulara jest to, że duża część logiki niezwiązanej z bezpośrednim przetwarzaniem danych utrzymywana jest po stronie klienta. Pozwala to na odciążenie serwera, którego głównym zadaniem jest wystawianie danych w stylu REST oraz szablonów html, interpretowanych przez bibliotekę już po stronie klienta. Oczywiście, nie zwalnia to programisty od walidacji danych przychodzących do serwera.

W poniższym artykule spróbujemy stworzyć prostą aplikację w opraciu o AngularJS. Na potrzeby tutoriala będę zakładał, że już masz uruchomiony serwer aplikacyjny z którym będzie przeprowadzana komunikacja. Jak uruchomić taki serwer korzystając z frameworku Spring Boot dowiesz się na naszym blogu.

Instalacja

Pierwszym krokiem prowadzącym do sukcesu jest pobranie samej biblioteki i wgranie jej do zasobów serwerowych tak, aby była dostępna dla użytkowników naszej nowej strony. W trakcie pisania tego bloga aktualna i stabilna wersja Angulara to 1.3.15, więc pobieramy go stąd: https://code.angularjs.org/1.3.15/. Możemy pobrać całego zipa ze wszystkimi modułami, albo tylko takie, które chcemy - na potrzeby tego tutoriala wymagany jest plik angular.js oraz angular-route.js.

Po wgraniu zasobów na serwer, sprawdzamy czy na pewno jest on dostępny z poziomu internetu. Zakładając, że wszystkie biblioteki zewnętrzne trzymamy na serwerze w folderze lib, a biblioteki dotyczące Angulara w folderze angularjs po wejściu w link http://localhost:port/lib/angularjs/angular.js
powinniśmy zobaczyć wnętrze cuda, z którego korzystamy. W razie problemów należy sprawdzić, czy dobrze skonfigurowaliśmy scieżkę do zasobów w naszym frameworku czy też narzędziu automatyzującym budowę oprogramowania (maven lub gradle).

Pierwsza strona

Mając solidne zaplecze możemy zacząć tworzyć szablon strony. Tworzymy więc plik index.html i umieszczamy go w naszym frameworku tak, aby ten szablon był serwowany jako 'root' strony. W tym momencie można zauważyć już pierwszą różnicę - serwer serwuje statyczne pliki html, javascript, a nie jak przypadku większości technologii (JSP, JSF, PHP), gdzie po stronie serwera generowany jest gotowy plik html, uwzględniający stan użytkownika wizytującego na naszej stronie. Zadaniem Angulara będzie dynamiczna zmiana zawartości DOM html'a. Poniżej przedstawiony kod wklejamy do pliku index.html (z dokładnością ze scieżkami do skryptów):

<html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
      <title>Hello world</title>
      <script src="/lib/angularjs/angular.js"></script>
      <script src="/lib/angularjs/angular-route.js"></script>
   </head>
   <body>
      "Hello world"
   </body>
</html>

Po ponownym uruchomieniu serwer'a, a następnie po wejściu w link http://localhost:port/ powinniśmy zobaczyć napis Hello world. Dodatkowo upewniamy się, że poprawnie pobierane są biblioteki angularowe - konsola Javascript'owa przeglądarki powinna zalogować możliwe problemy.

Mając taką podstawę, możemy przejść do możliwości jakie daje nam AngularJS.

Angular w akcji

Gratuluję wytrwałości - najtrudniejsze kwestie konfiguracyjne są za nami. Teraz nic nie powinno przeszkodzić przy tworzeniu pierwszej aplikacji w Angularze.
Pisanie w Angularze nie przypomina już typowego procesu tworzenia kodu w samym Javascriptcie lub przy pomocy biblioteki jQuery. Za taką zmianę odpowiada wprowadzenie do biblioteki mechanizmu wstrzykiwania zależności. W skrócie, mechanizm ten jest odpowiedzialny za dostarczanie obiektów w różnych miejscach aplikacji, umożliwiając na zwiększenie modularności aplikacji. O stworzenie wybranych obiektów, usunięcie, kontrolę i ich kontekst użycia dba Angular - my musimy jednak rozumieć gdzie i jak możemy ich użyć.
Tak więc obiekty Angularowe można podzielić na:
  • moduły, zawierające i obejmujące kontekst całej aplikacji (tzw. root scope)
  • kontrolery, specyfikujące zachowanie dla wybranej przez nas części aplikacji, każdy kontroler działa niezależnie od drugiego
  • serwisy, dostarczające ogólne zachowanie, funkcje w każdym miejscu jednego modułu (Angular ma własne serwisy ogólnego przeznaczenia, z których możemy skorzystać)
Powyższy przegląd obiekt jest pobieżny, ale wystarczający do tego, aby zacząć tworzyć aplikację w Angularze.

Moduł

Moduł definiowany jest dla głównego szablonu html naszej aplikacji (co nie oznacza, że nie możemy mieć takich szablonów więcej). Aby nie dawać użytkownikowi wrażenia przeładowanej strony (czyli tym samym korzystać jedynie z technologii AJAX) warto zdecydować się na jeden moduł dla całej aplikacji - tutaj musimy podjąć decyzję architektoniczną, na ile złożona jest nasza aplikacja. Zawsze możemy też stworzyć hierarchię modułów, jeżeli chcemy utrzymać modularność aplikacji.
Nie czekając dłużej stwórzmy pierwszy moduł - do tego musimy dodać nowy plik w naszym projekcie np. app.js i w nim dodajemy:
/**
 * Główny moduł aplikacji.
 * 
 * @type {module}
 */
var searchApp = angular.module("searchApplication", ['ngRoute']);

To było proste, prawda? Po załączeniu bilioteki Angular'owej mamy dostęp do "faktorii" angular, dzieki której tworzymy nowe moduły. Jako pierwszy argument podajemy nazwę modułu. Taka konwencja jest utrzymana w całym Angularze, co pozwala na identyfikację obiektów i wstrzykiwanie zależności po nazwie. Co więcej Angular sprawdza, czy poprawnie podaliśmy nazwę obiektu w innym miejscu, i w przypadku błędu dowiadujemy się wszystkiego z konsoli JavaScript w przeglądarce:
Uncaught Error: [$injector:modulerr] Failed to instantiate module searchApcplication due to:
Error: [$injector:nomod] Module 'searchApcplication' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.
Drugim argumentem jest tablica napisów oznaczających jakie inne moduły mają być zainicjalizowane dla naszej aplikacji i zarazem używane w stworzonym module.

Teraz musimy po stronie widoku zdefiniować, że chcemy skorzystać z modułu searchApp. Dodatkowo musimy dołączyć nasz nowy skrypt - u mnie znajduje się w katalogu scripts:
<html ng-app="searchApcplication">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>Search Application</title>
    <script src="/lib/angularjs/angular.js"></script>
    <script src="/lib/angularjs/angular-route.js"></script>
    <script src="/scripts/app.js"></script>
</head>

I tak mamy już omówioną kwestię modułu, teraz przejdziemy do mechaniki działania naszej aplikacji , za co odpowiadają kontrolery.

Kontroler

Kontrolery definiują działania dla pewnego wydzielonego fragmentu aplikacji - dostarczają dane, reagują na zdarzenia, a także walidują poprawność działania użytkownika czy też wprowadzanych danych. Tym samym realizują wzorzec MVC, gdzie widokiem są szablony html, kontrolerami one same, a modelem serwer, z którym się komunikują aby pobierać dane. W tym momencie, dla uproszczenia, model będzie się znajdował w kontrolerze.
W app.js dodajemy następujący kod:
var mainController = searchApp.controller('mainController', ['$scope', function($scope) {

    $scope.yourSearch;

    $scope.userInput;

    $scope.hello = "What are you looking for?";

    $scope.changeSearch = function() {
        if ($scope.userInput !== undefined && $scope.userInput.length !== 0) {
            $scope.yourSearch = "You are looking for " + $scope.userInput + "!";
        } else {
            $scope.yourSearch = "You seek: nothing?";
        }
    }
}]);
Dla naszego modułu searchApp zdefiniowaliśmy tym samym kontroler o nazwie mainController. Dodatkowo wstrzyknęliśmy obiekt $scope tworzony tylko i wyłącznie dla danego kontrolera. Ostatnim elementem tablicy jest funkcja, która przyjmuje wstrzykiwany obiekt - $scope. Obiekt $scope pozwala na zdefiniowanie dostępnych zachowań oraz obiektów dostępnych w widoku. Naszym zamiarem jest wywołanie funkcji changeSearch, za każdym razem kiedy zachodzi interakcja z użytkownikiem. Pole yourSearch ma się aktualizować wraz z każdą zmianą i wyświetlać w przeglądarce użytkownika.

Teraz musimy podpiąć nasze kontrolery do szablonu html. Poniższy kod pokazuje jak to zrobić:

<body ng-controller="mainController">
    {{hello}}
    <span ng-bind="yourSearch"></span>
    <div>
        <span>Search:</span>
        <input ng-change="changeSearch()" ng-model="userInput" type="text" />
    </div>
</body>

Jak widać, do tagu <body> dołączyliśmy definicję ng-controller="mainController". Kwestia, w którym miejscu chcemy skorzystać z kontrolera zależy od nas - tylko tam będzie dostępny. W przypadku dołączenia definicji kontrolera do tagu <div>, kontroler dostarczy danych i zachowań jedynie dla zawartości tego diva. Możemy też zagnieżdżać wiele kontrolerów, czyli dla tagu <div> możemy zdefiniować inny, drugi. Powoduje to jednak inny problem - trzeba precyzyjnie określić do którego z nich się odnosimy.
Do obiektu $scope po stronie szablonu html możemy odnieść się na dwa sposoby - {{nazwa_parametru}} lub w specjalnych atrybutach tagów jak ng-bind, ng-change, czy też ng-model.
W miejscu:
<span ng-bind="yourSearch"></span>
Mówimy Angularowi, że jeżeli nastąpi zmiana pola yourSearch w naszym kontrolerze, to ma zawartość span'a ma się odświeżyć. Znów w tagu input:
<input ng-change="changeSearch()" ng-model="userInput" type="text" />
Definujemy zachowanie dla zmian w tym polu (ng-change), co skutukuje wywołaniem funkcji kontrolera - changeSearch. Dodatkowo oznaczamy, że pole userInput w naszym kontrolerze ma się aktualizować przy zmianach zachodzących w tym polu.

Tym samym omówiliśmy w jaki sposób działa angularowy kontroler. Teraz musimy zdefiniować skąd będzie pobierać nasze dane. Do tego potrzebny jest jeden z wielu dostępnych serwisów 'ad hoc'  - $http.

Serwis

Serwisy pełnią rolę Singletonów dostarczających usług dla różnych obiektów tworzonych przez nas w Angularze. Istotne jest to, że powinny jedynie zmieniać stan globalny aplikacji, same nie mogą posiadać stanu. Typowym zastosowaniem serwisu może być proces logowania użytkownika na stronie, albo dostarczenie mechanizmu paginacji wśród wyników wyszukiwania.

transApp.factory('authorizationService', ['$http', '$rootScope', '$location', function($http, $rootScope, $location) {
    function updateAuthorizationData(data) {
        // metoda prywatna
    }
   // metody publiczne. udostępniane na świat
    return {
        /**
         * Sprawdza czy użytkownik uwierzytelniony.
         *
         * @returns {boolean}
         */
        isUserAuthorized : function() {
            return $rootScope.userAuthorized;
        },
        /**
         * Uwierzytelnia użytkownika.
         *
         * @param email
         * @param password
         * @returns {boolean} true, jeśli uwierzytelnienie się powiodło lub już uwierzytelniony, false w przeciwnym przypadku
         */
        authorizeUser : function(email, password) {
            if ($rootScope.userAuthorized === true) {
                return true;
            }
            return $http.post('/tryLogin', "email=" + email + "&password=" + password,
                { headers : { 'Content-Type' : 'application/x-www-form-urlencoded'}})
                .success(function(data, status, headers, config) {
                    if(data.code==401)
                        $rootScope.errors = data.value;
                    updateAuthorizationData(data);
                })
                .error(function(data, status, headers, config) {
                });
        },
        /**
         * Sprawdza, czy użytkownik zalogowany. Jeśli nie, przenosi go do strony logowania
         */
        tryAuthorize : function() {
            if($rootScope.userAuthorized !== true) {
                $location.path('/login');
            }
        },
        /**
         * Wylogowuje użytkownika.
         *
         * @returns {boolean} true, jeśli wylogowanie się powiodło lub już wylogowany, false - wystąpił błąd
         */
        logout : function() {
            if ($rootScope.userAuthorized === false) {
                return true;
            }
            $http.get('/logout')
                .success(function() {
                    $rootScope.userAuthorized = false;
                    return true;
                })
                .error(function() {
                    return false;
                });
        }
}]);
Serwis tworzymy dla naszego modułu searchApp wywyołując metodę factory. Pierwszą charakterystyczną cechą dla serwisu jest to, że zwraca obiekt zawierający funkcje (lub parametry). Nie mamy tu obiektu $scope, natomiast możemy skorzystać z globalnie dostępnego $rootScope - trzeba jednak panować nad danymi, które tam przechowujemy. Angular większość swoich obiektów zaczyna od znaku '$, aby nie doszło do konfliktów w nazwach, które tworzymy.
Zmienne i metody prywatne możemy zawrzeć przed klauzulą return, tak aby nie były widoczne dla użytkownika serwisu.
Zauważmy, że serwis jest na tyle elastyczny, że może wymagać 'wstrzyknięcia' innych serwisów. W naszym przykładzie wykorzystujemy dwa, wbudowane w Angulara serwisy - $http i $location. Pierwszy z nich pełni rolę wymiany danych z serwerem, drugi odpowiedzialny jest za stan kontekstu przeglądarki w stosunku do adresu URL i udostępnia metody umożliwiające przekierowania (bez przeładowania strony!).
Podstawowe użycie tych dwóch serwisów możemy zobaczyć w powyższym przykładzie. Mnogość funkcji, które udostępniają przekracza zakres tego tutoriala - na razie warto zapoznać się z podstawowymi funkcjami.

Koniec?

Wręcz przeciwnie - to dopiero początek. Na temat Angulara i jego możliwości powstało wiele artykułów, tutoriali, a ten jest tylko jednym z nich. Po więcej informacji polecam odnieść się do strony: https://docs.angularjs.org/guide - znajdziecie tam wiele ficzerów, z którymi trzeba się zapoznać, żeby wykorzystać moc Angulara w 100%. W niedalekiej przyszłości, na naszym blogu mogą pojawić się następne artykuły związane ze światem Angulara.
Pozdrawiam i życzę owocnej nauki.