Ten projekt zawiera przykłady (ćwiczenia oraz rozwiązania ćwiczeń) do szkolenia "Tworzenia aplikacji internetowych z wykorzystaniem Spring Framework"
Celem zadań będzie napisanie aplikacji która pobiera tłumaczenia słów wpisanych z konsoli oraz wyświetla je na ekranie. Wraz z rozwojem aplikacji (i poznawaniem Spring Framework) będziemy w kolejnych ćwiczeniach dodawać kolejne funkcjonalności.
To jest bazowa wersja programu; aplikacja zawiera podstawową konfigurację aplikacji w Springu oraz prosty kontroler umożliwiający podawanie komand z linii poleceń.
Dodaj serwis pobierający tłumaczenia angielskiego słowa z serwisu http://www.dict.pl dostępny pod komendą: search [słowo].
Możesz bezpośrednio wykorzystać klasę TranslationService
, którą należy odpowiednio rozbudowac o adnotację Spring Framework.
Metoda TranslationService#getTranslationsForWord())
zwraca listę obiektów DictionaryWord
(tupli) zawierających parę: słowo polskie, słowo angielskie.
Tłumaczenie mogą zostać wyświetlone na ekranie.
Napisany przez nas w ćwiczeniu 1 serwis należałoby przetestować, najlepiej w sposób automatyczny. Bazując na istniejącym ExampleConfigurationTests
napisz nowy test dla serwisu.
Wiedząc o tym co mówi konwencja Spring Framework nt. plików konfiguracyjnych dla testów, utwórz nową konfigurację XML dla testu albo wykorzystaj wewnętrzną klasę JavaConfig.
Test korzystający bezpośrednio z klasy TranslationService
nie jest najlepszy – odwołuje się do zasobów w Internecie, przez co (w przypadku braku dostępu do Internetu nie zadziała – nie będzie działał stabilnie). Co więcej, ze względu na wykonywanie połączenia ze zdalnym serwerem, test działa nieporównywalnie dłużej niż jakby wywoływał się w całości lokalnie (albo w całości w pamięci).
Aby uniknąć takiego typu zachowania, należałoby zastąpić bezpośrednie odwołanie do Internetu, np. uniezależniając serwis TranslationService
od konkretnego adresu URL (i przekazując go jako parametr, zmienną klasy – poprzez adnotację @Value
).
W Spring Framework najłatwiej uzyskać to poprzez dodanie zewnętrznego pliku properties, zdefiniowanie adresu URL, a następnie odwołanie się do niego w konfiguracjach XML albo JavaConfig
<context:property-placeholder location="classpath:META-INF/spring/dict.properties"/>
@Configuration
@ComponentScan(value = { "com.example.dictionary", "com.example.helloworld" })
@PropertySource("META-INF/spring/dict.properties")
public static class AppConfiguration {
@Bean
public PropertySourcesPlaceholderConfigurer properties() {
return new PropertySourcesPlaceholderConfigurer();
}
}
Do lokalnego pliku można dostać się bezpośrednio poprzez ClassPath this.getClass().getResource("/words/book.html").toExternalForm();
Do serwisu TranslationService
dodaj metodą sprawdzającą poprawność przekazanych parametrów. Dla serwisu wyszukującego poprawne komenda to: search [słowo do znalezienia]
Do walidacji wykorzystaj standard Bean Validation (walidacja przez adnotacje). W tym celu dodaj do projektu następujące zależności.
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.4.Final</version>
</dependency>
Tip
|
walidacja bazuje na obiektach (innych niż String) więc na początek zmień parametry (komendy) na obiekt. Dodatkowo, przeprowadź refaktoryzację klasy Command i TranslationService aby logikę przetwarzania parametrów wejściowych zawierać w jednym miejscu.
|
Na przykład, wprowadź klasę CommandParameters
o następującej strukturze, używaj jej do przekazywania parametrów CLI oraz na jej bazie przeprowadź walidację.
public class CommandParameters {
private String commandName;
private String[] attributes;
}
Odwołanie się do aplikacji zewnętrznej (w naszym przypadku do serwisu http://dict.pl) jest niezmierni ważne z biznesowego punktu widzenia. Wyobraźmy sobie sytuację że to nie jest zwykły słownik a zaawansowany Web Service, za którego wywołania pobierana jest opłata. Co miesiąc przychodzi faktura od dostawcy za wszystkie wywołania serwisu.
Aby zweryfikować taką fakturę, należy upewnić się ile razy serwis był wywoływany.
Jako że nie jest to wymaganie typowe dla naszego serwisu (można powiedzieć że jest to typowe wymaganie nie funkcjonalne) zaimplementuj je w sposób nie zmieniający głównego algorytmu działania serwisu TranslationService
Do implementacji logowania użyj komponentów Spring AOP
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
Zaloguj wywołanie publicznej metody serwisu, zaloguj informację o wywołaniu wraz z przekazywanym parametrem.
Dotychczas zupełnie pomijaliśmy kwestie zwracanych wyników – wyświetlaliśmy je na ekranie. Pora to zmienić.
Zacznijmy od wyświetlenia wszystkich słów na ekranie, w formie listy, a następnie wprowadźmy komendę zapisującą konkretne tłumaczenia: save [numer wyników z listy]
Zapisane tłumaczenie póki co przechowujmy w dowolnym miejscu w systemie (np. jako listy w kontrolerze).
Tip
|
aby poprawnie obsługiwać listy należy przechować wyniki wyszukiwania oraz osobno listę zapisanych słów. Co powoduje konieczność implementacji kolejnych komend poza save: show-saved oraz show-found |
„Pamięć” zaimplementowana w poprzednim ćwiczeniu jest rozwiązaniem stosunkowo naiwnym. Wszystkie dane przechowujemy w kontrolerze co czyni go grubym (antywzorzec Fat Controller). Aby to naprawić utwórzmy dodatkowy komponent obsługujący przechowywanie danych.
public interface Repository {
public List<DictionaryWord> getSavedWords();
public void addWord(DictionaryWord word);
public void printSavedWords();
}
Nasze repozytorium przechowuje dane w pamięci. Nic nie stoi na przeszkodzie abyśmy zaczęli zapisywać je do bazy danych. W tym celu utwórzmy tabelę words
create table words (
id int identity primary key,
polish_word varchar(100),
english_word varchar(100)
);
Do zapisywania danych użyjmy klasy JdbcTemplate
Tip
|
Konfiguracja bazy danych wymaga dodania sterownika HSQLDB, MySQL lub PostreSQL. |
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.26</version>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.1-901.jdbc3</version>
</dependency>
Dodatkowo należy dodać bibliotekę Spring odpowiedzialną za połączenie za bazą danych.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
Przydatne informację dot. połączenia
driver = com.mysql.jdbc.Driver
url = "jdbc:hsqldb:file:/tmp/testdb" //(1)
url = "jdbc:hsqldb:mem:testmemdb" //(2)
-
Baza w pliku
-
Baza w pamięci
Bardzo często spotykanym sposobem połączenia z bazą danych jest wykorzystanie standardu JPA. W przypadku naszej prostej aplikacji jest to niezwykle proste, wszakże posługujemy się już obiektem domenowym DictionaryWord
który moglibyśmy zapisać bezpośrednio w bazie danych.
Zmodyfikuj klasę DictionaryWord
oraz dodaj nowe repozytorium JpaRepository
– w ten sposób korzystając z JPA do zapisu danych do bazy
Przydatne biblioteki do dołączenia do projektu:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.2.6.Final</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.4</version>
</dependency>
Nowym wymaganiem w naszym systemie, jest zapisywanie tłumaczeń także do pliku. Przed zapisem do bazy danych należy parę słów zapisać także do pliku o losowej nazwie. Dopiero w kolejnym kroku można zapisać dane w bazie. Jeżeli transakcja w bazie danych się nie powiedzie, należy usunąć uprzedni utworzony plik.
Plik może znajdować się w katalogu tymczasowym a jego nazwa może być dowolnie generowana, np.:
public String createFile(String data) {
UUID id = UUID.randomUUID();
String filename = "/tmp/wordfile-" + id.toString();
try (PrintWriter out = new PrintWriter(filename)) {
out.println(data);
} catch (Exception e) {
throw new RuntimeException(e);
}
log.info("Saved file: " + filename);
return filename;
}
Wykorzystując klasy Spring TransactionSynchronisationManager
oraz interfejs TransactionSynchronisation
zaimplementuj poprawną obsługę transakcji i błędów.
Kolejnym krokiem będzie rozbudowa aplikacji o cześć serwerową - dodanie usługi www, umożliwiającej wykonanie tych samych operacji poprzez webservice REST.
Webservice ma udostępniać następujące metody
GET /show-saved (1)
GET /search/{word} (2)
POST /search/{word}/{n} (3)
-
Wyświetlenie wszystkich zapisanych słóœ
-
Wyświetlenie tłumaczeń dla słowa
{word}
-
Zapisanie wybranego tłumaczenia słow
{word}
, będącego{n}
-tym elementem listy
Aby ułatwić sobie pracę, wykorzystajmy już istniejącą aplikację (projekt Spring). W tym celu najlepiej utworzyć nowy projekt zawierający następujące zależności
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>web</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<failOnMissingWebXml>false</failOnMissingWebXml>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>2.0.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>app</artifactId> (1)
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
</plugins>
</build>
</project>
-
Zależność od bazowego projektu
Samą aplikację możemy skonfigurować zarówno poprzez plik web.xml
jak i poprzez adnotacje i JavaConfig
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.example.web")
public class DispatcherConfig {
}
public class WebInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
// Create the 'root' Spring application context
AnnotationConfigWebApplicationContext rootContext =
new AnnotationConfigWebApplicationContext();
rootContext.register(AppJavaConfig.AppConfiguration.class);
// Manage the lifecycle of the root application context
container.addListener(new ContextLoaderListener(rootContext));
// Create the dispatcher servlet's Spring application context
AnnotationConfigWebApplicationContext dispatcherContext =
new AnnotationConfigWebApplicationContext();
dispatcherContext.register(DispatcherConfig.class);
// Register and map the dispatcher servlet
ServletRegistration.Dynamic dispatcher =
container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/*");
}
}
Do pełni działającej aplikacji potrzeba już jedynie odpowiedniej konfiguracji kontrolerów.