Spis treści
O autorach . ...................................................................................................... 13 O recenzentach technicznych . .......................................................................... 15 Podziękowania . ............................................................................................... 17 Wprowadzenie . ............................................................................................... 19 Dla kogo przeznaczona jest ta książka? . ................................................................................... 19 Struktura książki .......................................................................................................................... 20 Konwencje .................................................................................................................................... 21 Wymagania wstępne ................................................................................................................... 21 Pobieranie kodu ........................................................................................................................... 21 Kontakt z autorami ...................................................................................................................... 21
Rozdział 1.
Wprowadzenie do platformy Spring . ............................................................... 23 1.1. Tworzenie egzemplarza kontenera IoC w Springu . ........................................................ 23 1.2. Konfigurowanie ziaren w kontenerze IoC . ....................................................................... 26 1.3. Tworzenie ziaren za pomocą konstruktora . ..................................................................... 34 1.4. Wybieranie konstruktora w przypadku wieloznaczności . .............................................. 37 1.5. Podawanie referencji do ziaren . ......................................................................................... 39 1.6. Określanie typu danych elementów kolekcji . ................................................................... 43 1.7. Tworzenie ziaren za pomocą interfejsu FactoryBean Springa . ...................................... 45 1.8. Definiowanie kolekcji za pomocą ziaren fabrycznych i schematu util . ........................ 47 1.9. Sprawdzanie właściwości na podstawie zależności . ........................................................ 49 1.10. Sprawdzanie właściwości z adnotacją @Required . ........................................................ 51 1.11. Automatyczne łączenie ziaren za pomocą konfiguracji w pliku XML . ...................... 53 1.12. Automatyczne łączenie ziaren z adnotacjami @Autowired i @Resource . ................. 57 1.13. Dziedziczenie konfiguracji ziarna . ................................................................................... 62 1.14. Skanowanie komponentów z parametru classpath . ...................................................... 65 Podsumowanie ............................................................................................................................. 70
Rozdział 2.
Zaawansowany kontener IoC w Springu . ........................................................ 71 2.1. Tworzenie ziaren za pomocą statycznych metod fabrycznych . ..................................... 71 2.2. Tworzenie ziaren za pomocą fabrycznej metody egzemplarza . .................................... 72 2.3. Deklarowanie ziaren na podstawie pól statycznych . ....................................................... 74 2.4. Deklarowanie ziaren na podstawie właściwości obiektów . ............................................ 75 2.5. Używanie języka wyrażeń dostępnego w Springu . .......................................................... 77
SPIS TREŚCI
2.6. Ustawianie zasięgu ziarna . .................................................................................................. 82 2.7. Modyfikowanie procesu inicjowania i usuwania ziaren . ................................................ 84 2.8. Skracanie konfiguracji w XML-u za pomocą projektu Java Config . ............................. 88 2.9. Ziarna znające zasoby kontenera . ...................................................................................... 92 2.10. Wczytywanie zasobów zewnętrznych . ............................................................................ 93 2.11. Tworzenie postprocesorów ziaren . .................................................................................. 96 2.12. Zewnętrzne przechowywanie konfiguracji ziarna . ........................................................ 99 2.13. Określanie komunikatów tekstowych . .......................................................................... 100 2.14. Komunikowanie się ze zdarzeniami aplikacji . ............................................................. 102 2.15. Rejestrowanie edytorów właściwości w Springu . ........................................................ 105 2.16. Tworzenie niestandardowych edytorów właściwości . ................................................ 108 2.17. Obsługa współbieżności za pomocą interfejsu TaskExecutor . .................................. 109 Podsumowanie ........................................................................................................................... 117
Rozdział 3.
Programowanie aspektowe i obsługa języka AspectJ w Springu .................. 119 3.1. Włączanie obsługi adnotacji języka AspectJ w Springu . ............................................... 120 3.2. Deklarowanie aspektów za pomocą adnotacji języka AspectJ . .................................... 122 3.3. Dostęp do informacji o punkcie złączenia . ..................................................................... 127 3.4. Określanie pierwszeństwa aspektów . ............................................................................... 128 3.5. Ponowne wykorzystanie definicji punktu przecięcia . ................................................... 130 3.6. Pisanie wyrażeń z punktami przecięcia w języku AspectJ . ........................................... 132 3.7. Dodawanie operacji do ziaren . ......................................................................................... 136 3.8. Dodawanie stanu do ziarna . .............................................................................................. 138 3.9. Deklarowanie aspektów za pomocą konfiguracji w XML-u . ....................................... 140 3.10. Wplatanie aspektów języka AspectJ w Springu w czasie ładowania . ........................ 143 3.11. Konfigurowanie w Springu aspektów języka AspectJ . ................................................ 148 3.12. Wstrzykiwanie ziaren Springa do obiektów domenowych . ....................................... 149 Podsumowanie ........................................................................................................................... 152
Rozdział 4.
Skrypty w Springu . ........................................................................................ 153 4.1. Implementowanie ziaren za pomocą języków skryptowych . ....................................... 153 4.2. Wstrzykiwanie ziaren Springa do skryptów . .................................................................. 157 4.3. Aktualizowanie ziaren ze skryptów . ................................................................................ 160 4.4. Wewnątrzwierszowe definiowanie kodu źródłowego skryptów . ................................ 161 Podsumowanie ........................................................................................................................... 162
Rozdział 5.
Bezpieczeństwo w Springu . ........................................................................... 163 5.1. Zabezpieczanie dostępu do adresów URL . ..................................................................... 163 5.2. Logowanie się do aplikacji sieciowych . ........................................................................... 172 5.3. Uwierzytelnianie użytkowników . ..................................................................................... 176 5.4. Podejmowanie decyzji z zakresu kontroli dostępu . ....................................................... 185 5.5. Zabezpieczanie wywołań metod . ...................................................................................... 188 5.6. Obsługa zabezpieczeń w widokach . ................................................................................. 190 5.7. Zabezpieczanie obiektów domenowych . ......................................................................... 192 Podsumowanie ........................................................................................................................... 200
Rozdział 6.
Integrowanie Springa z innymi platformami do tworzenia aplikacji sieciowych . ............................................................. 203 6.1. Dostęp do Springa w dowolnych aplikacjach sieciowych . ........................................... 204 6.2. Używanie Springa w serwletach i filtrach . ...................................................................... 208 6.3. Integrowanie Springa z platformą Struts 1.x . ................................................................. 212
6
SPIS TREŚCI
6.4. Integrowanie Springa z platformą JSF . ............................................................................ 218 6.5. Integrowanie Springa z platformą DWR . ....................................................................... 223 Podsumowanie ........................................................................................................................... 227
Rozdział 7.
Platforma Spring Web Flow . .......................................................................... 229 7.1. Zarządzanie prostym przepływem sterowania za pomocą platformy Spring Web Flow . ..................................................................... 229 7.2. Modelowanie przepływów sterowania za pomocą różnych rodzajów stanów . ......... 236 7.3. Zabezpieczanie przepływów sterowania w aplikacjach sieciowych . ........................... 247 7.4. Utrwalanie obiektów w przepływach sterowania w aplikacjach sieciowych . ............ 249 7.5. Integrowanie platformy Spring Web Flow z technologią JSF . ..................................... 255 7.6. Korzystanie z platformy RichFaces w platformie Spring Web Flow . ......................... 262 Podsumowanie ........................................................................................................................... 266
Rozdział 8.
Platforma Spring MVC . .................................................................................. 267 8.1. Tworzenie prostej aplikacji sieciowej za pomocą platformy Spring MVC . ............... 267 8.2. Wiązanie żądań za pomocą adnotacji @RequestMapping . .......................................... 278 8.3. Przechwytywanie żądań przy użyciu interceptorów przetwarzania . .......................... 282 8.4. Określanie ustawień regionalnych użytkowników . ....................................................... 285 8.5. Pliki zewnętrzne z tekstem dostosowanym do ustawień regionalnych . ..................... 287 8.6. Określanie widoków na podstawie nazw . ....................................................................... 289 8.7. Widoki i negocjowanie treści . ........................................................................................... 291 8.8. Wiązanie wyjątków z widokami . ...................................................................................... 294 8.9. Przypisywanie wartości w kontrolerze za pomocą adnotacji @Value . ....................... 296 8.10. Obsługiwanie formularzy za pomocą kontrolerów . .................................................... 297 8.11. Obsługa wielu formularzy za pomocą kontrolerów formularzy kreatora . .............................................. 310 8.12. Sprawdzanie poprawności ziaren za pomocą adnotacji (na podstawie standardu JSR-303) . .............................................................................. 319 8.13. Tworzenie widoków w formatach XLS i PDF . ............................................................. 321 Podsumowanie ........................................................................................................................... 327
Rozdział 9.
Usługi REST w Springu . ................................................................................. 329 9.1. Publikowanie usług typu REST w Springu . .................................................................... 329 9.2. Dostęp do usług typu REST w Springu . .......................................................................... 333 9.3. Publikowanie danych z kanałów informacyjnych RSS i Atom . ................................... 338 9.4. Publikowanie danych w formacie JSON w usługach typu REST . .................... 345 9.5. Dostęp do usług typu REST zwracających skomplikowane odpowiedzi w formacie XML ............................................................................................................. 348 Podsumowanie ........................................................................................................................... 356
Rozdział 10. Spring i Flex . .................................................................................................. 357 10.1. Wprowadzenie do środowiska Flex . .............................................................................. 359 10.2. Poza piaskownicę .............................................................................................................. 364 10.3. Dodawanie obsługi narzędzia Spring BlazeDS Integration do aplikacji . ................. 374 10.4. Udostępnianie usług za pomocą technologii BlazeDS i Springa . .............................. 378 10.5. Używanie obiektów działających po stronie serwera . ........................................................... 384 10.6. Korzystanie z usług opartych na komunikatach w narzędziach BlazeDS i Spring . .................................................................................. 387 10.7. Wstrzykiwanie zależności w kliencie w ActionScripcie . ............................................. 398 Podsumowanie ........................................................................................................................... 402
7
SPIS TREŚCI
Rozdział 11. Grails . ............................................................................................................ 403 11.1. Pobieranie i instalowanie platformy Grails . ................................................................. 403 11.2. Tworzenie aplikacji za pomocą platformy Grails . ....................................................... 404 11.3. Wtyczki platformy Grails . ............................................................................................... 408 11.4. Środowisko rozwojowe, produkcyjne i testowe w platformie Grails . ....................... 410 11.5. Tworzenie klas domenowych aplikacji . ............................................................... 412 11.6. Generowanie kontrolerów CRUD i widoków na potrzeby klas domenowych aplikacji . ..................................................................... 414 11.7. Właściwości związane z umiędzynarodawianiem komunikatów . ............................ 417 11.8. Zmienianie systemu pamięci trwałej . ............................................................................ 420 11.9. Rejestrowanie danych wyjściowych . .............................................................................. 423 11.10. Przeprowadzanie testów jednostkowych i integracyjnych . ...................................... 425 11.11. Stosowanie niestandardowych układów i szablonów . .............................................. 430 11.12. Zapytania GORM ........................................................................................................... 432 11.13. Tworzenie niestandardowych znaczników . ............................................................... 434 Podsumowanie ........................................................................................................................... 436
Rozdział 12. Spring Roo . .................................................................................................... 437 12.1. Konfigurowanie środowiska programistycznego pod kątem narzędzia Spring Roo . ................................................................................ 439 12.2. Tworzenie pierwszego projektu opartego na narzędziu Roo . .................................... 441 12.3. Importowanie istniejących projektów do środowiska STS . ....................................... 446 12.4. Szybsze budowanie lepszych aplikacji . .......................................................................... 448 12.5. Usuwanie Roo z projektu . ............................................................................................... 454 Podsumowanie ........................................................................................................................... 456
Rozdział 13. Testy w Springu . ............................................................................................ 457 13.1. Tworzenie testów za pomocą platform JUnit i TestNG . ............................................ 458 13.2. Tworzenie testów jednostkowych i testów integracyjnych . ....................................... 463 13.3. Testy jednostkowe kontrolerów platformy Spring MVC . .......................................... 471 13.4. Zarządzanie kontekstem aplikacji w testach integracyjnych . .................................... 472 13.5. Wstrzykiwanie konfiguracji testów w testach integracyjnych . .................................. 478 13.6. Zarządzanie transakcjami w testach integracyjnych . .................................................. 482 13.7. Dostęp do bazy danych w testach integracyjnych . ...................................................... 487 13.8. Stosowanie w Springu standardowych adnotacji związanych z testami . ................. 491 Podsumowanie ........................................................................................................................... 493
Rozdział 14. Platforma Spring Portlet MVC . ....................................................................... 495 14.1. Tworzenie prostego portletu za pomocą platformy Spring Portlet MVC . .............. 495 14.2. Wiązanie żądań kierowanych do portletów z metodami obsługi . ............................ 503 14.3. Obsługa formularzy z portletów za pomocą prostych kontrolerów formularzy . ... 510 Podsumowanie ........................................................................................................................... 517
Rozdział 15. Dostęp do danych . ......................................................................................... 519 Problemy z bezpośrednim korzystaniem z JDBC . ................................................................ 520 15.1. Używanie szablonu JDBC do aktualizowania bazy danych . ...................................... 526 15.2. Używanie szablonów JDBC do pobierania danych z bazy . ........................................ 530 15.3. Upraszczanie tworzenia szablonów JDBC . ................................................................... 535 15.4. Używanie prostego szablonu JDBC w Javie 1.5 . .......................................................... 537 15.5. Stosowanie nazwanych parametrów w szablonach JDBC . ......................................... 540 15.6. Obsługa wyjątków w platformie Spring JDBC . ............................................................ 542 8
SPIS TREŚCI
15.7. Problemy z bezpośrednim używaniem platform do tworzenia odwzorowań ORM . ................................................................................ 547 15.8. Konfigurowanie fabryk zasobów ORM w Springu . .................................................... 556 15.9. Utrwalanie obiektów za pomocą szablonów ORM Springa . ...................................... 562 15.10. Utrwalanie obiektów za pomocą sesji kontekstowych platformy Hibernate . ....... 567 15.11. Utrwalanie obiektów za pomocą wstrzykiwania kontekstu w JPA . ........................ 570 Podsumowanie ........................................................................................................................... 573
Rozdział 16. Zarządzanie transakcjami w Springu . ............................................................ 575 16.1. Problemy z zarządzaniem transakcjami . ....................................................................... 576 16.2. Jak wybrać implementację menedżera transakcji? . ......................................................................... 581 16.3. Programowe zarządzanie transakcjami za pomocą interfejsu menedżera transakcji . ............................................................... 583 16.4. Programowe zarządzanie transakcjami za pomocą szablonu transakcji . ................ 585 16.5. Deklaratywne zarządzanie transakcjami za pomocą rad transakcji . ......................... 588 16.6. Deklaratywne zarządzanie transakcjami za pomocą adnotacji @Transactional . .... 590 16.7. Ustawianie atrybutu propagation transakcji . ............................................................... 591 16.8. Ustawianie atrybutu określającego poziom izolacji transakcji . ................................. 596 16.9. Ustawianie atrybutu dotyczącego wycofywania transakcji . ....................................... 602 16.10. Ustawianie atrybutów związanych z limitem czasu i trybem tylko do odczytu . ... 604 16.11. Zarządzanie transakcjami za pomocą wplatania w czasie ładowania . .................... 605 Podsumowanie ........................................................................................................................... 608
Rozdział 17. Ziarna EJB, zdalne wywołania i usługi sieciowe ............................................ 609 17.1. Udostępnianie i wywoływanie usług za pomocą technologii RMI . .......................... 609 17.2. Tworzenie komponentów EJB 2.x za pomocą Springa . .............................................. 613 17.3. Dostęp do dawnych komponentów EJB 2.x w Springu . ............................................. 618 17.4. Tworzenie komponentów EJB 3.0 w Springu . ............................................................. 621 17.5. Dostęp do komponentów EJB 3.0 w Springu . .............................................................. 623 17.6. Udostępnianie i wywoływanie usług za pomocą protokołu HTTP . ......................... 625 17.7. Wybieranie sposobu tworzenia usług sieciowych SOAP . .............................................. 628 17.8. Udostępnianie i wywoływanie usług sieciowych SOAP z kontraktem pisanym na końcu za pomocą JAX-WS . ............................................. 630 17.9. Definiowanie kontraktu usługi sieciowej . ..................................................................... 636 17.10. Implementowanie usług sieciowych za pomocą platformy Spring-WS . ............... 640 17.11. Wywoływanie usług sieciowych za pomocą platformy Spring-WS . ....................... 645 17.12. Tworzenie usług sieciowych za pomocą serializowania dokumentów XML . ....... 648 17.13. Tworzenie punktów końcowych usług za pomocą adnotacji . ................................. 653 Podsumowanie ........................................................................................................................... 655
Rozdział 18. Spring w Javie EE . ......................................................................................... 657 18.1. Eksportowanie ziaren Springa jako ziaren MBeans technologii JMX . ..................... 657 18.2. Publikowanie i odbieranie powiadomień JMX . ........................................................... 667 18.3. Dostęp do zdalnych ziaren MBeans technologii JMX w Springu . ........................................................................ 669 18.4. Wysyłanie e-maili za pomocą dostępnej w Springu obsługi poczty elektronicznej . .................................................................... 672 18.5. Planowanie zadań za pomocą dostępnej w Springu obsługi Quartza . ..................... 679 18.6. Planowanie zadań za pomocą przestrzeni nazw Scheduling ze Springa 3.0 . ........... 683 Podsumowanie ........................................................................................................................... 686
9
SPIS TREŚCI
Rozdział 19. Komunikaty . .................................................................................................. 687 19.1. Wysyłanie i pobieranie komunikatów JMS w Springu . .............................................. 688 19.2. Przekształcanie komunikatów JMS . .............................................................................. 698 19.3. Zarządzanie transakcjami JMS . ...................................................................................... 700 19.4. Tworzenie w Springu obiektów POJO sterowanych komunikatami . ....................... 701 19.5. Nawiązywanie połączeń . .................................................................................................. 706 Podsumowanie ........................................................................................................................... 708
Rozdział 20. Platforma Spring Integration . ........................................................................ 709 20.1. Integrowanie jednego systemu z innym za pomocą EAI . .......................................... 710 20.2. Integrowanie dwóch systemów za pomocą technologii JMS . ......................................................................... 712 20.3. Wyszukiwanie informacji o kontekście w komunikatach platformy Spring Integration . ........................................................ 716 20.4. Integrowanie dwóch systemów za pomocą systemu plików . ..................................... 718 20.5. Przekształcanie komunikatów z jednego typu na inny . .............................................. 720 20.6. Obsługa błędów za pomocą platformy Spring Integration . ............................................. 723 20.7. Rozdzielanie operacji w integrowanych mechanizmach — rozdzielacze i agregatory . ......................................................................................... 726 20.8. Warunkowe przekazywanie za pomocą komponentu router . ................................... 729 20.9. Dostosowywanie zewnętrznych systemów do magistrali . .......................................... 730 20.10. Podział zdarzeń na etapy za pomocą projektu Spring Batch . .................................. 739 20.11. Używanie bram ............................................................................................................... 740 Podsumowanie ........................................................................................................................... 745
Rozdział 21. Platforma Spring Batch . ................................................................................. 747 21.1. Konfigurowanie infrastruktury platformy Spring Batch . ........................................... 749 21.2. Odczyt i zapis . .......................... 751 21.3. Tworzenie niestandardowych implementacji interfejsów ItemWriter i ItemReader . ......................................................................... 756 21.4. Przetwarzanie danych wejściowych przed ich zapisaniem . ....................................... 758 21.5. Łatwiejsza praca dzięki transakcjom . ............................................................................ 761 21.6. Ponawianie prób ............................................................................................................... 762 21.7. Kontrolowanie wykonywania kroków . ......................................................................... 765 21.8. Uruchamianie zadania . .................................................................................................... 769 21.9. Parametry w zadaniach . ................................................................................................... 772 Podsumowanie ........................................................................................................................... 774
Rozdział 22. Przetwarzanie rozproszone w Springu . ......................................................... 775 22.1. Przechowywanie stanu obiektów w klastrach za pomocą narzędzia Terracotta ..... 777 22.2. Przetwarzanie w gridzie . .................................................................................................. 785 22.3. Równoważenie obciążenia przy wykonywaniu metody . ............................................ 787 22.4. Przetwarzanie równoległe . .............................................................................................. 790 22.5. Instalowanie aplikacji korzystających z narzędzia GridGain . .................................... 792 Podsumowanie ........................................................................................................................... 796
Rozdział 23. Spring i jBPM . ................................................................................................ 797 Procesy w oprogramowaniu . .................................................................................................... 798 23.1. Modele przepływu pracy . ................................................................................................ 800 23.2. Instalowanie systemu jBPM . ........................................................................................... 802 23.3. Integrowanie systemu jBPM 4 ze Springiem . ............................................................... 804
10
SPIS TREŚCI
23.4. Tworzenie usług za pomocą Springa . ............................................................................ 809 23.5. Tworzenie procesu biznesowego . ................................................................................... 812 Podsumowanie ........................................................................................................................... 814
Rozdział 24. Spring i OSGi . ................................................................................................ 815 24.1. Początki pracy z OSGi . .................................................................................................... 816 24.2. Wprowadzenie do korzystania z platformy Spring Dynamic Modules . .................. 821 24.3. Eksportowanie usług za pomocą platformy Spring Dynamic Modules . .................. 825 24.4. Wyszukiwanie konkretnych usług w rejestrze OSGi . ................................................. 828 24.5. Publikowanie usług zgodnych z wieloma interfejsami . .............................................. 830 24.6. Dostosowywanie działania platformy Spring Dynamic Modules . ............................ 831 24.7. Używanie serwera dm Server firmy SpringSource . ..................................................... 832 24.8. Narzędzia firmy SpringSource . ...................................................................................... 834 Podsumowanie ........................................................................................................................... 835
Skorowidz . .................................................................................................... 837
11
SPIS TREŚCI
12
O autorach
Gary Mak jest założycielem firmy Meta-Archit Software Technology, gdzie zajmuje stanowisko głównego konsultanta. Przez ponad siedem lat był architektem technicznym i programistą aplikacji opartych na platformie Java EE. Jest autorem wydanych przez wydawnictwo Apress książek Spring Recipes: A Problem-Solution Approach i Pro SpringSource dm Server. W trakcie swojej kariery opracował wiele projektów oprogramowania bazujących na Javie. Większość z nich to: platformy do tworzenia aplikacji, projekty infrastruktury systemów i narzędzia do tworzenia oprogramowania. Gary uwielbia projektować i implementować skomplikowane aspekty oprogramowania. Posiada dyplom magistra nauk komputerowych, a jego zainteresowania obejmują: technologie obiektowe, technologie aspektowe, wzorce projektowe, ponowne wykorzystanie oprogramowania i programowanie domenowe. Gary specjalizuje się w tworzeniu aplikacji korporacyjnych za pomocą takich technologii jak: Spring, Hibernate, JPA, JSF, Portlet, AJAX i OSGi. Platformy Spring używa w projektach od pięciu lat, od czasu pojawienia się wersji Spring 1.0. Gary prowadził szkolenia z zakresu technologii: Jave EE, Spring, Hibernate, Web Services i programowania zwinnego. Na potrzeby tych kursów napisał serię samouczków na temat platform Spring i Hibernate. Niektóre z tych materiałów zostały udostępnione publicznie i zyskują coraz większą popularność w społeczności związanej z Javą. W wolnym czasie Gary lubi grać w tenisa i oglądać rozgrywki tenisowe. Josh Long pracuje na stanowisku Spring Developer Advocate w firmie SpringSource. Jest inżynierem (najczęściej pełni rolę architekta) o ponaddziesięcioletnim doświadczeniu, a także aktywnie uczestniczy w życiu społeczności związanej z Javą. Współtworzy projekty o otwartym dostępie do kodu źródłowego, w tym projekt Spring Integration. Uczestniczy w programie JCP i jest redaktorem popularnego serwisu poświęconego technologiom, http://InfoQ.com. Josh często występuje na krajowych i międzynarodowych konferencjach, gdzie prowadzi wykłady na temat rozmaitych zagadnień: od zarządzania procesami biznesowymi, przez platformy do tworzenia aplikacji sieciowych, po integrację aplikacji korporacyjnych i wzorce architektoniczne. Do jego zainteresowań należą: skalowalność, zarządzanie procesami biznesowymi, przetwarzanie siatkowe, praca na urządzeniach przenośnych i systemy inteligentne. Na stanowisku Spring Developer Advocate w firmie SpringSource Josh koncentruje się na powiększaniu i rozwijaniu społeczności skupionej wokół platformy Spring. Mieszka w słonecznej południowej części Kalifornii razem ze swoją żoną Richelle. Prowadzi blog http://www.joshlong.com. Skontaktować się z Joshem można pod adresem
[email protected] .
O AUTORACH
Daniel Rubio jest konsultantem i ma ponaddziesięcioletnie doświadczenie z zakresu technologii korporacyjnych i sieciowych. W czasie swojej kariery korzystał z Javy i Pythona oraz z technologii CORBA i .NET do tworzenia niedrogich rozwiązań w branżach finansowej i produkcyjnej. Od niedawna Daniel koncentruje się na platformach do tworzenia aplikacji sieciowych działających w modelu Convention Over Configuration (Spring, Grails, Roo i Django), przy czym najbardziej interesują go wydajność i skalowalność takich narzędzi w środowisku korporacyjnym. Daniel ponadto pisze artykuły na temat nowych technologii dla różnych sieci informacyjnych (takich jak Oracle Technology Network i Dzone), a także prowadzi blog http://www.WebForefront.com.
14
O recenzentach technicznych
Manuel Jordan Elera jest programistą Javy pracującym jako wolny strzelec. Dla swoich klientów projektował i tworzył spersonalizowane systemy, używając do tego między innymi rozbudowanych platform Javy, takich jak Spring i Hibernate. Manuel jest obecnie programistą samoukiem i lubi poznawać nowe platformy, które ułatwiają mu pracę. Manuel posiada dyplom inżyniera systemów (studia ukończył z wyróżnieniem) oraz jest profesorem na uczelniach Universidad Católica de Santa María i Universidad Alas Peruanas w Peru. W wolnym czasie, którego ma niewiele, lubi czytać Biblię i komponować muzykę gitarową. Manuel jest zasłużonym członkiem forum Spring Community, gdzie posługuje się ksywką dr_pompeii. Z Manuelem możesz się skontaktować przez jego blog: http://manueljordan.wordpress.com/. Mario Gray jest inżynierem i ma ponaddziesięcioletnie doświadczenie w nast.ępujących dziedzinach: integracji systemów, zarządzaniu systemami, programowaniu gier i wysoce dostępnych architektur korporacyjnych. Nieustannie śledzi nowe technologie usprawniające pracę, które mogą ułatwiać funkcjonowanie firm. Opracował niezliczone systemy, w tym: systemy CRM, fabryki komunikatów i wysoce dostępne aplikacje sieciowe. Stosował przy tym popularne systemy o otwartym dostępie do kodu źródłowego, a także platformy i narzędzia związane z Javą EE. Mario ma doświadczenie w udanym wykorzystywaniu platform o otwartym dostępie do kodu źródłowego do usprawniania pracy firm. Mario mieszka w Chandler w stanie Arizona razem ze swoją żoną Fumiko i córką Makani. Poza pracą lubi rekreację na świeżym powietrzu oraz spędzanie czasu z rodziną. Mario prowadzi blog http://www.sudoinit5.com i jest dostępny pod adresem
[email protected] .
O RECENZENTACH TECHNICZNYCH
Greg Turnquist jest wielkim fanem testów i skryptów, który do wykonania każdego zadania szuka odpowiedniego narzędzia. Zawodowo zajmuje się programowaniem od 1997 roku. Pracował nad wieloma różnymi systemami, w tym nad krytycznymi, które muszą pracować 24 godziny na dobę, 7 dni w tygodniu przez 365 dni w roku. W 2006 roku Greg opracował projekt Spring Python. W jego ramach przeniósł rozwiązania z platformy Spring do Pythona. W 2010 roku Greg dołączył do firmy SpringSource. Napisał książkę Spring Python 1.1 i wygłaszał wykłady na konferencji SpringOne. Otrzymał dyplom magistra uczelni Auburn University i mieszka w Stanach Zjednoczonych razem ze swoją rodziną.
16
Podziękowania Nie zamierzam kłamać — konieczność napisania podziękowań jest dla mnie przerażająca. Ocena (przeze mnie lub kogokolwiek innego) bezpośredniego lub pośredniego wkładu poszczególnych osób w powstanie tej książki wydaje mi się niemożliwa. Postaram się zrobić to, co w mojej mocy, ale niewykluczone, że kogoś pominę. Za to z góry gorąco przepraszam. Dziękuję mojej żonie Richelle za nieustającą cierpliwość, wyrozumiałość, mądrość, wsparcie i miłość. Jestem wdzięczny moim rodzicom (mamie i tacie oraz ich partnerom) i teściom za ich wpływ na mnie, wyrozumiałość, wsparcie i miłość. Dziękuję współautorom tej pozycji, Gary’emu Makowi i Danielowi Rubio, którzy przyczynili się do tego, że jest to najciekawsza książka, jaką kiedykolwiek przeczytałem (a tym bardziej napisałem)! Jestem wdzięczny fantastycznym redaktorom. Oto oni: Steve Anglin, Laurin Becker, Tom Welsh, Manuel Jordan, Mario Gray i Greg Turnquist. Dbali oni o najwyższą jakość książki na każdym etapie procesu jej powstawania i robili to z wielkim profesjonalizmem. Dziękuję zwłaszcza Gregowi Turnquistowi, który przez cały czas trwania prac nad tą książką przekazywał nam bardzo cenne wskazówki. Greg jest inżynierem w firmie SpringSource i kierownikiem projektu Spring Python, w ramach którego powstaje pythonowa wersja platformy Spring, przeznaczona do użytku w Pythonie. Greg pisze też wydawaną przez wydawnictwo Manning książkę Spring Python in Action, która powinna być niezwykle ciekawą lekturą. Dziękuję także Markowi Fisherowi, kierownikowi projektu Spring Integration. Cenię ten projekt, ponieważ pozwala (podobnie jak podstawowa platforma) w elegancki sposób rozwiązywać bardzo skomplikowane problemy. Dzięki niemu niemożliwe do wykonania zadania stają się trudne, a skomplikowane problemy — łatwe. Jestem wdzięczny Markowi za jego projekt, a także wyrozumiałość, entuzjazm i wsparcie w związku z moim wkładem w prace nad tym projektem. Dziękuję mu także za jego dobroć. Jestem redaktorem serwisu InfoQ.com. Ta książka powstała w wyniku roku ciężkiej pracy w obliczu zupełnie nieprzewidywalnych harmonogramów i bardzo wymagających terminów. Choć w tym czasie pracowałem w chaotyczny sposób, zespół odpowiedzialny za wspomniany serwis był dla mnie niezwykle wyrozumiały. Dziękuję Floydowi Marinescu, Charlesowi Humble’owi i Ryanowi Slobojanowi, a także całemu zespołowi z serwisu InfoQ.com za podnoszenie poprzeczki w dziedzinie dziennikarstwa technicznego oraz za nieustające wsparcie i inspiracje. Na zakończenie wyrażam wdzięczność wszystkim moim współpracownikom z firmy Shopzilla. W czasie gdy powstawała ta książka, przenosiłem się z Shopzilli do SpringSource. Shopzilla to zdecydowanie najlepsza firma, w jakiej miałem przyjemność pracować. Jest pełna fantastycznych ludzi. Każdego dnia uczyłem się tam czegoś nowego i cieszyłem się przyjacielską atmosferą, jakiej nie widziałem wcześniej w żadnej innej firmie. Kultura pracy była tam bardzo wysoka — nie da się znaleźć lepszego miejsca zatrudnienia dla zmotywowanego inżyniera. Efekty naszej pracy były rewelacyjne. W czasie gdy byłem tam zatrudniony, miałem szczęście pracować z wieloma świetnymi ludźmi, dlatego opracowanie krótkiej listy, którą dalej przytaczam, było bardzo trudne. Oto osoby, którym dziękuję za ich wskazówki, przyjaźń i wsparcie: Tim Morrow, Rodney Barlow, Andy Chan, Rob Roland, Paul Snively, Phil Dixon i Jody Mulkey. — Josh Long
PODZIĘKOWANIA
Przede wszystkim dziękuję współautorom tej książki. Gary Mak napisał jej pierwsze wydanie i zadbał o jego wysoki poziom, dzięki czemu stało się ono jednym z bestsellerów w dziedzinie Springa i Javy. Z kolei Josh Long napisał książkę Spring Enterprise Recipes. Obie te pozycje zostały ściśle zintegrowane i zaktualizowane, a w efekcie powstało niniejsze nowe, drugie wydanie książki Spring. Receptury. Dziękuję też wszystkim osobom z wydawnictwa Apress. Steve Anglin, Matt Moodie i Tom Welsh brali udział we wszystkich etapach procesu powstawania tej książki — od określania struktury spisu treści po lekturę każdej strony. Laurin Becker pełniła rolę menedżera projektu i dbała o połączenie wszystkich elementów, a także o to, aby wszyscy na czas wykonywali swoją pracę. Ponadto jestem oczywiście wdzięczny wszystkim osobom z wydawnictwa Apress, które przyczyniły się do powstania tej książki i z którymi nie miałem okazji kontaktować się bezpośrednio. Są to między innymi: adiustator, korektor i projektant okładki. Jeśli chodzi o kwestie techniczne i pomoc w zapewnieniu poprawności treści tej książki, specjalne podziękowania należą się Manuelowi Jordanowi, który pełnił rolę recenzenta technicznego tej pozycji. Dziękuję też Mario Grayowi, który również brał udział w procesie recenzji technicznej. Powstanie tej książki nie byłoby możliwe, gdyby nie liczne artykuły, wpisy na blogach i fragmenty dokumentacji przygotowane przez wiele osób pracujących nad narzędziami Spring i Grails. Byli to między innymi: Arjen Poutsma, Juergen Hoeller, Alef Arendsen i Graeme Rocher. Oczywiście dziękuję też wielu innym aktywnym w tym obszarze osobom, które z pewnością wpłynęły na mój sposób myślenia. Niestety z powodu ograniczonej ilości miejsca nie mogę wymienić ich wszystkich z imienia i nazwiska. Na zakończenie dziękuję mojej rodzinie i przyjaciołom. Dużym zaskoczeniem dla nich było już to, że wziąłem się za pisanie. Dowodem na ich cierpliwość były puste spojrzenia w momencie, gdy próbowałem w przystępny sposób przedstawić im treść tej książki. Jednak nawet mimo tych spojrzeń moim bliskim udawało się zadawać poważne pytania, które pomogły mi zachować entuzjazm. Dlatego dziękuję wszystkim, którzy zwyczajnie zadawali mi pytania. Dzięki udzielonemu mi bezpośredniemu wsparciu przyczyniliście się do powstania tej książki. — Daniel Rubio
18
Wprowadzenie Platforma Spring wciąż jest rozwijana. Od zawsze jest budowana po to, aby zapewnić programistom większy wybór. W Javie EE skoncentrowano się na kilku technologiach, a zrezygnowano z innych, czasem lepszych rozwiązań. Gdy pojawiła się platforma Spring, mało kto uważał, że Java EE była najlepszą architekturą w swojej klasie. Wprowadzenie Springa spotkało się z dużym zainteresowaniem, ponieważ platforma ta pozwalała uprościć Javę EE. W każdej kolejnej wersji pojawiały się nowe funkcje, które ułatwiały lub umożliwiały tworzenie określonych rozwiązań. Od wersji 2.0 platforma Spring współdziała z różnymi systemami. Platforma, podobnie jak wcześniej, udostępnia usługi oparte na istniejących systemach, ale tam, gdzie to możliwe, jest od nich niezależna. Spring wciąż jest przeznaczony głównie dla Javy EE, jednak teraz nie jest to już jedyna docelowa architektura. Ważnym aspektem strategii wybranej przez firmę SpringSource jest wykorzystanie OSGi — obiecującej technologii do tworzenia architektury modułowej. Ponadto Spring współdziała z platformą Google App Engine. Po wprowadzeniu platform opartych na adnotacjach i schematów XML firma SpringSource zaczęła budować rozwiązania, które odzwierciedlają dziedziny konkretnych problemów. W efekcie powstają języki specyficzne dla domeny (ang. Domain-Specific Language — DSL). Pojawiły się też platformy budowane na podstawie Springa, które umożliwiają integrowanie aplikacji, przetwarzanie wsadowe, integrację z technologiami Flex i Flash, obsługę pakietu GWT, obsługę technologii OSGi itd. Kiedy nadeszła pora na zaktualizowanie ważnej książki Spring. Receptury, szybko dostrzegliśmy, że minęło dużo czasu od momentu, gdy istniała tylko jedna wersja platformy Spring. Firma SpringSource udostępnia obecnie kilka platform, z których każda zapewnia znacznie większe możliwości niż konkurencyjne produkty. Dzięki tej książce dobrze poznasz różne z tych platform. Jeśli nie potrzebujesz niektórych z omawianych rozwiązań, nie musisz ich stosować ani dodawać do projektu. Może jednak okazać się, że poszczególne platformy będą dla Ciebie przydatne, więc warto wiedzieć o ich istnieniu.
Dla kogo przeznaczona jest ta książka? Książka ta jest przeznaczona dla programistów Javy chcących uprościć stosowaną architekturę i rozwiązywać problemy, z którymi trudno poradzić sobie za pomocą samej Javy EE. Jeśli już korzystasz ze Springa w swoich projektach, w rozdziałach przeznaczonych dla zaawansowanych użytkowników znajdziesz omówienie nowych technologii, których być może jeszcze nie znasz. Jeżeli dopiero poznajesz Spring, dzięki tej książce szybko nauczysz się używać tej platformy. Zakładamy, że znasz już Javę i potrafisz korzystać z wybranego środowiska IDE. Choć możliwe (i przydatne) jest stosowanie Javy wyłącznie w aplikacjach klienckich, język ten najczęściej używany jest w środowisku korporacyjnym. To właśnie tam większość z omawianych tu technologii przynosi największe korzyści. Dlatego zakładamy też, że znasz podstawowe zagadnienia z zakresu programowania aplikacji korporacyjnych (na przykład interfejs Servlet API).
WPROWADZENIE
Struktura książki Rozdział 1., „Wprowadzenie do platformy Spring”, zawiera ogólne omówienie platformy Spring. Dowiesz się tu, jak skonfigurować platformę Spring, czym jest ta platforma i jak się jej używa. Rozdział 2., „Zaawansowany kontener IoC w Springu”, zawiera opis zagadnień, które wprawdzie nie są tak powszechnie spotykane jak kwestie z rozdziału 1., ale są ważne, jeśli chcesz wykorzystać wszystkie możliwości Springa. Rozdział 3., „Programowanie aspektowe i obsługa języka AspectJ w Springu”, to omówienie dostępnej w Springu obsługi programowania aspektowego w języku AspectJ. Rozdział 4., „Skrypty w Springu”, przedstawia stosowanie w Springu języków skryptowych, takich jak: Groovy, BeanShell i JRuby. Rozdział 5., „Bezpieczeństwo w Springu”, zawiera opis projektu Spring Security (jego wcześniejsza nazwa to Acegi), który ułatwia zabezpieczanie aplikacji. Rozdział 6., „Integrowanie Springa z innymi platformami do tworzenia aplikacji sieciowych”, zawiera wprowadzenie do dostępnej w Springu obsługi warstwy sieciowej. Opisane tu mechanizmy stanowią podstawę wszystkich technologii udostępnianych przez Spring w warstwie sieciowej. Rozdział 7., „Platforma Spring Web Flow”, to wprowadzenie do platformy Spring Web Flow. Umożliwia ona projektowanie przepływu sterowania w warstwie sieciowej. Rozdział 8., „Platforma Spring MVC”, przedstawia tworzenie aplikacji sieciowych za pomocą platformy Spring Web MVC. Rozdział 9., „Usługi REST w Springu”, zawiera wprowadzenie do dostępnej w Springu obsługi usług sieciowych typu REST. Rozdział 10., „Spring i Flex”, to omówienie stosowania technologii BlazeDS do integrowania bogatych aplikacji internetowych z ziarnami Springa. Ponadto znajdziesz tu wprowadzenie do platformy Spring ActionScript. Zapewnia ona użytkownikom tworzącym aplikacje flashowe w języku ActionScript te same usługi kontenerowe i udogodnienia, z których mogą korzystać programiści Javy. Rozdział 11., „Grails”, zawiera omówienie platformy Grails. Pozwala ona zwiększyć produktywność w wyniku zastosowania optymalnych elementów i połączenia ich kodem w języku Groovy. Rozdział 12., „Spring Roo”, przedstawia nową, przełomową platformę Spring Roo. Została ona opracowana w firmie SpringSource i ma przyspieszać pracę programistów używających Javy. Rozdział 13., „Testy w Springu”, to opis przeprowadzania testów jednostkowych w platformie Spring. Rozdział 14., „Platforma Spring Portlet MVC”, jest poświęcony używaniu platformy Spring Portlet MVC do budowania aplikacji i wykorzystywaniu zalet kontenera Portlet. Rozdział 15., „Dostęp do danych”, dotyczy stosowania Springa do komunikowania się z magazynami danych za pomocą takich interfejsów API jak JDBC, Hibernate i JPA. Rozdział 16., „Zarządzanie transakcjami w Springu”, to wprowadzenie do dostępnych w Springu mechanizmów zarządzania transakcjami. Rozdział 17., „Ziarna EJB, zdalne wywołania i usługi sieciowe”, zawiera wprowadzenie do różnych mechanizmów obsługi zdalnych wywołań, w tym do projektu Spring Web Services. Rozdział 18., „Spring w Javie EE”, to omówienie licznych dostępnych w platformie Spring narzędzi służących na przykład do obsługi technologii JMX, planowania zadań i obsługi poczty elektronicznej. Rozdział 19., „Komunikaty”, dotyczy stosowania Springa w warstwie pośredniej opartej na komunikatach. Wykorzystuje się przy tym usługi JMS i ułatwiające pracę abstrakcje Springa. Rozdział 20., „Platforma Spring Integration”, zawiera opis używania platformy Spring Integration do integrowania różnych usług i danych. Rozdział 21., „Platforma Spring Batch”, jest poświęcony platformie Spring Batch. Umożliwia ona modelowanie rozwiązań tradycyjnie stosowanych w komputerach typu mainframe. Rozdział 22., „Przetwarzanie rozproszone w Springu”, zawiera omówienie różnych sposobów skalowania Springa za pomocą rozproszonego stanu i przetwarzania siatkowego. Rozdział 23., „Spring i jBPM”, to wprowadzenie do zagadnień z zakresu zarządzania procesami biznesowymi i integrowania ze Springiem popularnej platformy jBPM JBoss. Rozdział 24., „Spring i OSGi”, to omówienie dostępnej w Springu rozbudowanej obsługi technologii OSGi.
20
KONWENCJE
Konwencje Czasem gdy chcemy zwrócić szczególną uwagę na fragment przykładowego kodu, stosujemy pogrubioną czcionkę. Zauważ, że takie wyróżnienie nie zawsze oznacza zmianę kodu w porównaniu z jego wcześniejszą wersją. Jeśli wiersz kodu jest zbyt długi, aby zmieścił się na stronie, używamy znaku kontynuacji wiersza. Gdy będziesz próbował wpisać taki kod, wpisz wiersz w całości, bez stosowania dodatkowych spacji.
Wymagania wstępne Ponieważ język Java jest niezależny od systemu, możesz korzystać z dowolnego systemu operacyjnego. Jednak w niektórych przykładach z tej książki stosujemy ścieżki dostosowane do konkretnych systemów. W razie potrzeby przy wpisywaniu kodu przekształć takie ścieżki na format odpowiedni dla używanego systemu. Aby jak najwięcej skorzystać na lekturze tej książki, zainstaluj pakiet JDK w wersji 1.5 lub nowszej. Powinieneś też mieć zainstalowane środowisko IDE Javy, co ułatwia programowanie. Przykładowy kod tej książki jest oparty na Mavenie. Jeśli używasz środowiska Eclipse i zainstalowałeś wtyczkę m2Eclipse, możesz otworzyć kod w tym środowisku, a parametr classpath i zależności zostaną ustawione na podstawie metadanych Mavena. Jeżeli korzystasz ze środowiska Eclipse, możesz użyć pakietu SpringSource Tool Suite (STS), ponieważ zawiera on wtyczki potrzebne do wydajnego stosowania platformy Spring w tym środowisku. Jeśli posługujesz się środowiskiem NetBeans lub IDEA IntelliJ, nie musisz ustawiać specjalnej konfiguracji — te narzędzia mają wbudowaną obsługę Mavena. W tej książce zastosowaliśmy Mavena, ponieważ platforma Spring od wersji 3.0.3 nie jest już udostępniana ze wszystkimi potrzebnymi zależnościami. Zalecane podejście to wykorzystanie Mavena (lub podobnych narzędzi, takich jak Ant lub Ivy) do zarządzania zależnościami. Jeśli nie używałeś wcześniej Mavena, zajrzyj do rozdziału 12., „Spring Roo”, gdzie opisane jest konfigurowanie środowiska — w tym narzędzia Apache Maven — na potrzeby platformy Spring Roo.
Pobieranie kodu Kod źródłowy do tej książki jest dostępny pod adresem ftp://ftp.helion.pl/przyklady/sprire.zip. Kod jest uporządkowany na podstawie rozdziałów; do każdego z nich dołączone są niezależne przykłady.
Kontakt z autorami Zawsze czekamy na Wasze pytania i informacje zwrotne dotyczące treści książki. Z Joshem Longiem możesz się skontaktować pod adresem
[email protected] (lub przez stronę internetową http://www.joshlong.com). Gary Mak jest dostępny pod adresem
[email protected] (lub przez stronę internetową http://www.metaarchit.com). Z Danielem Rubio skontaktujesz się przez jego stronę internetową, http://www.webforefront.com.
21
WPROWADZENIE
22
ROZDZIAŁ 1
Wprowadzenie do platformy Spring � � � ��
. . . 'k'1e wprawadzeme ' . . . .mformaCJl W tym rozdZlale znaJ'dZlesz krot p ngu, podstawowym lub odsw1ezeme kontenerze, a tak:l.e wybranych globalnie dostępnych mechanizmach u . ni nian h przez ten kontener. Poznasz również format, w jakim konfiguruje się Spring w XML-u, oraz �jc. Dzięki temu rozdziałowi zdobędziesz wiedzę potrzebną do zrozum �� adnień omawianych w dalszych tcncrzc IoC Springa. Najważniejszy częściach książki. Poznasz podstawową konfigurację komponent element platformy Spring, kontener IoC, jest zaprojektowa y t' � można go by o w dużym zakresie ' dostosowywać i konfigurować. Dostępny jest zestaw mechaniz atwiających konfigurowanie komponentów. Dzięki temu możesz w prosty sposób ustawić komponent� amiane w kontenerze ToC. W Springu komponenty są czasem nazywane ziar · uważ, że różnią się one od specyfikacji ziaren JavaReans opracowanej przez firmę Sun. Ziarna dekl o ne w kontenerze ToC nie muszą być ziarnami JavaBeans. Mogą też być zwykłymi obiektami . lain Old ]ava Objects- POJO). Pojęcie POJO oznacza zwyczajny obiekt Javy, który nie mu ać żadnych specjalnych warunków (na przykład iczyć po danej klasie bazowej). Pojęcie to stosuje się w celu implementować określonego interfejsu lub d odróżnienia prostych komponentów a od �omplikowanych komponentów w innych złożonych modelach (na przykład komponentów EJB prze wadzeniem wersji 3.1 specyfikacji EJB). Gdy zakończysz lekturę tego oz zia , będziesz potrafił zbudować w Javie kompletną aplikację za pomocą kontenera IoC Springa. Ponadt e ajrzysz do swoich starszych aplikacji w Javie, może się okazać, że dzięki temu kontenerowi zdołas cz ie uprościć i usprawnić. · ·
� l(oE }�
$ vj �
� � �
J� n� �
�
�
·
1.1. Tworzenie egzemplarza kontenera loC w Springu Problem
Aby tworzyć egzemplarze ziaren na podstawie ich konfiguracji, trzeba najpierw utworzyć egzemplarz kontenera IoC. Następnie można pobierać egzemplarze ziaren z tego kontenera i korzystać z nich.
Rozwiązanie
Spring udostępnia dwa typy implementacji kontenera IoC. Podstawową wersją jest fabryka ziaren. Bardziej zaawansowana odmiana to kontekst aplikacji, który jest rozszerzeniem fabryki ziaren. Zauważ, że w obu wersjach kontcnerów IoC pliki z konfiguracją ziaren są identyczne.
ROZDZIAŁ 1. • WPROWADZENIE DO PLATFORMY SPRING
Kontekst aplikacji udostępnia bardziej zaawansowane funkcje niż fabryka ziaren, a jednocześnie jest zgodny z podstawowymi funkcjami. Dlatego gorąco zachęcamy do stosowania w każdym programie kontekstu aplikacji, chyba że dostępne zasoby są ograniczone (może tak się zdarzyć na przykład przy uruchamianiu kodu w apletach lub na urządzeniach przenośnych) . Interfejsy odpowiadające fabryce ziaren i kontekstowi aplikacji to BeanFactory i Appl icationContext. Interfejs Appl icationContext jest pochodny od interfejsu BeanFactory, co pozwala zachować zgodność między nimi.
•
Uwaga Aby skompilować i uruchomić oparty na Springu kod przedstawiony w tym i w dalszych rozdziałach, trzeba zapisać w parametrze cl asspath zależności potrzebne w platformie Spring. Zalecaną metodą jest tu zastosowanie narzędzia do zarządzania kompilacją, takiego jak Apache Maven, Apache Ant lub lvy. Jeśli korzystasz z Mavena, dodaj do projektu Mavena wymienione poniżej zależności. Tu (podobnie jak dalej) człon ${spring. versionl określa wersję Springa. Zastąp ten człon nazwą używanej wersji. Tę książkę napisano i skompilowano za pomocą wersji 3. 0.2. RELEASE.
org. springframework spri ng-aop $ {spring. version l
�
�
org. springframework spri ng-web $ {spring. version l
'>s-�
'-L O''Y
org. springframework spring-context-support $ {spring. version l
O
�
O1 �
org. springframeworkspring-beans$ {spring. version l< ·
A...\. �
org. spri wark spri ng-context $ {spring. version l org. springframework spri ng-core $ {spring. version l
24
1.1. TWORZENIE EGZEMPLARZA KONTENERA IOC W SPRINGU
Jak to działa? Tworzenie egzemplarza kontekstu aplikacji Appl icati onContext to tylko interfejs. Musisz utworzyć obiekt z jego implementacją. Implementacja Cl assPathXml Appl icationContext jest oparta na kontekście aplikacji i wczytuje z katalogu z parametru cl asspath plik z konfiguracją w XML-u. W tej implementacji można też zastosować kilka plików konfiguracyjnych. Appl icationContext context
new Cl assPathXml Appl icationContext("beans.xml ");
=
Spring udostępnia także inne (obok Cl assPathXml Appl icationContext) implementacje interfejsu Appl icationContext. Implementacja Fil eSystemXml Appl icationContext służy do wczytywania plików z konfiguracją w XML-u z systemu plików lub na podstawie adresów URL, a implementacje Xml Web 4Appl icationContext i Xml Portl etAppl icationContext są przeznaczone do użytku tylko w aplikacjach sieciowych i portalowych.
�';-yw �� '
Pobieranie ziaren z kontenera loC
ołać metodę Aby pobrać zadeklarowane ziarno z fabryki ziaren lub kontekstu aplikacji, wyst � przez metodę getBean O a getBean O i przekazać do niej niepowtarzalną nazwę ziarna. Typ obiektu �ła ciwy typ. to java. l ang. Object, dlatego przed użyciem ziarna obiekt trzeba zrzut
��
SequenceGenerator generator (SequenceGenerator) context.getBean("sequenceGenerator") =
()...7 . ��V
�� �onego za pomocą konstruktora. L ziesz w przedstawionej poniżej klasie Main. �'V
Teraz można używać ziarna tak jak każdego innego ob Kompletny kod źródłowy aplikacji generatora kolejnych lic package com. apress. springrecipes. sequence;
O
��; � athXml Appl icationContext; V
import org. springframework. context. Appl ica import org. springframework. context. support
0
publ ic cl ass Main {
f-\, { ��� � SequenceGenerator ge � or (SequenceGenerato �text. getBean ("sequenceGenerator");
publ ic static void main(String[' Appl icationContext contex new Cl assPathXml Appl ic ontext("beans. xml "); =
System. out. printl n (generator. getSequence O); System. out. printl n (generator. getSequence O);
Jeśli wszystko przebiegło poprawnie, powinieneś zobaczyć w danych wyjściowych kolejne liczby (a także komunikaty dziennika, które zapewne nie będą Cię interesować). 30100000A 30100001A
25
ROZDZIAŁ 1. • WPROWADZENIE DO PLATFORMY SPRING
1.2. Konfigurowanie ziaren w kontenerze loC Problem
Spring udostępnia rozbudowany kontener ToC:, który pozwala zarządzać ziarnami używanymi w aplikacji. Aby wykorzystać usługi kontenera, trzeba skonfigurować ziarna pod kątem uruchamiania ich w tym kontenerze.
Rozwiązanie
Ziarna w kontenerze I o C można skonfigurować za pomocą plików XML, plików właściwości, adnotacji, a nawet interfejsów API. W Springu ziarna można skonfigurować w jednym lub w kilku plikach konfiguracyjnych. W prostych aplikacjach wystarczy ustawić ziarna w jednym takim pliku. Jednak w dużych programach, zawierających wiele ziaren, należy rozdzielić je na podstawie funkcji (na przykład na kontrolery, ziarna DAO i ziarna JMS) pomiędzy kilka plików konf1guracyjnych. Przydatnym sposobem podziału jest wyko stanie warstw architektury obsługiwanych przez dany kontekst.
�
"''-�
Jak to działa?
�� )'r
� lkaCJa ta ma generować rózne ;cUi'st ma określony przedrostek,
Załózmy, ze chcesz opracować aphkac)ę do generowama koleJny h listy kolejnych liczb przeznaczone do odmiennych celów. Każd t p z rostek i wartość początkową. W aplikacji trzeba więc u o y\ł � � mml.
�f ,-v
a egzemplarzy generatora i zarządzać
{"'\O
Tworzenie klasy ziarna
�� � � d�
equenceGenerator o trzech właściwościach: prefix, Na podstawie podanych wymagań należy utwo suffi xiinit ial . Wartości tych właściwości moż wić za pomocą settera lub konstruktora. Pole prywatne � counter przechowuje numer generatora. Prz dym wywołaniu metody getSequence() danego egzemplarza generatora zwracana jest następna li zb· z onym odpowiednim przedrostkiem i przyrostkiem. Ws pomniana metoda jest zadeklarow mtdyfikatorem synchronized, dzięki czemu jest bezpieczna ze wzgl<;du na wątki.
�
package com. apress. springrec publ ic cl ass SequenceGe private private private private
A...\ quence; �
�
{
String prefix; String suffix; int initial ; int counter;
publ ic SequenceGenerator() {} publ ic SequenceGenerator(String prefix, String suffix, int initial ) { this. prefix prefix; this.suffix suffix; this. initial initial ; =
=
=
publ ic void setPrefix(String prefix) { this. prefix prefix; =
26
1.2. KONFIGU ROWANIE ZIAREN W KONTENERZE IOC
publ ic void setSuffi x(String suf fix) { this.suffix = suffix;
publ ic void setlnitial (int initial ) { this. initial = initial ;
publ ic synchronized String getSequence() { StringBuffer buffer = new StringBuffer(); buffer.append(prefix); buffer. append(initial + counter++); buffer.append(suffix); return buffer. toString();
�
Jak widać, klasę SequenceGenerator można skonfigurować za pomocą getterów i setterów lub konstruktora (przy konfigurowaniu ziaren z poziomu kontenera operacje te są nazywane wst kiwaniem za pomocą konstruktora i wstrzykiwaniem za pomocą settera) .
�"""' ....'._ .'
A.,;;
schematów (t�, jndi, jee itd.). Dzięki temu konfiguracja zia
�
�V
możliwa konfiguracja w pliku XML:
�
ostsza i bardziej przejrzysta. Oto najprostsza
b��ns>
� ��m r
Deklarowanie ziare
�p � •
konfiguracyjnym
Ahy kontener To C: mógł ut . ć egze pla z ziarna, powinno ono mieć niepowtarzalne właściwości name i id oraz pełną nazwę klasy. Kaz rzypisaną do ziarna właściwość typu prostego (na przykład typu String) można określić w elemencie . Spring próbuje przekształcić podaną wartość na zadeklarowany typ właściwości. Aby skonfigurować właściwość w wyniku wstrzykiwania za pomocą settera, można wykorzystać element i podać nazwę właściwości w atrybucie name. Przy stosowaniu elementu ziarno musi zawierać odpowiedni setter.
cl ass= 11com. apress. springrecipes. sequence. SequenceGenerator 11 >
30
A lOOOOO
27
ROZDZIAŁ 1. • WPROWADZENIE DO PLATFORMY SPRING
Właściwości ziarna można też skonfigurować w wyniku wstrzykiwania za pomocą konstruktora. Wtedy właściwości należy zadeklarować jako elementy . Takie elementy nie mają atrybutu name, ponieważ argumenty konstruktora są określane na podstawie pozycji. 30
/
A< val ue> < constructor-arg> lOOOOO< val ue> < constructor-arg> < bean>
/
/
/
/
�
W kontenerze IoC każda nazwa ziarna powinna być niepowtarzalna, przy czym dozwolone są powtarzające się nazwy (pozwalają one zastąpić deklarację ziarna, jeśli wczytywanych jest kilka ntekstów). Nazwę ziarna definiuje się w atrybucie name elementu . Zalecanym sposobem identyfikowa ·. ziaren jest używanie standardowego atrybutu id XML-a. Atryhut ten słu7.y do identyfikowania ele dokumentach XML Jeśli edytor tekstu obsługuje język XML, może w czasie pisania kodu po eniu, czy poszczególne wu ziarna się nie powtarzają.
�
�
� N •
< bean> W XML-u występują ograniczenia dotyczące znaków, j ki ożna stosować w atrybucie id. Zwykle jednak nie używa się znaków specjalnych w nazwach ziaren. Sf1 wala ustawić w atrybucie name kilka rozdzielonych · st to możliwe, ponieważ stosowanie przecinków przecinkami nazw ziarna. W przypadku atrybu jest w nim niedozwolone.
� �V
�
Ziarno nic musi mieć ani nazwy, ani idcn tora. Ziarno bez nazwy to ziarno anonimowe. Takie ziarna tworzy się zwykle tylko na potrzeby int cjiez kontenerem Springa, gdy wiadomo, że właściwości będą wstrzykiwane na podstawie typu lub zagnieżdżone wewnątrzwierszowo w ziarnie zewnętrznym.
z_�
�
·aren za pomocą skrótów Spring udost<;pnia skróty d w elemencie , za
cślania wartości właściwości typu prostego. Atrybut val ue można ustawić st umieszczać wartość w zagnieżdżonym elemencie .
< bean>
/
/
/
/
Skrót ten działa też przy podawaniu argumentów dla konstruktora. < bean>
/
28
/
/
/
1.2. KONFIGU ROWANIE ZIAREN W KONTENERZE IOC
W wersji Spring 2.0 pojawił się inny wygodny skrót służący do definiowania właściwości. Wymaga on zastosowania schematu p do zdefiniowania właściwości ziarna jako atrybutów elementu . Pozwala to skrócić kod konfiguracji w formacie XML.
Konfigurowanie kolekcji na potrzeby ziaren List, Set i Map to podstawowe interfejsy reprezentujące trzy główne typy kolekcji w pakiecie Java SDK. Są one częścią platformy Java Collections. Dla każdego rodzaju kolekcji w Javie dostępn jest kilka implementacji o różnych funkcjach i cechach. W Springu kolekcje można łatwo skonfigurować ocą zestawu wbudowanych znaczników XML-a, takich jak , i .
� �� � Załóżmy, że chcesz umożliwić podanie więcej niż jednego przyrost' �en•�orze kolejnych liczb. Przyrostki dodawane do liczb mają być rozdzielone łącznikami. Prog rx._ �� przyjmować przyrostki dowolnego typu i przekształcać je na łańcuchy znaków w mome
ania do liczb.
'L
M�rzyrostków. Lista to uporządkowana �ać za pomocą indeksu lub pętli for each.
listy, tablice i zbiory
Zacznijmy od zastosowania kolekcji java. util . List do z i indeksowana kolekcja. Dostęp do jej elementów można . kage com. apress. springrecipes. sequence;
��� pu�� � c cl ass SequenceGenerator
�er'
{
�O 0V
4...\._ publ ic void setSuffixes(Li � t� � � suffixes) ... this. suffixes = suffixes� private List suffixes;
:
•
�
,bl io 'yoohrooi"' S getSeq"e"" () StringBuffer buffer = new StringBuffer(); for (Object suffix : suffixes) buffer.append("-"); buffer.append(suffix);
return buffer. toString();
Aby zdefiniować właściwość interfejsu java. util . List w konfiguracji ziarna, należy zastosować znacznik i podać w nim potrzebne elementy. Tymi elementami mogą hyć: proste stałe podane w znaczniku , referencja do ziarna podana w znaczniku [, definicja wewnętrznego ziarna określona w znaczniku ], referencja podana za pomocą identyfikatora w znaczniku lub element null- . W kolekcji można też zagnieżdżać inne kolekcje.
29
ROZDZIAŁ 1. • WPROWADZENIE DO PLATFORMY SPRING
A
�y jest stała i nie można jej
� ��
Tablica bardzo przypomina listę- też jest uporządkowaną i indeksowaną kolekcją, do której można uzyskać dostęp za pomocą indeksu. Główna różnica polega na tym, że długość ta
dynamicznie powiększać. W platformie Java Collections można przekształcać t ·
listy i na odwrót przy
użyciu metod Arrays. asList() i List. toArray(). W generatorze kolejnych licz
· na wykorzystać tablicę
Object[] do zapisania pr .yrost ów i uzyskiwania dostępu do nich za p
. u luh w pętli for each.
�
�
pook•go oom. •prm . 'pnogreo' poo . "•"'"";
,� in�
'()-........." .",
publ ic cl ass SequenceGenerator private Object[] suffixes; publ ic void setSuffixes(Object[] suffixes) { this. suffixes = suffixes;
��
o o
� ��
Definicp tabhcy w phku z konfig rac · ą zi
wygląda tak samo jak analogiczna definicja listy ze
•
znacznikiem .
kcJi jest zbiór. Interfejsy java. util . List i java. util . Set dziedziczą Innym często spotykanym rodza· po tym samym interfejsie- java. t1 . Col l ection. Zbiór różm się od listy tym, że nie jest am uporządkowany,
ani indeksowany, a ponadto
r
mogą znajdować si<; duplik zastąpi wcześniejszą. Do
chowywać tylko niepowtarzalne obiekty. Oznacza to, że w zbiorze nie
eśli do zbioru po raz drugi dodany zostanie ten sam element, nowa wersja a ia, czy elementy są takie same, służy metoda equal s().
package com. apress. springrecipes.sequence; publ ic cl ass SequenceGenerator { private Set suffixes; publ ic void setSuffixes(Set suffixes) { this. suffixes = suffixes;
Aby zdefiniować właściwość typu java. util . Set, podaj elementy w znaczniku (tak samo jak dla listy).
30
1.2. KONFIGUROWANIE ZIAREN W KONTENERZE IOC
A
Choć zbiory według pierwotnej specyfikacji nie zachowują kolejności, Spring określa porządek ich elementów za pomocą implementacji java.util.LinkedHashSet (jest to implementacja interfejsu java.util.Set zachowująca kolejność elementów).
Odwzorowania i właściwości Odwzorowanie to tabela przechowująca dane w postaci par klucz-wartość. Określoną wartość można pobrać z odwzorowania na podstawie klucza. Można też przejść przez elementy za pomocą pętli for each. Zarówno klucze, jak i wartości mogą mieć dowolny typ. Do ustalania, czy klucze są sobie równe, służy metoda equals(). Można na przykład zmodyfikować generator kolejnych liczb w taki sposób, aby przyjmował kolekcję java.util.Map zawierającą przyrostki z kluczami. package com.apress.springrecipes.sequence; ... public class SequenceGenerator { ... private Map suffixes; public void setSuffixes(Map suffixes) { this.suffixes = suffixes; } public synchronized String getSequence() { StringBuffer buffer = new StringBuffer(); ... for (Map.Entry entry : suffixes.entrySet()) { buffer.append("-"); buffer.append(entry.getKey()); buffer.append("@"); buffer.append(entry.getValue()); } return buffer.toString(); } }
W Springu odwzorowanie definiowane jest przy użyciu znacznika , w którym należy zagnieździć zestaw znaczników . Klucz trzeba zdefiniować w znaczniku . Nie ma ograniczeń dotyczących typu klucza i wartości, dlatego można zastosować znacznik , [, ], lub . W Springu używana jest implementacja java.util.LinkedHashMap, dlatego kolejność wpisów w odwzorowaniu zostaje zachowana. ...
31
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
type A url
Istnieją skróty przeznaczone do definiowania kluczy i wartości odwzorowania jako atrybutów w znaczniku . Jeśli klucze lub wartości to proste stałe, można zdefiniować je za pomocą atrybutów key i value. Jeżeli potrzebne są referencje do ziaren, należy zastosować atrybuty key-ref i value-ref. ...
We wszystkich opisanych do tego miejsca klasach kolekcji do ustawiania właściwości służyły wartości. Jednak czasem należy zapisać w odwzorowaniu wartość null. Schemat XML-owej konfiguracji Springa zawiera przeznaczony do tego mechanizm. Poniżej widoczne jest odwzorowanie, w którym wartością jest null. null
Kolekcja java.util.Properties działa bardzo podobnie do odwzorowania. Także implementuje interfejs java.util.Map i przechowuje dane w parach klucz-wartość. Jedyna różnica polega na tym, że w kolekcji Properties klucze i wartości zawsze są łańcuchami znaków. package com.apress.springrecipes.sequence; ... public class SequenceGenerator { ...
32
1.2. KONFIGUROWANIE ZIAREN W KONTENERZE IOC
private Properties suffixes; public void setSuffixes(Properties suffixes) { this.suffixes = suffixes; } ... }
Aby zdefiniować w Springu kolekcję java.util.Properties, należy zastosować znacznik z zagnieżdżonymi znacznikami . Każdy znacznik musi zawierać atrybut key i powiązaną wartość. ... A http://www.apress.com/ null
Scalanie kolekcji z ziaren nadrzędnego i podrzędnego Jeśli ziarno jest zdefiniowane za pomocą dziedziczenia, kolekcję z ziarna podrzędnego można scalić z kolekcją z ziarna nadrzędnego. Wymaga to ustawienia atrybutu merge na true. W kolekcji elementy z ziarna podrzędnego są zapisywane po elementach z ziarna nadrzędnego (pozwala to zachować ich kolejność). Dlatego poniższy generator kolejnych liczb będzie zawierał cztery przyrostki: A, B, A i C. A B
A C
...
W kolekcjach i elementy podrzędne są stosowane zamiast elementów nadrzędnych o tych samych wartościach. Tak więc w poniższym generatorze używane będą trzy przyrostki: A, B i C.
33
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
A B A C ...
1.3. Tworzenie ziaren za pomocą konstruktora Problem Programista chce utworzyć ziarno w kontenerze IoC w wyniku wywołania konstruktora. Jest to najczęściej stosowana i najbardziej bezpośrednia metoda tworzenia ziaren. Odpowiada ona używaniu operatora new do tworzenia obiektów w Javie.
Rozwiązanie Gdy podawany jest atrybut class ziarna, standardowo oznacza to, że kontener IoC ma tworzyć egzemplarze ziarna przez wywołanie konstruktora.
Jak to działa? Załóżmy, że chcesz zbudować aplikację do obsługi sklepu internetowego. Przede wszystkim należy utworzyć klasę Product o kilku właściwościach (takich jak nazwa i cena produktu). Ponieważ sklep sprzedaje produkty różnego rodzaju, klasę Product należy utworzyć jako abstrakcyjną, a następnie dodać klasy pochodne od niej. package com.apress.springrecipes.shop; public abstract class Product { private String name; private double price; public Product() {} public Product(String name, double price) { this.name = name; this.price = price; }
34
1.3. TWORZENIE ZIAREN ZA POMOCĄ KONSTRUKTORA
// Gettery i settery ... public String toString() { return name + " " + price; } }
Następnie można utworzyć dwie klasy pochodne, Battery i Disc. Każda z nich ma własne właściwości. package com.apress.springrecipes.shop; public class Battery extends Product { private boolean rechargeable; public Battery() { super(); } public Battery(String name, double price) { super(name, price); } // Gettery i settery ... } package com.apress.springrecipes.shop; public class Disc extends Product { private int capacity; public Disc() { super(); } public Disc(String name, double price) { super(name, price); } // Gettery i settery ... }
Aby zdefiniować produkty w kontenerze IoC, należy przygotować następujący plik konfiguracyjny ziaren:
35
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
Jeśli konfiguracja nie obejmuje elementu , wywoływany jest domyślny konstruktor bezargumentowy. Wtedy dla każdego elementu Spring wstrzykuje wartość za pomocą settera. Przedstawiona wcześniej konfiguracja ziarna odpowiada poniższemu fragmentowi kodu: Product aaa = new Battery(); aaa.setName("AAA"); aaa.setPrice(2.5); aaa.setRechargeable(true); Product cdrw = new Disc(); cdrw.setName("CD-RW"); cdrw.setPrice(1.5); cdrw.setCapacity(700);
Jeżeli podany jest element (lub kilka takich elementów), Spring wywołuje najlepiej dopasowany do argumentów konstruktor.
Ponieważ wywołania konstruktorów z klasy Product i jej klas pochodnych są jednoznaczne, przedstawiona wcześniej konfiguracja ziarna jest odpowiednikiem poniższego fragmentu kodu: Product aaa = new Battery("AAA", 2.5); aaa.setRechargeable(true); Product cdrw = new Disc("CD-RW", 1.5); cdrw.setCapacity(700);
Można napisać poniższą klasę Main, aby pobrać produkty z kontenera IoC w celu ich przetestowania: package com.apress.springrecipes.shop; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) throws Exception { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); Product aaa = (Product) context.getBean("aaa"); Product cdrw = (Product) context.getBean("cdrw"); System.out.println(aaa); System.out.println(cdrw); } }
36
1.4. WYBIERANIE KONSTRUKTORA W PRZYPADKU WIELOZNACZNOŚCI
1.4. Wybieranie konstruktora w przypadku wieloznaczności Problem Jeśli podasz jeden lub kilka argumentów konstruktora ziarna, Spring spróbuje znaleźć odpowiedni konstruktor w klasie ziarna i przekazać argumenty w celu utworzenia obiektu ziarna. Jeśli jednak podane argumenty pasują do więcej niż jednego konstruktora, nie można go jednoznacznie wybrać. W takiej sytuacji Spring czasem nie potrafi wywołać oczekiwanego konstruktora.
Rozwiązanie Możesz podać atrybuty type i index elementu , aby pomóc Springowi w ustaleniu pożądanego konstruktora.
Jak to działa? Dodajmy do klasy SequenceGenerator nowy konstruktor, przyjmujący argumenty prefix i suffix. package com.apress.springrecipes.sequence; public class SequenceGenerator { ... public SequenceGenerator(String prefix, String suffix) { this.prefix = prefix; this.suffix = suffix; } }
W deklaracji ziarna można następnie za pomocą elementów podać jeden lub więcej argumentów konstruktora. Spring spróbuje wtedy znaleźć odpowiedni konstruktor danej klasy i przekazać do niego argumenty w celu utworzenia obiektu ziarna. Pamiętaj, że elementy nie mają atrybutu name, ponieważ argumenty konstruktora podaje się na podstawie pozycji.
Spring potrafi łatwo znaleźć konstruktor odpowiadający dwóm podanym argumentom, ponieważ istnieje tylko jeden konstruktor przyjmujący dwa argumenty. Dodajmy jednak do klasy SequenceGenerator jeszcze jeden konstruktor, przyjmujący argumenty prefix i initial. package com.apress.springrecipes.sequence; public class SequenceGenerator { ... public SequenceGenerator(String prefix, String suffix) { this.prefix = prefix; this.suffix = suffix; } public SequenceGenerator(String prefix, int initial) { this.prefix = prefix;
37
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
this.initial = initial; } }
Aby wywołać ten konstruktor, należy utworzyć pokazaną poniżej deklarację ziarna, przekazującą przedrostek i wartość początkową. Brakujący przyrostek jest wstrzykiwany za pomocą settera.
Jeśli jednak teraz uruchomisz aplikację, otrzymasz następujący wynik: 300A 301A
Powodem uzyskania tego nieoczekiwanego wyniku jest to, że wywołany został pierwszy konstruktor (z argumentami prefix i suffix) zamiast drugiego. Stało się tak, ponieważ Spring przyjął, że oba argumenty mają domyślny typ String, i „uznał”, że najodpowiedniejszy jest pierwszy konstruktor, ponieważ nie wymaga konwersji typów. Aby określić typ argumentów, należy ustawić go za pomocą atrybutu type w elemencie .
Teraz dodajmy do klasy SequenceGenerator jeszcze jeden konstruktor, o argumentach initial i suffix, oraz odpowiednio zmodyfikujmy deklarację ziarna. package com.apress.springrecipes.sequence; public class SequenceGenerator { ... public SequenceGenerator(String prefix, String suffix) { this.prefix = prefix; this.suffix = suffix; } public SequenceGenerator(String prefix, int initial) { this.prefix = prefix; this.initial = initial; } public SequenceGenerator(int initial, String suffix) { this.initial = initial; this.suffix = suffix; } }
38
1.5. PODAWANIE REFERENCJI DO ZIAREN
Jeśli ponownie uruchomisz aplikację, możesz uzyskać poprawny wynik lub przedstawione poniżej nieoczekiwane dane: 30100000null 30100001null
Powodem różnych wyników jest to, że Spring ocenia każdy konstruktor pod kątem zgodności z argumentami. Jednak w procesie oceniania kolejność występowania argumentów w XML-u nie jest brana pod uwagę. To oznacza, że dla Springa konstruktory drugi i trzeci są równie dobre. To, który z nich zostanie wybrany, zależy od momentu ich sprawdzania. Interfejs Reflection API (a dokładniej — metoda Class.getDeclaredConstructors()) sprawia, że konstruktory są zwracane w dowolnym porządku, który może różnić się od kolejności z deklaracji. Wszystkie te czynniki prowadzą do wieloznaczności przy wybieraniu konstruktorów. Aby uniknąć tego problemu, trzeba bezpośrednio określić indeksy argumentów. Umożliwia to atrybut index elementu . Gdy ustawisz atrybuty type i index, Spring powinien precyzyjnie wybrać oczekiwany konstruktor ziarna.
Jeśli jednak masz pewność, że konstruktory są jednoznaczne, możesz pominąć atrybuty type i index.
1.5. Podawanie referencji do ziaren Problem Ziarna z aplikacji często muszą współdziałać ze sobą przy wykonywaniu funkcji programu. Aby ziarna miały dostęp do siebie, trzeba podać referencje do nich w plikach konfiguracyjnych.
Rozwiązanie W pliku konfiguracyjnym ziarna możesz za pomocą elementu [ podać referencję do ziarna na potrzeby ustawienia właściwości lub argumentu konstruktora. W tym celu wystarczy określić potrzebną wartość w elemencie ]. Można też bezpośrednio umieścić deklarację ziarna (jako ziarno wewnętrzne) we właściwości lub w argumencie konstruktora.
Jak to działa? W generatorze sekwencji przyjmowanie przedrostka w postaci łańcucha znaków powoduje zbyt wiele ograniczeń w kontekście wymagań, które mogą się pojawić w przyszłości. Lepszym rozwiązaniem jest ustalanie za pomocą kodu sposobu generowania przedrostka. W tym celu można utworzyć interfejs PrefixGenerator definiujący proces generowania przedrostków. package com.apress.springrecipes.sequence; public interface PrefixGenerator { public String getPrefix(); }
Jedna ze strategii generowania przedrostków polega na wykorzystaniu określonego wzorca do formatowania bieżącej daty systemowej. Utwórzmy klasę DatePrefixGenerator z implementacją interfejsu PrefixGenerator.
39
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
package com.apress.springrecipes.sequence; ... public class DatePrefixGenerator implements PrefixGenerator { private DateFormat formatter; public void setPattern(String pattern) { this.formatter = new SimpleDateFormat(pattern); } public String getPrefix() { return formatter.format(new Date()); } }
Wzorzec z tego generatora jest wstrzykiwany za pomocą settera setPattern(), a następnie używany do utworzenia obiektu typu java.text.DateFormat w celu sformatowania daty. Ponieważ łańcuch znaków z wzorcem nie jest potrzebny po utworzeniu obiektu typu DateFormat, nie trzeba zapisywać go w prywatnym polu. Teraz można zadeklarować ziarno typu DatePrefixGenerator z dowolnym łańcuchem znaków z wzorcem używanym do formatowania daty.
Podawanie referencji do ziaren na potrzeby setterów Aby można było zastosować przedstawiony generator przedrostków, klasa SequenceGenerator powinna przyjmować obiekt typu PrefixGenerator zamiast zwykłego łańcucha znaków z przedrostkiem. Do przyjmowania generatora można wykorzystać technikę wstrzykiwania za pomocą settera. Należy usunąć właściwość prefix, a także związane z nią settery i konstruktory, które powodują błędy kompilacji. package com.apress.springrecipes.sequence; public class SequenceGenerator { ... private PrefixGenerator prefixGenerator; public void setPrefixGenerator(PrefixGenerator prefixGenerator) { this.prefixGenerator = prefixGenerator; } public synchronized String getSequence() { StringBuffer buffer = new StringBuffer(); buffer.append(prefixGenerator.getPrefix()); buffer.append(initial + counter++); buffer.append(suffix); return buffer.toString(); } }
Następnie w klasie SequenceGenerator można zastosować ziarno datePrefixGenerator przy użyciu właściwości prefixGenerator. Wymaga to zagnieżdżenia elementu [ w tej właściwości. ]
40
1.5. PODAWANIE REFERENCJI DO ZIAREN
Nazwa ziarna w atrybucie bean elementu [ może dotyczyć dowolnego ziarna z kontenera IoC, nawet jeśli ziarno to nie jest zdefiniowane w tym samym XML-owym pliku konfiguracyjnym. Jeżeli ziarno znajduje się w tym samym pliku, należy zastosować atrybut local, ponieważ podawana jest referencja do identyfikatora z dokumentu XML. Niektóre edytory XML-a pomagają w sprawdzaniu, czy ziarno o danym identyfikatorze znajduje się w tym samym pliku (pomaga to stwierdzić, czy zachowana jest integralność referencji). ] ...
Istnieje też wygodny skrót, który pozwala podać referencję do ziarna w atrybucie ref elementu . ...
Jeśli jednak zastosujesz tę notację, edytor XML-a nie będzie mógł sprawdzić integralności referencji. To podejście działa podobnie jak ustawienie atrybutu bean w elemencie [. W wersjach Spring 2.x dostępny jest inny wygodny skrót służący do podawania referencji do ziaren. Polega on na zastosowaniu schematu p i umożliwia określanie referencji jako atrybutów elementu ]. Pozwala to skrócić wiersze pliku XML z konfiguracją.
Aby odróżnić referencję do ziarna od prostej wartości właściwości, trzeba dodać do nazwy właściwości przyrostek –ref.
Podawanie referencji do ziaren na potrzeby argumentów konstruktora Referencje do ziaren można też stosować przy wstrzykiwaniu za pomocą konstruktora. Można na przykład dodać konstruktor, który jako argument przyjmuje obiekt typu PrefixGenerator. package com.apress.springrecipes.sequence; public class SequenceGenerator { ... private PrefixGenerator prefixGenerator; public SequenceGenerator(PrefixGenerator prefixGenerator) { this.prefixGenerator = prefixGenerator; } }
41
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
W elemencie można podać referencję do ziarna, korzystając z zagnieżdżonego elementu [ (tak samo jak w elemencie ]).
Skrót używany do podawania referencji do ziaren działa także w elemencie . ...
Deklarowanie ziaren wewnętrznych Gdy dany egzemplarz ziarna jest używany tylko dla jednej właściwości, można zadeklarować go jako ziarno wewnętrzne. Deklaracje ziaren wewnętrznych umieszcza się bezpośrednio w elementach lub . Nie wymaga to ustawiania atrybutów id lub name. Dlatego ziarno jest wtedy anonimowe i nie można go stosować w innych miejscach. Nawet jeśli zdefiniujesz w ziarnie wewnętrznym atrybut id lub name, zostanie on pominięty.
Ziarno wewnętrzne można też zadeklarować w argumencie dla konstruktora.
42
1.6. OKREŚLANIE TYPU DANYCH ELEMENTÓW KOLEKCJI
1.6. Określanie typu danych elementów kolekcji Problem Spring domyślnie przyjmuje, że wszystkie elementy kolekcji to łańcuchy znaków. Jeśli chcesz zastosować inny typ danych, musisz go podać.
Rozwiązanie Można albo podać typ danych każdego elementu kolekcji za pomocą atrybutu type znacznika , albo określić typ danych wszystkich elementów przy użyciu atrybutu value-type znacznika danej kolekcji. W Javie 1.5 i w nowszych wersjach można definiować kolekcje bezpieczne ze względu na typ, dzięki czemu Spring wczytuje informacje o typie kolekcji.
Jak to działa? Załóżmy, że generator kolejnych wartości przyjmuje przyrostki w postaci listy liczb całkowitych. Każda liczba jest formatowana za pomocą obiektu typu java.text.DecimalFormat do postaci czterocyfrowej. package com.apress.springrecipes.sequence; ... public class SequenceGenerator { ... private List suffixes; public void setSuffixes(List suffixes) { this.suffixes = suffixes; } public synchronized String getSequence() { StringBuffer buffer = new StringBuffer(); ... DecimalFormat formatter = new DecimalFormat("0000"); for (Object suffix : suffixes) { buffer.append("-"); buffer.append(formatter.format((Integer) suffix)); } return buffer.toString(); } }
Teraz w standardowy sposób zdefiniuj dla generatora kilka przyrostków w pliku konfiguracyjnym. 5 10 20
43
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
Po uruchomieniu aplikacji wystąpi wyjątek ClassCastException oznaczający, że przyrostków nie można zrzutować na liczby całkowite, ponieważ ich typ to String. Spring domyślnie traktuje każdy element kolekcji jak wartość typu String. Dlatego trzeba ustawić atrybut type w znaczniku i określić w ten sposób typ elementów. ... 5 10 20
Można też ustawić atrybut value-type znacznika kolekcji i tą metodą określić typ wszystkich elementów danej kolekcji. ... 5 10 20
Od wersji 1.5 Javy można zdefiniować listę przyrostków za pomocą bezpiecznej ze względu na typ kolekcji liczb całkowitych. package com.apress.springrecipes.sequence; ... public class SequenceGenerator { ... private List suffixes; public void setSuffixes(List suffixes) { this.suffixes = suffixes; } public synchronized String getSequence() { StringBuffer buffer = new StringBuffer(); ... DecimalFormat formatter = new DecimalFormat("0000"); for (int suffix : suffixes) { buffer.append("-"); buffer.append(formatter.format(suffix)); } return buffer.toString(); } }
Jeśli zdefiniujesz kolekcję w sposób bezpieczny ze względu na typ, Spring będzie potrafił wczytać informacje o typie kolekcji za pomocą mechanizmu refleksji. Dzięki temu nie trzeba ustawiać atrybutu value-type w znaczniku .
44
1.7. TWORZENIE ZIAREN ZA POMOCĄ INTERFEJSU FACTORYBEAN SPRINGA
... 5 10 20
1.7. Tworzenie ziaren za pomocą interfejsu FactoryBean Springa Problem Programista chce utworzyć ziarno w kontenerze IoC przy użyciu ziarna fabrycznego Springa. Ziarno fabryczne pełni funkcję fabryki generującej inne ziarna w kontenerze IoC. Ziarno fabryczne działa bardzo podobnie do metod fabrycznych, jest jednak charakterystycznym dla Springa ziarnem, które w trakcie tworzenia ziaren można zidentyfikować w kontenerze IoC.
Rozwiązanie Ziarno fabryczne musi przede wszystkim zawierać implementację interfejsu FactoryBean. Dla wygody programistów w Springu dostępna jest abstrakcyjna klasa szablonowa AbstractFactoryBean, na podstawie której można tworzyć klasy pochodne. Ziarna fabryczne najczęściej stosuje się do implementowania mechanizmów potrzebnych w platformach. Oto wybrane przykłady: Przy wyszukiwaniu obiektów (na przykład źródeł danych) w interfejsie JNDI można stosować ziarna fabryczne typu JndiObjectFactoryBean. W programowaniu aspektowym w Springu do tworzenia pośredników dla ziaren można wykorzystać ziarna fabryczne typu ProxyFactoryBean. Przy tworzeniu fabryki sesji Hibernate w kontenerze IoC można zastosować ziarna fabryczne typu LocalSessionFactoryBean. Użytkownicy platformy rzadko muszą pisać niestandardowe ziarna fabryczne, ponieważ ziarna te działają tylko w ramach platformy i nie można ich używać poza kontenerem IoC. Zawsze można też zaimplementować metodę fabryczną będącą odpowiednikiem ziarna fabrycznego.
Jak to działa? Choć pisanie niestandardowych ziaren fabrycznych rzadko jest konieczne, warto zrozumieć ich wewnętrzne mechanizmy. Możesz na przykład napisać ziarno fabryczne do generowania produktów z obniżoną ceną. Takie ziarno powinno przyjmować właściwości product i discount, a następnie uwzględniać rabat w cenie produktu i zwracać produkt jako nowe ziarno. package com.apress.springrecipes.shop; import org.springframework.beans.factory.config.AbstractFactoryBean; public class DiscountFactoryBean extends AbstractFactoryBean {
45
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
private Product product; private double discount; public void setProduct(Product product) { this.product = product; } public void setDiscount(double discount) { this.discount = discount; } public Class getObjectType() { return product.getClass(); } protected Object createInstance() throws Exception { product.setPrice(product.getPrice() * (1 - discount)); return product; } }
Można utworzyć ziarno fabryczne w postaci klasy pochodnej od klasy AbstractFactoryBean i przesłonić w nowej klasie metodę createInstance() służącą do tworzenia egzemplarzy docelowego ziarna. Ponadto w metodzie getObjectType() trzeba zwracać typ docelowego ziarna, aby platforma mogła automatycznie połączyć wszystkie elementy ze sobą. Następnie można zadeklarować egzemplarze produktów za pomocą utworzonej wcześniej klasy DiscountFactoryBean. Za każdym razem, gdy żądane jest ziarno z implementacją interfejsu FactoryBean, kontener IoC używa danego ziarna fabrycznego do wygenerowania i zwrócenia docelowego ziarna. Jeśli chcesz uzyskać egzemplarz samego ziarna fabrycznego, podaj jego nazwę poprzedzoną symbolem &.
Pokazana tu konfiguracja ziarna fabrycznego działa podobnie jak poniższy fragment kodu: DiscountFactoryBean aaa = new DiscountFactoryBean(); aaa.setProduct(new Battery("AAA", 2.5)); aaa.setDiscount(0.2); Product aaa = (Product) aaa.createInstance();
46
1.8. DEFINIOWANIE KOLEKCJI ZA POMOCĄ ZIAREN FABRYCZNYCH I SCHEMATU UTIL
DiscountFactoryBean cdrw = new DiscountFactoryBean(); cdrw.setProduct(new Disc("CD-RW", 1.5)); cdrw.setDiscount(0.1); Product cdrw = (Product) cdrw.createInstance();
1.8. Definiowanie kolekcji za pomocą ziaren fabrycznych i schematu util Problem Przy definiowaniu kolekcji za pomocą podstawowych znaczników nie można określić konkretnej klasy kolekcji (na przykład LinkedList, TreeSet lub TreeMap). Ponadto nie da się wtedy utworzyć kolekcji jako niezależnego ziarna i współużytkować jej w kilku różnych ziarnach.
Rozwiązanie Spring udostępnia kilka sposobów umożliwiających rozwiązanie problemów związanych z podstawowymi znacznikami kolekcji. Jedną z możliwości jest wykorzystanie ziaren fabrycznych odpowiadających kolekcjom, takich jak ListFactoryBean, SetFactoryBean i MapFactoryBean. Ziarno fabryczne to specjalny rodzaj ziarna Springa przeznaczony do tworzenia innych ziaren. Druga metoda to zastosowanie znaczników kolekcji w postaci , i . Pochodzą one ze schematu util dostępnego w Springu 2.x.
Jak to działa? Określanie konkretnej klasy kolekcji Za pomocą ziarna fabrycznego kolekcji można zdefiniować kolekcję i określić jej docelową klasę. Możesz na przykład ustawić właściwość targetSetClass ziarna fabrycznego SetFactoryBean. W efekcie Spring przy generowaniu obiektu danej kolekcji utworzy egzemplarz określonej klasy. java.util.TreeSet 5 10 20
47
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
Można też zastosować znacznik kolekcji w schemacie util do zdefiniowania kolekcji i ustawienia docelowej klasy (wymaga to na przykład ustawienia atrybutu set-class w elemencie ). Trzeba wtedy pamiętać o dodaniu definicji schematu util do głównego elementu . ... 5 10 20 ...
Definiowanie niezależnych kolekcji Inną zaletą ziaren fabrycznych kolekcji jest to, że umożliwiają one zdefiniowanie kolekcji jako niezależnego ziarna, które można stosować w innych ziarnach. Można na przykład zdefiniować niezależny zbiór za pomocą ziarna fabrycznego SetFactoryBean. ... 5 10 20 ...
Niezależny zbiór można też zdefiniować przy użyciu znacznika ze schematu util. ...
48
1.9. SPRAWDZANIE WŁAŚCIWOŚCI NA PODSTAWIE ZALEŻNOŚCI
5 10 20 ...
1.9. Sprawdzanie właściwości na podstawie zależności Problem W aplikacjach korporacyjnych używane są czasem setki lub tysiące ziaren zadeklarowanych w kontenerze IoC. Zależności między tymi ziarnami bywają bardzo skomplikowane. Jedną z wad wstrzykiwania danych za pomocą setterów jest to, że nie ma pewności, czy właściwość zostanie wstrzyknięta. Bardzo trudno jest sprawdzić, czy wszystkie wymagane właściwości zostały ustawione.
Rozwiązanie Mechanizm sprawdzania zależności w Springu pomaga sprawdzić, czy dla ziarna ustawione zostały wszystkie właściwości określonego typu. Wystarczy w tym celu określić typ sprawdzania zależności w atrybucie dependency-check elementu . Warto zauważyć, że za pomocą omawianego mechanizmu można sprawdzić tylko to, czy właściwość została ustawiona. Nie da się natomiast ustalić, czy wartość właściwości jest różna od null. W tabeli 1.1 znajdziesz listę wszystkich obsługiwanych w Springu trybów sprawdzania zależności. Tabela 1.1. Tryby sprawdzania zależności obsługiwane w Springu Tryb
Opis
none*
Sprawdzanie poprawności jest pomijane; każda właściwość może pozostać nieustawiona.
simple
Jeśli właściwości typów prostych lub kolekcji nie są ustawione, zgłoszony zostanie wyjątek UnsatisfiedDependencyException.
objects
Jeśli właściwości typów obiektowych (czyli typów innych niż typy proste lub kolekcje) nie są ustawione, zgłoszony zostanie wyjątek UnsatisfiedDependencyException.
all
Jeśli właściwości dowolnego rodzaju nie są ustawione, zgłoszony zostanie wyjątek UnsatisfiedDependencyException.
* Tryb domyślny to none, można to jednak zmienić dzięki ustawieniu atrybutu default-dependency-check elementu głównego . Tryb domyślny jest zastępowany trybem danego ziarna (jeśli jest on ustawiony). Należy zachować ostrożność przy ustawianiu wspomnianego atrybutu, ponieważ zmienia on domyślny tryb sprawdzania zależności dla wszystkich ziaren z kontenera IoC
49
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
Jak to działa? Sprawdzanie właściwości typów prostych Załóżmy, że właściwość suffix generatora kolejnych liczb nie jest ustawiona. Generator zwraca wtedy kolejne liczby, w których przyrostkiem jest łańcuch znaków null. Źródło problemów tego rodzaju często bardzo trudno wykryć (zwłaszcza w skomplikowanych ziarnach). Na szczęście Spring potrafi sprawdzić, czy wszystkie właściwości określonego rodzaju są ustawione. Aby zażądać od Springa zbadania właściwości typów prostych i kolekcji, ustaw atrybut dependency-check elementu na wartość simple.
Jeśli któraś z właściwości określonego rodzaju nie jest ustawiona, zgłoszony zostanie wyjątek Unsatisfied DependencyException. Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'sequenceGenerator' defined in class path resource [beans.xml]: Unsatisfied dependency expressed through bean property 'suffix': Set this property value or disable dependency checking for this bean.
Sprawdzanie właściwości typów obiektowych Jeśli generator przedrostków nie jest ustawiony, to gdy program go zażąda, zgłoszony zostanie wyjątek NullPointerException. Aby umożliwić sprawdzanie zależności dla właściwości typów obiektowych (różnych od typów prostych i kolekcji), należy zmienić wartość atrybutu dependency-check na objects.
Wtedy po uruchomieniu aplikacji Spring poinformuje o tym, że właściwość prefixGenerator nie jest ustawiona. Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'sequenceGenerator' defined in class path resource [beans.xml]: Unsatisfied dependency expressed through bean property 'prefixGenerator': Set this property value or disable dependency checking for this bean.
Sprawdzanie właściwości wszystkich typów Jeśli chcesz sprawdzać wszystkie właściwości ziarna niezależnie od ich rodzaju, zmień wartość atrybutu dependency-check na all.
50
1.10. SPRAWDZANIE WŁAŚCIWOŚCI Z ADNOTACJĄ @REQUIRED
Sprawdzanie zależności i wstrzykiwanie danych za pomocą konstruktora Mechanizm sprawdzania zależności w Springu bada tylko to, czy właściwość została ustawiona za pomocą settera. Tak więc nawet jeśli program za pomocą konstruktora wstrzykuje generator przedrostków, zgłoszony zostanie wyjątek UnsatisfiedDependencyException.
1.10. Sprawdzanie właściwości z adnotacją @Required Problem Mechanizm sprawdzania zależności w Springu potrafi badać tylko wszystkie właściwości określonego rodzaju. Nie umożliwia sprawdzania konkretnych właściwości. W większości sytuacji programista chce sprawdzić, czy ustawiona jest konkretna właściwość, a nie wszystkie właściwości danego rodzaju.
Rozwiązanie RequiredAnnotationBeanPostProcessor to używany w Springu postprocesor sprawdzający w ziarnie, czy wszystkie właściwości z adnotacją @Required są ustawione. Postprocesor ziarna w Springu to ziarno specjalnego typu,
które wykonuje dodatkowe zadania dla wszystkich ziaren przed ich zainicjowaniem. Aby włączyć postprocesor sprawdzający właściwości, trzeba zarejestrować go w kontenerze IoC. Zauważ, że ten postprocesor bada jedynie to, czy właściwości są ustawione — nie sprawdza, czy ich wartość jest różna od null.
Jak to działa? Załóżmy, że w generatorze kolejnych liczb potrzebne są właściwości prefixGenerator i suffix. Można dodać do odpowiadających im setterów adnotację @Required. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Required; public class SequenceGenerator { private PrefixGenerator prefixGenerator; private String suffix; ... @Required public void setPrefixGenerator(PrefixGenerator prefixGenerator) { this.prefixGenerator = prefixGenerator; } @Required public void setSuffix(String suffix) { this.suffix = suffix; } ... }
51
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
Aby zażądać od Springa sprawdzania, czy wspomniane właściwości są ustawione we wszystkich egzemplarzach generatora, należy zarejestrować egzemplarz postprocesora RequiredAnnotationBeanPostProcessor w kontenerze IoC. Jeśli korzystasz z fabryki ziaren, musisz zarejestrować postprocesor za pomocą odpowiedniego interfejsu API. W przeciwnym razie wystarczy zadeklarować egzemplarz postprocesora w kontekście aplikacji.
Jeśli używasz Springa 2.5 lub nowszej wersji tej platformy, możesz dodać element do pliku z konfiguracją ziarna, a egzemplarz postprocesora RequiredAnnotationBeanPost Processor zostanie automatycznie zarejestrowany. ...
Jeśli któreś właściwości z adnotacją @Required nie są ustawione, postprocesor RequiredAnnotationBeanPost Processor zgłosi wyjątek BeanInitializationException. Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sequenceGenerator' defined in class path resource [beans.xml]: Initialization of bean failed; nested exception is org.springframework.beans.factory. BeanInitializationException: Property 'prefixGenerator' is required for bean 'sequenceGenerator'
Oprócz właściwości z adnotacją @Required postprocesor RequiredAnnotationBeanPostProcessor może też sprawdzać właściwości z niestandardowymi adnotacjami. Utwórz na przykład poniższą adnotację: package com.apress.springrecipes.sequence; ... @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Mandatory { }
Następnie zastosuj tę adnotację do setterów wymaganych właściwości. package com.apress.springrecipes.sequence; public class SequenceGenerator { private PrefixGenerator prefixGenerator; private String suffix; ... @Mandatory public void setPrefixGenerator(PrefixGenerator prefixGenerator) { this.prefixGenerator = prefixGenerator; } @Mandatory public void setSuffix(String suffix) { this.suffix = suffix;
52
1.11. AUTOMATYCZNE ŁĄCZENIE ZIAREN ZA POMOCĄ KONFIGURACJI W PLIKU XML
} ... }
Aby sprawdzać właściwości z tą adnotacją, trzeba podać ją we właściwości requiredAnnotationType postprocesora RequiredAnnotationBeanPostProcessor. com.apress.springrecipes.sequence.Mandatory
1.11. Automatyczne łączenie ziaren za pomocą konfiguracji w pliku XML Problem Gdy ziarno potrzebuje dostępu do innego ziarna, można połączyć je ze sobą dzięki bezpośredniemu podaniu referencji. Jednak kontener potrafi automatycznie łączyć ziarna, co zwalnia programistę z konieczności ręcznego konfigurowania powiązań.
Rozwiązanie Kontener IoC może pomóc w automatycznym łączeniu ziaren. Trzeba tylko ustawić tryb automatycznego łączenia w atrybucie autowire elementu . W tabeli 1.2 znajdziesz opis trybów automatycznego łączenia obsługiwanych w Springu. Tabela 1.2. Tryby automatycznego łączenia ziaren obsługiwane w Springu Tryb
Opis
no*
Automatyczne łączenie nie jest włączone. Trzeba bezpośrednio zarządzać zależnościami.
byName
Dla każdej właściwości ziarna łączone jest ziarno o nazwie takiej samej jak nazwa właściwości.
byType
Dla każdej właściwości ziarna łączone jest ziarno, którego typ jest zgodny z typem właściwości. Jeśli istnieje kilka takich ziaren, zgłaszany jest wyjątek Unsatisfied DependencyException.
constructor
Dla każdego argumentu każdego konstruktora najpierw znajdowane jest ziarno o typie zgodnym z typem argumentu. Następnie wybierany jest konstruktor o największej liczbie pasujących argumentów. Gdy nie można jednoznacznie wybrać konstruktora, zgłaszany jest wyjątek UnsatisfiedDependencyException.
autodetect
Jeśli istnieje domyślny konstruktor bezargumentowy, zależności są automatycznie łączone na podstawie typu. W przeciwnym razie automatyczne łączenie odbywa się na podstawie konstruktora.
* Tryb domyślny to no, jednak można to zmienić dzięki ustawieniu atrybutu default-autowire elementu głównego . Tryb domyślny jest zastępowany trybem danego ziarna (jeśli ten ostatni jest ustawiony)
53
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
Choć mechanizm automatycznego łączenia daje duże możliwości, zmniejsza czytelność plików z konfiguracją ziaren. Ponieważ Spring przeprowadza automatyczne łączenie w czasie wykonywania programu, na podstawie pliku z konfiguracją ziarna nie można ustalić połączeń między ziarnami. W praktyce stosowanie automatycznego łączenia zaleca się tylko w tych aplikacjach, w których zależności między komponentami są stosunkowo proste.
Jak to działa? Automatyczne łączenie na podstawie typu Atrybut autowire ziarna sequenceGenerator można ustawić na wartość byType i pozostawić nieustawioną właściwość prefixGenerator. Wtedy Spring spróbuje połączyć ziarno o typie zgodnym z PrefixGenerator. Tu automatycznie połączone zostanie ziarno datePrefixGenerator.
Główny problem z automatycznym łączeniem na podstawie typu polega na tym, że czasem w kontenerze IoC znajduje się więcej niż jedno ziarno zgodne z docelowym typem. Wtedy Spring nie potrafi zdecydować, które ziarno najlepiej odpowiada właściwości, dlatego automatyczne połączenie ziaren nie jest możliwe. Na przykład jeśli dodasz inny generator przedrostków, zwracający przedrostki w postaci bieżącego roku, automatyczne łączenie przestanie działać.
Jeśli w trakcie automatycznego łączenia wykryte zostanie więcej niż jedno pasujące ziarno, Spring zgłosi wyjątek UnsatisfiedDependencyException. Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'sequenceGenerator' defined in class path resource [beans.xml]:
54
1.11. AUTOMATYCZNE ŁĄCZENIE ZIAREN ZA POMOCĄ KONFIGURACJI W PLIKU XML
Unsatisfied dependency expressed through bean property 'prefixGenerator': No unique bean of type [com.apress.springrecipes.sequence.PrefixGenerator] is defined: expected single matching bean but found 2: [datePrefixGenerator, yearPrefixGenerator]
Automatyczne łączenie na podstawie nazwy Inny tryb automatycznego łączenia ziaren to byName. Czasem pozwala on rozwiązać problemy związane z automatycznym łączeniem na podstawie typu. Tryb ten działa bardzo podobnie do trybu byType, ale Spring —zamiast posługiwać się typami — próbuje podłączyć ziarno o nazwie klasy pasującej do nazwy właściwości. Ponieważ nazwa ziarna jest niepowtarzalna w ramach kontenera, automatyczne łączenie na podstawie nazwy pozwala uniknąć wieloznaczności.
Jednak nie zawsze można zastosować automatyczne łączenie na podstawie nazwy. Czasem nie można nadać docelowemu ziarnu nazwy identycznej z nazwą właściwości. W praktyce często niektóre zależności trzeba podawać bezpośrednio, natomiast pozostałe — określać automatycznie. Wymaga to jednoczesnego stosowania bezpośredniego i automatycznego łączenia.
Automatyczne łączenie na podstawie konstruktora Tryb constructor działa podobnie jak tryb byType, ale jest bardziej skomplikowany. Gdy ziarno ma jeden konstruktor, Spring próbuje połączyć ziarna o typach zgodnych z poszczególnymi argumentami konstruktora. Jeśli jednak istnieje więcej konstruktorów, proces się komplikuje. Spring najpierw próbuje znaleźć ziarna o typach zgodnych z argumentami poszczególnych konstruktorów, a następnie wybiera konstruktor o największej liczbie argumentów, do których dopasowano ziarna. Załóżmy, że klasa SequenceGenerator ma jeden konstruktor domyślny i jeden konstruktor z argumentem typu PrefixGenerator. package com.apress.springenterpriserecipes.sequence; public class SequenceGenerator { public SequenceGenerator() {} public SequenceGenerator(PrefixGenerator prefixGenerator) { this.prefixGenerator = prefixGenerator; } ... }
W tej sytuacji Spring dopasuje i wybierze drugi konstruktor, ponieważ można znaleźć ziarno o typie zgodnym z typem PrefixGenerator.
55
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
autowire="constructor">
Jednak większa liczba konstruktorów w klasie może prowadzić do wieloznaczności przy dopasowywaniu argumentów konstruktorów. Sytuacja komplikuje się jeszcze bardziej, jeśli Spring ma automatycznie wybierać konstruktor. Dlatego jeśli stosujesz ten tryb automatycznego łączenia ziaren, koniecznie zadbaj o uniknięcie wieloznaczności.
Automatyczne łączenie z automatycznym wyborem trybu Tryb autodetect sprawia, że Spring ma wybrać sposób automatycznego łączenia spomiędzy trybów byType i constructor. Jeśli dane ziarno ma przynajmniej bezargumentowy konstruktor domyślny, stosowany jest tryb byType. W przeciwnym razie używany jest tryb constructor. Ponieważ klasa SequenceGenerator ma konstruktor domyślny, stosowany jest dla niej tryb byType. To oznacza, że generator przedrostków będzie wstrzykiwany za pomocą settera.
Automatyczne łączenie ziaren a sprawdzanie zależności Gdy Spring wykryje więcej niż jedno ziarno nadające się do automatycznego połączenia, zgłosi wyjątek UnsatisfiedDependencyException. Z drugiej strony jeśli używany tryb automatycznego łączenia to byName lub byType, a Spring nie potrafi znaleźć pasującego ziarna, właściwość nie jest ustawiana, co może prowadzić do wyjątku NullPointerException lub niezainicjowanej wartości. Jeżeli chcesz otrzymywać powiadomienia o tym, że ziaren nie można połączyć, powinieneś ustawić atrybut dependency-check na objects lub all. W takiej sytuacji wyjątek UnsatisfiedDependencyException jest zgłaszany zawsze, gdy automatyczne łączenie kończy się niepowodzeniem. Ustawienie objects sprawia, że Spring zgłasza błąd, kiedy pasującego ziarna nie można znaleźć w tej samej fabryce ziaren. Przy ustawieniu all kontener ponadto informuje o błędach, gdy dowolna wymieniona w zależnościach ziarna właściwość typu prostego lub typu String nie została ustawiona.
56
1.12. AUTOMATYCZNE ŁĄCZENIE ZIAREN Z ADNOTACJAMI @AUTOWIRED I @RESOURCE
1.12. Automatyczne łączenie ziaren z adnotacjami @Autowired i @Resource Problem Automatyczne łączenie oparte na ustawieniu atrybutu autowire w pliku z konfiguracją ziarna powoduje połączenie wszystkich właściwości ziarna. To rozwiązanie nie pozwala na łączenie konkretnych właściwości. Ponadto ziarna można automatycznie łączyć tylko na podstawie typu lub nazwy. Jeśli żadna z tych technik nie jest w danej sytuacji odpowiednia, trzeba zastosować bezpośrednie łączenie.
Rozwiązanie Od Springa 2.5 dostępnych jest kilka usprawnień mechanizmu automatycznego łączenia. Można automatycznie podłączyć wybraną właściwość w wyniku dodania adnotacji do settera, konstruktora, pola, a nawet dowolnej metody. Używane są do tego adnotacje @Autowired i @Resource zdefiniowane w dokumencie JSR-250: Common Annotations for the Java Platform. Dzięki nim ustawianie atrybutu autowire nie jest już jedynym rozwiązaniem. Jednak stosowanie wymienionych adnotacji wymaga korzystania z Javy 1.5 lub nowszej.
Jak to działa? Aby zażądać od Springa automatycznego łączenia właściwości ziarna opatrzonych adnotacjami @Autowired lub @Resource, trzeba zarejestrować w kontenerze IoC egzemplarz postprocesora AutowiredAnnotationBeanPostProcessor. Jeśli używasz fabryki ziaren, musisz zarejestrować ten postprocesor za pomocą interfejsu API. W przeciwnym razie wystarczy zadeklarować egzemplarz postprocesora w kontekście aplikacji.
Możesz też dodać element do pliku z konfiguracją ziarna, a egzemplarz postprocesora AutowiredAnnotationBeanPostProcessor zostanie automatycznie zarejestrowany. ...
Automatyczne łączenie jednego ziarna o zgodnym typie Adnotację @Autowired można zastosować do konkretnej właściwości, aby Spring automatycznie połączył tę właściwość. Możesz na przykład dodać tę adnotację do settera właściwości prefixGenerator. Spring spróbuje wtedy podłączyć ziarno o typie zgodnym z typem PrefixGenerator. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; public class SequenceGenerator { ...
57
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
@Autowired public void setPrefixGenerator(PrefixGenerator prefixGenerator) { this.prefixGenerator = prefixGenerator; } }
Jeśli w kontenerze IoC zdefiniowane jest ziarno o typie zgodnym z typem PrefixGenerator, ziarno to zostanie automatycznie przypisane do właściwości prefixGenerator. ...
Domyślnie wszystkie właściwości z adnotacją @Autowired są wymagane. Gdy Spring nie może znaleźć pasującego ziarna, zgłasza wyjątek. Jeśli chcesz, aby dana właściwość była opcjonalna, ustaw atrybut required adnotacji @Autowired na wartość false. Dzięki temu gdy Spring nie znajdzie pasującego ziarna, pozostawi właściwość nieustawioną. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; public class SequenceGenerator { ... @Autowired(required = false) public void setPrefixGenerator(PrefixGenerator prefixGenerator) { this.prefixGenerator = prefixGenerator; } }
Adnotację @Autowired można zastosować nie tylko do settera, ale też do konstruktora. Spring spróbuje wtedy znaleźć dla każdego z argumentów konstruktora ziarno o zgodnym typie. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; public class SequenceGenerator { ... @Autowired public SequenceGenerator(PrefixGenerator prefixGenerator) { this.prefixGenerator = prefixGenerator; } }
Adnotację @Autowired można także zastosować do pola, nawet jeśli nie ma ono modyfikatora public. W ten sposób można uniknąć deklarowania settera lub konstruktora dla danego pola. Spring przypisze do tego pola pasujące ziarno za pomocą mechanizmu refleksji. Jednak stosowanie adnotacji @Autowired do pól niepublicznych utrudnia testowanie kodu, ponieważ trudno jest wtedy pisać testy jednostkowe. Wynika to z tego, że w ramach testów czarnej skrzynki nie da się wówczas manipulować stanem (normalnie można do tego wykorzystać na przykład atrapy obiektów). 58
1.12. AUTOMATYCZNE ŁĄCZENIE ZIAREN Z ADNOTACJAMI @AUTOWIRED I @RESOURCE
package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; public class SequenceGenerator { @Autowired private PrefixGenerator prefixGenerator; ... }
Adnotację @Autowired można dodać nawet do metody o dowolnej nazwie i liczbie argumentów. Wtedy Spring próbuje połączyć z każdym z argumentów metody ziarno odpowiedniego typu. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; public class SequenceGenerator { ... @Autowired public void inject(PrefixGenerator prefixGenerator) { this.prefixGenerator = prefixGenerator; } }
Automatyczne łączenie wszystkich ziaren zgodnych typów Adnotację @Autowired można też zastosować do właściwości typu tablicowego, aby Spring automatycznie połączył wszystkie pasujące ziarna. Możesz na przykład dodać tę adnotację do właściwości PrefixGenerator[]. Wtedy Spring automatycznie połączy wszystkie ziarna o typie zgodnym z typem PrefixGenerator. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; public class SequenceGenerator { @Autowired private PrefixGenerator[] prefixGenerators; ... }
Jeśli w kontenerze IoC zdefiniowanych jest kilka ziaren o typie zgodnym z typem PrefixGenerator, wszystkie te ziarna zostaną automatycznie dodane do tablicy prefixGenerators. ...
59
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
W podobny sposób można zastosować adnotację @Autowired do kolekcji bezpiecznej ze względu na typ. Spring potrafi wczytać informacje o typie kolekcji i automatycznie połączyć z nią wszystkie ziarna zgodnego typu. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; public class SequenceGenerator { @Autowired private List prefixGenerators; ... }
Gdy Spring wykryje, że adnotacja @Autowired jest ustawiona dla bezpiecznej ze względu na typ kolekcji java.util.Map, w której kluczami są łańcuchy znaków, doda do niej wszystkie ziarna zgodnego typu, przy czym nazwy tych ziaren posłużą za klucze. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; public class SequenceGenerator { @Autowired private Map prefixGenerators; ... }
Automatyczne łączenie na podstawie typu z wykorzystaniem adnotacji @Qualifier Automatyczne łączenie na podstawie typu domyślnie nie działa, gdy w kontenerze IoC istnieje więcej niż jedno ziarno zgodnego typu. Spring pozwala jednak określić potencjalnie zgodne ziarna. W tym celu należy podać ich nazwy w adnotacji @Qualifier. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; public class SequenceGenerator { @Autowired @Qualifier("datePrefixGenerator") private PrefixGenerator prefixGenerator; ... }
Wtedy Spring spróbuje znaleźć w kontenerze IoC ziarno o podanej nazwie i połączyć je z właściwością.
Adnotację @Qualifier można też zastosować do argumentu metody w celu automatycznego połączenia ziarna z danym argumentem.
60
1.12. AUTOMATYCZNE ŁĄCZENIE ZIAREN Z ADNOTACJAMI @AUTOWIRED I @RESOURCE
package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; public class SequenceGenerator { ... @Autowired public void inject( @Qualifier("datePrefixGenerator") PrefixGenerator prefixGenerator) { this.prefixGenerator = prefixGenerator; } }
Można utworzyć niestandardową adnotację kwalifikatora na potrzeby automatycznego łączenia ziaren. Adnotacja tego rodzaju sama musi być opatrzona adnotacją @Qualifier. Takie rozwiązanie jest przydatne, gdy aplikacja ma wstrzykiwać ziarno o danym typie i określonej konfiguracji, jeśli do pola lub settera jest dodana taka adnotacja. package com.apress.springrecipes.sequence; import java.lang.annotation.Target; import java.lang.annotation.Retention; import java.lang.annotation.ElementType; import java.lang.annotation.RetentionPolicy;import org.springframework.beans.factory.annotation.Qualifier; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.PARAMETER }) @Qualifier public @interface Generator { String value(); }
Następnie można zastosować nową adnotację do właściwości z adnotacją @Autowired. Spring automatycznie połączy wtedy z właściwością ziarno o określonej wartości powiązane z daną adnotacją. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; public class SequenceGenerator { @Autowired @Generator("prefix") private PrefixGenerator prefixGenerator; ... }
Nowy kwalifikator należy zastosować do docelowego ziarna, które ma być automatycznie łączone z przedstawioną właściwością. Kwalifikator jest dodawany za pomocą atrybutu type w elemencie . Wartość kwalifikatora należy podać w atrybucie value. Wartość ta odpowiada atrybutowi String value() adnotacji.
61
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
Automatyczne łączenie na podstawie nazwy Jeśli chcesz automatycznie łączyć właściwości ziaren na podstawie nazwy, możesz dodać do settera, konstruktora lub pola adnotację @Resource opisaną w dokumencie JSR-250. Domyślnie Spring próbuje znaleźć ziarno o nazwie identycznej z nazwą właściwości. Można też bezpośrednio podać nazwę ziarna w atrybucie name. Uwaga Aby korzystać z adnotacji z dokumentu JSR-250, trzeba dodać te adnotacje jako zależność. Jeśli używasz Mavena, zastosuj następujący kod: javax.annotation jsr250-api 1.0
package com.apress.springrecipes.sequence; import javax.annotation.Resource; public class SequenceGenerator { @Resource(name = "datePrefixGenerator") private PrefixGenerator prefixGenerator; ... }
1.13. Dziedziczenie konfiguracji ziarna Problem Gdy konfigurujesz ziarna w kontenerze IoC, czasem kilka z nich ma identyczne fragmenty konfiguracji, na przykład właściwości i atrybuty ziarna w elemencie . Często trzeba powtórnie wpisywać te fragmenty dla wielu ziaren.
Rozwiązanie Spring umożliwia wyodrębnienie wspólnych fragmentów konfiguracji i umieszczenie ich w ziarnie nadrzędnym. Ziarna dziedziczące po nadrzędnym to ziarna podrzędne. Ziarna podrzędne dziedziczą po nadrzędnym konfigurację (w tym właściwości i atrybuty z elementu ), dzięki czemu nie trzeba ponownie jej wpisywać. W razie potrzeby w ziarnach podrzędnych można też zmodyfikować odziedziczone fragmenty konfiguracji. Ziarno nadrzędne może pełnić funkcję szablonu konfiguracyjnego i jednocześnie pozwalać na tworzenie swoich egzemplarzy. Jeśli jednak chcesz, aby nie można było tworzyć egzemplarzy ziarna nadrzędnego, musisz ustawić jego atrybut abstract na true. Jest to dla Springa informacja, że nie należy tworzyć takich egzemplarzy. Zauważ, że nie wszystkie atrybuty zdefiniowane w nadrzędnym elemencie są dziedziczone. Przy dziedziczeniu pomijane są na przykład atrybuty autowire i dependency-check. Aby dowiedzieć się więcej o tym, które atrybuty są dziedziczone, a które nie są, zapoznaj się z fragmentem dokumentacji Springa poświęconym dziedziczeniu ziaren.
62
1.13. DZIEDZICZENIE KONFIGURACJI ZIARNA
Jak to działa? Załóżmy, że chcesz dodać nowy egzemplarz generatora kolejnych liczb. W egzemplarzu tym wartość początkowa i przyrostek mają być takie same jak w istniejącym już egzemplarzu.
Aby uniknąć powielania tych samych właściwości, można zadeklarować bazowy generator kolejnych liczb, w którym potrzebne właściwości są ustawione. Następnie oba przedstawione generatory mogą dziedziczyć po generatorze bazowym, dzięki czemu właściwości będą w nich ustawiane automatycznie. W ziarnach podrzędnych nie trzeba określać atrybutów class, jeśli są one takie same jak w ziarnie nadrzędnym. ...
W ziarnach podrzędnych można zmodyfikować odziedziczone właściwości. Na przykład można dodać podrzędny generator kolejnych liczb mający inną wartość początkową. ...
63
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
Na podstawie pokazanego bazowego generatora kolejnych liczb można utworzyć egzemplarz ziarna. Jeśli chcesz, aby generator ten pełnił wyłącznie funkcję szablonu, musisz ustawić atrybut abstract na wartość true. Wtedy Spring nie będzie tworzył egzemplarzy tego ziarna. ...
Można też pominąć klasę ziarna nadrzędnego i określić w ziarnie podrzędnym inną klasę. Technikę tę stosuje się zwłaszcza wtedy, gdy ziarna nadrzędne i podrzędne nie znajdują się w tej samej hierarchii klas, ale mają właściwości o tych samych nazwach. W takiej sytuacji atrybut abstract ziarna nadrzędnego musi mieć wartość true, ponieważ nie powinno się tworzyć egzemplarzy tego ziarna. Dodaj teraz nową klasę, ReverseGenerator, z właściwością initial. package com.apress.springrecipes.sequence; public class ReverseGenerator { private int initial; public void setInitial(int initial) { this.initial = initial; } }
Teraz klasy SequenceGenerator i ReverseGenerator nie dziedziczą po tej samej klasie bazowej. Nie należą więc do tej samej hierarchii klas, ale mają właściwość o identycznej nazwie (initial). Aby wyodrębnić tę wspólną właściwość, należy utworzyć ziarno nadrzędne (tu nosi ono nazwę baseGenerator) bez zdefiniowanego atrybutu class. ...
Rysunek 1.1 przedstawia diagram obiektów z hierarchią ziaren z rodziny generatorów.
64
1.14. SKANOWANIE KOMPONENTÓW Z PARAMETRU CLASSPATH
Rysunek 1.1. Diagram obiektów przedstawiający hierarchię ziaren z rodziny generatorów
1.14. Skanowanie komponentów z parametru classpath Problem Aby kontener IoC zarządzał komponentami, należy zadeklarować je jeden po drugim w pliku z konfiguracją ziarna. Spring potrafi automatycznie wykrywać komponenty (bez konieczności ich ręcznego konfigurowania), co pozwala programistom przyspieszyć pracę.
Rozwiązanie Spring udostępnia rozbudowany mechanizm skanowania komponentów. Mechanizm ten potrafi automatycznie skanować, wykrywać i tworzyć egzemplarze komponentów, które są opatrzone odpowiednimi adnotacjami i występują w parametrze classpath. Podstawową adnotacją komponentów zarządzanych przez Spring jest @Component. Inne, bardziej precyzyjne adnotacje tego rodzaju to: @Repository, @Service i @Controller. Oznaczają one komponenty z następujących warstw: utrwalania danych, usług i prezentacji.
Jak to działa? Załóżmy, że poproszono Cię o utworzenie aplikacji do generowania kolejnych liczb. Masz przy tym wykorzystać sekwencje liczb z bazy danych i zapisywać przedrostek i przyrostek każdej sekwencji w tabeli. Najpierw należy utworzyć klasę domeny Sequence obejmującą właściwości id, prefix i suffix. package com.apress.springrecipes.sequence; public class Sequence { private String id; private String prefix; private String suffix; // Konstruktory, gettery i settery ... }
65
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
Następnie utwórz interfejs DAO (ang. Data Access Object) odpowiedzialny za dostęp do danych z bazy. Metoda getSequence() powinna wczytywać na podstawie identyfikatora obiekt Sequence z tabeli, a metoda getNextValue() ma pobierać z bazy następną wartość z określonej sekwencji liczb. package com.apress.springrecipes.sequence; public interface SequenceDao { public Sequence getSequence(String sequenceId); public int getNextValue(String sequenceId); }
W produkcyjnej wersji aplikacji interfejs DAO należy zaimplementować z wykorzystaniem technologii dostępu do danych, na przykład JDBC lub odwzorowań obiektowo-relacyjnych. Jednak na potrzeby testów wystarczy zastosować zwykłe odwzorowania i zapisać w nich wartości oraz egzemplarze sekwencji. package com.apress.springrecipes.sequence; ... public class SequenceDaoImpl implements SequenceDao { private Map sequences; private Map values; public SequenceDaoImpl() { sequences = new HashMap(); sequences.put("IT", new Sequence("IT", "30", "A")); values = new HashMap(); values.put("IT", 100000); } public Sequence getSequence(String sequenceId) { return sequences.get(sequenceId); } public synchronized int getNextValue(String sequenceId) { int value = values.get(sequenceId); values.put(sequenceId, value + 1); return value; } }
Potrzebny jest też działający jak fasada obiekt usługi, który posłuży do generowania kolejnych liczb. Wewnętrznie obiekt ten powinien w ramach obsługiwania żądań generowania kolejnych liczb komunikować się z obiektem DAO. Dlatego potrzebna jest referencja do odpowiedniego obiektu DAO. package com.apress.springrecipes.sequence; public class SequenceService { private SequenceDao sequenceDao; public void setSequenceDao(SequenceDao sequenceDao) { this.sequenceDao = sequenceDao; } public String generate(String sequenceId) { Sequence sequence = sequenceDao.getSequence(sequenceId); int value = sequenceDao.getNextValue(sequenceId); return sequence.getPrefix() + value + sequence.getSuffix(); } }
66
1.14. SKANOWANIE KOMPONENTÓW Z PARAMETRU CLASSPATH
Ponadto trzeba skonfigurować komponenty w pliku z konfiguracją ziarna, aby aplikacja generująca kolejne liczby mogła poprawnie działać. Możesz automatycznie połączyć komponenty i skrócić w ten sposób kod konfiguracji.
Następnie możesz przetestować utworzone komponenty za pomocą poniższej klasy Main: package com.apress.springrecipes.sequence; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); SequenceService sequenceService = (SequenceService) context.getBean("sequenceService"); System.out.println(sequenceService.generate("IT")); System.out.println(sequenceService.generate("IT")); } }
Automatyczne skanowanie komponentów Mechanizm skanowania komponentów jest dostępny w Springu od wersji 2.5 i umożliwia automatyczne skanowanie, wykrywanie oraz tworzenie egzemplarzy komponentów z parametru classpath. Domyślnie Spring potrafi wykrywać wszystkie komponenty z adnotacjami stereotypów. Podstawową adnotacją opisującą komponenty zarządzane przez Spring jest adnotacja @Component. Można zastosować ją do klasy SequenceDaoImpl. package com.apress.springrecipes.sequence; import org.springframework.stereotype.Component; import java.util.Map; @Component public class SequenceDaoImpl implements SequenceDao { ... }
Ponadto tę adnotację stereotypu można dodać do klasy SequenceService, aby Spring ją wykrywał. Możesz też zastosować adnotację @Autowired do pola DAO, by Spring automatycznie łączył to pole z wartością na podstawie typu. Zauważ, że ponieważ dla pola ustawiana jest ta adnotacja, nie trzeba stosować settera. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;
67
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
@Component public class SequenceService { @Autowired private SequenceDao sequenceDao; ... }
Dzięki zastosowaniu do klas komponentów adnotacji stereotypów można za pomocą jednego elementu XML-a () zażądać od Springa przeskanowania tych komponentów. We wspomnianym elemencie trzeba wskazać pakiet, w którym należy szukać komponentów. Spring przeskanuje wtedy podany pakiet i wszystkie jego pakiety podrzędne. Aby wskazać kilka pakietów, należy rozdzielić ich nazwy przecinkami. Przedstawiony wcześniej stereotyp wystarczy, by można było zastosować ziarno. Spring sam tworzy nazwę takiego ziarna. W tym celu zmienia pierwszą literę nazwy klasy na małą, a w pozostałej części stosuje notację wielbłądzią. Dlatego poniższy kod jest poprawny (przy założeniu, że utworzono egzemplarz kontekstu aplikacji z elementem ). SequenceService sequenceService = (SequenceService) context.getBean("sequenceService");
Zauważ, że ten element powoduje też zarejestrowanie egzemplarza postprocesora AutowiredAnnotationBean PostProcessor, który automatycznie łączy właściwości opatrzone adnotacją @Autowired.
Adnotacja @Component to podstawowy stereotyp do oznaczania komponentów ogólnego użytku. Istnieją też inne, bardziej szczegółowe stereotypy do oznaczania komponentów z poszczególnych warstw aplikacji. Stereotyp @Repository stosuje się do komponentów DAO z warstwy utrwalania danych. package com.apress.springrecipes.sequence; import org.springframework.stereotype.Repository; @Repository public class SequenceDaoImpl implements SequenceDao { ... }
Stereotyp @Service używany jest do komponentów usługowych z warstwy usług. package com.apress.springrecipes.sequence; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class SequenceService { @Autowired private SequenceDao sequenceDao; ... }
68
1.14. SKANOWANIE KOMPONENTÓW Z PARAMETRU CLASSPATH
Istnieje jeszcze jeden stereotyp, @Controller, stosowany dla kontrolerów z warstwy prezentacji. Jego omówienie znajdziesz w rozdziale 8.
Filtrowanie skanowanych komponentów Spring domyślnie wykrywa wszystkie klasy z adnotacjami @Component, @Repository, @Service i @Controller, a także z niestandardowymi adnotacjami, które same są opatrzone adnotacją @Component. Procesem skanowania można sterować za pomocą filtrów include i exclude. Spring obsługuje cztery rodzaje wyrażeń filtrujących. Wyrażenia typu annotation i assignable pozwalają określić rodzaj adnotacji i klasę lub interfejs uwzględniane przy filtrowaniu. W wyrażeniach typu regex i aspectj można podać wyrażenie regularne i wyrażenie określające punkt przecięcia (ang. pointcut) kodu w języku AspectJ. Wyrażenia te są używane do dopasowywania klas. Ponadto za pomocą atrybutu use-default-filters można wyłączyć filtry domyślne. W opisanym poniżej skanowaniu komponentów uwzględniane są wszystkie klasy, których nazwy zawierają słowa Dao lub Service, natomiast pomijane są klasy z adnotacją @Controller.
Ponieważ zastosowano tu filtry include do wykrywania wszystkich klas o nazwach zawierających słowo Dao lub Service, komponenty SequenceDaoImpl i SequenceService zostaną wykryte automatycznie nawet wtedy, gdy nie będą miały adnotacji stereotypów.
Generowanie nazw dla wykrytych komponentów Domyślny proces generowania w Springu nazw dla wykrytych komponentów polega na zmianie pierwszej litery nazwy klasy na małą. Na przykład nazwa klasy SequenceService jest przekształcana na postać sequenceService. Nazwę komponentu można też zdefiniować bezpośrednio, podając ją jako wartość adnotacji stereotypu. package com.apress.springrecipes.sequence; ... import org.springframework.stereotype.Service; @Service("sequenceService") public class SequenceService { ... } package com.apress.springrecipes.sequence; import org.springframework.stereotype.Repository; @Repository("sequenceDao") public class SequenceDaoImpl implements SequenceDao { ... }
Możesz opracować własną strategię generowania nazw. W tym celu utwórz implementację interfejsu BeanNameGenerator i podaj ją w atrybucie name-generator elementu .
69
ROZDZIAŁ 1. WPROWADZENIE DO PLATFORMY SPRING
Podsumowanie W tym rozdziale poznałeś podstawy konfigurowania ziaren w kontenerze IoC w Springu. Spring obsługuje kilka rodzajów konfiguracji ziaren. Najprostszą i najbardziej dojrzałą wśród nich jest konfiguracja w formacie XML. Spring udostępnia dwa typy implementacji kontenera IoC. Podstawowym typem jest fabryka ziaren, natomiast bardziej zaawansowany jest kontekst aplikacji. Jeśli to możliwe (czyli gdy dostępne są wystarczające zasoby), należy stosować kontekst aplikacji. Spring obsługuje wstrzykiwanie za pomocą setterów i konstruktora. Przy użyciu tej techniki można definiować właściwości ziarna. Do tych właściwości można przypisywać: proste wartości, kolekcje i referencje do ziaren. Sprawdzanie zależności i automatyczne łączenie to dwie cenne funkcje kontenera udostępniane w Springu. Sprawdzanie zależności pomaga ustalić, czy wszystkie wymagane właściwości są ustawione. Automatyczne łączenie umożliwia automatyczne podłączanie ziaren na podstawie typu, nazwy lub adnotacji. Starszy sposób konfigurowania tych mechanizmów jest oparty na atrybutach XML-a. W nowym podejściu wykorzystuje się adnotacje i postprocesory ziaren (te techniki zapewniają większą swobodę). Spring obsługuje dziedziczenie ziaren. Pozwala to umieścić wspólne aspekty konfiguracji w ziarnie nadrzędnym. Takie ziarno może być używane jako szablon z konfiguracją, jako klasa do tworzenia egzemplarzy ziarna, a także w obu tych rolach. Ponieważ kolekcje są ważnym elementem Javy, Spring udostępnia kilka różnych znaczników kolekcji, pozwalających łatwo konfigurować kolekcje w plikach z konfiguracją ziaren. Można wykorzystać ziarna fabryczne kolekcji lub znaczniki kolekcji ze schematu util do szczegółowego skonfigurowania kolekcji. Ponadto można definiować kolekcje jako niezależne ziarna współużytkowane przez inne ziarna. Spring potrafi też automatycznie wykrywać komponenty z parametru classpath. Domyślnie wykrywane są wszystkie komponenty o określonych adnotacjach stereotypów, jednak można dodatkowo zastosować filtry, aby uwzględniać lub pomijać wybrane komponenty. Skanowanie komponentów to wartościowa funkcja, która pozwala skrócić konfigurację.
70
ROZDZIAŁ 2
Zaawansowany kontener IoC w Springu W tym rozdziale poznasz zaawansowane funkcje i wewnętrzne mechanizmy kontenera IoC Springa. Pomoże Ci to w wydajniejszym tworzeniu aplikacji opartych na Springu. Choć niektóre z opisanych tu funkcji stosuje się rzadko, są one niezbędne w rozbudowanym i dającym dużo możliwości kontenerze. Są też podstawą innych modułów Springa. Sam kontener IoC w Springu jest zaprojektowany w taki sposób, aby można go było łatwo dostosowywać do własnych potrzeb i rozszerzać. Za pomocą konfiguracji można zmodyfikować domyślne działanie kontenera, a dodatki zgodne ze specyfikacją kontenera pozwalają wzbogacić jego możliwości. Po zakończeniu lektury tego rozdziału będziesz znał większość funkcji kontenera IoC. Zapewni Ci to podstawy przydatne przy nauce różnych aspektów Springa opisanych w dalszych rozdziałach.
2.1. Tworzenie ziaren za pomocą statycznych metod fabrycznych Problem Programista chce utworzyć w kontenerze IoC ziarno przy użyciu statycznej metody fabrycznej (pozwala to ukryć proces tworzenia obiektów w statycznej metodzie). Klient, który żąda obiektu, może w prosty sposób wywołać taką metodę — nie musi w tym celu znać szczegółów związanych z tworzeniem obiektów.
Rozwiązanie Spring obsługuje tworzenie ziaren za pomocą wywołania statycznej metody fabrycznej. Taką metodę należy podać w atrybucie factory-method.
Jak to działa? Możesz napisać poniższą przykładową statyczną metodę fabryczną createProduct(), która tworzy produkt na podstawie wstępnie zdefiniowanego identyfikatora. Przy użyciu tego identyfikatora metoda określa, jakiej klasy produkt ma utworzyć. Jeśli nie istnieje produkt pasujący do podanego identyfikatora, metoda zgłasza wyjątek IllegalArgumentException.
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
package com.apress.springrecipes.shop; public class ProductCreator { public static Product createProduct(String productId) { if ("aaa".equals(productId)) { return new Battery("AAA", 2.5); } else if ("cdrw".equals(productId)) { return new Disc("CD-RW", 1.5); } throw new IllegalArgumentException("Nieznany produkt"); } }
Aby zadeklarować ziarno tworzone za pomocą statycznej metody fabrycznej, należy w atrybucie class podać zawierającą tę metodę klasę, a w atrybucie factory-method wpisać nazwę tej metody. Do przekazywania argumentów do metody służą elementy .
Wszystkie wyjątki zgłaszane przez tę metodę fabryczną Spring umieszcza w wyjątkach typu BeanCreation Exception. Poniższy fragment kodu jest odpowiednikiem przedstawionej wcześniej konfiguracji ziarna: Product aaa = ProductCreator.createProduct("aaa"); Product cdrw = ProductCreator.createProduct("cdrw");
2.2. Tworzenie ziaren za pomocą fabrycznej metody egzemplarza Problem Programista chce utworzyć ziarno w kontenerze IoC przy użyciu fabrycznej metody egzemplarza. Takie metody pozwalają ukryć proces tworzenia obiektów w metodzie innego obiektu. Klient, który żąda obiektu, może w prosty sposób wywołać tę metodę — nie musi w tym celu znać szczegółów związanych z tworzeniem takich obiektów.
Rozwiązanie Spring umożliwia tworzenie ziaren w wyniku wywołania fabrycznej metody egzemplarza. Egzemplarz ziarna należy podać w atrybucie factory-bean, a odpowiednią metodę fabryczną — w atrybucie factory-method.
Jak to działa? Można na przykład napisać poniższą klasę ProductCreator, w której konfigurowalne odwzorowanie jest używane do przechowywania wstępnie zdefiniowanych produktów. Fabryczna metoda egzemplarza createProduct() 72
2.2. TWORZENIE ZIAREN ZA POMOCĄ FABRYCZNEJ METODY EGZEMPLARZA
określa produkt przez sprawdzenie w odwzorowaniu podanego identyfikatora productId. Jeśli nie istnieje produkt o tym identyfikatorze, metoda zgłasza wyjątek IllegalArgumentException. package com.apress.springrecipes.shop; ... public class ProductCreator { private Map products; public void setProducts(Map products) { this.products = products; } public Product createProduct(String productId) { Product product = products.get(productId); if (product != null) { return product; } throw new IllegalArgumentException("Nieznany produkt"); } }
Aby tworzyć produkty za pomocą klasy ProductCreator, najpierw trzeba zadeklarować egzemplarz tej klasy w kontenerze IoC i skonfigurować w tym egzemplarzu odwzorowanie z produktami. Produkty można zadeklarować w odwzorowaniu jako ziarna wewnętrzne. Aby zadeklarować ziarno tworzone za pomocą fabrycznej metody egzemplarza, należy w atrybucie factory-bean podać ziarno zawierające tę metodę, a następnie w atrybucie factory-method określić nazwę metody fabrycznej. Później wystarczy przekazać argumenty metody przy użyciu elementów .
73
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
Wszystkie wyjątki zgłaszane przez tę metodę fabryczną Spring umieszcza w wyjątkach typu BeanCreation Exception. Poniższy fragment kodu jest odpowiednikiem przedstawionej wcześniej konfiguracji ziarna: ProductCreator productCreator = new ProductCreator(); productCreator.setProducts(...); Product aaa = productCreator.createProduct("aaa"); Product cdrw = productCreator.createProduct("cdrw");
2.3. Deklarowanie ziaren na podstawie pól statycznych Problem Programista chce zadeklarować w kontenerze IoC ziarno na podstawie pola statycznego. W Javie stałe często deklaruje się jako pola statyczne.
Rozwiązanie Aby zadeklarować ziarno na podstawie pola statycznego, można wykorzystać albo wbudowane ziarno fabryczne FieldRetrievingFactoryBean, albo znacznik (dostępny w wersjach 2.x Springa).
Jak to działa? Najpierw zdefiniuj w klasie Product dwie stałe reprezentujące produkty. package com.apress.springrecipes.shop; public abstract class Product { public static final Product AAA = new Battery("AAA", 2.5); public static final Product CDRW = new Disc("CD-RW", 1.5); ... }
Aby zadeklarować ziarno na podstawie pola statycznego, można zastosować wbudowane ziarno fabryczne FieldRetrievingFactoryBean i we właściwości staticField podać pełną nazwę pola. com.apress.springrecipes.shop.Product.AAA com.apress.springrecipes.shop.Product.CDRW
74
2.4. DEKLAROWANIE ZIAREN NA PODSTAWIE WŁAŚCIWOŚCI OBIEKTÓW
Przedstawiona konfiguracja ziarna jest odpowiednikiem poniższego fragmentu kodu: Product aaa = com.apress.springrecipes.shop.Product.AAA; Product cdrw = com.apress.springrecipes.shop.Product.CDRW;
Zamiast bezpośrednio podawać nazwę pola we właściwości staticField, można ustawić ją w identyfikatorze powiązanym z ziarnem FieldRetrievingFactoryBean. Wadą tego podejścia jest to, że nazwy ziaren bywają stosunkowo długie.
W Springu 2 i w nowszych wersjach tej platformy można zadeklarować ziarno na podstawie pola statycznego za pomocą znacznika . W porównaniu ze stosowaniem ziarna FieldRetrieving FactoryBean jest to łatwiejszy sposób deklarowania ziaren na podstawie pól statycznych. Jednak zanim będzie można wykorzystać wspomniany znacznik, trzeba dodać do głównego elementu definicję schematu util.
2.4. Deklarowanie ziaren na podstawie właściwości obiektów Problem Programista chce zadeklarować w kontenerze IoC ziarno na podstawie właściwości obiektu lub właściwości zagnieżdżonej (ścieżki do właściwości).
Rozwiązanie Aby zadeklarować ziarno na podstawie właściwości obiektu lub ścieżki do właściwości, można wykorzystać albo wbudowane ziarno fabryczne PropertyPathFactoryBean, albo znacznik (jest on dostępny w wersjach 2.x Springa).
75
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
Jak to działa? W ramach przykładu utwórzmy klasę ProductRanking o właściwości bestSeller typu Product. package com.apress.springrecipes.shop; public class ProductRanking { private Product bestSeller; public Product getBestSeller() { return bestSeller; } public void setBestSeller(Product bestSeller) { this.bestSeller = bestSeller; } }
W poniższej deklaracji ziarna właściwość bestSeller jest zadeklarowana jako ziarno wewnętrzne. Z definicji nie można pobierać takich ziaren na podstawie nazwy. Można jednak pobrać je jako właściwość ziarna productRanking. Za pomocą ziarna fabrycznego PropertyPathFactoryBean można zadeklarować ziarno na podstawie właściwości obiektu lub ścieżki do właściwości.
Zauważ, że właściwość propertyPath ziarna PropertyPathFactoryBean przyjmuje nie tylko pojedynczą nazwę właściwości, ale też ścieżkę do właściwości (należy zastosować w niej separatory w postaci kropek). Pokazana wcześniej konfiguracja ziarna jest odpowiednikiem poniższego fragmentu kodu: Product bestSeller = productRanking.getBestSeller();
Właściwości targetObject i propertyPath można podać bezpośrednio, można też połączyć je z nazwą ziarna typu PropertyPathFactoryBean. Wadą tego podejścia jest to, że nazwy ziaren bywają stosunkowo długie.
W Springu 2.x ziarno można zadeklarować na podstawie właściwości obiektu lub ścieżki do właściwości także za pomocą znacznika . W porównaniu ze stosowaniem ziarna PropertyPath FactoryBean technika ta jest prostszym sposobem deklarowania ziaren na podstawie właściwości. Jednak zanim będzie można zastosować wspomniany znacznik, trzeba dodać definicję schematu util do głównego elementu .
76
2.5. UŻYWANIE JĘZYKA WYRAŻEŃ DOSTĘPNEGO W SPRINGU
...
Aby przetestować podaną ścieżkę do właściwości, można pobrać na jej podstawie ziarno z kontenera IoC i wyświetlić je w konsoli. package com.apress.springrecipes.shop; ... public class Main { public static void main(String[] args) throws Exception { ... Product bestSeller = (Product) context.getBean("bestSeller"); System.out.println(bestSeller); } }
2.5. Używanie języka wyrażeń dostępnego w Springu Problem Programista chce dynamicznie przetwarzać warunek lub właściwość, aby wykorzystać wynik jako wartość w kontenerze IoC. Możliwe też, że chce odroczyć przetwarzanie wyrażenia, tak by było wykonywane w czasie pracy programu, a nie na etapie kompilacji. Jeszcze inna możliwość to chęć wykorzystania solidnego języka wyrażeń we własnej aplikacji.
Rozwiązanie Należy zastosować wprowadzony w Springu 3.0 język SpEL (ang. Spring Expression Language). Zapewnia on funkcje podobne do języka Unified EL z technologii JSF i JSP oraz języka OGNL (ang. Object Graph Navigation Language). Język SpEL udostępnia łatwą w użyciu infrastrukturę, którą można wykorzystać poza kontenerem Springa. W ramach kontenera język ten często pozwala znacznie uprościć konfigurację.
Jak to działa? Obecnie w środowisku aplikacji korporacyjnych używanych jest wiele różnych języków wyrażeń. Jeśli korzystasz z technologii WebWork, Struts 2 lub Tapestry 4, bez wątpienia stosowałeś język OGNL. Jeżeli w ostatnich latach posługiwałeś się technologiami JSP lub JSF, używałeś jednego lub obu dostępnych w nich języków wyrażeń. Jeśli korzystałeś z platformy JBoss Seam, stosowałeś dostępny w niej język wyrażeń, który jest nadzbiorem standardowego języka wyrażeń używanego w technologii JSF (języka Unified EL). Omawiany tu język wyrażeń jest inspirowany wieloma innymi językami. Ma możliwości bardziej rozbudowane od języka Unified EL. W platformie Spring.NET przez pewien czas dostępny był podobny język wyrażeń, a użytkownicy mieli na jego temat bardzo dobre zdanie. Niektóre cechy języka SpEL wynikają z potrzeby przetwarzania wyrażeń w dowolnym punkcie cyklu życia aplikacji, na przykład w trakcie inicjowania ziaren o określonym zasięgu.
77
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
Niektóre z dostępnych języków wyrażeń mają bardzo bogate możliwości, dorównujące niemal funkcjom języków skryptowych. Dotyczy to także języka SpEL. Można z niego korzystać w prawie każdym miejscu: od adnotacji po konfigurację w XML-u. Pakiet SpringSource Tool Suite zapewnia dobrą obsługę tego języka; oferuje między innymi automatyczne uzupełnianie kodu i wyszukiwanie nazw.
Składnia języka Język wyrażeń SpEL udostępnia liczne mechanizmy. W tabeli 2.1 pokrótce opisano różne konstrukty tego języka i ich zastosowania. Tabela 2.1. Mechanizmy języka wyrażeń SpEL Typ
Zastosowanie
Przykład
Literał
Jest to najprostszy konstrukt języka SpEL, działający tak samo jak w kodzie Javy. Język SpEL obsługuje literały typu String, a także różnego typu literały liczbowe.
2342
Operator logiczny i relacyjny
Język SpEL umożliwia sprawdzanie wyrażeń warunkowych za pomocą standardowych technik Javy.
T(java.lang.Math).random() > .5
Standardowe wyrażenie
Można poruszać się po ziarnach i zwracać ich właściwości w taki sam sposób jak w języku Unified EL — należy oddzielić każdą właściwość kropką i stosować konwencje nazewnicze typowe dla ziaren JavaBeans. Przykładowe wyrażenie przedstawione po prawej to odpowiednik wywołania getCat().getMate().getName().
cat.mate.name
Wyrażenia klasowe
Instrukcja T() oznacza, że język wyrażeń ma wykonywać operacje na klasie, a nie na jej egzemplarzu. Pierwszy z przykładów pokazanych po prawej stronie zwraca obiekt typu Class reprezentujący typ java.lang.Math; jest to odpowiednik wywołania java.lang.Math.class. Drugi wiersz wywołuje statyczną metodę danego typu i jest odpowiednikiem wywołania java.lang.Math.random().
T(java.lang.Math)
Dostęp do tablic, list i odwzorowań
Indeksy list, tablic i odwzorowań można podawać za pomocą nawiasów kwadratowych i kluczy (w tablicach i listach kluczem jest indeks, a w odwzorowaniach — obiekt). W pierwszym przykładzie pobierany jest element o indeksie 1 (litera 'b') z kolekcji java.util.List zawierającej cztery wartości typu char. Drugi przykład ilustruje dostęp do elementu odwzorowania przy użyciu indeksu 'OR'. W efekcie zwracana jest wartość powiązana z tym kluczem.
T(java.util.Arrays).asList(
Metody można wywoływać w taki sam sposób jak w Javie. Jest to znaczne usprawnienie w porównaniu z prostymi językami wyrażeń z technologii JSF i JSP.
'Witaj, świecie'.toLowerCase()
Wywoływanie metod
78
'Poznaj receptury Springa'
T(java.lang.Math).random()
'a','b','c','d')[1] T(SpelExamplesDemo) .MapOfStatesAndCapitals['OR']
2.5. UŻYWANIE JĘZYKA WYRAŻEŃ DOSTĘPNEGO W SPRINGU
Tabela 2.1. Mechanizmy języka wyrażeń SpEL — ciąg dalszy Typ
Zastosowanie
Przykład
Operatory relacyjne
Można porównywać wartości. Zwracana jest wtedy wartość logiczna.
23 == person.age
Wywoływanie konstruktora
Można tworzyć obiekty i wywoływać ich konstruktory. Tu tworzone są proste obiekty typu String i Cat.
new String('Ponownie poznaj
Operator trójargumentowy
Wyrażenia trójargumentowe działają w oczekiwany sposób i zwracają odpowiednią wartość w zależności od tego, czy warunek jest spełniony.
T(java.lang.Math).random() >
Zmienna
W języku SpEL można ustawiać i pobierać wartości zmiennych. Zmienne można tworzyć dla kontekstu parsera wyrażenia. Ponadto istnieją zmienne niejawne, na przykład #this, które zawsze dotyczą głównego obiektu kontekstu.
#this.firstName
Projekcje kolekcji
Bardzo cenną cechą języka SpEL jest możliwość wykonywania bardzo zaawansowanych manipulacji odwzorowaniami i kolekcjami. Wyrażenie pokazane po prawej tworzy projekcję listy cats. Tu zwracana wartość to kolekcja imion uzyskanych w wyniku sprawdzenia właściwości name wszystkich kotów z kolekcji. W tym przypadku cats to kolekcja obiektów typu Cat, a zwracana wartość to kolekcja obiektów typu String.
cats.![name]
Wybieranie obiektów z kolekcji
Wybieranie pozwala dynamicznie filtrować obiekty z kolekcji lub odwzorowania na podstawie predykatu sprawdzanego dla każdego elementu. Pozostawiane są tylko elementy, dla których predykat jest prawdziwy. Tu sprawdzana jest właściwość java.util.Map.Entry.value każdego obiektu Entry z kolekcji Map. Jeśli pierwszy znak tej właściwości (typu String) przekształcony na małą literę to 's', dany element zostaje zachowany. Pozostałe wartości są odrzucane.
mapOfStatesAndCapitals.?
Wyrażenia szablonowe
Za pomocą języka wyrażeń można przetwarzać wyrażenia podane w łańcuchach znaków. Zwracany jest ich wynik. Tu wynik ('dobra' lub 'zła') jest generowany dynamicznie na podstawie wyrażenia trójargumentowego.
Twoja przyszłość jest ${T
'fala' < 'fido' receptury Springa') new Cat('Felix') .5 ? 'Kocha' : 'Nie kocha'
#customer.email
[value.toLowerCase(). startsWith('s')]
(java.lang.Math).random()> .5 ? 'dobra' : 'zła'}
Stosowanie języka SpEL w konfiguracji Język wyrażeń można stosować za pomocą XML-a lub adnotacji. Wyrażenia są przetwarzane w momencie tworzenia ziarna, a nie w trakcie inicjowania kontekstu. Dlatego ziarna utworzone w niestandardowym zasięgu nie są konfigurowane do momentu umieszczenia ziarna w odpowiednim zasięgu. Korzystać z ziaren można także przy użyciu XML-a lub adnotacji. Pierwszy przedstawiany tu przykład dotyczy wstrzykiwania nazwanej zmiennej z języka wyrażeń, systemProperties. Jest to zmienna specjalna typu java.util.Properties dostępna za pomocą wywołania System.getProperties(). Drugi przykład ilustruje wstrzykiwanie właściwości systemowej bezpośrednio do zmiennej typu String:
79
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
@Value("#{ systemProperties }") private Properties systemProperties; @Value("#{ systemProperties['user.region'] }") private String userRegion;
Wstrzykiwać można też wyniki obliczeń i wywołania metod. Poniższy kod wstrzykuje wynik obliczeń bezpośrednio do zmiennej: @Value("#{ T(java.lang.Math).random() * 100.0 }") private double randomNumber;
Teraz przyjmij, że w kontekście skonfigurowane jest inne ziarno o nazwie emailUtilities. Ziarno to ma właściwości typowe dla ziaren JavaBeans. Właściwości te są wstrzykiwane do poniższych pól: @Value("#{ emailUtilities.email }") private String email; @Value("#{ emailUtilities.password }") private String password; @Value("#{ emailUtilities.host}") private String host;
Za pomocą języka SpEL można też wstrzykiwać referencje do innych ziaren nazwanych z tego samego kontekstu: @Value("#{ emailUtilities }") private EmailUtilities emailUtilities ;
Ponieważ w kontekście znajduje się tylko jedno ziarno z implementacją interfejsu EmailUtilities, można też zastosować następujący zapis: @Autowired private EmailUtilities emailUtilities ;
Choć istnieją też inne mechanizmy rozróżniania ziaren implementujących ten sam interfejs, język wyrażeń jest w tym zakresie bardzo przydatny, ponieważ pozwala wykrywać ziarna na podstawie ich identyfikatorów. Język wyrażeń można stosować w konfiguracji w XML-u w dokładnie taki sam sposób jak w adnotacjach. Nawet przedrostek (#{) i przyrostek (}) są tu identyczne.
Używanie parsera języka SpEL w Springu Język SpEL jest używany najczęściej w konfiguracji w XML-u i w adnotacjach w sposób obsługiwany przez platformę Spring, jednak można też korzystać z niego jak ze zwykłego języka wyrażeń. Główne jego funkcje zapewnia parser wyrażeń, org.springframework.expression.spel.antlr. SpelAntlrExpressionParser, którego egzemplarz można utworzyć bezpośrednio: ExpressionParser parser = new SpelAntlrExpressionParser();
Można utworzyć implementację zgodną z interfejsem ExpressionParser i za pomocą tego interfejsu wykonywać potrzebne operacje. Wspomniany interfejs jest najważniejszym mechanizmem przetwarzania wyrażeń napisanych w języku SpEL. Oto najprostszy sposób przetwarzania wyrażeń: Expression exp = parser.parseExpression("'ceci n''est pas une String'" ); String val = exp.getValue(String.class);
80
2.5. UŻYWANIE JĘZYKA WYRAŻEŃ DOSTĘPNEGO W SPRINGU
Tu kod przetwarza literał typu String (zauważ, że dla apostrofu stosowany jest znak ucieczki w postaci drugiego apostrofu, a nie lewego ukośnika) i zwraca wynik. Wywołanie getValue() jest generyczne (oparte na typie parametru), dlatego nie trzeba rzutować typu wyniku. Często w wyrażeniach przetwarzane są obiekty. Właściwościami i metodami obiektu można później manipulować niezależnie od egzemplarza lub klasy. W parserze języka SpEL egzemplarz lub klasa to obiekt główny. Poniżej używany jest przykładowy obiekt SocialNetworkingSiteContext. Ma on atrybuty sprawdzane w celu przyjrzenia się osobom zarejestrowanym w witrynie. SocialNetworkingSiteContext socialNetworkingSiteContext = new SocialNetworkingSiteContext(); // Tu należy się upewnić, czy obiekt jest poprawnie zainicjowany Expression firstNameExpression = parser.parseExpression("loggedInUser.firstName"); StandardEvaluationContext ctx = new StandardEvaluationContext(); ctx.setRootObject(socialNetworkingSiteContext); String valueOfLoggedInUserFirstName = firstNameExpression.getValue(ctx, String.class );
Ponieważ obiekt socialNetworkingSiteContext jest ustawiony jako obiekt główny, można zastosować dowolną jego właściwość bez podawania nazwy obiektu. Załóżmy, że zamiast określać obiekt główny, chcesz podać nazwaną zmienną, aby móc uzyskać do niej dostęp w wyrażeniu. Parser języka SpEL pozwala określać zmienne uwzględniane przy przetwarzaniu wyrażeń. W poniższym przykładowym kodzie jest to zmienna socialNetworkingSiteContext. W wyrażeniu dla tej zmiennej używany jest przedrostek #: StandardEvaluationContext ctx1 = new StandardEvaluationContext (); SocialNetworkingSiteContext socialNetworkingSiteContext = new SocialNetworkingSiteContext(); Friend myFriend = new Friend() ; myFriend.setFirstName("Manuel"); socialNetworkingSiteContext.setLoggedInUser(myFriend); ctx1.setVariable("socialNetworkingSiteContext",socialNetworkingSiteContext ); Expression loggedInUserFirstNameExpression = parser.parseExpression("#socialNetworkingSiteContext.loggedInUser.firstName"); String loggedInUserFirstName = loggedInUserFirstNameExpression.getValue (ctx1, String.class);
W podobny sposób można stosować w wyrażeniach funkcje bez podawania ich pełnych nazw: StandardEvaluationContext ctx1 = new StandardEvaluationContext(); ctx1.registerFunction("empty", StringUtils.class.getDeclaredMethod( "isEmpty", new Class[] { String.class })); Expression functionEval = parser.parseExpression( " #empty(null) ? 'empty' : 'not empty' "); String result = functionEval.getValue(ctx1, String.class );
Za pomocą infrastruktury parsera języka wyrażeń można utworzyć szablon dla typu String. Zwracana wartość to obiekt tego typu, natomiast w samej wartości parser może wstawiać wynik przetworzonego wyrażenia. Może to okazać się przydatne w wielu sytuacjach, na przykład przy generowaniu prostych komunikatów. Najpierw należy utworzyć obiekt typu org.springframework.expression.ParserContext. Ta klasa informuje parser o tym, który token jest przedrostkiem ("${") i jak wygląda przyrostek ("}"). Poniższy przykładowy kod zwraca tekst w postaci "Liczba milisekund: 1246953975093 ". ParserContext pc = new ParserContext() { public String getExpressionPrefix() { return "${"; } public String getExpressionSuffix() { return "}"; } public boolean isTemplate() { return true;
81
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
} }; String templatedExample = parser.parseExpression( "Liczba milisekund: ${ T(System).currentTimeMillis() }.", pc).getValue(String.class);
2.6. Ustawianie zasięgu ziarna Problem Zadeklarowanie ziarna w pliku konfiguracyjnym oznacza zdefiniowanie szablonu potrzebnego do utworzenia ziarna (nie powoduje natomiast wygenerowania egzemplarza tego ziarna). Dopiero gdy aplikacja zażąda ziarna za pomocą metody getBean() lub referencji w innych ziarnach, Spring na podstawie zasięgu ziarna decyduje, jaki egzemplarz ziarna należy zwrócić. Czasem trzeba ustawić odpowiedni zasięg (różny od domyślnego).
Rozwiązanie W Springu 2.x i w nowszych wersjach tej platformy zasięg ziarna jest ustawiany za pomocą atrybutu scope elementu . Domyślnie Spring tworzy dokładnie jeden egzemplarz każdego ziarna zadeklarowanego w kontenerze IoC. Taki egzemplarz jest współużytkowany w zasięgu całego kontenera IoC. Jest zwracany przy wszystkich późniejszych wywołaniach getBean() i dla każdej referencji do danego ziarna. Tak działa zasięg singleton. Jest to zasięg domyślny dla wszystkich ziaren. W tabeli 2.2 znajduje się lista wszystkich zasięgów ziaren Springa. Tabela 2.2. Zasięgi ziaren w Springu Zasięg
Opis
singleton
Powoduje utworzenie jednego egzemplarza ziarna w kontenerze IoC.
prototype
Powoduje utworzenie nowego egzemplarza ziarna dla każdego żądania.
request
Powoduje utworzenie jednego egzemplarza ziarna dla każdego żądania HTTP (ten zasięg jest dozwolony tylko w kontekście aplikacji sieciowych).
session
Powoduje utworzenie jednego egzemplarza ziarna dla każdej sesji HTTP (ten zasięg jest dozwolony tylko w kontekście aplikacji sieciowych).
globalSession
Powoduje utworzenie jednego egzemplarza ziarna dla globalnej sesji HTTP (ten zasięg jest dozwolony tylko w kontekście aplikacji portalowych).
W Springu 1.x dostępne są tylko zasięgi singleton i prototype, a ustawia się je w atrybucie singleton (singleton="true" i singleton="false"), a nie w atrybucie scope.
Jak to działa? Aby lepiej zrozumieć zasięg ziaren, wyobraź sobie, że potrzebny jest koszyk zakupów w aplikacji do obsługi sklepu internetowego. Najpierw w następujący sposób należy utworzyć klasę ShoppingCart: package com.apress.springrecipes.shop; ... public class ShoppingCart { private List items = new ArrayList();
82
2.6. USTAWIANIE ZASIĘGU ZIARNA
public void addItem(Product item) { items.add(item); } public List getItems() { return items; } }
Następnie trzeba w standardowy sposób zadeklarować w kontenerze IoC ziarna produktów i ziarno koszyka zakupów:
W poniższej klasie Main można dodać do koszyka zakupów kilka produktów, aby go przetestować. Załóżmy, że w danym momencie ze sklepu korzysta dwóch klientów. Pierwszy z nich pobiera za pomocą metody getBean() koszyk zakupów i dodaje do niego dwa produkty. Drugi klient także pobiera koszyk przy użyciu metody getBean() i umieszcza w nim jeden produkt. package com.apress.springrecipes.shop; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); Product aaa = (Product) context.getBean("aaa"); Product cdrw = (Product) context.getBean("cdrw"); Product dvdrw = (Product) context.getBean("dvdrw"); ShoppingCart cart1 = (ShoppingCart) context.getBean("shoppingCart"); cart1.addItem(aaa); cart1.addItem(cdrw); System.out.println("Koszyk nr 1 zawiera: " + cart1.getItems()); ShoppingCart cart2 = (ShoppingCart) context.getBean("shoppingCart"); cart2.addItem(dvdrw); System.out.println("Koszyk nr 2 zawiera: " + cart2.getItems()); } }
83
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
Przy pokazanej wcześniej deklaracji obaj klienci korzystają z tego samego egzemplarza koszyka zakupów. Koszyk nr 1 zawiera: [AAA 2.5, CD-RW 1.5] Koszyk nr 2 zawiera: [AAA 2.5, CD-RW 1.5, DVD-RW 3.0]
Wynika to z tego, że domyślny zasięg ziaren w Springu to singleton, dlatego Spring tworzy dokładnie jeden egzemplarz koszyka zakupów dla kontenera IoC.
W aplikacji do obsługi sklepu internetowego każdy klient po wywołaniu metody getBean() powinien otrzymać inny egzemplarz koszyka zakupów. Aby uzyskać ten efekt, należy zmienić zasięg ziarna shoppingCart na prototype. Wtedy Spring utworzy nowy egzemplarz ziarna dla każdego wywołania metody getBean() i dla każdej referencji z innych ziaren.
Jeśli teraz ponownie uruchomisz klasę Main, zobaczysz, że każdy z klientów otrzymał inny egzemplarz koszyka zakupów. Koszyk nr 1 zawiera: [AAA 2.5, CD-RW 1.5] Koszyk nr 2 zawiera: [DVD-RW 3.0]
2.7. Modyfikowanie procesu inicjowania i usuwania ziaren Problem Wiele stosowanych w praktyce komponentów przed rozpoczęciem pracy musi wykonywać określone operacje inicjujące. Do tych zadań należą: otwarcie pliku, nawiązanie połączenia z siecią lub bazą danych, przydzielenie pamięci itd. Ponadto w końcowej fazie cyklu życia komponenty muszą wykonywać analogiczne operacje związane z usuwaniem ziarna. Trzeba więc dostosować proces inicjowania i usuwania ziarna w kontenerze IoC.
Rozwiązanie Kontener IoC odpowiada nie tylko za rejestrowanie ziaren, ale też za zarządzanie ich cyklem życia. Pozwala to wykonywać niestandardowe zadania w określonych momentach tego cyklu. Zadania te należy umieszczać w wywoływanych zwrotnie metodach, aby kontener IoC mógł je w odpowiednim momencie uruchomić. Na poniższej liście opisane są kroki wykonywane w ramach zarządzania cyklem życia ziaren przez kontener IoC. Po omówieniu dalszych funkcji kontenera IoC lista ta będzie wydłużana. 1. Tworzenie egzemplarza ziarna (za pomocą konstruktora lub metody fabrycznej). 2. Ustawianie wartości i referencji do ziaren we właściwościach ziarna. 3. Uruchamianie wywoływanych zwrotnie metod inicjujących. 4. Ziarno jest gotowe do użytku. 5. Przy zamykaniu kontenera uruchamiane są wywoływane zwrotnie metody związane z usuwaniem ziarna. Spring potrafi wykrywać wywoływane zwrotnie metody związane z inicjowaniem i usuwaniem ziaren na trzy sposoby. Po pierwsze, w ziarnie można zaimplementować dotyczące cyklu życia interfejsy InitializingBean i DisposableBean oraz metody afterPropertiesSet() i destroy() (są one wywoływane
84
2.7. MODYFIKOWANIE PROCESU INICJOWANIA I USUWANIA ZIAREN
przy inicjowaniu oraz usuwaniu ziarna). Po drugie, można ustawić atrybuty init-method i destroy-method w deklaracji ziarna oraz podać w nich nazwy wywoływanych zwrotnie metod. Po trzecie, w Springu 2.5 oraz w nowszych wersjach tej platformy można ponadto dodać do wywoływanych zwrotnie metod związane z cyklem życia adnotacje @PostConstruct i @PreDestroy (adnotacje te są zdefiniowane w dokumencie JSR-250: Common Annotations for the Java Platform). Następnie można zarejestrować w kontenerze IoC egzemplarz postprocesora CommonAnnotationBeanPostProcessor na potrzeby uruchamiania wywoływanych zwrotnie metod.
Jak to działa? Aby zrozumieć zarządzanie cyklem życia ziaren w kontenerze IoC, zastanów się nad funkcją obsługującą proces płacenia za zakupy. Poniższą klasę Cashier można wykorzystać do ustalenia ceny produktów z koszyka zakupów. Klasa ta rejestruje w pliku tekstowym czas i wartość każdej transakcji. package com.apress.springrecipes.shop; ... public class Cashier { private String name; private String path; private BufferedWriter writer; public void setName(String name) { this.name = name; } public void setPath(String path) { this.path = path; } public void openFile() throws IOException { File logFile = new File(path, name + ".txt"); writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(logFile, true))); } public void checkout(ShoppingCart cart) throws IOException { double total = 0; for (Product product : cart.getItems()) { total += product.getPrice(); } writer.write(new Date() + "\t" + total + "\r\n"); writer.flush(); } public void closeFile() throws IOException { writer.close(); } }
W klasie Cashier metoda openFile() otwiera plik tekstowy, do którego prowadzi określona ścieżka. Nazwą pliku jest nazwa ziarna reprezentującego kasjera. Przy każdym wywołaniu metody checkout() do pliku dodawane są informacje o transakcji. Na końcu metoda closeFile() zamyka plik, aby zwolnić zajmowane zasoby systemowe. Następnie należy zadeklarować w kontenerze IoC ziarno cashier1 reprezentujące kasjera. Dane o transakcjach tego kasjera są zapisywane w pliku c:/cashier/cashier1.txt. Należy wcześniej utworzyć taki katalog lub podać w ścieżce inny, istniejący katalog.
85
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
...
Jeśli jednak w klasie Main spróbujesz za pomocą tego ziarna obsłużyć koszyk zakupów, wystąpi wyjątek NullPointerException. Wynika to z tego, że nie wywołano wcześniej metody openFile() potrzebnej w ramach inicjowania. package com.apress.springrecipes.shop; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class Main { public static void main(String[] args) throws Exception { ApplicationContext context = new FileSystemXmlApplicationContext("beans.xml"); Cashier cashier1 = (Cashier) context.getBean("cashier1"); cashier1.checkout(cart1); } }
Gdzie należy wywołać metodę openFile() w ramach inicjowania ziarna? W Javie zadania związane z inicjowaniem należy wykonywać w konstruktorze. Jednak czy wystarczy wywołać metodę openFile() w konstruktorze domyślnym klasy Cashier? Nie, ponieważ metoda ta wymaga ustawienia właściwości name i path, aby można było określić otwierany plik. package com.apress.springrecipes.shop; ... public class Cashier { ... public void openFile() throws IOException { File logFile = new File(path, name + ".txt"); writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(logFile, true))); } }
Gdy wywoływany jest konstruktor domyślny, potrzebne właściwości nie są jeszcze ustawione. Dlatego można dodać konstruktor przyjmujący te właściwości jako argumenty i wywoływać metodę openFile() w końcowej części tego konstruktora. Jednak czasem jest to niemożliwe lub programista chce wstrzykiwać właściwości za pomocą settera. Najlepiej więc wywoływać metodę openFile() po ustawieniu wszystkich potrzebnych właściwości przez kontener IoC.
Implementowanie interfejsów InitializingBean i DisposableBean Spring umożliwia wykonywanie zadań związanych z inicjowaniem i usuwaniem ziarna w wywoływanych zwrotnie metodach afterPropertiesSet() i destroy(). Należą one do interfejsów InitializingBean i DisposableBean. W trakcie tworzenia ziarna Spring wykrywa, że dane ziarno zawiera implementację wspomnianych interfejsów, i w odpowiednim momencie uruchamia wywoływane zwrotnie metody. package com.apress.springrecipes.shop; ... import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean;
86
2.7. MODYFIKOWANIE PROCESU INICJOWANIA I USUWANIA ZIAREN
public class Cashier implements InitializingBean, DisposableBean { ... public void afterPropertiesSet() throws Exception { openFile(); } public void destroy() throws Exception { closeFile(); } }
Jeśli teraz ponownie uruchomisz klasę Main, zobaczysz, że do pliku tekstowego c:/cashier/cashier1.txt zostaną dodane informacje na temat transakcji. Jednak dodawanie tego rodzaju niestandardowych interfejsów sprawia, że ziarno będzie działać tylko w Springu, dlatego nie można go ponownie wykorzystać poza kontenerem IoC tej platformy.
Stosowanie atrybutów init-method i destroy-method Lepszym sposobem wskazywania wywoływanych zwrotnie metod związanych z inicjowaniem i usuwaniem ziarna jest ustawianie w deklaracji ziarna atrybutów init-method i destroy-method.
Dzięki tym dwóm atrybutom ustawionym w deklaracji ziarna nie trzeba w klasie Cashier implementować interfejsów InitializingBean i DisposableBean. Można też usunąć z tej klasy metody afterPropertiesSet() i destroy().
Adnotacje @PostConstruct i @PreDestroy W Springu 2.5 i w nowszych wersjach tej platformy można opatrzyć wywoływane zwrotnie metody związane z inicjowaniem i usuwaniem ziarna adnotacjami @PostConstruct i @PreDestroy. Są to adnotacje dotyczące cyklu życia, zdefiniowane w dokumencie JSR-250. Uwaga Aby móc stosować adnotacje z dokumentu JSR-250, należy dodać element dependency o identyfikatorze jsr250-api. Jeśli używasz Mavena, dodaj następujący kod: javax.annotation jsr250-api 1.0
package com.apress.springrecipes.shop; ... import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; public class Cashier { ... @PostConstruct public void openFile() throws IOException { File logFile = new File(path, name + ".txt");
87
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(logFile, true))); } @PreDestroy public void closeFile() throws IOException { writer.close(); } }
Następnie należy zarejestrować w kontenerze IoC egzemplarz postprocesora CommonAnnotationBeanPost Processor, aby uruchamiał wywoływane zwrotnie metody opatrzone adnotacjami dotyczącymi cyklu życia. Dzięki temu nie trzeba ustawiać dla ziarna atrybutów init-method i destroy-method. ...
Można też dodać do pliku z konfiguracją ziarna element , a egzemplarz postprocesora CommonAnnotationBeanPostProcessor zostanie automatycznie zarejestrowany. Najpierw jednak trzeba dodać do elementu głównego definicję schematu context. ...
2.8. Skracanie konfiguracji w XML-u za pomocą projektu Java Config Problem Programista chętnie korzysta z oferowanych przez kontener możliwości wstrzykiwania zależności, jednak chce zmienić niektóre aspekty konfiguracji. Możliwe też, że chce przenieść część ustawień konfiguracyjnych z XML-a do Javy, co daje większe możliwości w zakresie refaktoryzacji i zapewnia większe bezpieczeństwo ze względu na typ.
88
2.8. SKRACANIE KONFIGURACJI W XML-U ZA POMOCĄ PROJEKTU JAVA CONFIG
Rozwiązanie Można wykorzystać projekt Java Config. Prace nad nim trwają już od 2005 roku, rozpoczęto je więc na długo przed uruchomieniem projektu Google Guice. Od niedawna projekt Java Config stał się częścią platformy Spring.
Jak to działa? Projekt Java Config zapewnia duże możliwości i pozwala wykonywać zadania w sposób zupełnie inny niż przy stosowaniu odmiennych metod konfigurowania rozwiązań (formatu XML i adnotacji). Należy pamiętać, że projekt Java Config można wykorzystywać razem z innymi technikami. Najprostszy sposób wczytywania konfiguracji do Javy polega na zastosowaniu zwykłego pliku z konfiguracją w XML-u. Na podstawie tego pliku Spring wykona wszystkie potrzebne operacje. ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("myApplicationContext.xml");
W tym pliku należy umieścić standardową konfigurację: ... ...
Takie rozwiązanie pozwala Springowi znaleźć wszystkie klasy opatrzone adnotacją @Configuration. Z kolei ta adnotacja sama jest oznaczona adnotacją @Component, dzięki czemu jest obsługiwana w specjalny sposób. To sprawia, że możliwe jest między innymi wstrzykiwanie danych po dodaniu adnotacji @Autowired. Gdy klasa jest opatrzona adnotacją @Configuration, Spring szuka w niej definicji ziaren (definicje ziaren to metody Javy mające adnotację @Bean). Każda taka definicja jest uwzględniana w kontekście aplikacji (ApplicationContext), a nazwa danego ziarna (beanName) jest tworzona na podstawie nazwy metody. Inna możliwość to bezpośrednie podanie nazwy ziarna w adnotacji @Bean. Oto przykładowa konfiguracja z jedną definicją ziarna: import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class PersonConfiguration { @Bean public Person josh() { Person josh = new Person(); josh.setName("Jarek"); return josh ; } }
Jest to odpowiednik XML-owego kontekstu aplikacji z poniższą definicją:
Dostęp do ziarna z kontekstu aplikacji w Springu możesz uzyskać w standardowy sposób: ApplicationContext context = … ; Person person = context.getBean("jarek", Person.class);
Jeśli chcesz ustawić identyfikator ziarna, możesz to zrobić za pomocą atrybutu id w definicji z adnotacją @Bean: @Bean(id="theArtistFormerlyKnownAsJosh") public Person josh() { // … }
89
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
Dostęp do ziarna można następnie uzyskać w następujący sposób: ApplicationContext context = … ; Person person = context.getBean("theArtistFormerlyKnownAsJosh", Person.class);
Wyobrażamy sobie, co teraz myślisz: „I to ma być usprawnienie? Przecież trzeba wpisać pięć razy więcej kodu!”. Nie lekceważ jednak czytelności rozwiązania opartego na Javie. Ponadto jeśli ten kod się skompiluje, możesz z dużą dozą pewności założyć, że konfiguracja jest poprawna. Wersja XML-owa nie ma tych zalet. Jeśli chcesz wskazać metody do zarządzania cyklem życia, możesz to zrobić na kilka sposobów. Wcześniej metody te były zaimplementowane w Springu jako wywołania zwrotne metod ze znanych interfejsów, na przykład InitializingBean i DisposableBean. Metody te były wywoływane zwrotnie po wstrzyknięciu zależności (metoda public void afterPropertiesSet() throws Exception) oraz przed usunięciem ziarna z kontekstu (metoda public void destroy() throws Exception). Metody związane z inicjowaniem i usuwaniem ziarna możesz też ustawić ręcznie, w konfiguracji w XML-u. Służą do tego atrybuty init-method i destroy-method elementu bean. W Springu 2.5 i w nowszych wersjach tej platformy można też stosować opisane w dokumencie JSR-250 adnotacje służące do wskazywania metod inicjujących (adnotacja @PostConstruct) i usuwających ziarno (adnotacja @PreDestroy). Java Config zapewnia więc dużo możliwości. Możesz ustawić metody cyklu życia za pomocą adnotacji @Bean, ale możesz też samodzielnie wywoływać potrzebne metody. Pierwsze z tych rozwiązań, oparte na atrybutach initMethod i destroyMethod, jest proste: @Bean( initMethod = "startLife", destroyMethod = "die") public Person companyLawyer() { Person companyLawyer = new Person(); companyLawyer.setName("Alan Nowak"); return companyLawyer; }
Można też łatwo samodzielnie zainicjować ziarno: @Bean public Person companyLawyer() { Person companyLawyer = new Person(); companyLawyer.startLife() ; companyLawyer.setName("Alan Nowak"); return companyLawyer; }
Używanie referencji do innych ziaren jest proste i wygląda bardzo podobnie jak w standardowym kodzie: @Configuration public class PetConfiguration { @Bean public Cat cat(){ return new Cat(); } @Bean public Person master(){ Person person = new Person() ; person.setPet( cat() ); return person; } // … }
Trudno wymyślić prostsze rozwiązanie. Gdy potrzebujesz referencji do innego ziarna, wystarczy pobrać ją w taki sam sposób jak w innych aplikacjach Javy. Spring gwarantuje utworzenie tylko jednego egzemplarza ziarna i ustawienie odpowiedniego zasięgu. Do ziaren zdefiniowanych za pomocą projektu Java Config można stosować wszystkie opcje konfiguracyjne dostępne dla ziaren zdefiniowanych w XML-u.
90
2.8. SKRACANIE KONFIGURACJI W XML-U ZA POMOCĄ PROJEKTU JAVA CONFIG
Adnotacje @Lazy, @Primary i @DependsOn działają dokładnie tak samo jak ich XML-owe odpowiedniki. Adnotacja @Lazy powoduje odroczenie tworzenia ziarna do momentu, w którym jest ono potrzebne jako zależność lub jest bezpośrednio używane w kontekście aplikacji. Adnotacja @DependsOn informuje, że ziarno można utworzyć dopiero po wygenerowaniu innego ziarna, którego dostępność może być niezbędna do poprawnego powstania pierwszego ziarna. Adnotacja @Primary oznacza, że powiązane z nią ziarno należy zwrócić, jeśli istnieje kilka ziaren implementujących ten sam interfejs. Jeśli w kontenerze do wskazywania ziaren służą nazwy, stosowanie tej adnotacji oczywiście nie jest przydatne. Wymienione adnotacje należy umieszczać (podobnie jak inne adnotacje) nad związaną z konfiguracją ziarna metodą, której dotyczą. Oto przykład: @Bean @Lazy public NetworkFileProcessor fileProcessor(){ … }
Często programista chce podzielić konfigurację ziarna na kilka klas konfiguracyjnych. Dzięki temu rozwiązanie jest łatwiejsze w konserwacji i bardziej modułowe. Dlatego Spring umożliwia importowanie innych ziaren. W XML-u służy do tego element import ( ). W projekcie Java Config podobny efekt można uzyskać za pomocą adnotacji @Import. Stosuje się ją na poziomie klasy. @Configuration @Import(BusinessConfiguration.class) public class FamilyConfiguration { // ... }
Tu adnotacja ta powoduje dodanie do zasięgu ziaren zdefiniowanych w klasie BusinessConfiguration. Następnie można uzyskać dostęp do tych ziaren za pomocą adnotacji @Autowired lub @Value. Jeśli wstrzykujesz kontekst aplikacji przy użyciu adnotacji @Autowired, możesz uzyskać dostęp do ziarna z poziomu tego kontekstu. W pokazanym poniżej kodzie kontener importuje ziarna zdefiniowane w klasie konfiguracyjnej AttorneyConfiguration, po czym można je wstrzyknąć na podstawie nazwy, korzystając z adnotacji @Value. Gdyby istniał tylko jeden egzemplarz ziarna danego typu, możliwe byłoby zastosowanie adnotacji @Autowired. package com.apress.springrecipes.spring3.javaconfig; import static java.lang.System.*; import java.util.Arrays; import import import import import
org.springframework.beans.factory.annotation.Value; org.springframework.context.annotation.Bean; org.springframework.context.annotation.Configuration; org.springframework.context.annotation.Import; org.springframework.context.support.ClassPathXmlApplicationContext;
@Configuration @Import(AttorneyConfiguration.class) public class LawFirmConfiguration { @Value("#{denny}") private Attorney denny; @Value("#{alan}") private Attorney alan; @Value("#{shirley}") private Attorney shirley; @Bean public LawFirm bostonLegal() { LawFirm lawFirm = new LawFirm();
91
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
lawFirm.setLawyers(Arrays.asList(denny, alan, shirley)); lawFirm.setLocation("Opole"); return lawFirm; } }
Stosowanie tego podejścia przy definiowaniu prostych ziaren jest często przesadą. Jeśli chcesz pozwolić Springowi na tworzenie egzemplarza ziarna i nie chcesz nic zmieniać w tym procesie, możesz albo napisać metodę z adnotacją @Bean, albo skonfigurować ziarno w XML-u. To, które rozwiązanie wybierzesz, zależy od Ciebie. To kwestia gustu. My przy tworzeniu obiektu używanego tylko w danej aplikacji stosujemy konfigurację w Javie, natomiast wielu dostępnych w Springu implementacji interfejsu FactoryBean używamy w XML-u, gdzie możemy je szybko wykorzystać i posłużyć się przydatnymi schematami.
2.9. Ziarna znające zasoby kontenera Problem Dobrze zaprojektowany komponent nie powinien bezpośrednio zależeć od kontenera. Czasem jednak ziarno musi znać zasoby kontenera.
Rozwiązanie Aby ziarna znały zasoby kontenera IoC, należy zaimplementować określone interfejsy opisane w tabeli 2.3. Spring wstrzykuje odpowiednie zasoby do ziaren za pomocą setterów zdefiniowanych w tych interfejsach. Tabela 2.3. Dostępne w Springu standardowe interfejsy do poznawania zasobów Interfejs do poznawania zasobów
Docelowy zasób
BeanNameAware
Nazwy egzemplarzy ziarna skonfigurowanych w kontenerze IoC.
BeanFactoryAware
Bieżąca fabryka ziaren, za pomocą której można wywoływać usługi kontenera.
ApplicationContextAware*
Bieżący kontekst aplikacji, za pomocą którego można wywoływać usługi kontenera.
MessageSourceAware
Źródło komunikatów, które służy do określania komunikatów tekstowych.
ApplicationEventPublisherAware
Mechanizm publikowania zdarzeń aplikacji, który pozwala publikować takie zdarzenia.
ResourceLoaderAware
Mechanizm wczytywania zasobów, który umożliwia wczytywanie zewnętrznych zasobów.
*Interfejs ApplicationContext dziedziczy po interfejsach MessageSource, ApplicationEventPublisher i ResourceLoader, dlatego aby uzyskać dostęp do wszystkich usług oferowanych przez te interfejsy, wystarczy poznać kontekst aplikacji. Jednak do dobrych praktyk należy wybieranie najbardziej szczegółowego interfejsu, który spełnia wymagania programisty Settery z wymienionych interfejsów są uruchamiane w Springu po ustawieniu właściwości ziarna, ale przed wywoływanymi zwrotnie metodami związanymi z inicjowaniem ziarna. Opisuje to poniższa lista: 1. Tworzenie egzemplarza ziarna (za pomocą konstruktora lub metody fabrycznej). 2. Ustawianie wartości i referencji do ziaren we właściwościach. 3. Wywoływanie setterów zdefiniowanych w interfejsach do poznawania zasobów.
92
2.10. WCZYTYWANIE ZASOBÓW ZEWNĘTRZNYCH
4. Uruchamianie wywoływanych zwrotnie metod inicjujących. 5. Ziarno jest gotowe do użytku. 6. Po zamknięciu kontenera należy uruchomić wywoływane zwrotnie metody związane z usuwaniem ziarna. Pamiętaj, że gdy ziarno zawiera implementację interfejsów do poznawania zasobów, jest powiązane ze Springiem i może działać nieprawidłowo poza kontenerem IoC. Musisz więc dobrze przemyśleć, czy konieczne jest implementowanie takich niestandardowych interfejsów.
Jak to działa? Możesz sprawić, aby ziarno reprezentujące kasjera znało swoją nazwę używaną w kontenerze IoC. Wymaga to zaimplementowania interfejsu BeanNameAware. Po wstrzyknięciu nazwy ziarna można ją zapisać jako nazwę kasjera. Pozwala to uniknąć ustawiania dodatkowej właściwości name dotyczącej kasjera. package com.apress.springrecipes.shop; ... import org.springframework.beans.factory.BeanNameAware; public class Cashier implements BeanNameAware { ... public void setBeanName(String beanName) { this.name = beanName; } }
Aby uprościć deklarację ziarna reprezentującego kasjera, zastosuj nazwę ziarna jako nazwę kasjera. Dzięki temu możesz zrezygnować w konfiguracji z właściwości name, a także z metody setName().
Uwaga Czy pamiętasz, że możesz bezpośrednio podać nazwę pola i ścieżkę do właściwości jako nazwy ziaren w ziarnach fabrycznych typu FieldRetrievingFactoryBean i PropertyPathFactoryBean? Oba te ziarna fabryczne zawierają implementację interfejsu BeanNameAware.
2.10. Wczytywanie zasobów zewnętrznych Problem Czasem aplikacja musi wczytać zasoby zewnętrzne (na przykład pliki tekstowe, pliki XML, pliki właściwości lub pliki graficzne) z innych lokalizacji (takich jak system plików, lokalizacja z parametru classpath lub adres URL). Zwykle trzeba używać różnych interfejsów API do wczytywania zasobów z poszczególnych miejsc.
Rozwiązanie W Springu mechanizm wczytywania zasobów udostępnia uniwersalną metodę getResource() pozwalającą pobierać zewnętrzne zasoby na podstawie prowadzącej do nich ścieżki. Możesz stosować różne przedrostki ścieżki, aby wczytywać zasoby z określonych lokalizacji. Jeśli chcesz wczytać zasób z systemu plików, podaj przedrostek file. Przy wczytywaniu zasobu z parametru classpath podaj przedrostek classpath. Możesz też podać adres URL. 93
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
Resource to ogólny interfejs używany w Springu do reprezentowania zewnętrznych zasobów. Spring udostępnia kilka implementacji tego interfejsu. Metoda getResource() mechanizmu wczytywania zasobów określa na podstawie ścieżki, którą implementację interfejsu Resource należy wybrać.
Jak to działa? Załóżmy, że programista chce, aby przy uruchamianiu sklepu internetowego aplikacja wyświetlała banner. Składa się on z poniższych znaków i jest zapisany w pliku tekstowym banner.txt. Plik ten można umieścić w ścieżce aplikacji. **************************** * Witamy w naszym sklepie! * ****************************
Następnie należy napisać klasę BannerLoader, aby wczytywała banner i wyświetlała go w konsoli. Ponieważ klasa ta musi mieć dostęp do mechanizmu wczytywania zasobów, trzeba w niej zaimplementować interfejs ApplicationContextAware lub ResourceLoaderAware. package com.apress.springrecipes.shop; ... import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; public class BannerLoader implements ResourceLoaderAware { private ResourceLoader resourceLoader; public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } public void showBanner() throws IOException { Resource banner = resourceLoader.getResource("file:banner.txt"); InputStream in = banner.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); while (true) { String line = reader.readLine(); if (line == null) break; System.out.println(line); } reader.close(); } }
Dzięki wywołaniu metody getResource() kontekstu aplikacji można pobrać zewnętrzny zasób określony za pomocą ścieżki. Ponieważ plik z bannerem znajduje się w systemie plików, ścieżka do zasobu rozpoczyna się od przedrostka file. Możesz wywołać metodę getInputStream(), aby pobrać zasób jako strumień wejścia. Następnie można wczytać zawartość pliku wiersz po wierszu (za pomocą obiektu klasy BufferedReader) i wyświetlić ją w konsoli. Należy też zadeklarować egzemplarz klasy BannerLoader w pliku z konfiguracją ziarna. Ponieważ banner ma pojawiać się przy uruchamianiu aplikacji, trzeba ustawić metodę showBanner() jako metodę inicjującą.
94
2.10. WCZYTYWANIE ZASOBÓW ZEWNĘTRZNYCH
Przedrostki ścieżek do zasobów Pokazana wcześniej ścieżka jest ścieżką względną i prowadzi do zasobu zapisanego w systemie plików. Możesz też podać pełną ścieżkę: file:c:/shop/banner.txt
Jeśli zasób znajduje się w lokalizacji określonej w parametrze classpath, należy zastosować przedrostek classpath. Jeżeli ścieżka nie jest podana, wczytywany jest plik z katalogu głównego aplikacji. classpath:banner.txt
Gdy zasób znajduje się w określonym pakiecie, można podać pełną ścieżkę z katalogu głównego z parametru classpath: classpath:com/apress/springrecipes/shop/banner.txt
Zasoby można wczytywać nie tylko za pomocą ścieżki do systemu plików lub z parametru classpath, ale też na podstawie adresu URL: http://springrecipes.apress.com/shop/banner.txt
Jeśli w ścieżce do zasobu nie podano przedrostka, zasób jest wczytywany z lokalizacji zgodnej z używanym kontekstem aplikacji. W przypadku kontekstu FileSystemXmlApplicationContext tą lokalizacją jest system plików, a dla kontekstu ClassPathXmlApplicationContext jest to parametr classpath.
Wstrzykiwanie zasobów Zasób można wczytać bezpośrednio przy użyciu metody getResource(), ale można też wstrzyknąć go za pomocą settera: package com.apress.springrecipes.shop; ... import org.springframework.core.io.Resource; public class BannerLoader { private Resource banner; public void setBanner(Resource banner) { this.banner = banner; } public void showBanner() throws IOException { InputStream in = banner.getInputStream(); ... } }
W konfiguracji ziarna wystarczy podać we właściwości Resource ścieżkę do zasobu. Spring wykorzysta wtedy automatycznie rejestrowany edytor właściwości ResourceEditor i przekształci zasób na obiekt typu Resource, a następnie wstrzyknie go do ziarna. classpath:com/apress/springrecipes/shop/banner.txt
95
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
2.11. Tworzenie postprocesorów ziaren Problem Programista chce zarejestrować własne dodatki w kontenerze IoC, aby móc przetwarzać egzemplarze ziarna w trakcie ich tworzenia.
Rozwiązanie Postprocesor ziarna umożliwia dodatkowe przetwarzanie ziarna przed uruchomieniem wywoływanych zwrotnie metod inicjujących i po nich. Główną cechą postprocesora ziarna jest to, że przetwarza jeden po drugim wszystkie egzemplarze ziarna z kontenera IoC (nie ogranicza się do jednego egzemplarza). Zwykle postprocesory stosuje się do sprawdzania poprawności właściwości ziarna lub modyfikowania takich właściwości na podstawie określonych kryteriów. Postprocesor ziarna musi przede wszystkim zawierać implementację interfejsu BeanPostProcessor. Każde ziarno można przetwarzać przed uruchomieniem wywoływanych zwrotnie metod inicjujących i po zakończeniu pracy tych metod. Służą do tego metody postProcessBeforeInitialization() i postProcess AfterInitialization(). Spring przekazuje do tych dwóch metod każdy egzemplarz ziarna przed uruchomieniem metod inicjujących i po zakończeniu ich wykonywania. Opis tego procesu znajdziesz na poniższej liście: 1. Tworzenie egzemplarza ziarna (za pomocą konstruktora lub metody fabrycznej). 2. Ustawianie wartości i referencji do ziaren we właściwościach. 3. Wywoływanie setterów zdefiniowanych w interfejsach do poznawania zasobów. 4. Przekazywanie egzemplarza ziarna do metody postProcessBeforeInitialization() każdego postprocesora ziarna. 5. Uruchamianie wywoływanych zwrotnie metod inicjujących. 6. Przekazywanie egzemplarza ziarna do metody postProcessAfterInitialization() każdego postprocesora ziarna. 7. Ziarno jest gotowe do użytku. 8. Po zamknięciu kontenera należy uruchomić wywoływane zwrotnie metody związane z usuwaniem ziarna. Gdy używasz fabryki ziaren jako kontenera IoC, postprocesory ziaren można rejestrować tylko programowo (za pomocą metody addBeanPostProcessor()). Jeśli jednak korzystasz z kontekstu aplikacji, proces rejestracji wymaga tylko zadeklarowania egzemplarza postprocesora w pliku z konfiguracją ziarna. Postprocesor zostanie wtedy automatycznie zarejestrowany.
Jak to działa? Załóżmy, że przed otwarciem pliku dziennika chcesz się upewnić, że prowadząca do niego ścieżka podana w klasie Cashier istnieje (pozwala to uniknąć wyjątków FileNotFoundException). Ponieważ w komponentach przechowujących dane w systemie plików nieraz trzeba sprawdzać dostępność ścieżki, warto zaimplementować to rozwiązanie w ogólny sposób, umożliwiający powtórne jego wykorzystanie. W Springu doskonałym narzędziem do implementowania takich rozwiązań jest właśnie postprocesor. Aby postprocesor ziaren mógł wykrywać ziarna, które należy sprawdzić, trzeba utworzyć interfejs znacznikowy StorageConfig. Interfejs ten implementuje się w docelowych ziarnach. Ponadto aby postprocesor sprawdzał dostępność ścieżki, musi mieć dostęp do właściwości path. By zapewnić ten dostęp, dodaj do wspomnianego interfejsu metodę getPath().
96
2.11. TWORZENIE POSTPROCESORÓW ZIAREN
package com.apress.springrecipes.shop; public interface StorageConfig { public String getPath(); }
Następnie należy zaimplementować nowy interfejs znacznikowy w klasie Cashier. Postprocesor będzie sprawdzał tylko te ziarna, w których zaimplementowany jest wspomniany interfejs. package com.apress.springrecipes.shop; ... public class Cashier implements BeanNameAware, StorageConfig { ... public String getPath() { return path; } }
Teraz można napisać postprocesor do sprawdzania ścieżek. Ponieważ ścieżki najlepiej jest sprawdzać przed otwarciem pliku w metodzie inicjującej, do wykonania zadania należy posłużyć się metodą postProcess BeforeInitialization(). package com.apress.springrecipes.shop; ... import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; public class PathCheckingBeanPostProcessor implements BeanPostProcessor { public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof StorageConfig) { String path = ((StorageConfig) bean).getPath(); File file = new File(path); if (!file.exists()) { file.mkdirs(); } } return bean; } public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } }
W trakcie tworzenia ziarna kontener IoC przekazuje do postprocesora jeden po drugim wszystkie egzemplarze ziarna, dlatego trzeba je przefiltrować na podstawie dostępności implementacji interfejsu znacznikowego StoreConfig. Jeśli ziarno zawiera implementację tego interfejsu, można za pomocą metody getPath() uzyskać dostęp do właściwości path i sprawdzić, czy dana ścieżka istnieje w systemie plików. Jeżeli danej ścieżki nie ma, należy ją utworzyć przy użyciu metody File.mkdirs(). Metody postProcessBeforeInitialization() i postProcessAfterInitialization() muszą zwracać egzemplarz przetwarzanego ziarna. To oznacza, że w postprocesorze możesz nawet zastąpić pierwotny egzemplarz ziarna nowym egzemplarzem. Pamiętaj, że nawet jeśli dana metoda nie wykonuje żadnych operacji, musi zwracać egzemplarz ziarna.
97
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
Aby zarejestrować postprocesor w kontekście aplikacji, wystarczy zadeklarować egzemplarz postprocesora w pliku z konfiguracją ziarna. Kontekst aplikacji potrafi wykryć, które ziarno zawiera implementację interfejsu BeanPostProcessor, i rejestruje je w celu przetwarzania wszystkich pozostałych egzemplarzy ziarna używanych w kontenerze. ... ...
Zauważ, że jeśli podałeś w atrybucie init-method wywoływaną zwrotnie metodę inicjującą lub zaimplementowałeś interfejs InitializingBean, postprocesor PathCheckingBeanPostProcessor zadziała poprawnie, ponieważ będzie przetwarzał reprezentujące kasjera ziarno przed wywołaniem metody inicjującej. Jeżeli jednak ziarno to oparte jest na adnotacjach @PostConstruct i @PreDestroy z dokumentu JSR-250, a metodę inicjującą wywołujesz za pomocą egzemplarza postprocesora CommonAnnotationBeanPostProcessor, postprocesor PathCheckingBeanPostProcessor nie będzie działał prawidłowo. Wynika to z tego, że taki postprocesor domyślnie ma mniejszy priorytet niż postprocesor CommonAnnotationBeanPostProcessor. Dlatego metoda inicjująca zostanie uruchomiona przed sprawdzeniem ścieżki. ... ...
Aby zdefiniować kolejność uruchamiania postprocesorów, można zaimplementować w nich interfejs Ordered lub PriorityOrdered, a następnie zwracać pożądaną kolejność za pomocą metody getOrder(). Niższe wartości zwracane przez tę metodę oznaczają wyższy priorytet, a dane z interfejsu PriorityOrdered są uznawane za ważniejsze od informacji z interfejsu Ordered. Ponieważ postprocesor CommonAnnotationBeanPostProcessor zawiera implementację interfejsu PriorityOrdered, w postprocesorze PathCheckingBeanPostProcessor także trzeba zaimplementować
ten interfejs, aby móc uzyskać pożądaną kolejność. package com.apress.springrecipes.shop; ... import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.core.PriorityOrdered; public class PathCheckingBeanPostProcessor implements BeanPostProcessor, PriorityOrdered { private int order; public int getOrder() { return order; } public void setOrder(int order) {
98
2.12. ZEWNĘTRZNE PRZECHOWYWANIE KONFIGURACJI ZIARNA
this.order = order; } ... }
Teraz w pliku z konfiguracją ziarna należy przypisać niższą wartość postprocesorowi PathChecking BeanPostProcessor, aby sprawdzał (i ewentualnie tworzył) ścieżkę dla ziarna reprezentującego kasjera przed wywołaniem metody inicjującej przez postprocesor CommonAnnotationBeanPostProcessor. Ponieważ domyślne ustawienie postprocesora CommonAnnotationBeanPostProcessor to Ordered.LOWEST_PRECEDENCE, do postprocesora PathCheckingBeanPostProcessor wystarczy przypisać wartość zero. ...
Ponieważ wartość domyślna określająca kolejność postprocesora PathCheckingBeanPostProcessor to zero, nie trzeba jej ustawiać. Ponadto można nadal używać elementu , aby postprocesor CommonAnnotationBeanPostProcessor był rejestrowany automatycznie. ...
2.12. Zewnętrzne przechowywanie konfiguracji ziarna Problem W trakcie konfigurowania ziarna w pliku konfiguracyjnym trzeba pamiętać o tym, że łączenie szczegółów związanych z instalacją (ścieżki do plików, adresu serwera, nazwy i hasła użytkownika itd.) z konfiguracją nie jest dobrym rozwiązaniem. Zwykle konfigurację ziarna piszą programiści aplikacji, natomiast za szczegóły związane z instalacją odpowiadają pracownicy wdrażający program lub administratorzy systemu.
Rozwiązanie Spring udostępnia postprocesor fabryki ziaren PropertyPlaceholderConfigurer, który pozwala wyodrębnić część konfiguracji ziarna i zapisać ją w pliku właściwości. W pliku konfiguracyjnym ziarna można stosować zmienne w postaci ${var}, a postprocesor wczyta właściwości z pliku właściwości i zastąpi nimi zmienne. Postprocesor fabryki ziaren różni się od postprocesora ziarna tym, że docelowo działa dla kontenera IoC (fabryki ziaren lub kontekstu aplikacji), a nie dla poszczególnych egzemplarzy ziaren. Taki postprocesor wykonuje swoje zadania po wczytaniu przez kontener IoC konfiguracji ziaren, ale przed utworzeniem egzemplarzy ziaren. Postprocesor fabryki ziaren zwykle służy do modyfikowania konfiguracji ziaren przed utworzeniem ich egzemplarzy. Spring udostępnia kilka takich postprocesorów. W praktyce rzadko zachodzi potrzeba tworzenia własnych postprocesorów tego rodzaju. 99
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
Jak to działa? Wcześniej ścieżka do pliku z transakcjami kasjera była zapisana w pliku z konfiguracją ziarna. Łączenie tego typu informacji związanych z instalacją z konfiguracją ziarna to złe rozwiązanie. Lepszym podejściem jest umieszczenie danych o instalacji w pliku właściwości (na przykład config.properties) w katalogu głównym z parametru classpath. Następnie można zapisać w tym pliku ścieżkę do pliku z transakcjami. cashier.path=c:/cashier
Teraz należy zastosować w pliku z konfiguracją ziarna zmienne w postaci ${var}. Aby wczytać zewnętrzne właściwości z pliku właściwości i wykorzystać je do zastąpienia zmiennych, trzeba w kontekście aplikacji zarejestrować postprocesor fabryki ziaren PropertyPlaceholderConfigurer. Można zapisać albo jeden plik właściwości (we właściwości location), albo kilka takich plików (we właściwości locations). ... config.properties
Postprocesor fabryki ziaren PropertyPlaceholderConfigurer przed utworzeniem egzemplarzy ziaren zastąpi zmienne z pliku z konfiguracją ziarna zewnętrznymi właściwościami. W Springu 2.5 i nowszych wersjach tej platformy postprocesor PropertyPlaceholderConfigurer można zarejestrować za pomocą elementu . ...
2.13. Określanie komunikatów tekstowych Problem Aby aplikacja obsługiwała internacjonalizację, musi umieć określać komunikaty tekstowe pasujące do różnych ustawień regionalnych.
100
2.13. OKREŚLANIE KOMUNIKATÓW TEKSTOWYCH
Rozwiązanie Kontekst aplikacji w Springu potrafi określać komunikaty tekstowe dla docelowych ustawień regionalnych na podstawie kluczy. Zwykle komunikaty dla danych ustawień regionalnych powinny być zapisane w odrębnym pliku właściwości. Taki plik jest nazywany pakietem zasobów. MessageSource to interfejs definiujący kilka metod określania komunikatów. Interfejs ApplicationContext dziedziczy po interfejsie MessageSource, dlatego każdy kontekst aplikacji potrafi określać komunikaty tekstowe. Kontekst aplikacji deleguje określanie komunikatów do ziarna o nazwie messageSource. Najczęściej używaną implementacją interfejsu MessageSource jest ResourceBundleMessageSource. Implementacja ta określa komunikaty na podstawie pakietów zasobów przeznaczonych dla różnych ustawień regionalnych.
Jak to działa? Można na przykład utworzyć pakiet zasobów messages_en_US.properties dla amerykańskiej wersji języka angielskiego. Pakiety zasobów są wczytywane z katalogu głównego z parametru classpath. alert.checkout=A shopping cart has been checked out.
Aby móc określać komunikaty na podstawie pakietu zasobów, należy zastosować klasę ResourceBundle MessageSource jako implementację interfejsu MessageSource. Nazwę ziarna trzeba ustawić na messageSource, by kontekst aplikacji wykrył potrzebne ziarno. Należy też określić podstawową nazwę pakietów zasobów używanych przez klasę ResourceBundleMessageSource. ... messages
Ta definicja MessageSource sprawia, że gdy program będzie szukał komunikatu tekstowego dla ustawień regionalnych dla Stanów Zjednoczonych z preferowanym językiem angielskim, jako pierwszy wybrany zostanie pakiet zasobów messages_en_US.properties (pasuje on zarówno do języka, jak i do kraju). Jeśli nie istnieje taki pakiet zasobów lub gdy nie można znaleźć potrzebnego komunikatu, sprawdzany jest pakiet messages_en.properties, pasujący tylko do języka. Jeżeli także ten pakiet nie istnieje, ostatecznie wybierany jest domyślny pakiet messages.properties, przeznaczony dla wszystkich ustawień regionalnych. Więcej informacji na temat wczytywania pakietów zasobów znajdziesz w dokumentacji Javadoc klasy java.util.ResourceBundle. Teraz można zażądać od kontekstu aplikacji określenia komunikatu za pomocą metody getMessage(). Pierwszym argumentem tej metody jest klucz odpowiadający komunikatowi, a trzecim są docelowe ustawienia regionalne. package com.apress.springrecipes.shop; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class Main { public static void main(String[] args) throws Exception { ApplicationContext context = new FileSystemXmlApplicationContext("beans.xml"); ... String alert = context.getMessage("alert.checkout", null, Locale.US); System.out.println(alert); } }
101
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
Drugim argumentem metody getMessage() jest tablica z parametrami komunikatu. W komunikatach tekstowych można za pomocą indeksów zdefiniować różne parametry: alert.checkout=A shopping cart costing {0} dollars has been checked out at {1}.
Aby uzupełnić parametry komunikatu, trzeba przekazać tablicę z obiektami. Elementy tej tablicy przed wykorzystaniem ich w roli parametrów są przekształcane na łańcuchy znaków. package com.apress.springrecipes.shop; ... public class Main { public static void main(String[] args) throws Exception { ... String alert = context.getMessage("alert.checkout", new Object[] { 4, new Date() }, Locale.US); System.out.println(alert); } }
W klasie Main można określać komunikaty tekstowe, ponieważ istnieje bezpośredni dostęp do kontekstu aplikacji. Jednak by ziarno mogło określać takie komunikaty, trzeba w nim zaimplementować interfejs ApplicationContextAware lub MessageSourceAware. Wtedy można usunąć z klasy Main kod do określania komunikatów. package com.apress.springrecipes.shop; ... import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; public class Cashier implements BeanNameAware, MessageSourceAware, StorageConfig { ... private MessageSource messageSource; public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } public void checkout(ShoppingCart cart) throws IOException { ... String alert = messageSource.getMessage("alert.checkout", new Object[] { total, new Date() }, Locale.US); System.out.println(alert); } }
2.14. Komunikowanie się ze zdarzeniami aplikacji Problem W typowym modelu komunikowania się między komponentami nadawca musi zlokalizować odbiorcę, aby wywołać jego metodę. W tej sytuacji komponent odbiorczy musi być znany komponentowi nadawczemu. Tego rodzaju komunikacja jest bezpośrednia i prosta, jednak komponenty nadawczy i odbiorczy są ze sobą ściśle powiązane.
102
2.14. KOMUNIKOWANIE SIĘ ZE ZDARZENIAMI APLIKACJI
Przy korzystaniu z kontenera IoC komponenty mogą komunikować się za pomocą interfejsu, a nie przy użyciu implementacji. Ten model komunikacji pomaga ograniczyć powiązanie. Jest on jednak wydajny tylko wtedy, gdy nadawca komunikuje się wyłącznie z jednym odbiorcą. Gdy nadawca musi komunikować się z większą liczbą odbiorców, musi wywołać każdego z nich jednego po drugim.
Rozwiązanie Kontekst aplikacji w Springu obsługuje opartą na zdarzeniach komunikację między ziarnami. W tym modelu nadawca tylko publikuje zdarzenie i nie musi wiedzieć, kto je odbierze (odbiorców może być więcej niż jeden). Ponadto odbiorca nie musi wiedzieć, kto opublikował zdarzenie, i może w tym samym czasie oczekiwać na różne zdarzenia od rozmaitych nadawców. W ten sposób komponenty nadawczy i odbiorczy są luźno powiązane. W Springu wszystkie klasy zdarzeń muszą dziedziczyć po klasie ApplicationEvent. Każde ziarno może opublikować zdarzenie w wyniku wywołania metody publishEvent() nadawcy zdarzeń aplikacji. Aby ziarno odbierało określone zdarzenia, musi zawierać implementację interfejsu ApplicationListener i obsługiwać zdarzenia w metodzie onApplicationEvent(). Spring powiadamia odbiorców o wszystkich zdarzeniach, dlatego trzeba je samodzielnie filtrować. Jeśli jednak korzystasz z typów generycznych, Spring będzie dostarczał tylko komunikaty pasujące do typów podanych w nich parametrów.
Jak to działa? Definiowanie zdarzeń Pierwszym krokiem przy konfigurowaniu komunikacji opartej na zdarzeniach jest zdefiniowanie zdarzenia. Załóżmy, że reprezentujące kasjera ziarno ma po zakończeniu transakcji publikować zdarzenie CheckoutEvent. Zdarzenie to obejmuje dwie właściwości: zapłaconą kwotę i godzinę przeprowadzenia transakcji. W Springu wszystkie zdarzenia muszą dziedziczyć po klasie abstrakcyjnej ApplicationEvent i przekazywać źródło zdarzenia jako argument konstruktora. package com.apress.springrecipes.shop; ... import org.springframework.context.ApplicationEvent; public class CheckoutEvent extends ApplicationEvent { private double amount; private Date time; public CheckoutEvent(Object source, double amount, Date time) { super(source); this.amount = amount; this.time = time; } public double getAmount() { return amount; } public Date getTime() { return time; } }
103
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
Publikowanie zdarzeń Aby opublikować zdarzenie, wystarczy utworzyć jego egzemplarz i wywołać metodę publishEvent() nadawcy zdarzeń aplikacji. Dostęp do tej metody można uzyskać dzięki zaimplementowaniu interfejsu ApplicationEventPublisherAware. package com.apress.springrecipes.shop; ... import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; public class Cashier implements BeanNameAware, MessageSourceAware, ApplicationEventPublisherAware, StorageConfig { ... private ApplicationEventPublisher applicationEventPublisher; public void setApplicationEventPublisher( ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } public void checkout(ShoppingCart cart) throws IOException { ... CheckoutEvent event = new CheckoutEvent(this, total, new Date()); applicationEventPublisher.publishEvent(event); } }
Oczekiwanie na zdarzenia Każde zdefiniowane w kontekście aplikacji ziarno z implementacją interfejsu ApplicationListener jest powiadamiane o wszystkich zdarzeniach. Dlatego w aplikacji onApplicationEvent() trzeba filtrować zdarzenia, które odbiornik ma obsługiwać. Poniższy odbiornik ma wysyłać do klienta e-mail z powiadomieniem o dokonaniu transakcji. W ramach filtrowania używany jest tu test instanceof na niegenerycznym parametrze typu ApplicationEvent. package com.apress.springrecipes.shop; ... import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; public class CheckoutListener implements ApplicationListener { public void onApplicationEvent(ApplicationEvent event) { if (event instanceof CheckoutEvent) { double amount = ((CheckoutEvent) event).getAmount(); Date time = ((CheckoutEvent) event).getTime(); // Można wykonać dowolne operacje na kwocie i czasie transakcji System.out.println("Zdarzenie CheckoutEvent [" + amount + ", " + time + "]"); } } }
Zmodyfikowana wersja tego kodu, w której wykorzystano typy generyczne, jest nieco krótsza: package com.apress.springrecipes.shop; ... import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener;
104
2.15. REJESTROWANIE EDYTORÓW WŁAŚCIWOŚCI W SPRINGU
public class CheckoutListener implements ApplicationListener { public void onApplicationEvent(CheckoutEvent event) { double amount = ((CheckoutEvent) event).getAmount(); Date time = ((CheckoutEvent) event).getTime(); // Można wykonać dowolne operacje na kwocie i czasie transakcji System.out.println("Zdarzenie CheckoutEvent [" + amount + ", " + time + "]"); } }
Następnie trzeba zarejestrować odbiornik w kontekście aplikacji, aby oczekiwał na wszystkie zdarzenia. Proces rejestrowania jest prosty — wystarczy zadeklarować egzemplarz ziarna odbiornika. Kontekst aplikacji wykrywa ziarna z implementacją interfejsu ApplicationListener i powiadamia je o każdym zdarzeniu. ...
Zauważ też, że sam kontekst aplikacji także publikuje zdarzenia kontenera (takie jak: ContextClosedEvent, ContextRefreshedEvent i RequestHandledEvent). Jeśli któreś z ziaren ma być powiadamiane o takich zdarzeniach, należy zaimplementować w nim interfejs ApplicationListener.
2.15. Rejestrowanie edytorów właściwości w Springu Problem Edytor właściwości to mechanizm interfejsu API JavaBeans służący do przekształcania wartości właściwości na postać tekstową i w odwrotną stronę. Każdy edytor właściwości jest przeznaczony wyłącznie dla właściwości określonego typu. Takie edytory pozwalają uprościć konfigurację ziaren.
Rozwiązanie Kontener IoC umożliwia stosowanie edytorów właściwości, co pozwala uprościć konfigurację ziaren. Na przykład za pomocą edytora właściwości typu java.net.URL można określić łańcuch znaków z adresem URL dla właściwości typu URL. Spring automatycznie przekształci wtedy łańcuch znaków z adresem na obiekt typu URL i wstrzyknie go do właściwości. Spring udostępnia zestaw edytorów właściwości przeznaczonych do przekształcania właściwości różnych typów. Standardowo edytor właściwości należy przed zastosowaniem zarejestrować w kontenerze IoC. Klasa CustomEditorConfigurer zawiera implementację postprocesora fabryki ziaren, co pozwala zarejestrować niestandardowe edytory właściwości przed utworzeniem egzemplarzy ziaren.
Jak to działa? Załóżmy, że chcesz utworzyć listę produktów uporządkowanych na podstawie wartości ich sprzedaży w danym okresie. Aby wprowadzić tę zmianę, należy dodać do klasy ProductRanking właściwości fromDate i toDate. package com.apress.springrecipes.shop; ... public class ProductRanking { private Product bestSeller; private Date fromDate;
105
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
private Date toDate; // Gettery i settery ... }
By ustawić wartość właściwości java.util.Date w programie w Javie, można za pomocą metody DateFormat. parse() przekształcić na typ tej właściwości łańcuch znaków z datą podaną w określonym formacie: DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); productRanking.setFromDate(dateFormat.parse("2007-09-01")); productRanking.setToDate(dateFormat.parse("2007-09-30"));
Żeby napisać analogiczną konfigurację ziarna w Springu, trzeba najpierw zadeklarować ziarno dateFormat z określonym wzorcem. Ponieważ metoda parse() jest wywoływana w celu przekształcenia łańcuchów znaków z datą na obiekty Date, możesz potraktować ją jak metodę fabryczną egzemplarza tworzącą ziarna reprezentujące daty. ...
Jak widać, konfiguracja ta jest zbyt skomplikowana jak na potrzeby ustawiania właściwości w postaci dat. Jednak kontener IoC potrafi przekształcać wartości tekstowe na właściwości. Umożliwiają to edytory właściwości. Dostępna w Springu klasa CustomDateEditor służy do przekształcania łańcuchów znaków z datami na właściwości typu java.util.Date. Trzeba zacząć od zadeklarowania egzemplarza tej klasy w pliku z konfiguracją ziarna. ...
106
2.15. REJESTROWANIE EDYTORÓW WŁAŚCIWOŚCI W SPRINGU
Omawiany edytor wymaga podania obiektu typu DateFormat jako pierwszego argumentu konstruktora. Drugi argument określa, czy w edytorze dozwolone są puste wartości. Następnie należy zarejestrować edytor właściwości w egzemplarzu klasy CustomEditorConfigurer, aby Spring mógł przekształcać właściwości typu java.util.Date. Od tego momentu można dla wszystkich właściwości typu java.util.Date podawać daty w formacie tekstowym: ...
Za pomocą poniższej klasy Main możesz sprawdzić, czy konfiguracja edytora CustomDateEditor działa poprawnie: package com.apress.springrecipes.shop; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) throws Exception { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); ... ProductRanking productRanking = (ProductRanking) context.getBean("productRanking"); System.out.println( "Ranking produktów za okres od " + productRanking.getFromDate() + " do " + productRanking.getToDate()); } }
Oprócz edytora CustomDateEditor Spring udostępnia też kilka innych edytorów właściwości służących do przekształcania często używanych typów danych. Są to między innymi edytory: CustomNumberEditor, ClassEditor, FileEditor, LocaleEditor, StringArrayPropertyEditor i URLEditor. Spośród nich ClassEditor, FileEditor, LocaleEditor i URLEditor są automatycznie rejestrowane w Springu, dlatego nie trzeba ich ponownie rejestrować. Więcej informacji na temat stosowania tych edytorów znajdziesz w dokumentacji Javadoc, w miejscach dotyczących poszczególnych klas z pakietu org.springframework.beans.propertyeditors.
107
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
2.16. Tworzenie niestandardowych edytorów właściwości Problem Oprócz rejestrowania wbudowanych edytorów właściwości możesz tworzyć własne edytory do przekształcania niestandardowych typów danych.
Rozwiązanie Aby utworzyć niestandardowy edytor właściwości, zaimplementuj interfejs java.beans.PropertyEditor lub utwórz klasę pochodną od wygodnej klasy pomocniczej java.beans.PropertyEditorSupport.
Jak to działa? Napiszmy edytor właściwości dla klasy Product. Tekstowa reprezentacja produktu może obejmować trzy człony: nazwę konkretnej klasy, nazwę produktu i jego cenę. Poszczególne części rozdziel przecinkami. Następnie napisz poniższą klasę ProductEditor do przekształcania reprezentacji tekstowej: package com.apress.springrecipes.shop; import java.beans.PropertyEditorSupport; public class ProductEditor extends PropertyEditorSupport { public String getAsText() { Product product = (Product) getValue(); return product.getClass().getName() + "," + product.getName() + "," + product.getPrice(); } public void setAsText(String text) throws IllegalArgumentException { String[] parts = text.split(","); try { Product product = (Product) Class.forName(parts[0]).newInstance(); product.setName(parts[1]); product.setPrice(Double.parseDouble(parts[2])); setValue(product); } catch (Exception e) { throw new IllegalArgumentException(e); } } }
Metoda getAsText() przekształca właściwość na łańcuch znaków, natomiast metoda setAsText() przetwarza łańcuch znaków z powrotem na właściwość. Do pobierania i ustawiania wartości właściwości służą metody getValue() i setValue(). Następnie należy zarejestrować niestandardowy edytor w egzemplarzu klasy CustomEditorConfigurer. Proces rejestrowania przebiega tak samo jak dla wbudowanych edytorów. Po wykonaniu tych kroków w dowolnej właściwości typu Product możesz podać produkt w postaci tekstowej. ...
108
2.17. OBSŁUGA WSPÓŁBIEŻNOŚCI ZA POMOCĄ INTERFEJSU TASKEXECUTOR
... com.apress.springrecipes.shop.Disc,CD-RW,1.5 ...
Interfejs API JavaBeans automatycznie szuka edytorów właściwości dla klas. Aby możliwe było znalezienie edytora, należy go umieścić w tym samym pakiecie, w którym znajduje się docelowa klasa. Ponadto nazwa edytora musi składać się z nazwy docelowej klasy i przyrostka Editor. Jeśli edytor właściwości utworzono zgodnie z tymi zasadami (tak jak przedstawiony wcześniej edytor ProductEditor), nie trzeba rejestrować go w kontenerze IoC.
2.17. Obsługa współbieżności za pomocą interfejsu TaskExecutor Problem Istnieje wiele sposobów tworzenia wielowątkowych, współbieżnych programów, jednak żadna z tych metod nie jest standardowa. Co więcej, pisanie takich programów zwykle wymaga tworzenia licznych klas narzędziowych do wykonywania typowych zadań.
Rozwiązanie Można zastosować dostępną w Springu abstrakcję TaskExecutor. Istnieje wiele jej implementacji przeznaczonych dla różnych środowisk. Te implementacje to na przykład Executor (dla Javy SE), WorkManager (dla specyfikacji CommonJ) i implementacje niestandardowe. W Springu 3.0 wszystkie te implementacje zostały ujednolicone i można je zrzutować na interfejs Executor Javy SE.
Jak to działa? Wielowątkowość to skomplikowana kwestia. Poradzenie sobie z niektórymi trudnymi sytuacjami wymaga dużo pracy. W innych przypadkach konieczne jest bardzo żmudne stosowanie standardowych wątków ze środowiska Java SE. Dla twórców komponentów działających po stronie serwera współbieżność jest ważnym aspektem architektury, jednak w środowisku Java EE do tej pory nie doczekała się standaryzacji. Co więcej, niektóre fragmenty specyfikacji Javy EE zabraniają bezpośredniego tworzenia wątków i manipulowania nimi!
Java SE W Javie SE przez lata pojawiło się mnóstwo rozwiązań z zakresu wątków. Przede wszystkim od początku dostępny jest standardowy typ java.lang.Thread. Wątki są też obsługiwane do wersji 1.0 pakietu JDK
109
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
(ang. Java Development Kit). W Javie 1.3 wprowadzono typ java.util.TimerTask, który umożliwia wykonywanie zadań co określony czas. W Javie 5 pojawił się pakiet java.util.concurrent, a ponadto zmodyfikowano hierarchię służącą do tworzenia pul wątków (jest ona oparta na typie java.util.concurrent. Executor). Interfejs API typu Executor jest prosty: package java.util.concurrent; public interface Executor { void execute(Runnable command); }
Interfejs pochodny ExecutorService udostępnia więcej funkcji z zakresu zarządzania wątkami i obsługuje zgłaszanie zdarzeń związanych z wątkami (na przykład za pomocą metody shutdown()). Istnieje kilka implementacji, które udostępniano w pakiecie JDK od wersji 5.0 Javy SE. Wiele z tych implementacji jest dostępnych poprzez statyczne metody fabryczne klasy java.util.concurrent.Executors (podobnie jak metody narzędziowe do manipulowania egzemplarzami klasy java.util.Collection są dostępne w klasie java.util.Collections). Dalej znajdziesz przykłady stosowania takich metod. Interfejs ExecutorService udostępnia też metodę submit(), która zwraca wartość typu Future. Egzemplarz tego typu można wykorzystać do śledzenia postępów wykonywanego (zwykle asynchronicznie) wątku. Możesz wywołać metodę Future.isDone() lub Future.isCancelled(), aby ustalić, czy zadanie zostało zakończone lub anulowane. Gdy używasz interfejsu ExecutoreService i wywołasz metodę submit() z argumentem typu Runnable, którego metoda run nie zwraca wartości określonego typu, wywołanie metody get() zwróconego obiektu Future da wartość null lub wartość podaną w wywołaniu submit(): Runnable task = new Runnable(){ public void run(){ try{ Thread.sleep( 1000 * 60 ) ; System.out.println("Koniec minutowej drzemki – zwracam sterowanie." ); } catch (Exception ex) { /* … */ } } }; ExecutorService executorService = Executors.newCachedThreadPool() ; if(executorService.submit(task, Boolean.TRUE).get().equals( Boolean.TRUE )) System.out.println( "Zadanie zostało wykonane.");
Te podstawy pozwalają przeanalizować wybrane cechy różnych implementacji. Możesz na przykład wykorzystać poniższy egzemplarz typu Runnable: package com.apress.springrecipes.spring3.executors; import java.util.Date; import org.apache.commons.lang.exception.ExceptionUtils; public class DemonstrationRunnable implements Runnable { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println( ExceptionUtils.getFullStackTrace(e)); } System.out.println(Thread.currentThread().getName()); System.out.printf("Witaj w dniu %s \n", new Date()); } }
Przedstawiona klasa ma służyć tylko do opisywania upływu czasu. Ten sam egzemplarz posłuży do omawiania działania typów Executors z Javy SE i TaskExecutor Springa.
110
2.17. OBSŁUGA WSPÓŁBIEŻNOŚCI ZA POMOCĄ INTERFEJSU TASKEXECUTOR
package com.apress.springrecipes.spring3.executors; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ExecutorsDemo { public static void main(String[] args) throws Throwable { Runnable task = new DemonstrationRunnable(); // Generuje pulę wątków i próbuje ponownie wykorzystać // wcześniej utworzone wątki (jeśli to możliwe) ExecutorService cachedThreadPoolExecutorService = Executors .newCachedThreadPool(); if (cachedThreadPoolExecutorService.submit(task).get() == null) System.out.printf("Obiekt cachedThreadPoolExecutorService " + "zakończył pracę o %s \n", new Date()); // Ogranicza liczbę tworzonych wątków; nadmiarowe wątki są umieszczane w kolejce ExecutorService fixedThreadPool = Executors.newFixedThreadPool(100); if (fixedThreadPool.submit(task).get() == null) System.out.printf("Obiekt fixedThreadPool " + "zakończył pracę o %s \n", new Date()); // W danym momencie można używać tylko jednego wątku ExecutorService singleThreadExecutorService = Executors .newSingleThreadExecutor(); if (singleThreadExecutorService.submit(task).get() == null) System.out.printf("Obiekt singleThreadExecutorService " + "zakończył pracę o %s \n", new Date()); // Wykonywanie zadania o znanym wyniku ExecutorService es = Executors.newCachedThreadPool(); if (es.submit(task, Boolean.TRUE).get().equals(Boolean.TRUE)) System.out.println("Zadanie zostało zakończone"); // Symulowanie pracy obiektu TimerTask ScheduledExecutorService scheduledThreadExecutorService = Executors .newScheduledThreadPool(10); if (scheduledThreadExecutorService.schedule( task, 30, TimeUnit.SECONDS).get() == null) System.out.printf("Obiekt scheduledThreadExecutorService " + "zakończył pracę o %s \n", new Date()); // Kontynuuje pracę do momentu zgłoszenia wyjątku // lub wywołania metody cancel() scheduledThreadExecutorService.scheduleAtFixedRate(task, 0, 5, TimeUnit.SECONDS); } }
Jeśli metodę submit() wywołasz na egzemplarzu typu ExecutorService przyjmującym obiekt typu Callable, metoda ta zwróci wartość zwróconą przez główną metodę interfejsu Callable, metodę call(). Interfejs Callable wygląda tak:
111
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
package java.util.concurrent; public interface Callable { V call() throws Exception; }
Java EE W środowisku Java EE opracowano inne (w wielu przypadkach niezbyt udane) techniki wykonywania opisanych wcześniej zadań. Java EE przez długi czas nie pomagała w obsłudze wielowątkowości. Istnieją jednak zewnętrzne rozwiązania problemów z wielowątkowością. Platforma Quartz umożliwia planowanie wykonywania zadań i obsługuje współbieżność. JCA 1.5 (ang. J2EE Connector Architecture; akronim JCA najczęściej dotyczy właśnie tej technologii, choć pierwotnie miał być nazwą architektury Java Cryptography Architecture) to specyfikacja mechanizmów z zakresu współbieżności, udostępniająca prosty sposób na zintegrowanie różnych technik. Dzięki tej specyfikacji komponenty mogą być powiadamiane o nadchodzących komunikatach i mogą współbieżnie przesyłać odpowiedzi. JCA 1.5 zapewnia prostą, mającą ograniczone możliwości magistralę dla usług korporacyjnych. Technologia ta przypomina nieco mechanizmy do obsługi integracji, jednak nie ma tak zaawansowanych funkcji jak rozwijana przez firmę SpringSource platforma Spring Integration. Jeśli jednak przed rokiem 2006 musiałeś powiązać starszą aplikację napisaną w języku C z serwerem aplikacji opartym na Javie EE i umożliwić opcjonalne korzystanie z usług kontenera, a ponadto chciałeś to zrobić w stosunkowo przenośny sposób, technologia JCA 1.5 była dobrym rozwiązaniem. O współbieżności nie zapomnieli jednak producenci serwerów aplikacji. W 2003 roku firmy IBM i BEA wspólnie opracowały interfejsy API Timer i Work Manager. Te interfejsy ostatecznie opisano w dokumencie JSR-237, który potem wycofano i połączono z dokumentem JSR-236. Skoncentrowano się przy tym na implementowaniu współbieżności w środowisku zarządzanym (głównie w Javie EE). Nadal nie istnieje ostateczna wersja dokumentu JSR-236. W ramach specyfikacji SDO (ang. Service Data Object), JSR-235, także pracowano nad podobnym rozwiązaniem, jednak również ona nie jest jeszcze gotowa. Zarówno specyfikacja SDO, jak i interfejs API WorkManager pierwotnie opracowano na potrzeby Javy EE 1.4, później prace nad tymi technologiami potoczyły się niezależnie. Interfejsy API Timer i WorkManager (znane też jako CommonJ WorkManager) są obsługiwane w platformach WebLogic (wersja 9.0 i nowsze) oraz WebSphere (wersja 6.0 i nowsze), choć nie są w pełni przenośne. Ponadto w ostatnich latach pojawiły się otwarte implementacje interfejsu API CommonJ. Łatwo się w tym pogubić, prawda? Problem polega na tym, że nie istnieje przenośny, standardowy i prosty sposób kontrolowania wątków oraz zapewniania współbieżności komponentów w środowisku zarządzanym (a także niezarządzanym). Nawet jeśli ograniczysz się do rozwiązań typowych dla Javy SE, nadal będziesz miał do wyboru wiele różnych możliwości.
Rozwiązanie zastosowane w Springu W Springu 2.0 wprowadzono rozwiązanie pozwalające ujednolicić inne techniki. Był to interfejs org.spring framework.core.task.TaskExecutor. Interfejs ten dobrze spełniał stawiane mu wymagania. Ponieważ Spring zapewniał obsługę Javy 1.4, interfejs TaskExecutor nie zapewniał implementacji interfejsu java.util.concurrent. Executor (wprowadzonego w Javie 1.5), choć oba interfejsy były ze sobą zgodne. Ponadto w każdej klasie z implementacją interfejsu TaskExecutor można było także zaimplementować interfejs Executor, ponieważ definicje metod z obu tych interfejsów miały identyczne sygnatury. Interfejs TaskExecutor jest dostępny także w Springu 3.0, co pozwala zapewnić zgodność z pakietem JDK 1.4 ze Springa 2.x. To oznacza, że osoby używające starszych pakietów JDK mogą budować aplikacje z zaawansowanymi funkcjami bez korzystania z pakietu JDK 5. W powiązanym z Javą 5 Springu 3.0 interfejs TaskExecutor dziedziczy po interfejsie Executor. To sprawia, że mechanizmy dostępne w Springu współdziałają obecnie z podstawowym pakietem JDK. Interfejs TaskExecutor jest używany wewnętrznie w wielu miejscach platformy Spring. Na przykład obsługa integracji z platformą Quartz (która oczywiście udostępnia wątki) i sterowanym komunikatami kontenerem POJO jest oparta na tym interfejsie.
112
2.17. OBSŁUGA WSPÓŁBIEŻNOŚCI ZA POMOCĄ INTERFEJSU TASKEXECUTOR
// Abstrakcja dostępna w Springu package org.springframework.core.task; import java.util.concurrent.Executor; public interface TaskExecutor extends Executor { void execute(Runnable task); }
W niektórych miejscach różne rozwiązania odzwierciedlają mechanizmy z podstawowego pakietu JDK. W innych przypadkach stosowane są nietypowe techniki, a także metody zapewniające integrację z innymi platformami (takimi jak CommonJ WorkManager). Mechanizmy integracji zwykle mają postać klasy znajdującej się w docelowej platformie, ale którą można manipulować jak dowolną inną abstrakcją typu TaskExecutor. Choć możliwe jest dostosowanie istniejących interfejsów Executor lub ExecutorService z Javy SE do interfejsu TaskExecutor, w Springu 3.0 nie jest to potrzebne, ponieważ TaskExecutor dziedziczy po interfejsie Executor. Dzięki temu interfejs TaskExecutor w Springu pozwala połączyć różne rozwiązania dostępne w Javie EE i Javie SE. Warto najpierw przyjrzeć się prostej obsłudze interfejsu TaskExecutor. Używany jest tu zdefiniowany wcześniej obiekt typu Runnable. Klientem jest proste ziarno Springa, do którego wstrzykiwane są różne egzemplarze obiektów z implementacją interfejsu TaskExecutor. Jedynym zadaniem tych obiektów jest umożliwienie przesyłania wspomnianego obiektu typu Runnable. package com.apress.springrecipes.spring3.executors; import import import import import import import
org.springframework.beans.factory.annotation.Autowired; org.springframework.context.support.ClassPathXmlApplicationContext; org.springframework.core.task.SimpleAsyncTaskExecutor; org.springframework.core.task.SyncTaskExecutor; org.springframework.core.task.support.TaskExecutorAdapter; org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; org.springframework.scheduling.timer.TimerTaskExecutor;
public class SpringExecutorsDemo { public static void main(String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("context2.xml"); SpringExecutorsDemo demo = ctx.getBean( "springExecutorsDemo", SpringExecutorsDemo.class); demo.submitJobs(); } @Autowired private SimpleAsyncTaskExecutor asyncTaskExecutor; @Autowired private SyncTaskExecutor syncTaskExecutor; @Autowired private TaskExecutorAdapter taskExecutorAdapter; /* Ten fragment nie jest konieczny, ponieważ planowanie wykonywania zadań jest już skonfigurowane w kontekście aplikacji @Resource(name = "timerTaskExecutorWithScheduledTimerTasks") private TimerTaskExecutor timerTaskExecutorWithScheduledTimerTasks; */ @Resource(name = "timerTaskExecutorWithoutScheduledTimerTasks") private TimerTaskExecutor timerTaskExecutorWithoutScheduledTimerTasks;
113
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
@Autowired private ThreadPoolTaskExecutor threadPoolTaskExecutor; @Autowired private DemonstrationRunnable task; public void submitJobs() { syncTaskExecutor.execute(task); taskExecutorAdapter.submit(task); asyncTaskExecutor.submit(task); timerTaskExecutorWithoutScheduledTimerTasks.submit(task); /* Uruchamia 100 wątków naraz, następnie umieszcza pozostałe w kolejce W sumie wykonanie zadania zajmie około pięciu sekund */ for (int i = 0; i < 500; i++) threadPoolTaskExecutor.submit(task); } }
W kontekście aplikacji tworzone są różne implementacje interfejsu TaskExecutor. Większość z nich jest tak prosta, że można je utworzyć ręcznie. Tylko w jednym przypadku (dla implementacji timerTaskExecutor) należy zażądać wykonania tego zadania od ziarna fabrycznego:
114
2.17. OBSŁUGA WSPÓŁBIEŻNOŚCI ZA POMOCĄ INTERFEJSU TASKEXECUTOR
class="org.springframework.core.task.SimpleAsyncTaskExecutor" p:daemon="false" />
W tym kodzie pokazano różne implementacje interfejsu TaskExecutor. Pierwsze ziarno, egzemplarz typu TaskExecutorAdapter, to prosta nakładka na egzemplarz typu java.util.concurrence.Executors, dlatego można z niego korzystać jak z interfejsu TaskExecutor Springa. To udogodnienie nie ma jednak dużego znaczenia, ponieważ obecnie i tak można korzystać z egzemplarzy za pomocą interfejsu Executor (w Springu 3.0 interfejs TaskExecutor dziedziczy po interfejsie Executor). Tu Spring używany jest do skonfigurowania egzemplarza typu Executor i przekazania go jako argumentu konstruktora. Egzemplarz typu SimpleAsyncTaskExecutor udostępnia nowy wątek (Thread) dla każdego przesłanego zadania. Nie stosuje przy tym puli wątków ani nie umożliwia ich wielokrotnego wykorzystania. Każde przesłane zadanie jest asynchronicznie uruchamiane w wątku.
115
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
SyncTaskExecutor to najprostsza implementacja interfejsu TaskExecutor. Przesyłanie zadania odbywa się tu synchronicznie i jest odpowiednikiem utworzenia wątku, uruchomienia go, a następnie wywołania metody join() w celu natychmiastowego podłączenia nowego wątku do wątku wywołującego. Efekt jest taki sam jak przy ręcznym uruchomieniu metody run() w wątku wywołującym (program działa wtedy bez wielowątkowości). TimerTaskExecutor używa egzemplarza typu java.util.Timer i zarządza zadaniami (egzemplarzami typu java.util.concurrent.Callable lub java.lang.Runnable), uruchamiając je za pomocą obiektu Timer. Przy tworzeniu egzemplarza typu TimerTaskExecutor można ustawić opóźnienie, po którym program ma zacząć uruchamianie wszystkich przesłanych zadań. Wewnętrznie TimerTaskExecutor przekształca egzemplarze typu Callable lub Runnable przesłane jako zadania TimerTask, a następnie planuje ich wykonanie przy użyciu obiektu Timer. Jeśli planujesz wykonanie wielu zadań, są one uruchamiane po kolei w tym samym wątku powiązanym z jednym obiektem Timer. Jeśli nie podasz bezpośrednio obiektu Timer, zostanie utworzony domyślny obiekt tego typu. Jeżeli chcesz bezpośrednio zarejestrować zadania TimerTask w obiekcie Timer, zastosuj właściwość scheduledTimerTasks z klasy org.springframework.scheduling.timer.TimerFactoryBean. Typ TimerTaskExecutor (w odróżnieniu od typu Timer) nie udostępnia metod przeznaczonych do zaawansowanego planowania zadań. Jeśli chcesz planować wykonywanie zadań w stałych odstępach czasu, w określonym momencie (podawanym za pomocą typu Date) lub przez dany czas, musisz zmodyfikować samo zadanie TimerTask. Umożliwia to klasa org.springframework.scheduling.timer.ScheduledTimerTask. Udostępnia ona łatwe do skonfigurowania zadania TimerTask, których wykonanie można odpowiednio zaplanować przy użyciu klasy TimerFactoryBean. Aby uruchamiać zadania w taki sam sposób jak za pomocą innych implementacji interfejsu TaskExecutor, należy określić opóźnienie, skonfigurować egzemplarz typu TimerFactoryBean, a następnie w zwyczajny sposób przesłać zadanie:
Bardziej skomplikowane sposoby planowania zadań, na przykład wykonywanie ich w stałych odstępach czasu, wymagają bezpośredniego skonfigurowania zadania TimerTask. W tej sytuacji ręczne przesyłanie zadań nie jest dobrym podejściem. Aby korzystać z zaawansowanych funkcji, warto posłużyć się platformą Quartz lub innym narzędziem obsługującym wyrażenia cron.
Ostatni przykład dotyczy typu ThreadPoolTaskExecutor. Jest to kompletna implementacja puli wątków wykorzystująca typ java.util.concurrent.ThreadPoolExecutor. Jeśli chcesz tworzyć aplikacje za pomocą mechanizmów obsługi interfejsów CommonJ WorkManager i TimeManager dostępnych w WebSphere 6.0 firmy IBM i WebLogic 9.0 firmy BEA, możesz użyć typu 116
PODSUMOWANIE
org.springframework.scheduling.commonj.WorkManagerTaskExecutor. Wykorzystuje on referencję do
interfejsu CommonJ WorkManager dostępną w platformach WebSphere i WebLogic. Zwykle należy podać wtedy referencję JNDI do odpowiedniego zasobu. To rozwiązanie współdziała całkiem dobrze na przykład z serwerem Geronimo, jednak przy serwerach JBoss i GlassFish niezbędne są dodatkowe operacje. Spring udostępnia klasy delegujące zadania do dostępnych w tych serwerach mechanizmów obsługi platformy JCA. Dla serwera GlassFish należy używać klasy org.springframework.jca.work.glassfish.GlassFishWorkManagerTaskExecutor, a dla serwera JBoss przeznaczona jest klasa org.springframework.jca.work.jboss.JBossWorkManagerTaskExecutor. Mechanizmy obsługi interfejsu TaskExecutor zapewniają duże możliwości w zakresie dostępu do usług planowania zadań oferowanych przez serwer aplikacji. Z usług tych można korzystać za pomocą jednolitego interfejsu. Jeśli szukasz bardziej rozbudowanych (i wymagających znacznie więcej zasobów) rozwiązań, które można zastosować na dowolnym serwerze (nawet na serwerach Tomcat i Jetty!), pomyśl o wykorzystaniu dostępnych w Springu mechanizmów obsługi platformy Quartz.
Podsumowanie W tym rozdziale poznałeś różne sposoby tworzenia ziaren: wywoływanie konstruktora, wywoływanie statycznych metod fabrycznych, wywoływanie metod fabrycznych egzemplarza, używanie ziarna fabrycznego i pobieranie ziarna z pola statycznego lub właściwości obiektu. Kontener IoC umożliwia łatwe tworzenie ziaren za pomocą tych technik. W Springu można ustawić zasięg ziarna, aby określić, który egzemplarz powinien zostać zwrócony. Domyślny zasięg ziarna to singleton. Przy tym ustawieniu Spring tworzy dla kontenera IoC jeden współużytkowany egzemplarz ziarna. Inny często używany zasięg to prototype. Wtedy Spring dla każdego żądania tworzy nowy egzemplarz ziarna. Aby zmodyfikować proces inicjowania i usuwania ziaren, podaj odpowiednie wywoływane zwrotnie metody. Ponadto w ziarnie można zaimplementować określone interfejsy pozwalające na poznanie konfiguracji i infrastruktury kontenera. Kontener IoC uruchamia wywoływane zwrotnie metody we właściwych momentach cyklu życia ziarna. Spring obsługuje rejestrowanie w kontenerze IoC postprocesorów ziaren. Te postprocesory wykonują dodatkowe operacje na ziarnie przed uruchomieniem metod inicjujących i po zakończeniu ich wykonywania. Postprocesory ziaren mogą przetwarzać wszystkie ziarna z kontenera IoC. Postprocesory zwykle służą do sprawdzania poprawności właściwości ziaren lub modyfikowania takich właściwości na podstawie podanych kryteriów. W tym rozdziale poznałeś też różne zaawansowane funkcje kontenera IoC, takie jak: zapisywanie konfiguracji ziaren w plikach właściwości, określanie komunikatów tekstowych z pakietów zasobów, publikowanie i odbieranie zdarzeń aplikacji, używanie edytorów właściwości do przekształcania wartości właściwości na tekst i odwrotnie, a także wczytywanie zasobów zewnętrznych. Mechanizmy te będą bardzo przydatne w trakcie tworzenia aplikacji za pomocą Springa. Poznałeś też rzadziej stosowane rozwiązania służące do zarządzania współbieżnością przy użyciu implementacji typu Executor Springa.
117
ROZDZIAŁ 2. ZAAWANSOWANY KONTENER IOC W SPRINGU
118
ROZDZIAŁ 3
Programowanie aspektowe i obsługa języka AspectJ w Springu W tym rozdziale poznasz programowanie aspektowe w Springu oraz wybrane zaawansowane zagadnienia z tego obszaru, takie jak pierwszeństwo rad (ang. advice precedence) i wprowadzenie (ang. introduction). Programowanie aspektowe jest ważną częścią platformy Spring od czasu jej powstania. Między wersjami 1.x i 2.x Springa obsługa programowania aspektowego została znacznie zmodyfikowana, w wersjach 3.x nie wprowadzono jednak nowych zmian. Z tego rozdziału dowiesz się też, jak używać języka AspectJ w aplikacjach opartych na Springu. Począwszy od wersji 2.x Springa, aspekty można pisać jako obiekty POJO zarówno za pomocą adnotacji języka AspectJ, jak i przy użyciu XML-owej konfiguracji w plikach z konfiguracją ziaren. Ponieważ te dwa podejścia prowadzą do uzyskania tych samych efektów, w większej części rozdziału koncentrujemy się na adnotacjach języka AspectJ. Konfigurację opartą na XML-u opisujemy tylko w ramach porównania z tymi adnotacjami. Podstawowa metoda implementacji programowania aspektowego w Springu jest we wszystkich wersjach tej platformy taka sama — są to dynamiczne jednostki pośredniczące. Oznacza to, że programowanie aspektowe w Springu jest zgodne wstecz. Można więc we wszystkich wersjach Springa stosować klasyczne rady, punkty przecięcia i automatyczne generowanie jednostek pośredniczących. AspectJ został przekształcony w kompletną i popularną platformę do programowania aspektowego, a Spring obsługuje aspekty w postaci obiektów POJO utworzone za pomocą adnotacji języka AspectJ. Ponieważ takie adnotacje są obsługiwane w coraz większej liczbie platform do programowania aspektowego, aspekty oparte na AspectJ można ponownie wykorzystać w innych platformach obsługujących ten język. Warto jednak pamiętać, że choć można stosować aspekty języka AspectJ przy programowaniu aspektowym w Springu, to nie to samo co używanie takich aspektów w platformie AspectJ. Występują pewne ograniczenia związane ze stosowaniem aspektów języka AspectJ w Springu, ponieważ w Springu można ich używać tylko do ziaren zadeklarowanych w kontenerze IoC. Jeśli chcesz korzystać z aspektów w innym kontekście, musisz użyć platformy AspectJ (omawiamy ją w końcowej części tego rozdziału). Po zakończeniu lektury tego rozdziału będziesz potrafił pisać aspekty w postaci obiektów POJO na potrzeby programowania aspektowego w Springu. Nauczysz się też korzystać z platformy AspectJ w aplikacjach Springa.
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
3.1. Włączanie obsługi adnotacji języka AspectJ w Springu Problem Spring umożliwia stosowanie w programowaniu aspektowym aspektów w postaci obiektów POJO utworzonych za pomocą adnotacji języka AspectJ. Najpierw jednak trzeba włączyć obsługę takich adnotacji w kontenerze IoC.
Rozwiązanie Aby włączyć w kontenerze IoC obsługę adnotacji języka AspectJ, wystarczy zdefiniować pusty element XML-a w pliku z konfiguracją ziarna. Wtedy Spring automatycznie utworzy jednostki pośredniczące dla wszystkich ziaren pasujących do aspektów języka AspectJ. Gdy interfejsy są niedostępne lub gdy nie używa się ich w projekcie aplikacji, można utworzyć jednostki pośredniczące za pomocą biblioteki CGLIB. Aby włączyć obsługę tej biblioteki, należy ustawić atrybut proxy-target-class=true w elemencie .
Jak to działa? Poniższe interfejsy kalkulatora posłużą dalej do pokazania, jak włączyć obsługę języka AspectJ w Springu. package com.apress.springrecipes.calculator; public interface ArithmeticCalculator { public public public public
double double double double
add(double sub(double mul(double div(double
a, a, a, a,
double double double double
b); b); b); b);
} package com.apress.springrecipes.calculator; public interface UnitCalculator { public double kilogramToPound(double kilogram); public double kilometerToMile(double kilometer); }
Warto też dodać implementację każdego z tych interfejsów i umieścić w niej instrukcje println, aby było wiadomo, kiedy poszczególne metody są wykonywane. package com.apress.springrecipes.calculator; public class ArithmeticCalculatorImpl implements ArithmeticCalculator { public double add(double a, double b) { double result = a + b; System.out.println(a + " + " + b + " = " + result); return result; } public double sub(double a, double b) { double result = a - b; System.out.println(a + " - " + b + " = " + result);
120
3.1. WŁĄCZANIE OBSŁUGI ADNOTACJI JĘZYKA ASPECTJ W SPRINGU
return result; } public double mul(double a, double b) { double result = a * b; System.out.println(a + " * " + b + " = " + result); return result; } public double div(double a, double b) { if (b == 0) { throw new IllegalArgumentException("Dzielenie przez zero"); } double result = a / b; System.out.println(a + " / " + b + " = " + result); return result; } } package com.apress.springrecipes.calculator; public class UnitCalculatorImpl implements UnitCalculator { public double kilogramToPound(double kilogram) { double pound = kilogram * 2.2; System.out.println("Liczba kilogramów " + kilogram + " = Liczba funtów " + pound); return pound; } public double kilometerToMile(double kilometer) { double mile = kilometer * 0.62; System.out.println("Liczba kilometrów " + kilometer + " = Liczba mil " + mile); return mile; } }
Aby włączyć dla tej aplikacji obsługę adnotacji języka AspectJ, wystarczy w pliku z konfiguracją ziarna zdefiniować pusty element XML-a . Ponadto trzeba dodać do głównego elementu definicję schematu aop. Gdy kontener IoC wykryje w pliku z konfiguracją ziarna element , automatycznie utworzy dla ziaren jednostki pośredniczące pasujące do aspektów języka AspectJ. Uwaga Aby można było stosować w aplikacjach Springa adnotacje języka AspectJ, trzeba podać w parametrze classpath odpowiednie zależności. Jeśli korzystasz z Mavena, dodaj poniższe deklaracje w pliku pom.xml projektu Mavena: org.springframework spring-aop ${spring.version}
3.2. Deklarowanie aspektów za pomocą adnotacji języka AspectJ Problem Od czasu scalenia języka AspectJ (w wersji 5) z platformą AspectWerkz AspectJ obsługuje aspekty napisane jako obiekty POJO z adnotacjami z tego języka. Aspekty tego rodzaju są też obsługiwane przez platformę Spring, jednak trzeba je zarejestrować w kontenerze IoC.
Rozwiązanie Aby zarejestrować w Springu aspekty języka AspectJ, wystarczy zadeklarować je jako egzemplarze ziaren w kontenerze IoC. Gdy w tym kontenerze jest włączona obsługa języka AspectJ, kontener tworzy jednostki pośredniczące dla ziaren pasujących do stosowanych aspektów tego języka. Aspekt oparty na adnotacjach języka AspectJ to zwykła klasa Javy z adnotacją @Aspect. Rada to prosta metoda Javy z jedną z adnotacji oznaczających rady. AspectJ udostępnia pięć adnotacji tego rodzaju: @Before, @After, @AfterReturning, @AfterThrowing i @Around.
Jak to działa? Rady typu Before Aby utworzyć radę typu Before w celu obsługi zagadnień przecinających (ang. crosscutting concern) przed określonymi punktami wykonania w programie, należy zastosować adnotację @Before i podać wyrażenie z punktem przecięcia jako wartość tej adnotacji. package com.apress.springrecipes.calculator; import import import import
org.apache.commons.logging.Log; org.apache.commons.logging.LogFactory; org.aspectj.lang.annotation.Aspect; org.aspectj.lang.annotation.Before;
@Aspect public class CalculatorLoggingAspect { private Log log = LogFactory.getLog(this.getClass()); @Before("execution(* ArithmeticCalculator.add(..))") public void logBefore() {
122
3.2. DEKLAROWANIE ASPEKTÓW ZA POMOCĄ ADNOTACJI JĘZYKA ASPECTJ
log.info("Początek metody add()"); } }
Uwaga Aby generowane były komunikaty dziennika, trzeba odpowiednio skonfigurować system Log4J. W podstawowym pliku z konfiguracją tego systemu (domyślnie jest to plik log4j.properties) powinny znaleźć się następujące wiersze: log4j.rootLogger=DEBUG, A1 log4j.appender.A1=org.apache.log4j.ConsoleAppender log4j.appender.A1.layout=org.apache.log4j.PatternLayout
Zastosowane wyrażenie z punktem przecięcia pasuje do metody add() interfejsu ArithmeticCalculator. Symbol wieloznaczny w tym wyrażeniu pasuje do dowolnego modyfikatora ( public, protected lub private) i dowolnego typu zwracanej wartości. Dwie kropki na liście argumentów pasują do dowolnej liczby argumentów. Aby zarejestrować ten aspekt, wystarczy zadeklarować odpowiadający mu egzemplarz ziarna w kontenerze IoC. Ziarno aspektu może być anonimowe, jeśli w innych ziarnach nie są potrzebne referencje do niego. ...
Za pomocą poniższej klasy Main możesz przetestować ten aspekt: package com.apress.springrecipes.calculator; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); ArithmeticCalculator arithmeticCalculator = (ArithmeticCalculator) context.getBean("arithmeticCalculator"); arithmeticCalculator.add(1, 2); arithmeticCalculator.sub(4, 3); arithmeticCalculator.mul(2, 3); arithmeticCalculator.div(4, 2); UnitCalculator unitCalculator = (UnitCalculator) context.getBean("unitCalculator"); unitCalculator.kilogramToPound(10); unitCalculator.kilometerToMile(5); } }
Punkty wykonania pasujące do punktów przecięcia to punkty złączenia (ang. join point). W tym kontekście punkt przecięcia jest wyrażeniem odpowiadającym zbiorowi punktów złączenia, natomiast rada to działanie wykonywane w danym punkcie złączenia. Aby rada miała dostęp do szczegółowych informacji o bieżącym punkcie złączenia, można zadeklarować w niej argument typu JoinPoint. Zapewnia to dostęp do informacji o punkcie złączenia (na przykład do nazwy metody i wartości argumentów). Następnie można rozbudować punkt przecięcia w taki sposób, by pasował do wszystkich metod. W tym celu wystarczy zmienić nazwę klasy i nazwę metody na symbole wieloznaczne.
123
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
package com.apress.springrecipes.calculator; ... import java.util.Arrays; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class CalculatorLoggingAspect { ... @Before("execution(* *.*(..))") public void logBefore(JoinPoint joinPoint) { log.info("Metoda " + joinPoint.getSignature().getName() + "() rozpoczyna się od " + Arrays.toString(joinPoint.getArgs())); } }
Rady typu After Rady typu After są wykonywane po zakończeniu wykonywania punktu złączenia (gdy punkt złączenia zwróci wynik lub zgłosi wyjątek). Poniższa rada tego rodzaju rejestruje zakończenie pracy metody kalkulatora. W aspekcie można umieścić dowolną liczbę rad. package com.apress.springrecipes.calculator; ... import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; @Aspect public class CalculatorLoggingAspect { ... @After("execution(* *.*(..))") public void logAfter(JoinPoint joinPoint) { log.info("Metoda " + joinPoint.getSignature().getName() + "() kończy pracę"); } }
Rady typu AfterReturning Rady After są wykonywane niezależnie od tego, czy punkt złączenia zwrócił sterowanie w normalny sposób, czy zgłosił wyjątek. Jeśli chcesz rejestrować informacje tylko po standardowym zwróceniu sterowania, powinieneś zastąpić radę After radą typu AfterReturning. package com.apress.springrecipes.calculator; ... import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; @Aspect public class CalculatorLoggingAspect { ... @AfterReturning("execution(* *.*(..))") public void logAfterReturning(JoinPoint joinPoint) { log.info("Metoda " + joinPoint.getSignature().getName() + "() kończy pracę"); } }
124
3.2. DEKLAROWANIE ASPEKTÓW ZA POMOCĄ ADNOTACJI JĘZYKA ASPECTJ
W radach AfterReturning można uzyskać dostęp do wartości zwróconej przez punkt złączenia. W tym celu należy dodać do adnotacji @AfterReturning atrybut returning. Wartością tego atrybutu powinna być nazwa argumentu rady, do której ma zostać przekazana zwrócona wartość. Następnie należy dodać do sygnatury rady argument o tej nazwie. W czasie wykonywania programu Spring przekaże zwróconą wartość za pomocą tego właśnie argumentu. Warto też zauważyć, że w atrybucie pointcut należy umieścić pierwotne wyrażenie z punktem przecięcia. package com.apress.springrecipes.calculator; ... import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; @Aspect public class CalculatorLoggingAspect { ... @AfterReturning( pointcut = "execution(* *.*(..))", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { log.info("Metoda " + joinPoint.getSignature().getName() + "() zwróciła wartość " + result); } }
Rady typu AfterThrowing Rada typu AfterThrowing jest wywoływana tylko po zgłoszeniu wyjątku w punkcie złączenia. package com.apress.springrecipes.calculator; ... import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; @Aspect public class CalculatorLoggingAspect { ... @AfterThrowing("execution(* *.*(..))") public void logAfterThrowing(JoinPoint joinPoint) { log.error("Zgłoszono wyjątek w metodzie " + joinPoint.getSignature().getName() + "()"); } }
Do wyjątku zgłoszonego w punkcie złączenia można uzyskać dostęp po dodaniu atrybutu throwing do adnotacji @AfterThrowing. Typ Throwable to nadklasa wszystkich błędów i wyjątków Javy. Dlatego poniższa rada przechwytuje wszystkie błędy i wyjątki zgłoszone w punktach złączenia. package com.apress.springrecipes.calculator; ... import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; @Aspect public class CalculatorLoggingAspect { ... @AfterThrowing( pointcut = "execution(* *.*(..))", throwing = "e")
125
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) { log.error("Wyjątek " + e + " został zgłoszony w metodzie " + joinPoint.getSignature().getName() + "()"); } }
Jeśli interesują Cię tylko wyjątki określonego typu, możesz podać ten typ jako argument w deklaracji. Wtedy rada zostanie wywołana wyłącznie w odpowiedzi na wystąpienie wyjątków zgodnego typu (czyli podanego typu i jego typów pochodnych). package com.apress.springrecipes.calculator; ... import java.util.Arrays; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; @Aspect public class CalculatorLoggingAspect { ... @AfterThrowing( pointcut = "execution(* *.*(..))", throwing = "e") public void logAfterThrowing(JoinPoint joinPoint, IllegalArgumentException e) { log.error("Niedozwolony argument " + Arrays.toString(joinPoint.getArgs()) + " w metodzie " + joinPoint.getSignature().getName() + "()"); } }
Rady typu Around Ostatnim typem rad są rady Around. Rady tego rodzaju dają największe możliwości. Zapewniają pełną kontrolę nad punktem złączenia, dlatego pozwalają umieścić w jednym miejscu wszystkie operacje opisane w radach pozostałych typów. Można nawet kontrolować to, od jakiego momentu (i czy w ogóle) kontynuować wykonywanie pierwotnego punktu złączenia. Poniższa rada Around jest połączeniem utworzonych wcześniej rad Before, AfterReturning i AfterThrowing. Warto zauważyć, że w radzie Around typem punktu złączenia musi być ProceedingJoinPoint. Jest to interfejs pochodny od interfejsu JoinPoint, umożliwiający kontrolowanie momentu wznowienia wykonywania pierwotnego punktu złączenia. package com.apress.springrecipes.calculator; ... import java.util.Arrays; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @Aspect public class CalculatorLoggingAspect { ... @Around("execution(* *.*(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { log.info("Metoda " + joinPoint.getSignature().getName() + "() rozpoczyna się od " + Arrays.toString(joinPoint.getArgs())); try { Object result = joinPoint.proceed();
126
3.3. DOSTĘP DO INFORMACJI O PUNKCIE ZŁĄCZENIA
log.info("Metoda " + joinPoint.getSignature().getName() + "() zwraca wartość " + result); return result; } catch (IllegalArgumentException e) { log.error("Niedozwolony argument " + Arrays.toString(joinPoint.getArgs()) + " w metodzie " + joinPoint.getSignature().getName() + "()"); throw e; } } }
Rady Around dają bardzo duże możliwości. Pozwalają nawet zmodyfikować wartości argumentów i zmienić zwracaną wartość. Przy stosowaniu rad tego typu należy zachować dużą ostrożność, ponieważ łatwo jest zapomnieć o dodaniu wywołania, które pozwala na kontynuowanie pracy pierwotnego punktu złączenia. Wskazówka Przy wyborze typu rady należy zdecydować się na najprostszy typ, który spełnia wymagania programisty.
3.3. Dostęp do informacji o punkcie złączenia Problem W programowaniu aspektowym rady są stosowane do różnych punktów wykonania programu, zwanych punktami złączenia. Aby rada mogła wykonać odpowiednie operacje, często potrzebuje szczegółowych informacji na temat punktu złączenia.
Rozwiązanie By uzyskać w radzie dostęp do informacji o punkcie złączenia, należy w sygnaturze rady zadeklarować argument typu org.aspectj.lang.JoinPoint.
Jak to działa? W przedstawionej dalej radzie dostępne są informacje na temat punktu złączenia. Informacje te obejmują: rodzaj punktu złączenia (w Springu używane są tylko punkty method-execution), sygnaturę metody (zadeklarowany typ i nazwę metody), wartości argumentów, a także obiekt docelowy i obiekt pośredniczący. package com.apress.springrecipes.calculator; ... import java.util.Arrays; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class CalculatorLoggingAspect { ... @Before("execution(* *.*(..))") public void logJoinPoint(JoinPoint joinPoint) { log.info("Rodzaj punktu złączenia: " + joinPoint.getKind());
127
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
log.info("Zadeklarowany typ z sygnatury: " + joinPoint.getSignature().getDeclaringTypeName()); log.info("Nazwa z sygnatury: " + joinPoint.getSignature().getName()); log.info("Argumenty: " + Arrays.toString(joinPoint.getArgs())); log.info("Klasa docelowa: " + joinPoint.getTarget().getClass().getName()); log.info("Klasa obiektu this: " + joinPoint.getThis().getClass().getName()); } }
Pierwotne ziarno, umieszczone w obiekcie pośredniczącym, jest nazywane obiektem docelowym, natomiast obiekt pośredniczący to obiekt this. Dostęp do tych obiektów można uzyskać za pomocą metod getTarget() i getThis() punktu złączenia. W poniższych danych wyjściowych widać, że są to obiekty różnych klas. Rodzaj punktu złączenia: method-execution Typ zadeklarowany w sygnaturze: com.apress.springrecipes.calculator.ArithmeticCalculator Nazwa z sygnatury: add Argumenty: [1.0, 2.0] Klasa docelowa: com.apress.springrecipes.calculator.ArithmeticCalculatorImpl Klasa obiektu this: $Proxy6
3.4. Określanie pierwszeństwa aspektów Problem Gdy do danego punktu złączenia jest stosowanych kilka aspektów, kolejność ich działania jest domyślnie niezdefiniowana. Trzeba ją określić samodzielnie.
Rozwiązanie Pierwszeństwo aspektów można ustawić albo dzięki implementacji interfejsu Ordered, albo za pomocą adnotacji @Order.
Jak to działa? Załóżmy, że napisałeś nowy aspekt do sprawdzania poprawności argumentów kalkulatora. Aspekt ten obejmuje tylko jedną radę (typu Before). package com.apress.springrecipes.calculator; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class CalculatorValidationAspect { @Before("execution(* *.*(double, double))") public void validateBefore(JoinPoint joinPoint) { for (Object arg : joinPoint.getArgs()) { validate((Double) arg); }
128
3.4. OKREŚLANIE PIERWSZEŃSTWA ASPEKTÓW
} private void validate(double a) { if (a < 0) { throw new IllegalArgumentException("Dozwolone są tylko liczby dodatnie"); } } }
Aby zarejestrować ten aspekt w Springu, wystarczy zadeklarować w pliku z konfiguracją ziarna egzemplarz ziarna odpowiadający temu aspektowi. ...
Na tym etapie pierwszeństwo aspektów jest niezdefiniowane. Warto wiedzieć, że nie zależy ono od kolejności deklaracji ziaren. Dlatego aby określić pierwszeństwo aspektów, trzeba w obu z nich zaimplementować interfejs Ordered. Niższa wartość zwracana przez metodę getOrder() reprezentuje wyższy priorytet. Dlatego jeśli chcesz, by najpierw działał aspekt sprawdzający poprawność, powinien on zwracać niższą wartość niż aspekt rejestrujący informacje. package com.apress.springrecipes.calculator; ... import org.springframework.core.Ordered; @Aspect public class CalculatorValidationAspect implements Ordered { ... public int getOrder() { return 0; } } package com.apress.springrecipes.calculator; ... import org.springframework.core.Ordered; @Aspect public class CalculatorLoggingAspect implements Ordered { ... public int getOrder() { return 1; } }
Inny sposób na określenie pierwszeństwa to zastosowanie adnotacji @Order. Wtedy kolejność aspektów należy zapisać jako wartość adnotacji. package com.apress.springrecipes.calculator; ... import org.springframework.core.annotation.Order; @Aspect @Order(0) public class CalculatorValidationAspect {
129
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
... } package com.apress.springrecipes.calculator; ... import org.springframework.core.annotation.Order; @Aspect @Order(1) public class CalculatorLoggingAspect { ... }
3.5. Ponowne wykorzystanie definicji punktu przecięcia Problem W trakcie pisania aspektów w języku AspectJ można bezpośrednio zagnieździć wyrażenie z punktem przecięcia w adnotacji rady. To samo wyrażenie można umieścić w wielu radach.
Rozwiązanie Podobnie jak wiele innych narzędzi do programowania aspektowego, język AspectJ umożliwia definiowanie niezależnych punktów przecięcia, które można wykorzystać w wielu radach.
Jak to działa? W aspekcie w języku AspectJ punkt przecięcia można zadeklarować jako prostą metodę opatrzoną adnotacją @Pointcut. Taka metoda jest zazwyczaj pusta, ponieważ nie powinno się łączyć definicji punktu przecięcia z logiką aplikacji. Modyfikator dostępu zastosowany do tej metody określa także dostępność danego punktu przecięcia. W innych radach punkt przecięcia można wywoływać za pomocą nazwy odpowiadającej mu metody. package com.apress.springrecipes.calculator; ... import org.aspectj.lang.annotation.Pointcut; @Aspect public class CalculatorLoggingAspect { ... @Pointcut("execution(* *.*(..))") private void loggingOperation() {} @Before("loggingOperation()") public void logBefore(JoinPoint joinPoint) { ... } @AfterReturning( pointcut = "loggingOperation()", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { ... }
130
3.5. PONOWNE WYKORZYSTANIE DEFINICJI PUNKTU PRZECIĘCIA
@AfterThrowing( pointcut = "loggingOperation()", throwing = "e") public void logAfterThrowing(JoinPoint joinPoint, IllegalArgumentException e) { ... } @Around("loggingOperation()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { ... } }
Jeśli punkty przecięcia są współużytkowane przez kilka aspektów, najlepiej jest umieścić te punkty w jednej klasie. Wtedy trzeba je zadeklarować jako publiczne (modyfikator public). package com.apress.springrecipes.calculator; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; @Aspect public class CalculatorPointcuts { @Pointcut("execution(* *.*(..))") public void loggingOperation() {} }
Gdy podajesz ten punkt przecięcia, musisz określić także nazwę klasy. Jeśli znajduje się ona w innym pakiecie niż dany aspekt, trzeba ponadto podać nazwę pakietu. package com.apress.springrecipes.calculator; ... @Aspect public class CalculatorLoggingAspect { ... @Before("CalculatorPointcuts.loggingOperation()") public void logBefore(JoinPoint joinPoint) { ... } @AfterReturning( pointcut = "CalculatorPointcuts.loggingOperation()", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { ... } @AfterThrowing( pointcut = "CalculatorPointcuts.loggingOperation()", throwing = "e") public void logAfterThrowing(JoinPoint joinPoint, IllegalArgumentException e) { ... } @Around("CalculatorPointcuts.loggingOperation()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { ... } }
131
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
3.6. Pisanie wyrażeń z punktami przecięcia w języku AspectJ Problem Zagadnienia przecinające mogą występować w różnych miejscach wykonywania programu (w tak zwanych punktach złączenia). Z powodu różnorodności punktów złączenia potrzebny jest rozbudowany język wyrażeń, który umożliwi znajdowanie odpowiednich punktów.
Rozwiązanie Język punktów przecięcia stosowany w AspectJ to rozbudowany język wyrażeń, który pozwala znajdować różnego rodzaju punkty złączenia. Jednak Spring AOP obsługuje dla ziaren zadeklarowanych w kontenerze IoC tylko punkty złączenia związane z wykonywaniem metod. Dlatego omawiane są tu jedynie wyrażenia z punktami przecięcia obsługiwane w Springu. Kompletne omówienie języka punktów przecięcia z AspectJ znajdziesz w poradniku dla programistów w witrynie poświęconej AspectJ (http://www.eclipse.org/aspectj/). W programowaniu aspektowym w Springu język punktów przecięcia z AspectJ jest używany do definiowania punktów przecięcia. Spring interpretuje wyrażenia z punktami przecięcia w czasie wykonywania programu za pomocą biblioteki z języka AspectJ. W trakcie pisania w Springu wyrażeń z punktami przecięcia z AspectJ trzeba pamiętać o tym, że Spring dla ziaren z kontenera IoC obsługuje tylko punkty złączenia związane z wykonywaniem metod. Jeśli zastosujesz inne wyrażenie z punktem przecięcia, wystąpi wyjątek IllegalArgumentException.
Jak to działa? Wzorce reprezentujące sygnatury metod Najbardziej typowe wyrażenia z punktami przecięcia służą do dopasowywania metod na podstawie ich sygnatur. Na przykład poniższe wyrażenie pasuje do wszystkich metod zadeklarowanych w interfejsie ArithmeticCalculator. Początkowy symbol wieloznaczny pasuje do metod z dowolnym modyfikatorem (public, protected lub private) oraz o dowolnym typie zwracanej wartości. Dwie kropki na liście argumentów pasują do dowolnej liczby argumentów. execution(* com.apress.springrecipes.calculator.ArithmeticCalculator.*(..))
Jeśli docelowa klasa (lub docelowy interfejs) znajduje się w tym samym pakiecie co aspekt, nazwę pakietu można pominąć. execution(* ArithmeticCalculator.*(..))
Poniższe wyrażenie z punktem przecięcia pasuje do wszystkich metod publicznych zadeklarowanych w interfejsie ArithmeticCalculator. execution(public * ArithmeticCalculator.*(..))
Można też ograniczyć typ wartości zwracanej przez metodę. Na przykład poniższy punkt przecięcia pasuje do metod zwracających wartości typu double. execution(public double ArithmeticCalculator.*(..))
Ponadto można ograniczyć listę argumentów metod. Poniższy punkt przecięcia pasuje do metod, których pierwszy argument jest typu double. Umieszczone dalej dwie kropki pasują do dowolnej liczby kolejnych argumentów. execution(public double ArithmeticCalculator.*(double, ..))
132
3.6. PISANIE WYRAŻEŃ Z PUNKTAMI PRZECIĘCIA W JĘZYKU ASPECTJ
Aby dopasować punkt przecięcia, możesz też podać wszystkie typy argumentów z sygnatury metody. execution(public double ArithmeticCalculator.*(double, double))
Choć język punktów przecięcia z AspectJ daje dużo możliwości w zakresie dopasowywania różnych punktów złączenia, czasem nie da się znaleźć żadnych wspólnych cech (na przykład modyfikatorów, typów zwracanych wartości, wzorców nazw metod lub argumentów) szukanych metod. Wtedy można zastosować do tych metod niestandardową adnotację. Poniżej pokazano przykładową adnotację znacznikową. Można ją stosować zarówno na poziomie metody, jak i na poziomie typu. package com.apress.springrecipes.calculator; import import import import import
java.lang.annotation.Documented; java.lang.annotation.ElementType; java.lang.annotation.Retention; java.lang.annotation.RetentionPolicy; java.lang.annotation.Target;
@Target( { ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LoggingRequired { }
Następnie można opatrzyć tą adnotacją wszystkie metody wymagające rejestrowania informacji. Zauważ, że adnotacje należy tu dodać do klasy z implementacją, a nie do interfejsu (ponieważ te adnotacje nie są dziedziczone). package com.apress.springrecipes.calculator; public class ArithmeticCalculatorImpl implements ArithmeticCalculator { @LoggingRequired public double add(double a, double b) { ... } @LoggingRequired public double sub(double a, double b) { ... } @LoggingRequired public double mul(double a, double b) { ... } @LoggingRequired public double div(double a, double b) { ... } }
Teraz można napisać wyrażenie z punktem przecięcia pasujące do wszystkich metod opatrzonych adnotacją @LoggingRequired. @annotation(com.apress.springrecipes.calculator.LoggingRequired)
133
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
Wzorce reprezentujące sygnatury typów Inny rodzaj wyrażeń z punktami przecięcia pasuje do wszystkich punktów złączenia z określonych typów. W Springu zasięg takich punktów przecięcia jest zawężany do wszystkich wywołań metod danego typu. Na przykład poniższy punkt przecięcia pasuje do wszystkich metod punktów złączenia z pakietu com.apress.springrecipes.calculator związanych z wywołaniami. within(com.apress.springrecipes.calculator.*)
Aby dopasować punkty złączenia z pakietu i jego pakietów pochodnych, trzeba przed symbolem wieloznacznym dodać jeszcze jedną kropkę. within(com.apress.springrecipes.calculator..*)
Poniższe wyrażenie z punktem przecięcia pasuje do związanych z wywołaniami metod punktów złączenia z podanej klasy. within(com.apress.springrecipes.calculator.ArithmeticCalculatorImpl)
Jeśli klasa docelowa znajduje się w tym samym pakiecie co dany aspekt, nazwę pakietu można pominąć. within(ArithmeticCalculatorImpl)
Za pomocą symbolu plus można dopasować punkty złączania związane z wywołaniami metod ze wszystkich klas implementujących interfejs ArithmeticCalculator. within(ArithmeticCalculator+)
Niestandardową adnotację @LoggingRequired można zastosować na poziomie klasy zamiast na poziomie metody. package com.apress.springrecipes.calculator; @LoggingRequired public class ArithmeticCalculatorImpl implements ArithmeticCalculator { ... }
Następnie można dopasować punkty złączenia z klas opatrzonych adnotacją @LoggingRequired. @within(com.apress.springrecipes.calculator.LoggingRequired)
Wzorce nazw ziaren Począwszy od Springa 2.5, dostępny jest typ punktu przecięcia dopasowywany do nazw ziaren. Na przykład poniższe wyrażenie pasuje do ziaren o nazwie kończącej się członem Calculator. bean(*Calculator)
Ostrzeżenie Ten typ punktów przecięcia jest obsługiwany w Springu tylko w konfiguracji w XML-u (nie można go stosować w adnotacjach języka AspectJ).
Łączenie wyrażeń z punktami przecięcia W języku AspectJ wyrażenia z punktami przecięcia można łączyć z operatorami && (i), || (lub) oraz ! (nie). Na przykład poniższy punkt przecięcia pasuje do punktów złączenia z klas, które implementują interfejs ArithmeticCalculator lub UnitCalculator. within(ArithmeticCalculator+) || within(UnitCalculator+)
Operandami tych operatorów mogą być wyrażenia z punktami przecięcia lub referencje do innych punktów przecięcia.
134
3.6. PISANIE WYRAŻEŃ Z PUNKTAMI PRZECIĘCIA W JĘZYKU ASPECTJ
package com.apress.springrecipes.calculator; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; @Aspect public class CalculatorPointcuts { @Pointcut("within(ArithmeticCalculator+)") public void arithmeticOperation() {} @Pointcut("within(UnitCalculator+)") public void unitOperation() {} @Pointcut("arithmeticOperation() || unitOperation()") public void loggingOperation() {} }
Deklarowanie parametrów punktów przecięcia Jednym ze sposobów na dostęp do informacji o punkcie złączenia jest wykorzystanie refleksji (poprzez argument typu org.aspectj.lang.JoinPoint w radzie). Ponadto informacje te są dostępne deklaratywnie, za pomocą specjalnych wyrażeń z punktami przecięcia. Na przykład wyrażenia target() i args() pobierają docelowy obiekt i wartości argumentów bieżącego punktu złączenia, a następnie udostępniają je jako parametry punktu przecięcia. Parametry te są przekazywane do rady w postaci argumentów o analogicznych nazwach. package com.apress.springrecipes.calculator; ... import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class CalculatorLoggingAspect { ... @Before("execution(* *.*(..)) && target(target) && args(a,b)") public void logParameter(Object target, double a, double b) { log.info("Docelowa klasa: " + target.getClass().getName()); log.info("Argumenty: " + a + ", " + b); } }
Jeśli chcesz zadeklarować niezależny punkt przecięcia udostępniający parametry, musisz podać je na liście argumentów metody powiązanej z tym punktem. package com.apress.springrecipes.calculator; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; @Aspect public class CalculatorPointcuts { ... @Pointcut("execution(* *.*(..)) && target(target) && args(a,b)") public void parameterPointcut(Object target, double a, double b) {} }
Każda rada dotycząca punktu przecięcia z parametrami ma dostęp do tych parametrów poprzez argumenty metody (mają one te same nazwy co parametry).
135
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
package com.apress.springrecipes.calculator; ... import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class CalculatorLoggingAspect { ... @Before("CalculatorPointcuts.parameterPointcut(target, a, b)") public void logParameter(Object target, double a, double b) { log.info("Docelowa klasa: " + target.getClass().getName()); log.info("Argumenty: " + a + ", " + b); } }
3.7. Dodawanie operacji do ziaren Problem Czasem grupa klas powinna wykonywać te same działania. W modelu obiektowym klasy te muszą dziedziczyć po tej samej klasie lub implementować ten sam interfejs. W programowaniu aspektowym problem można rozwiązać modułowo, za pomocą zagadnień przecinających. Ponadto mechanizm dziedziczenia w Javie pozwala klasie na dziedziczenie maksymalnie po jednej klasie bazowej. Nie można więc jednocześnie dziedziczyć operacji po kilku klasach z implementacją.
Rozwiązanie Wprowadzenie to specjalny rodzaj rad w programowaniu aspektowym. Technika ta pozwala na dynamiczne implementowanie interfejsów w obiektach. W tym celu należy podać klasę z implementacją danego interfejsu. Wygląda to tak, jakby obiekty w czasie wykonywania programu dziedziczyły operacje po klasie z implementacją. Ponadto można jednocześnie dodać do obiektu kilka interfejsów i klas z implementacjami. Pozwala to uzyskać ten sam efekt co wielodziedziczenie.
Jak to działa? Załóżmy, że istnieją dwa interfejsy, MacCalculator i MinCalculator, z operacjami max() i min(). package com.apress.springrecipes.calculator; public interface MaxCalculator { public double max(double a, double b); } package com.apress.springrecipes.calculator; public interface MinCalculator { public double min(double a, double b); }
Następnie potrzebna jest implementacja każdego interfejsu. Należy umieścić w niej instrukcje println informujące o wywołaniu poszczególnych metod. package com.apress.springrecipes.calculator; public class MaxCalculatorImpl implements MaxCalculator {
136
3.7. DODAWANIE OPERACJI DO ZIAREN
public double max(double a, double b) { double result = (a >= b) ? a : b; System.out.println("max(" + a + ", " + b + ") = " + result); return result; } } package com.apress.springrecipes.calculator; public class MinCalculatorImpl implements MinCalculator { public double min(double a, double b) { double result = (a <= b) ? a : b; System.out.println("min(" + a + ", " + b + ") = " + result); return result; } }
Teraz załóżmy, że chcesz, aby także klasa ArithmeticCalculatorImpl wykonywała operacje max() i min(). Ponieważ Java nie obsługuje wielodziedziczenia, klasa ta nie może jednocześnie dziedziczyć po klasach MaxCalculatorImpl i MinCalculatorImpl. Jedyne możliwe rozwiązanie to dziedziczenie po jednej klasie (na przykład MaxCalculatorImpl) i zaimplementowanie innego interfejsu (na przykład MinCalculator). W tym celu należy albo skopiować kod z implementacją, albo oddelegować obsługę danej operacji do klasy z potrzebną implementacją. W obu podejściach konieczne jest powtórzenie deklaracji metod. Dzięki wprowadzeniom można sprawić, aby klasa ArithmeticCalculatorImpl dynamicznie implementowała oba interfejsy, MaxCalculator i MinCalculator, za pomocą klas z implementacją, MaxCalculatorImpl i MinCalculatorImpl. Ma to ten sam efekt co wielodziedziczenie po klasach MaxCalculatorImpl i MinCalculatorImpl. Wprowadzenia są oparte na doskonałym pomyśle, dzięki któremu nie trzeba modyfikować klasy ArithmeticCalculatorImpl, aby dodać do niej nowe metody. To oznacza, że nawet bez dostępu do kodu źródłowego można dodawać do istniejących klas nowe metody. Wskazówka Może się zastanawiasz, jak wprowadzenia są obsługiwane w programowaniu aspektowym w Springu. Używane są do tego dynamiczne jednostki pośredniczące. Można określić grupę interfejsów implementowanych w takiej jednostce. Wprowadzenia dodają interfejs (na przykład MaxCalculator) do dynamicznej jednostki pośredniczącej. Gdy metody zadeklarowane w danym interfejsie są wywoływane dla tej jednostki, deleguje ona wywołania do używanej na zapleczu klasy z implementacją (na przykład MaxCalculatorImpl).
Wprowadzenia, podobnie jak rady, deklaruje się w aspektach. Możesz utworzyć nowy aspekt lub ponownie wykorzystać istniejący. Aby w danym aspekcie zadeklarować wprowadzenie, należy dodać do wybranego pola adnotację @DeclareParents. package com.apress.springrecipes.calculator; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.DeclareParents; @Aspect public class CalculatorIntroduction { @DeclareParents( value = "com.apress.springrecipes.calculator.ArithmeticCalculatorImpl", defaultImpl = MaxCalculatorImpl.class) public MaxCalculator maxCalculator; @DeclareParents( value = "com.apress.springrecipes.calculator.ArithmeticCalculatorImpl",
137
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
defaultImpl = MinCalculatorImpl.class) public MinCalculator minCalculator; }
Atrybut value w adnotacji @DeclareParents określa docelowe klasy dla danego wprowadzenia. Wprowadzany interfejs jest określony na podstawie typu pola z adnotacją. Klasę z implementacją nowego interfejsu wskazuje atrybut defaultImpl. Dwa dodane tu wprowadzenia pozwalają dynamicznie dołączyć do klasy ArithmeticCalculatorImpl dwa interfejsy. W atrybucie value adnotacji @DeclareParents można podać też wyrażenie języka AspectJ dopasowujące typ, aby wprowadzić interfejs do kilku klas. Na końcu trzeba pamiętać o tym, by zadeklarować egzemplarz danego aspektu w kontekście aplikacji. ...
Ponieważ do kalkulatora wprowadzono interfejsy MaxCalculator i MinCalculator, można zrzutować obiekt kalkulatora na typy tych interfejsów, aby wykonać obliczenia max() i min(). package com.apress.springrecipes.calculator; public class Main { public static void main(String[] args) { ... ArithmeticCalculator arithmeticCalculator = (ArithmeticCalculator) context.getBean("arithmeticCalculator"); ... MaxCalculator maxCalculator = (MaxCalculator) arithmeticCalculator; maxCalculator.max(1, 2); MinCalculator minCalculator = (MinCalculator) arithmeticCalculator; minCalculator.min(1, 2); } }
3.8. Dodawanie stanu do ziarna Problem Czasem programista chce dodać nowy stan do grupy istniejących obiektów, aby móc śledzić wybrane aspekty ich działania (na przykład liczbę wywołań, datę ostatniej modyfikacji itd.). Nie stanowi to problemu, jeśli wszystkie te obiekty mają tę samą klasę bazową. Jednak trudno jest dodać stan do różnych klas pochodzących z odmiennych hierarchii.
Rozwiązanie Możesz dodać do obiektów nowy interfejs oraz klasę implementacji z polem reprezentującym stan. Następnie należy napisać nową radę zmieniającą stan na podstawie występujących warunków.
Jak to działa? Załóżmy, że chcesz śledzić liczbę wywołań każdego obiektu kalkulatora. Ponieważ w pierwotnych klasach kalkulatora nie istnieje pole do przechowywania wartości licznika, trzeba dodać takie pole za pomocą technik programowania aspektowego. Najpierw utwórz interfejs z operacjami na liczniku.
138
3.8. DODAWANIE STANU DO ZIARNA
package com.apress.springrecipes.calculator; public interface Counter { public void increase(); public int getCount(); }
Następnie należy napisać prostą klasę z implementacją tego interfejsu. W tej klasie powinno znajdować się pole count przeznaczone na wartość licznika. package com.apress.springrecipes.calculator; public class CounterImpl implements Counter { private int count; public void increase() { count++; } public int getCount() { return count; } }
Aby do wszystkich kalkulatorów dodać interfejs Counter z implementacją CounterImpl, można napisać poniższe wprowadzenie z wyrażeniem dopasowującym typ do wszystkich implementacji kalkulatora. package com.apress.springrecipes.calculator; ... import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.DeclareParents; @Aspect public class CalculatorIntroduction { ... @DeclareParents( value = "com.apress.springrecipes.calculator.*CalculatorImpl", defaultImpl = CounterImpl.class) public Counter counter; }
To wprowadzenie dodaje implementację CounterImpl do wszystkich obiektów reprezentujących kalkulator. To jednak nie wystarczy, aby móc śledzić liczbę wywołań. Należy także zwiększać wartość licznika przy każdym wywołaniu metody kalkulatora. W tym celu można napisać radę typu After. Zauważ, że trzeba tu zastosować obiekt this, a nie obiekt docelowy, ponieważ tylko obiekt pośredniczący implementuje interfejs Counter. package com.apress.springrecipes.calculator; ... import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; @Aspect public class CalculatorIntroduction { ... @After("execution(* com.apress.springrecipes.calculator.*Calculator.*(..))" + " && this(counter)")
139
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
public void increaseCount(Counter counter) { counter.increase(); } }
W klasie Main można wyświetlić wartość licznika dla każdego z obiektów reprezentujących kalkulator. Wymaga to zrzutowania takich obiektów na typ Counter. package com.apress.springrecipes.calculator; public class Main { public static void main(String[] args) { ... ArithmeticCalculator arithmeticCalculator = (ArithmeticCalculator) context.getBean("arithmeticCalculator"); ... UnitCalculator unitCalculator = (UnitCalculator) context.getBean("unitCalculator"); ... Counter arithmeticCounter = (Counter) arithmeticCalculator; System.out.println(arithmeticCounter.getCount()); Counter unitCounter = (Counter) unitCalculator; System.out.println(unitCounter.getCount()); } }
3.9. Deklarowanie aspektów za pomocą konfiguracji w XML-u Problem W większości sytuacji deklarowanie aspektów przy użyciu adnotacji języka AspectJ jest dobrym rozwiązaniem. Jednak jeśli używasz maszyny JVM w wersji 1.4 lub starszej (wersje te nie obsługują adnotacji) albo nie chcesz, aby aplikacja była zależna od języka AspectJ, nie powinieneś stosować takich adnotacji.
Rozwiązanie W Springu aspekty można deklarować nie tylko za pomocą adnotacji języka AspectJ. Obsługiwane jest też deklarowanie aspektów w plikach z konfiguracją ziarna. Służą do tego elementy XML-a ze schematu aop. Standardowo zaleca się stosowanie deklaracji opartych na adnotacjach zamiast na XML-u. Dzięki adnotacjom języka AspectJ aspekty są zgodne z tym językiem, natomiast konfiguracja oparta na XML-u jest specyficzna dla Springa. Ponieważ język AspectJ jest obsługiwany w coraz większej liczbie platform do programowania aspektowego, łatwiej jest ponownie wykorzystać aspekty utworzone za pomocą adnotacji.
Jak to działa? Aby włączyć w Springu obsługę adnotacji języka AspectJ, zdefiniowałeś już w pliku z konfiguracją ziarna pusty element XML-a. Przy deklarowaniu aspektów w XML-u element ten nie jest potrzebny. Należy go usunąć, by Spring ignorował adnotacje języka AspectJ. Trzeba jednak zachować 140
3.9. DEKLAROWANIE ASPEKTÓW ZA POMOCĄ KONFIGURACJI W XML-U
definicję schematu aop w elemencie głównym , ponieważ zdefiniowane są w nim wszystkie XML-owe elementy związane z konfiguracją programowania aspektowego. --> ...
Deklarowanie aspektów W pliku z konfiguracją ziarna wszystkie ustawienia dotyczące programowania aspektowego trzeba umieścić w elemencie . Dla każdego aspektu należy utworzyć element określający egzemplarz ziarna z konkretną implementacją danego aspektu. Dlatego ziarna powiązane z aspektami muszą mieć identyfikator, aby można było go podać w elemencie . ...
Deklarowanie punktów przecięcia Punkt przecięcia można zdefiniować albo w elemencie , albo bezpośrednio w elemencie . W pierwszym z tych podejść punkt przecięcia jest dostępny tylko dla aspektu, w którym go zadeklarowano. W drugim przypadku punkt przecięcia jest globalny i mogą z niego korzystać wszystkie aspekty. Należy pamiętać, że konfiguracja programowania aspektowego oparta na XML-u (w odróżnieniu od adnotacji języka AspectJ) nie umożliwia wskazywania innych punktów przecięcia na podstawie nazw w wyrażeniach z takimi punktami. To oznacza, że trzeba skopiować docelowe wyrażenie i bezpośrednio je zagnieździć.
141
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
...
Przy stosowaniu adnotacji języka AspectJ dwa wyrażenia z punktami przecięcia można połączyć za pomocą operatora &&. Jednak w XML-u symbol & oznacza referencję, dlatego operator && w dokumentach XML nie jest dozwolony. Zamiast niego należy stosować słowo kluczowe and.
Deklarowanie rad W schemacie aop istnieją odrębne elementy XML-a odpowiadające radom poszczególnych typów. W takim elemencie należy podać albo atrybut pointcut-ref określający punkt przecięcia, albo atrybut pointcut z bezpośrednio zagnieżdżonym wyrażeniem z punktem przecięcia. Atrybut method określa nazwę odpowiadającej radzie metody z klasy aspektu. ...
Deklarowanie wprowadzeń Wprowadzenie można zadeklarować w aspekcie za pomocą elementu . ...
142
3.10. WPLATANIE ASPEKTÓW JĘZYKA ASPECTJ W SPRINGU W CZASIE ŁADOWANIA
3.10. Wplatanie aspektów języka AspectJ w Springu w czasie ładowania Problem Platforma programowania aspektowego w Springu obsługuje tylko niektóre typy punktów przecięcia z języka AspectJ i umożliwia stosowanie aspektów do ziaren zadeklarowanych w kontenerze IoC. Jeśli chcesz mieć dostęp do dodatkowych typów punktów przecięcia lub stosować aspekty do obiektów utworzonych poza kontenerem IoC, musisz w aplikacji Springa wykorzystać platformę AspectJ.
Rozwiązanie Wplatanie (ang. weaving) to proces stosowania aspektów do docelowych obiektów. W Springu wplatanie ma miejsce w czasie wykonywania programu i jest oparte na dynamicznych jednostkach pośredniczących. Platforma AspectJ obsługuje wplatanie w czasie kompilacji i w czasie ładowania. Przy wplataniu w czasie kompilacji w platformie AspectJ wykorzystuje się specjalny kompilator ajc z tej platformy. Pozwala on wplatać aspekty w pliki z kodem źródłowym w Javie i zwraca przeplecione binarne pliki z klasami. Ten kompilator potrafi też wplatać aspekty w pliki ze skompilowanymi klasami i w pliki JAR. Ten proces to wplatanie po kompilacji. Można zastosować wplatanie w czasie kompilacji lub po kompilacji do klas przed zadeklarowaniem ich w kontenerze IoC. Spring w ogóle nie uczestniczy w procesie wplatania. Więcej informacji o wplataniu w czasie kompilacji i po kompilacji znajdziesz w dokumentacji platformy AspectJ. Wplatanie w czasie ładowania w platformie AspectJ ma miejsce, gdy docelowe klasy są ładowane do maszyny JVM przez menedżer ładowania klas. Aby wpleść klasę, należy zastosować specjalny menedżer ładowania klas, który zmodyfikuje kod bajtowy docelowej klasy. Zarówno AspectJ, jak i Spring udostępniają mechanizmy wplatania w czasie ładowania, które pozwalają odpowiednio wzbogacić funkcje menedżera ładowania klas. Trzeba jedynie wprowadzić proste zmiany w konfiguracji, by umożliwić wplatanie w czasie ładowania.
143
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
Jak to działa? Aby zrozumieć oparty na języku AspectJ proces wplatania w czasie ładowania w aplikacjach Springa, utwórz kalkulator liczb zespolonych. Najpierw przygotuj klasę Complex reprezentującą liczby zespolone. W tej klasie należy zdefiniować metodę toString() przekształcającą liczby zespolone na ich reprezentację tekstową (a + bi). package com.apress.springrecipes.calculator; public class Complex { private int real; private int imaginary; public Complex(int real, int imaginary) { this.real = real; this.imaginary = imaginary; } // Gettery i settery ... public String toString() { return "(" + real + " + " + imaginary + "i)"; } }
Następnie należy zdefiniować interfejs do wykonywania operacji na liczbach zespolonych. Dla uproszczenia tu obsługiwane są tylko operacje add() i sub(). package com.apress.springrecipes.calculator; public interface ComplexCalculator { public Complex add(Complex a, Complex b); public Complex sub(Complex a, Complex b); }
Poniżej przedstawiony jest kod z implementacją tego interfejsu. Za każdym razem jako wynik zwracany jest nowy obiekt typu Complex. package com.apress.springrecipes.calculator; public class ComplexCalculatorImpl implements ComplexCalculator { public Complex add(Complex a, Complex b) { Complex result = new Complex(a.getReal() + b.getReal(), a.getImaginary() + b.getImaginary()); System.out.println(a + " + " + b + " = " + result); return result; } public Complex sub(Complex a, Complex b) { Complex result = new Complex(a.getReal() - b.getReal(), a.getImaginary() - b.getImaginary()); System.out.println(a + " - " + b + " = " + result); return result; } }
144
3.10. WPLATANIE ASPEKTÓW JĘZYKA ASPECTJ W SPRINGU W CZASIE ŁADOWANIA
Zanim będzie można użyć kalkulatora, trzeba zadeklarować go jako ziarno w kontenerze IoC.
Teraz można przetestować kalkulator liczb zespolonych za pomocą poniższego kodu klasy Main. package com.apress.springrecipes.calculator; ... public class Main { public static void main(String[] args) { ... ComplexCalculator complexCalculator = (ComplexCalculator) context.getBean("complexCalculator"); complexCalculator.add(new Complex(1, 2), new Complex(2, 3)); complexCalculator.sub(new Complex(5, 8), new Complex(2, 3)); } }
Do tej pory nowy kalkulator liczb zespolonych działa poprawnie. Można jednak poprawić jego wydajność dzięki zapisywaniu obiektów Complex w pamięci podręcznej. Ponieważ obsługa pamięci podręcznej to często spotykane zagadnienie przecinające, można zastosować podejście modułowe i umieścić potrzebny kod w aspekcie. package com.apress.springrecipes.calculator; import import import import import import
java.util.Collections; java.util.HashMap; java.util.Map; org.aspectj.lang.ProceedingJoinPoint; org.aspectj.lang.annotation.Around; org.aspectj.lang.annotation.Aspect;
@Aspect public class ComplexCachingAspect { private Map cache; public ComplexCachingAspect() { cache = Collections.synchronizedMap(new HashMap()); } @Around("call(public Complex.new(int, int)) && args(a,b)") public Object cacheAround(ProceedingJoinPoint joinPoint, int a, int b) throws Throwable { String key = a + "," + b; Complex complex = cache.get(key); if (complex == null) { System.out.println("BRAK w pamięci obiektu dla klucza (" + key + ")"); complex = (Complex) joinPoint.proceed(); cache.put(key, complex); } else { System.out.println("ZNALEZIONO w pamięci obiekt dla klucza (" + key + ")"); } return complex; } }
145
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
W tym aspekcie obiekty typu Complex są zapisywane w pamięci podręcznej w postaci odwzorowania, w którym kluczami są części rzeczywista i urojona. Aby odwzorowanie było bezpieczne ze względu na wątki, należy umieścić je w odwzorowaniu synchronizowanym. Najlepszym momentem na sprawdzanie zawartości pamięci podręcznej jest czas tworzenia obiektu typu Complex w wyniku wywołania konstruktora. Używane jest tu wyrażenie z punktem przecięcia call z języka AspectJ. Służy ono do przechwytywania punktów złączenia z wywołaniem konstruktora Complex(int, int). Wspomniany punkt przecięcia nie jest obsługiwany przez Spring, dlatego nie został opisany we wcześniejszej części tego rozdziału. Następnie potrzebna jest rada Around modyfikująca zwracaną wartość. Jeśli obiekt typu Complex o potrzebnej wartości występuje w pamięci podręcznej, można bezpośrednio zwrócić ten obiekt do jednostki wywołującej. W przeciwnym razie należy kontynuować wykonywanie pierwotnego konstruktora i utworzyć nowy obiekt typu Complex. Przed zwróceniem tego obiektu do jednostki wywołującej należy zapisać go w pamięci podręcznej na potrzeby późniejszych wywołań. Ponieważ używany tu punkt przecięcia nie jest obsługiwany przez Spring, trzeba wykorzystać platformę AspectJ, aby zastosować utworzony aspekt. Konfigurowanie platformy AspectJ odbywa się w pliku aop.xml w katalogu META-INF z katalogu głównego z parametru classpath.
W pliku z konfiguracją platformy AspectJ trzeba określić aspekty, a także podać, w które klasy należy je wpleść. Ta konfiguracja pozwala wpleść aspekt ComplexCachingAspect we wszystkie klasy z pakietu com.apress. springrecipes.calculator.
Wplatanie w czasie ładowania za pomocą agenta z platformy AspectJ AspectJ udostępnia agenta wplatania w czasie ładowania, co pozwala na wykonywanie tej operacji. Wystarczy dodać do polecenia uruchamiającego aplikację argument maszyny wirtualnej Javy, a agent wplecie potrzebny kod w klasy w momencie ich ładowania do tej maszyny. java -javaagent:c://lib/aspectjweaver.jar com.apress.springrecipes.calculator.Main
Uwaga Aby zastosować agenta wplatania z platformy AspectJ, trzeba w wywołaniu wskazać plik aspectjweaver.jar. Gdybyś chciał tylko wczytać plik .jar dostępny w parametrze classpath, mógłbyś dodać do projektu Mavena poniższą zależność: org.aspectj aspectjweaver 1.6.8
Jednak ponieważ plik trzeba dołączać w wywołaniu, zależność należy wczytywać ręcznie.
Jeśli uruchomisz aplikację z podanym wcześniej argumentem, uzyskasz przedstawione poniżej dane wyjściowe i informacje o stanie pamięci podręcznej. Agent AspectJ obsługuje wszystkie wywołania konstruktora Complex(int, int).
146
3.10. WPLATANIE ASPEKTÓW JĘZYKA ASPECTJ W SPRINGU W CZASIE ŁADOWANIA
BRAK w pamięci obiektu dla klucza (1,2) BRAK w pamięci obiektu dla klucza (2,3) BRAK w pamięci obiektu dla klucza (3,5) (1 + 2i) + (2 + 3i) = (3 + 5i) BRAK w pamięci obiektu dla klucza (5,8) ZNALEZIONO w pamięci obiekt dla klucza (2,3) ZNALEZIONO w pamięci obiekt dla klucza (3,5) (5 + 8i) - (2 + 3i) = (3 + 5i)
Wplatanie w czasie ładowania oparte na mechanizmach Springa Spring udostępnia kilka mechanizmów wplatania w czasie ładowania, przeznaczonych dla różnych środowisk. Aby włączyć mechanizm odpowiedni dla danej aplikacji, wystarczy zadeklarować pusty element XML-a . Element ten jest zdefiniowany w schemacie context. ...
Spring potrafi wykryć mechanizm najlepiej dopasowany do środowiska uruchomieniowego. Niektóre serwery aplikacji Javy EE mają menedżery ładowania klas obsługujące dostępny w Springu mechanizm wplatania w czasie ładowania, co pozwala pominąć agenta Javy w poleceniach uruchamiających aplikacje. Jednak w prostych aplikacji Javy konieczne jest wskazanie udostępnianego w Springu agenta, aby włączyć wplatanie w czasie ładowania. Agenta należy podać w argumencie maszyny wirtualnej w poleceniu uruchamiającym aplikację. java -javaagent:c:/lib/spring-instrument.jar com.apress.springrecipes.calculator.Main
Uwaga Aby zastosować agenta wplatania ze Springa, trzeba w wywołaniu wskazać plik spring-instrument.jar. Gdybyś chciał tylko wczytać plik .jar dostępny w parametrze classpath, mógłbyś dodać do projektu Mavena poniższą zależność: org.springframework spring-instrument ${spring.version}
Jednak ponieważ plik trzeba dołączać w wywołaniu, zależność należy wczytywać ręcznie.
Teraz gdy uruchomisz aplikację, uzyskasz przedstawione poniżej dane wyjściowe i informacje o stanie pamięci podręcznej. Wynikają one z tego, że agent Springa obsługuje tylko wywołania konstruktora Complex(int, int) z ziaren zadeklarowanych w kontenerze IoC. Ponieważ operandy typu Complex są tworzone w klasie Main, agent Springa nie obsługuje związanych z nimi wywołań konstruktora. BRAK w pamięci obiektu dla klucza (3,5) (1 + 2i) + (2 + 3i) = (3 + 5i) ZNALEZIONO w pamięci obiekt dla klucza (3,5) (5 + 8i) - (2 + 3i) = (3 + 5i)
147
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
3.11. Konfigurowanie w Springu aspektów języka AspectJ Problem Aspekty w Springu deklaruje się w pliku z konfiguracją ziarna, aby można je było łatwo ustawić. Jednak egzemplarze aspektów używane w platformie AspectJ są tworzone przez tę platformę. Dlatego trzeba pobrać takie egzemplarze z platformy AspectJ, by móc je skonfigurować.
Rozwiązanie Każdy aspekt w platformie AspectJ udostępnia statyczną metodę fabryczną aspectOf(), która zapewnia dostęp do bieżącego egzemplarza aspektu. W kontenerze IoC można za pomocą atrybutu factory-method zadeklarować ziarno utworzone przez metodę fabryczną.
Jak to działa? Możesz umożliwić konfigurowanie pamięci podręcznej z aspektu ComplexCachingAspect za pomocą settera i usunąć z konstruktora kod tworzący egzemplarze tego aspektu. package com.apress.springrecipes.calculator; ... import java.util.Collections; import java.util.Map; import org.aspectj.lang.annotation.Aspect; @Aspect public class ComplexCachingAspect { private Map cache; public void setCache(Map cache) { this.cache = Collections.synchronizedMap(cache); } ... }
Aby skonfigurować tę właściwość w kontenerze IoC, można zadeklarować ziarno utworzone przez metodę fabryczną aspectOf().
148
3.12. WSTRZYKIWANIE ZIAREN SPRINGA DO OBIEKTÓW DOMENOWYCH
Wskazówka Możliwe, że zastanawiasz się, dlaczego aspekt ComplexCachingAspect zawiera metodę aspectOf(), skoro jej nie zadeklarowałeś. Ta metoda jest wplatana przez platformę AspectJ w czasie ładowania, aby możliwy był dostęp do bieżącego egzemplarza aspektu. Dlatego jeśli używasz środowiska IDE Spring, może ono zgłosić ostrzeżenie, ponieważ w klasie wspomniana metoda jest niedostępna.
3.12. Wstrzykiwanie ziaren Springa do obiektów domenowych Problem Ziarna zadeklarowane w kontenerze IoC mogą łączyć się ze sobą dzięki dostępnemu w Springu mechanizmowi wstrzykiwania zależności. Jednak obiekty spoza kontenera IoC nie mogą łączyć się z ziarnami Springa na podstawie konfiguracji. Niezbędne jest ich ręczne połączenie za pomocą kodu.
Rozwiązanie Obiekty tworzone poza kontenerem IoC to zwykle obiekty domenowe. Często tworzy się je za pomocą operatora new lub na podstawie wyników zapytań do bazy danych. Aby wstrzyknąć ziarno Springa do obiektów domenowych utworzonych poza Springiem, trzeba wykorzystać mechanizmy programowania aspektowego. Wstrzykiwanie ziaren Springa jest pewnego rodzaju zagadnieniem przecinającym. Ponieważ obiekty domenowe nie są tworzone w Springu, nie można wstrzykiwać ziaren za pomocą mechanizmów samego Springa. Spring udostępnia na potrzeby wykonywania tego zadania specjalny aspekt języka AspectJ. Można go włączyć w platformie AspectJ.
Jak to działa? Załóżmy, że używasz globalnego formatera do formatowania liczb zespolonych. Formater ten przyjmuje wzorzec określający format liczby. package com.apress.springrecipes.calculator; public class ComplexFormatter { private String pattern; public void setPattern(String pattern) { this.pattern = pattern; } public String format(Complex complex) { return pattern.replaceAll("a", Integer.toString(complex.getReal())) .replaceAll("b", Integer.toString(complex.getImaginary())); } }
149
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
Następnie należy skonfigurować formater w kontenerze IoC i określić pożądany wzorzec.
W klasie Complex formater ma posłużyć do przekształcania liczb zespolonych na łańcuchy znaków w metodzie toString(). Klasa ta udostępnia setter obiektu klasy ComplexFormatter. package com.apress.springrecipes.calculator; public class Complex { private int real; private int imaginary; ... private ComplexFormatter formatter; public void setFormatter(ComplexFormatter formatter) { this.formatter = formatter; } public String toString() { return formatter.format(this); } }
Jednak ponieważ obiekty typu Complex nie są tworzone w kontenerze IoC, nie można ich skonfigurować pod kątem wstrzykiwania zależności. Trzeba więc napisać kod wstrzykujący egzemplarz typu ComplexFormatter do każdego obiektu typu Complex. Dobra wiadomość jest taka, że Spring w bibliotece aspektów udostępnia aspekt AnnotationBean ConfigurerAspect, przeznaczony do konfigurowania zależności dowolnych obiektów, nawet jeśli nie zostały one utworzone w kontenerze IoC. Przede wszystkim trzeba dodać do typu obiektu adnotację @Configurable, aby zadeklarować, że ten typ można konfigurować. package com.apress.springrecipes.calculator; import org.springframework.beans.factory.annotation.Configurable; @Configurable public class Complex { ... }
Spring udostępnia wygodny element XML-a, , w którym można włączyć wspomniany wcześniej aspekt. ...
150
3.12. WSTRZYKIWANIE ZIAREN SPRINGA DO OBIEKTÓW DOMENOWYCH
W trakcie tworzenia egzemplarza klasy opatrzonej adnotacją @Configurable aspekt szuka definicji ziarna o typie zgodnym z daną klasą, a następnie konfiguruje nowe egzemplarze na podstawie znalezionej definicji. Jeśli w definicji ziarna zadeklarowane są właściwości, aspekt ustawi te same właściwości w tworzonych egzemplarzach. Aspekt trzeba też włączyć w platformie AspectJ. Aspekt możesz za pomocą agenta Springa wpleść w klasy w czasie ich ładowania. java -javaagent:c:/lib/spring-instrument.jar com.apress.springrecipes.calculator.Main
Inny sposób wiązania konfigurowalnej klasy z definicją ziarna polega na wykorzystaniu identyfikatora ziarna. Możesz podać taki identyfikator jako wartość adnotacji @Configurable. package com.apress.springrecipes.calculator; import org.springframework.beans.factory.annotation.Configurable; @Configurable("complex") public class Complex { ... }
Następnie należy dodać atrybut id do odpowiedniej definicji ziarna, aby powiązać je z konfigurowalną klasą.
Podobnie jak zwykłe ziarna Springa, ziarna konfigurowalne także obsługują automatyczne łączenie i sprawdzanie zależności. package com.apress.springrecipes.calculator; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Configurable; @Configurable( value = "complex", autowire = Autowire.BY_TYPE, dependencyCheck = true) public class Complex { ... }
Warto zauważyć, że atrybut dependencyCheck jest typu logicznego, a nie wyliczeniowego. Jeśli ma wartość true, działa tak samo jak ustawienie dependency-check="objects" (powoduje sprawdzanie typów innych niż typy proste i kolekcje). Przy włączonym automatycznym łączeniu nie trzeba bezpośrednio ustawiać właściwości formatter.
Począwszy od Springa 2.5, nie trzeba już konfigurować automatycznego łączenia i sprawdzania zależności na poziomie klasy z adnotacją @Configurable. Zamiast tego można dodać adnotację @Autowired do settera w formaterze. package com.apress.springrecipes.calculator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Configurable;
151
ROZDZIAŁ 3. PROGRAMOWANIE ASPEKTOWE I OBSŁUGA JĘZYKA ASPECTJ W SPRINGU
@Configurable("complex") public class Complex { ... private ComplexFormatter formatter; @Autowired public void setFormatter(ComplexFormatter formatter) { this.formatter = formatter; } }
Następnie wystarczy dodać element do pliku z konfiguracją ziarna, aby przetwarzać metody opatrzone wspomnianymi adnotacjami. ...
Podsumowanie Z tego rozdziału dowiedziałeś się, jak pisać aspekty zarówno za pomocą adnotacji języka AspectJ, jak i przy użyciu plików XML z konfiguracją ziaren. Zobaczyłeś też, jak rejestrować aspekty w kontenerze IoC. Mechanizmy programowania aspektowego w Springu udostępniają pięć rodzajów rad: Before, After, AfterReturning, AfterThrowing i Around. Poznałeś też różne typy punktów przecięcia pozwalające dopasowywać punkty złączenia na podstawie: sygnatury metody, sygnatury typu i nazwy ziarna. Jednak mechanizmy programowania aspektowego w Springu obsługują dla ziaren zadeklarowanych w kontenerze IoC tylko punkty złączenia związane z wywołaniami metod. Jeśli użyjesz wyrażenia z punktem przecięcia spoza tego zasięgu, zostanie zgłoszony wyjątek. Wprowadzenie to specjalny rodzaj rad z programowania aspektowego. Wprowadzenie umożliwia dynamiczne implementowanie interfejsu w obiektach dzięki określeniu klasy z implementacją. Pozwala to uzyskać ten sam efekt co wielodziedziczenie. Wprowadzenia są często używane do dodawania operacji i stanu do grup istniejących obiektów. Jeśli chcesz wykorzystać punkt przecięcia o typie nieobsługiwanym w Springu lub zastosować aspekty do obiektów utworzonych poza kontenerem IoC, musisz użyć w aplikacji Springa platformy AspectJ. Aspekty można wplatać w klasy za pomocą agentów w czasie ładowania tych klas. Spring udostępnia w bibliotece kilka przydatnych aspektów języka AspectJ. Jeden z nich służy do wstrzykiwania ziaren Springa do obiektów domenowych utworzonych poza Springiem.
152
ROZDZIAŁ 4
Skrypty w Springu
Z tego rozdziału dowiesz się, jak w aplikacjach Springa korzystać z języków skryptowych. Spring obsługuje trzy różne języki skryptowe: JRuby, Groovy i BeanShell. Są to najpopularniejsze języki skryptowe w społeczności Javy. Dla większości programistów Javy opanowanie tych języków będzie łatwe. JRuby (http://jruby.codehaus.org/) to oparta na Javie otwarta implementacja popularnego języka programowania Ruby (http://www.ruby-lang.org/). JRuby zapewnia dwustronną komunikację między językami Java i Ruby, co oznacza, że można wywoływać skrypty języka Ruby bezpośrednio w programie Javy, a także korzystać z klas Javy w skryptach języka Ruby. Groovy (http://groovy.codehaus.org/) to dostępny w Javie dynamiczny język łączący funkcje kilku innych świetnych języków. Kod w języku Groovy można kompilować bezpośrednio do kodu bajtowego Javy. Ponadto Groovy może być stosowany jako dynamiczny język skryptowy. Składnia języka Groovy jest bardzo podobna do składni Javy, dlatego opanowanie języka Groovy nie powinno sprawiać programistom Javy żadnych trudności. Ponadto w języku Groovy można korzystać ze wszystkich klas i bibliotek Javy. BeanShell (http://www.beanshell.org/) to dostępny w Javie prosty język skryptowy. Pozwala dynamicznie wykonywać fragmenty kodu w Javie, a jednocześnie udostępnia mechanizmy znane z innych języków skryptowych. Dzięki językowi BeanShell można wykorzystać w skrypcie dynamiczny moduł z aplikacji Javy bez konieczności poznawania nowego języka. Po zakończeniu lektury tego rozdziału będziesz potrafił za pomocą wymienionych języków pisać fragmenty aplikacji Springa jako skrypty.
4.1. Implementowanie ziaren za pomocą języków skryptowych Problem Czasem aplikacja zawiera moduły, które wymagają częstego wprowadzania zmian. Jeśli zaimplementujesz te moduły w Javie, po wprowadzeniu każdej poprawki będziesz musiał ponownie skompilować, spakować i zainstalować aplikację. Możliwe, że nie będziesz mógł wykonać tych operacji w odpowiadającym Ci momencie, zwłaszcza jeśli aplikacja ma działać 24 godziny na dobę.
ROZDZIAŁ 4. SKRYPTY W SPRINGU
Rozwiązanie Pomyśl o zaimplementowaniu wymagających częstych zmian modułów za pomocą języków skryptowych. Zaletą tych języków jest to, że nie trzeba ponownie kompilować kodu po wprowadzeniu zmian. Dlatego wystarczy umieścić nowy skrypt w odpowiednim miejscu, aby program zaczął z niego korzystać. Spring umożliwia implementowanie ziaren za pomocą dowolnego z obsługiwanych języków skryptowych. Ziarno ze skryptu można skonfigurować w kontenerze IoC w taki sam sposób jak zwykłe ziarno zaimplementowane w Javie.
Jak to działa? Załóżmy, że chcesz utworzyć aplikację wymagającą obliczania odsetek. Najpierw należy zdefiniować poniższy interfejs InterestCalculator. package com.apress.springrecipes.interest; public interface InterestCalculator { public void setRate(double rate); public double calculate(double amount, double year); }
Zaimplementowanie tego interfejsu jest proste. Jednak ponieważ istnieje wiele sposobów naliczania odsetek, użytkownicy prawdopodobnie będą musieli często i dynamicznie zmieniać używaną implementację. Prawdopodobnie nie chcesz za każdym razem ponownie kompilować, pakować i instalować aplikacji. Dlatego warto rozważyć zaimplementowanie interfejsu za pomocą jednego z języków skryptowych obsługiwanych w Springu. Aby włączyć obsługę języków skryptowych, w pliku z konfiguracją ziarna Springa trzeba w elemencie głównym umieścić definicję schematu lang. ...
Spring 2.5 obsługuje trzy języki skryptowe: JRuby, Groovy i BeanShell. Dalej zobaczysz, jak za pomocą każdego z tych języków zaimplementować interfejs InterestCalculator. Dla uproszczenia przyjmijmy, że do naliczania odsetek służy poniższy prosty wzór: odsetki = kwota x oprocentowanie x lata
Skrypty z ziarnami w języku JRuby Najpierw zaimplementuj interfejs InterestCalculator za pomocą języka JRuby. W tym celu utwórz skrypt w tym języku, SimpleInterestCalculator.rb, i umieść go w pakiecie com.apress.springrecipes.interest określonym w parametrze classpath. class SimpleInterestCalculator def setRate(rate) @rate = rate end
154
4.1. IMPLEMENTOWANIE ZIAREN ZA POMOCĄ JĘZYKÓW SKRYPTOWYCH
def calculate(amount, year) amount * year * @rate end end SimpleInterestCalculator.new
Ten skrypt języka JRuby zawiera deklarację klasy SimpleInterestCalculator z setterem właściwości rate i metodą calculate(). W języku Ruby zmienne egzemplarza rozpoczynają się od symbolu @. Zauważ, że w ostatnim wierszu kod zwraca nowy egzemplarz docelowej klasy języka JRuby. Jeśli nie zwrócisz takiego egzemplarza, Spring będzie szukał odpowiedniej klasy języka Ruby, której egzemplarz mógłby utworzyć. Ponieważ w jednym pliku skryptu JRuby może znajdować się wiele klas, Spring zgłosi wyjątek, jeśli nie będzie potrafił znaleźć odpowiedniej klasy z implementacją metod zadeklarowanych w interfejsie. W pliku z konfiguracją ziarna należy zadeklarować ziarno zaimplementowane w języku JRuby. Służy do tego element ; w jego atrybucie script-source należy określić lokalizację skryptu. Można ją podać jako ścieżkę do zasobu z obsługiwanym w Springu przedrostkiem (na przykład file lub classpath). Uwaga Aby można było używać języka JRuby w aplikacjach Springa, trzeba dodać do parametru classpath odpowiednie zależności. Jeśli korzystasz z Mavena, dodaj do projektu Mavena poniższą definicję: org.jruby jruby 1.0
Trzeba też określić jeden lub kilka interfejsów. Służy do tego atrybut script-interfaces ziarna JRuby. Spring odpowiada za tworzenie dynamicznej jednostki pośredniczącej dla tego ziarna i przekształcanie wywołań metod Javy na wywołania metod JRuby. Aby określić wartości właściwości ziarna ze skryptu, użyj elementów . Teraz możesz pobrać ziarno interestCalculator z kontenera IoC, aby korzystać z tego ziarna lub wstrzykiwać je do właściwości innych ziaren. Poniższa klasa Main pozwala sprawdzić, czy ziarno ze skryptu działa prawidłowo. package com.apress.springrecipes.interest; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) throws Exception { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); InterestCalculator calculator = (InterestCalculator) context.getBean("interestCalculator"); System.out.println(calculator.calculate(100000, 1)); } }
155
ROZDZIAŁ 4. SKRYPTY W SPRINGU
Skrypty z ziarnami w języku Groovy Teraz zaimplementuj interfejs InterestCalculator za pomocą języka Groovy. W tym celu utwórz skrypt języka Groovy, SimpleInterestCalculator.groovy, i umieść go w pakiecie com.apress.springrecipes.interest określonym w parametrze classpath. import com.apress.springrecipes.interest.InterestCalculator; class SimpleInterestCalculator implements InterestCalculator { double rate double calculate(double amount, double year) { return amount * year * rate } }
Ten skrypt języka Groovy zawiera deklarację klasy SimpleInterestCalculator implementującej interfejs InterestCalculator. W języku Groovy można zadeklarować właściwość bez modyfikatora dostępu. Wtedy automatycznie wygenerowane zostanie prywatne pole z publicznym getterem i publicznym setterem. Zaimplementowane w języku Groovy ziarno można zadeklarować w pliku z konfiguracją ziarna. W tym celu należy zastosować element i określić lokalizację skryptu w atrybucie script-source. Wartości właściwości ziarna ze skryptu można podać w elementach . Uwaga Aby można było używać języka Groovy w aplikacjach Springa, trzeba dodać do parametru classpath odpowiednie zależności. Jeśli korzystasz z Mavena, dodaj do projektu Mavena poniższą definicję: groovy groovy-all-1.0-jsr 0.5
Warto zauważyć, że dla ziaren języka Groovy nie trzeba podawać atrybutu script-interfaces, ponieważ w klasach z tego języka podaje się implementowane interfejsy.
Skrypty z ziarnami w języku BeanShell Teraz zaimplementuj interfejs InterestCalculator za pomocą języka BeanShell. W tym celu utwórz skrypt języka BeanShell, SimpleInterestCalculator.bsh, i umieść go w pakiecie com.apress.springrecipes.interest określonym w parametrze classpath. double rate; void setRate(double aRate) { rate = aRate; } double calculate(double amount, double year) { return amount * year * rate; }
156
4.2. WSTRZYKIWANIE ZIAREN SPRINGA DO SKRYPTÓW
W języku BeanShell nie można bezpośrednio deklarować klas, można jednak deklarować zmienne i metody. Dlatego aby zaimplementować interfejs InterestCalculator, należy utworzyć wszystkie potrzebne w nim metody. Zaimplementowane w języku BeanShell ziarno można zadeklarować w pliku z konfiguracją ziarna. W tym celu należy zastosować element i określić lokalizację skryptu w atrybucie script-source. Wartości właściwości ziarna ze skryptu można podać w elementach . Uwaga Aby można było używać języka BeanShell w aplikacjach Springa, trzeba dodać do parametru classpath odpowiednie zależności. Jeśli korzystasz z Mavena, dodaj do projektu Mavena poniższą definicję: org.beanshell bsh 2.0b4
Ponadto dla ziarna zaimplementowanego w języku BeanShell należy określić w atrybucie script-interfaces jeden lub kilka interfejsów. Spring odpowiada za tworzenie dynamicznych jednostek pośredniczących dla ziarna i przekształcanie wywołań metod Javy na wywołania metod języka BeanShell.
4.2. Wstrzykiwanie ziaren Springa do skryptów Problem Czasem w skryptach potrzebne są określone obiekty Javy, aby można było wykonać zadanie. W Springu trzeba zapewnić skryptom dostęp do ziaren zadeklarowanych w kontenerze IoC.
Rozwiązanie Ziarna zadeklarowane w kontenerze IoC można wstrzykiwać do skryptów w taki sam sposób jak właściwości prostych typów danych.
Jak to działa? Załóżmy, że chcesz, aby kod dynamicznie obliczał oprocentowanie. Najpierw należy zdefiniować poniższy interfejs dla implementacji zwracających roczne, miesięczne i dzienne oprocentowanie: package com.apress.springrecipes.interest; public interface RateCalculator { public double getAnnualRate(); public double getMonthlyRate(); public double getDailyRate(); }
157
ROZDZIAŁ 4. SKRYPTY W SPRINGU
W tym przykładzie w implementacji interfejsu należy obliczyć oprocentowanie na podstawie stałej stawki rocznej, którą można wstrzyknąć za pomocą settera. package com.apress.springrecipes.interest; public class FixedRateCalculator implements RateCalculator { private double rate; public void setRate(double rate) { this.rate = rate; } public double getAnnualRate() { return rate; } public double getMonthlyRate() { return rate / 12; } public double getDailyRate() { return rate / 365; } }
Następnie można zadeklarować kalkulator oprocentowania w kontenerze IoC i podać roczną stopę oprocentowania.
W kalkulatorze należy wykorzystać obiekt typu RateCalculator zamiast stałej stopy oprocentowania. package com.apress.springrecipes.interest; public interface InterestCalculator { public void setRateCalculator(RateCalculator rateCalculator); public double calculate(double amount, double year); }
Wstrzykiwanie ziaren Springa do skryptów JRuby W skrypcie JRuby można zapisać wstrzyknięty obiekt typu RateCalculator jako zmienną egzemplarza i wykorzystać ją przy obliczaniu stopy oprocentowania. class SimpleInterestCalculator def setRateCalculator(rateCalculator) @rateCalculator = rateCalculator end def calculate(amount, year) amount * year * @rateCalculator.getAnnualRate end end SimpleInterestCalculator.new
158
4.2. WSTRZYKIWANIE ZIAREN SPRINGA DO SKRYPTÓW
W deklaracji ziarna można wstrzyknąć inne ziarno do właściwości ziarna ze skryptu. W tym celu należy podać nazwę wstrzykiwanego ziarna w atrybucie ref.
Wstrzykiwanie ziaren Springa w języku Groovy W skryptach Groovy wystarczy zadeklarować właściwość typu RateCalculator, a publiczny getter i publiczny setter zostaną automatycznie wygenerowane. import com.apress.springrecipes.interest.InterestCalculator; import com.apress.springrecipes.interest.RateCalculator; class SimpleInterestCalculator implements InterestCalculator { RateCalculator rateCalculator double calculate(double amount, double year) { return amount * year * rateCalculator.getAnnualRate() } }
Do właściwości ziarna ze skryptu można wstrzyknąć inne ziarno. Należy podać jego nazwę w atrybucie ref.
Wstrzykiwanie ziaren Springa w języku BeanShell W skryptach języka BeanShell potrzebna jest zmienna globalna typu RateCalculator i powiązany z nią setter. import com.apress.springrecipes.interest.RateCalculator; RateCalculator rateCalculator; void setRateCalculator(RateCalculator aRateCalculator) { rateCalculator = aRateCalculator; } double calculate(double amount, double year) { return amount * year * rateCalculator.getAnnualRate(); }
Do właściwości ziarna ze skryptu można wstrzyknąć inne ziarno. Należy podać jego nazwę w atrybucie ref.
159
ROZDZIAŁ 4. SKRYPTY W SPRINGU
4.3. Aktualizowanie ziaren ze skryptów Problem Ponieważ moduły zaimplementowane za pomocą języków skryptowych czasem trzeba często i dynamicznie modyfikować, kontener IoC powinien potrafić automatycznie wykrywać i wprowadzać zmiany na podstawie kodu źródłowego skryptów.
Rozwiązanie Spring potrafi aktualizować definicje ziaren ze skryptów na podstawie kodu źródłowego. Wymaga to określenia w atrybucie refresh-check-delay odstępów czasu między operacjami sprawdzania kodu. Gdy wywoływana jest metoda danego ziarna, a upłynął zadeklarowany czas, Spring sprawdza kod źródłowy skryptu. Jeśli w kodzie wprowadzono zmiany, Spring na ich podstawie aktualizuje definicję ziarna.
Jak to działa? Domyślnie atrybut refresh-check-delay ma wartość ujemną, co sprawia, że aktualizowanie ziaren jest wyłączone. Aby włączyć tę funkcję, należy podać w tym atrybucie odstęp czasu (w milisekundach) między kolejnymi operacjami sprawdzania kodu źródłowego. Można na przykład ustawić odstęp między aktualizacjami ziarna JRuby na pięć sekund. ...
Atrybut refresh-check-delay oczywiście działa również dla ziaren zaimplementowanych w językach Groovy i BeanShell. ... ...
160
4.4. WEWNĄTRZWIERSZOWE DEFINIOWANIE KODU ŹRÓDŁOWEGO SKRYPTÓW
4.4. Wewnątrzwierszowe definiowanie kodu źródłowego skryptów Problem Programista zamierza zdefiniować kod źródłowy skryptu, który prawdopodobnie nie będzie często modyfikowany. Chce to zrobić bezpośrednio w pliku z konfiguracją ziarna, a nie w zewnętrznym pliku skryptu.
Rozwiązanie Kod źródłowy skryptu można zdefiniować wewnątrzwierszowo w elemencie ziarna ze skryptu. Zastępuje on podawaną w atrybucie script-source referencję do zewnętrznego pliku ze skryptem. Warto zauważyć, że funkcja aktualizowania ziarna nie działa dla kodu wewnątrzwierszowego. Wynika to z tego, że kontener IoC wczytuje konfigurację ziarna tylko raz (w trakcie uruchamiania).
Jak to działa? Za pomocą elementu można na przykład zdefiniować skrypt JRuby. Aby zapobiec niezgodności znaków ze skryptu z zastrzeżonymi znakami XML-a, należy umieścić kod skryptu w znaczniku . W tym podejściu nie trzeba podawać w atrybucie script-source referencji do zewnętrznego pliku z kodem skryptu.
Za pomocą elementu oczywiście można też zdefiniować wewnątrzwierszowo kod źródłowy skryptów Groovy i BeanShell.
161
ROZDZIAŁ 4. SKRYPTY W SPRINGU
double calculate(double amount, double year) { return amount * year * rateCalculator.getAnnualRate() } } ]]>
Podsumowanie Z tego rozdziału dowiedziałeś się, jak używać obsługiwanych w Springu języków skryptowych do implementowania ziaren i jak deklarować ziarna ze skryptów w kontenerze IoC. Spring obsługuje trzy języki skryptowe: JRuby, Groovy i BeanShell. Możesz wskazać lokalizację zewnętrznego pliku z kodem źródłowym skryptu lub zdefiniować kod wewnątrzwierszowo w pliku z konfiguracją ziarna. Ponieważ kod skryptu może wymagać częstych i dynamicznych zmian, Spring umożliwia automatyczne wykrywanie zmian i aktualizowanie kodu na podstawie zawartości plików ze skryptami. Do skryptów można wstrzykiwać wartości właściwości, a także referencje do ziaren.
162
ROZDZIAŁ 5
Bezpieczeństwo w Springu
W tym rozdziale dowiesz się, jak zabezpieczać aplikacje za pomocą platformy Spring Security (jest to jeden z podprojektów związanych z platformą Spring). Platforma Spring Security była pierwotnie znana jako Acegi Security, jednak nazwę zmieniono po włączeniu tego narzędzia do rodziny projektów związanych ze Springiem. Spring Security umożliwia zabezpieczanie dowolnych aplikacji Javy, jednak najczęściej używana jest dla aplikacji sieciowych. Takie aplikacje, zwłaszcza te dostępne z poziomu internetu, jeśli nie zostaną właściwie zabezpieczone, będą podatne na ataki hakerów. Jeśli już korzystasz z platformy Spring Security, zauważ, że w wersji 3.0 wprowadzono w niej kilka zmian (wersja ta pojawiła się niemal jednocześnie z wydaniem 3.0 głównej platformy Spring). Te modyfikacje to między innymi nowe funkcje (na przykład obsługa adnotacji i architektury OpenID), a także nowe nazwy klas i podział pakietu (na różne pliki .jar). Ma to znaczenie zwłaszcza wtedy, gdy uruchamiasz kod oparty na platformie Spring Security 2.x, którą opisano w pierwszym wydaniu tej książki. Jeżeli jednak nigdy nie stosowałeś zabezpieczeń w aplikacjach, jest kilka pojęć i zagadnień, które warto najpierw poznać. Uwierzytelnianie to proces sprawdzania tożsamości uwierzytelnianej jednostki (ang. principal). Uwierzytelnianą jednostką może być użytkownik, urządzenie lub system (najczęściej jest to użytkownik). Taka jednostka musi udowodnić swoją tożsamość, aby została uwierzytelniona. W tym celu przedstawia dane uwierzytelniające, którymi — gdy jednostką jest użytkownik — jest zwykle hasło. Autoryzacja to proces przyznawania uprawnień uwierzytelnionemu użytkownikowi. Dzięki temu użytkownik uzyskuje dostęp do określonych zasobów docelowej aplikacji. Proces autoryzacji musi zachodzić po procesie uwierzytelniania. Uprawnienia zwykle przyznaje się w postaci ról. Kontrola dostępu polega na kontrolowaniu dostępu do zasobów aplikacji. Wymaga to ustalenia, czy użytkownikowi należy przyznać dostęp do określonego zasobu. Proces ustalania związany jest z decyzją z zakresu kontroli dostępu i polega na porównywaniu atrybutów dostępu danego zasobu z uprawnieniami lub innymi cechami użytkownika. Po zakończeniu lektury tego rozdziału będziesz rozumiał podstawowe zagadnienia z zakresu zabezpieczeń. Będziesz też wiedział, jak zabezpieczać aplikacje sieciowe na następujących poziomach: dostępu do adresów URL, wywołań metod, renderowania widoku i obiektów domenowych.
5.1. Zabezpieczanie dostępu do adresów URL Problem W licznych aplikacjach sieciowych niektóre adresy URL mają krytyczne znaczenie i powinny być prywatne. Trzeba zabezpieczyć takie adresy, aby zapobiec nieuwierzytelnionemu dostępowi do nich.
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
Rozwiązanie Platforma Spring Security umożliwia zabezpieczenie dostępu do adresu URL aplikacji sieciowej. Można to zrobić w deklaratywny sposób za pomocą prostej konfiguracji. Technika zabezpieczania polega na stosowaniu filtrów serwletów do żądań HTTP. Takie filtry można ustawić w plikach z konfiguracją ziaren Springa przy użyciu elementów XML-a zdefiniowanych w schemacie platformy Spring Security. Jednak aby filtry zadziałały, trzeba je zarejestrować w deskryptorze wdrażania. Dlatego należy zarejestrować w tym deskryptorze egzemplarz klasy DelegatingFilterProxy (jest to filtr serwletu delegujący żądania do filtra z kontekstu aplikacji Springa). Platforma Spring Security umożliwia skonfigurowanie zabezpieczeń aplikacji sieciowej za pomocą elementu . Jeśli wymagania związane z zabezpieczaniem danej aplikacji sieciowej są proste i typowe, można ustawić w tym elemencie atrybut auto-config na true. Wtedy platforma Spring Security automatycznie zarejestruje i skonfiguruje kilka podstawowych usług z zakresu zabezpieczeń. Oto wybrane z nich: Usługa logowania oparta na formularzu. Zapewnia ona domyślną stronę z formularzem logowania, pozwalającą użytkownikom logować się do danej aplikacji. Usługa wylogowywania. Zapewnia powiązany z adresem URL mechanizm obsługi, który umożliwia użytkownikom wylogowanie się z danej aplikacji. Podstawowe uwierzytelnianie HTTP. Umożliwia przetwarzanie podstawowych danych uwierzytelniających podanych w nagłówku żądania HTTP. Może też służyć do uwierzytelniania żądań zgłaszanych za pomocą protokołów zdalnych wywołań i usług sieciowych. Logowanie anonimowe. Powoduje przypisanie uwierzytelnianej jednostki do anonimowego użytkownika i przyznanie mu uprawnień. Pozwala to traktować użytkownika anonimowego tak samo jak zwykłego. Obsługa zapamiętywania użytkowników. Pozwala zapamiętywać tożsamość użytkownika między sesjami przeglądarki. Zwykle wymaga to zapisania pliku cookie w przeglądarce użytkownika. Integracja z interfejsem API serwletów. Umożliwia dostęp do informacji o zabezpieczeniach aplikacji sieciowej za pomocą standardowego interfejsu API serwletów (na przykład metod HttpServlet Request.isUserInRole() i HttpServletRequest.getUserPrincipal()). Po zarejestrowaniu tych usług można określić wzorce adresów URL, które mają być dostępne tylko dla użytkowników z określonymi uprawnieniami. Platforma Spring Security obsługuje zabezpieczenia na podstawie ustawionej konfiguracji. Użytkownik przed uzyskaniem dostępu do zabezpieczonych adresów URL musi zalogować się do aplikacji (chyba że dane adresy są dostępne dla anonimowych użytkowników). Platforma Spring Security udostępnia zestaw dostawców uwierzytelniania. Taki dostawca uwierzytelnia użytkownika i zwraca przyznane uprawnienia.
Jak to działa? Załóżmy, że chcesz utworzyć aplikację do obsługi forum. Ma ona umożliwiać użytkownikom zamieszczanie postów. Najpierw należy utworzyć klasę domenową Message o trzech właściwościach: author, title i body. package com.apress.springrecipes.board.domain; public class Message { private private private private
Long id; String author; String title; String body;
// Gettery i settery ... }
164
5.1. ZABEZPIECZANIE DOSTĘPU DO ADRESÓW URL
Następnie należy zdefiniować w interfejsie usługi operacje potrzebne na forum. Te operacje to: wyświetlanie wszystkich wiadomości, dodawanie wiadomości, usuwanie wiadomości i wyszukiwanie wiadomości na podstawie identyfikatora. package com.apress.springrecipes.board.service; ... public interface MessageBoardService { public public public public
List listMessages(); void postMessage(Message message); void deleteMessage(Message message); Message findMessageById(Long messageId);
}
Na potrzeby testów zaimplementuj ten interfejs. Dodane wiadomości zapisz na liście. Jako identyfikatory wiadomości możesz wykorzystać czas ich dodania (w milisekundach). Ponadto metody postMessage() i delete Message() należy zadeklarować z modyfikatorem synchronized, aby były bezpieczne ze względu na wątki. package com.apress.springrecipes.board.service; ... public class MessageBoardServiceImpl implements MessageBoardService { private Map messages = new LinkedHashMap(); public List listMessages() { return new ArrayList(messages.values()); } public synchronized void postMessage(Message message) { message.setId(System.currentTimeMillis()); messages.put(message.getId(), message); } public synchronized void deleteMessage(Message message) { messages.remove(message.getId()); } public Message findMessageById(Long messageId) { return messages.get(messageId); } }
Konfigurowanie aplikacji MVC Springa korzystającej z platformy Spring Security Aby utworzyć aplikację za pomocą platformy sieciowej Spring MVC i platformy zabezpieczeń Spring Security, należy najpierw utworzyć opisaną poniżej strukturę katalogów. Uwaga Przed zastosowaniem platformy Spring Security trzeba dodać do parametru classpath odpowiednie pliki .jar tej platformy. Jeśli używasz Mavena, dodaj do projektu Mavena podane poniżej zależności. Tu znajduje się kilka dodatkowych zależności (związanych na przykład z obsługą technologii LDAP i ACL), które są potrzebne tylko w niektórych aplikacjach. W tej książce do określania wersji służy zmienna ${spring.security.version}, a używana tu wersja to 3.0.2.RELEASE. org.springframework.security spring-security-core ${spring.security.version}
165
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
org.springframework.security spring-security-ldap ${spring.security.version} org.springframework.security spring-security-config ${spring.security.version} org.springframework.security spring-security-web ${spring.security.version} org.springframework.security spring-security-taglibs ${spring.security.version} org.springframework.security spring-security-acl ${spring.security.version}
Konfiguracja Springa dla tej aplikacji znajduje się w trzech różnych plikach: board-security.xml, board-service.xml i board-servlet.xml. Każdy z tych plików odpowiada za konfigurację jednej z warstw.
Tworzenie plików konfiguracyjnych W deskryptorze wdrażania (czyli w pliku web.xml) należy zarejestrować odbiornik ContextLoaderListener (aby wczytywał główny kontekst aplikacji przy jej uruchamianiu) i serwlet DispatcherServlet z platformy Spring MVC (na potrzeby rozdzielania żądań). contextConfigLocation /WEB-INF/board-service.xml org.springframework.web.context.ContextLoaderListener board
166
5.1. ZABEZPIECZANIE DOSTĘPU DO ADRESÓW URL
org.springframework.web.servlet.DispatcherServlet board /
Jeśli plik z konfiguracją głównego kontekstu aplikacji nie ma nazwy domyślnej (czyli applicationContext.xml) lub gdy używanych jest kilka plików konfiguracyjnych, należy określić lokalizacje tych plików za pomocą pewnego parametru kontekstu, contextConfigLocation. Zauważ, że wzorzec adresów URL / jest powiązany z serwletem DispatcherServlet, co powoduje, że ten serwlet będzie obsługiwał żądania wszystkich stron podrzędnych względem katalogu głównego aplikacji. W pliku z konfiguracją warstwy sieciowej (czyli w pliku board-servlet.xml) należy zdefiniować mechanizm określający widoki, odpowiedzialny za wiązanie nazw widoków z plikami JSP z katalogu /WEB-INF/jsp/. W tym samym pliku trzeba później skonfigurować kontrolery.
W pliku z konfiguracją warstwy usług (czyli w pliku board-service.xml) wystarczy zadeklarować usługę zarządzającą forum.
Tworzenie kontrolerów i widoków stron Załóżmy, że musisz zaimplementować funkcję przeznaczoną do wyświetlania wszystkich wiadomości zamieszczonych na forum. W pierwszym kroku należy utworzyć poniższy kontroler. package com.apress.springrecipes.board.web; ... @Controller @RequestMapping("/messageList*")
167
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
public class MessageListController { private MessageBoardService messageBoardService; @Autowired public MessageListController(MessageBoardService messageBoardService) { this.messageBoardService = messageBoardService; } @RequestMapping(method = RequestMethod.GET) public String generateList(Model model) { List messages = java.util.Collections.emptyList(); messages = messageBoardService.listMessages(); model.addAttribute("messages",messages); return "messageList"; } }
Ten kontroler jest wiązany z adresem URL w postaci /messageList. Główna metoda kontrolera, generateList(), pobiera listę wiadomości z usługi messageBoardService, zapisuje tę listę w obiekcie modelu pod nazwą messages, a następnie zwraca sterowanie do logicznego widoku messageList. Zgodnie z konwencjami
obowiązującymi w platformie Spring MVC ten ostatni widok jest łączony z plikiem JSP /WEB-INF/jsp/ messageList.jsp, wyświetlającym wszystkie wiadomości przekazane z kontrolera. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> Lista wiadomości Autor ${message.author} Tytuł ${message.title} Treść ${message.body} Usuń
Zamieść wiadomość
168
5.1. ZABEZPIECZANIE DOSTĘPU DO ADRESÓW URL
Inną funkcję trzeba zaimplementować, aby umożliwić użytkownikom zamieszczanie wiadomości na forum. W tym celu utwórz poniższy kontroler formularza. package com.apress.springrecipes.board.web; ... @Controller @RequestMapping("/messagePost*") public class MessagePostController { private MessageBoardService messageBoardService; @Autowired public void MessagePostController(MessageBoardService messageBoardService) { this.messageBoardService = messageBoardService; } @RequestMapping(method=RequestMethod.GET) public String setupForm(Model model) { Message message = new Message(); model.addAttribute("message",message); return "messagePost"; } @RequestMapping(method=RequestMethod.POST) public String onSubmit(@ModelAttribute("message") Message message, BindingResult result) { if (result.hasErrors()) { return "messagePost"; } else { messageBoardService.postMessage(message); return "redirect:messageList"; } } }
Użytkownik musi być zalogowany na forum, aby móc dodać wiadomość. Nazwę użytkownika można pobrać za pomocą metody getRemoteUser() zdefiniowanej w klasie HttpServletRequest. Nazwa użytkownika jest wykorzystywana jako nazwa autora wiadomości. Następnie trzeba utworzyć widok formularza, /WEB-INF/jsp/messagePost.jsp. Należy zastosować w nim dostępne w Springu znaczniki formularza, aby umożliwić użytkownikom wprowadzanie treści wiadomości. <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> Dodaj wiadomość
Ostatnia potrzebna funkcja ma umożliwiać użytkownikom usuwanie zamieszczonej wiadomości za pomocą odnośnika Usuń umieszczonego na stronie z listą wiadomości. Na potrzeby tej funkcji utwórz pokazany poniżej kontroler. package com.apress.springrecipes.board.web; ... @Controller @RequestMapping("/messageDelete*") public class MessageDeleteController { private MessageBoardService messageBoardService; @Autowired public void MessageDeleteController(MessageBoardService messageBoardService) { this.messageBoardService = messageBoardService; } @RequestMapping(method= RequestMethod.GET) public String messageDelte(@RequestParam(required = true, value = "messageId") Long messageId, Model model) { Message message = messageBoardService.findMessageById(messageId); messageBoardService.deleteMessage(message); model.addAttribute("messages", messageBoardService.listMessages()); return "redirect:messageList"; } }
Następnie można umieścić aplikację w kontenerze WWW, na przykład na serwerze Apache Tomcat 6.0. Domyślnie serwer Tomcat oczekuje na żądania w porcie 8080, dlatego jeśli umieścisz aplikację w ścieżce board, będziesz mógł wyświetlić listę wszystkich zamieszczonych wiadomości za pomocą podanego poniżej adresu URL. http://localhost:8080/board/messageList.htm
Na razie nie skonfigurowałeś jeszcze żadnej usługi zabezpieczającej aplikację, dlatego do stron można uzyskać dostęp bezpośrednio, bez konieczności logowania się.
Zabezpieczanie dostępu do adresów URL Teraz za pomocą platformy Spring Security zabezpieczysz dostęp do adresów URL utworzonej aplikacji sieciowej. Najpierw trzeba skonfigurować w pliku web.xml egzemplarz klasy DelegatingFilterProxy, aby delegował filtrowanie żądań HTTP do filtra zdefiniowanego w platformie Spring Security. contextConfigLocation /WEB-INF/board-service.xml /WEB-INF/board-security.xml
170
5.1. ZABEZPIECZANIE DOSTĘPU DO ADRESÓW URL
... springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy springSecurityFilterChain /* ...
Klasa DelegatingFilterProxy odpowiada za delegowanie filtrowania żądań HTTP do ziarna Springa implementującego interfejs java.util.logging.Filter. Domyślnie zadanie jest delegowane do ziarna o nazwie odpowiadającej wartości właściwości , jednak za pomocą parametru targetBeanName można podać nazwę innego ziarna. Ponieważ w odpowiedzi na włączenie zabezpieczeń aplikacji sieciowej platforma Spring Security automatycznie konfiguruje łańcuch filtrów o nazwie springSecurityFilterChain, można wykorzystać tę nazwę dla tworzonego egzemplarza klasy DelegatingFilterProxy. Choć platformę Spring Security można skonfigurować w tym samym pliku, w którym jest ustawiana warstwa sieciowa i warstwa usług, lepiej jest umieścić konfigurację zabezpieczeń w odrębnym pliku (na przykład w board-security.xml). W pliku web.xml należy podać nazwę tego pliku w odpowiednim parametrze kontekstu (contextConfigLocation) odbiornika ContextLoaderListener, dzięki czemu plik z konfiguracją zabezpieczeń zostanie wczytany w trakcie uruchamiania aplikacji.
Plik ten wygląda nieco inaczej niż pliki z konfiguracją zwykłych ziaren. Standardowo domyślną przestrzenią nazw dla plików z konfiguracją ziaren jest beans, dlatego elementów i można używać bez konieczności podawania przedrostka beans. Jeśli jednak zastosujesz to podejście przy deklarowaniu usług platformy Spring Security, wszystkie elementy związane z zabezpieczeniami trzeba będzie poprzedzić przedrostkiem security. Ponieważ w pliku z konfiguracją zabezpieczeń stosuje się głównie elementy platformy Spring Security, jako domyślną przestrzeń nazw można zdefiniować przestrzeń security, dzięki
171
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
czemu nie będzie trzeba poprzedzać takich elementów przedrostkiem security. Jednak wtedy przy deklarowaniu zwykłych ziaren Springa należy dodawać przedrostek przed elementami i . Element powoduje automatyczne skonfigurowanie podstawowych usług zabezpieczeń potrzebnych w typowej aplikacji sieciowej. Usługi te możesz precyzyjnie ustawić za pomocą odpowiednich elementów podrzędnych. W elemencie można ograniczyć dostęp do konkretnych adresów URL. Wymaga to zastosowania jednego lub więcej elementów . Każdy taki element określa wzorzec adresów URL i zestaw atrybutów dostępu potrzebnych, aby uzyskać dostęp do tych adresów. Pamiętaj, że na końcu wzorca adresów URL zawsze należy umieścić symbol wieloznaczny. Jego brak spowoduje, że wzorzec nie będzie pasował do adresów URL z parametrami żądania. W efekcie hakerzy będą mogli łatwo ominąć zabezpieczenia — wystarczy, że dodadzą dowolny parametr żądania. Atrybuty dostępu są porównywane z uprawnieniami użytkownika, aby ustalić, czy dana osoba może wyświetlić stronę o danym adresie URL. W większości sytuacji atrybuty dostępu są zdefiniowane za pomocą ról. Na przykład użytkownicy o roli ROLE_USER i użytkownicy anonimowi (mający domyślnie przypisaną rolę ROLE_ANONYMOUS) mają dostęp do strony o adresie /messageList, która pozwala wyświetlić wszystkie wiadomości. Jednak użytkownik musi mieć rolę ROLE_USER, aby móc dodać nową wiadomość przy użyciu strony o adresie /messagePost. Tylko administrator (mający rolę ROLE_ADMIN) może usuwać wiadomości za pomocą strony o adresie /messageDelete. Usługi uwierzytelniania można skonfigurować w elemencie zagnieżdżonym w elemencie . Platforma Spring Security obsługuje kilka sposobów uwierzytelniania użytkowników, w tym uwierzytelnianie na podstawie danych z bazy lub repozytorium LDAP. Można też zdefiniować informacje o użytkownikach bezpośrednio w elemencie , aby skonfigurować proste wymogi z zakresu zabezpieczeń. W ten sposób można podać nazwę, hasło i zestaw uprawnień dla każdego z użytkowników. Teraz możesz ponownie zainstalować aplikację, by przetestować skonfigurowane zabezpieczenia. Możesz wpisać ścieżkę /messageList, aby w standardowy sposób wyświetlić wszystkie zamieszczone wiadomości, ponieważ ta strona jest dostępna dla użytkowników anonimowych. Jednak gdy klikniesz odnośnik służący do dodawania wiadomości, trafisz na domyślną stronę logowania wygenerowaną przez platformę Spring Security. Musisz wtedy wpisać poprawne dane (nazwę i hasło), żeby zalogować się do aplikacji w celu zamieszczenia wiadomości. Jeśli chcesz usunąć wiadomość, zaloguj się jako administrator.
5.2. Logowanie się do aplikacji sieciowych Problem W bezpiecznej aplikacji użytkownicy muszą się zalogować przed uzyskaniem dostępu do niektórych zabezpieczonych funkcji. Jest to ważne zwłaszcza w aplikacjach sieciowych działających w internecie, ponieważ hakerzy łatwo mogą do nich dotrzeć. Większość aplikacji sieciowych musi umożliwiać użytkownikom wprowadzenie danych uwierzytelniających na potrzeby logowania.
Rozwiązanie Platforma Spring Security obsługuje wiele sposobów logowania się użytkowników do aplikacji sieciowych. Obsługiwane jest między innymi logowanie oparte na formularzu, kiedy to aplikacja wyświetla domyślną stronę z formularzem logowania. Można też utworzyć niestandardową stronę logowania. Ponadto Spring Security udostępnia podstawowe uwierzytelnianie HTTP bazujące na przetwarzaniu podstawowych danych uwierzytelniających podanych w nagłówku żądania HTTP. Tę technikę można wykorzystać także do uwierzytelniania żądań zgłaszanych za pomocą protokołów zdalnych wywołań i usług sieciowych. Niektóre fragmenty aplikacji (na przykład strona powitalna) mogą być dostępne dla użytkowników anonimowych. Spring Security udostępnia usługę logowania anonimowego. Przypisuje ona do użytkownika anonimowego uwierzytelnianą jednostkę i przyznaje mu uprawnienia. Dzięki temu przy definiowaniu zabezpieczeń można traktować takiego użytkownika jak normalnych użytkowników. 172
5.2. LOGOWANIE SIĘ DO APLIKACJI SIECIOWYCH
Spring Security udostępnia też zapamiętywanie zalogowanych użytkowników. Pozwala to zapamiętać tożsamość użytkownika między sesjami przeglądarki, dzięki czemu użytkownik po pierwszym zalogowaniu się nie musi powtarzać tej operacji.
Jak to działa? Aby lepiej zrozumieć różne mechanizmy logowania, najpierw wyłącz automatyczną konfigurację obsługi żądań HTTP. W tym celu usuń atrybut auto-config.
Zauważ, że gdy jest włączony atrybut auto-config żądań HTTP, dodawane później usługi logowania są rejestrowane automatycznie. Jeśli jednak wyłączysz ten atrybut lub zechcesz zmodyfikować pracę tych usług, musisz bezpośrednio skonfigurować odpowiednie elementy XML-a.
Podstawowe uwierzytelnianie HTTP Obsługę podstawowego uwierzytelniania HTTP można skonfigurować za pomocą elementu . Gdy potrzebne jest uwierzytelnianie tego rodzaju, przeglądarka zwykle wyświetla okno dialogowe logowania lub określoną stronę, na której użytkownicy mogą się zalogować. ...
Zauważ, że jeśli jednocześnie włączone są podstawowe uwierzytelnianie HTTP i logowanie oparte na formularzu, używana jest druga z tych technik. Dlatego jeśli chcesz, aby użytkownicy aplikacji sieciowej logowali się za pomocą podstawowego uwierzytelniania HTTP, nie włączaj logowania opartego na formularzu.
Logowanie oparte na formularzu Usługa logowania opartego na formularzu wyświetla stronę zawierającą formularz, w którym użytkownicy mogą wprowadzić dane uwierzytelniające, a następnie przetwarza te dane. Do konfigurowania tej usługi służy element . ...
Domyślnie platforma Spring Security automatycznie tworzy stronę logowania i łączy ją z adresem URL /spring_security_login. Można więc dodać do aplikacji odnośnik (na przykład na stronie messageList.jsp) prowadzący do adresu URL formularza logowania. ">Zaloguj się
Jeśli nie chcesz używać domyślnej strony logowania, możesz samodzielnie utworzyć taką stronę. Utwórz poniższy plik login.jsp i umieść go w katalogu głównym aplikacji sieciowej. Zauważ, że tego pliku nie należy umieszczać w katalogu WEB-INF, ponieważ wtedy użytkownicy nie będą mogli bezpośrednio uzyskać do niego dostępu. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> Zaloguj się
173
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
">
Zauważ, że adres URL z akcją formularza i nazwy pól wejściowych są charakterystyczne dla platformy Spring Security. Jednak adres URL akcji można zmienić za pomocą atrybutu login-url elementu . Teraz trzeba zmienić wcześniejszy odnośnik do strony logowania (na stronie messageList.jsp) i umieścić w nim adres URL nowej strony: ">Zaloguj się
Aby platforma Spring Security w momencie, gdy użytkownik zechce się zalogować, wyświetlała niestandardową stronę logowania, należy ustawić adres URL tej strony w atrybucie login-page. ...
Jeśli strona logowania jest wyświetlana przez platformę Spring Security w momencie zażądania przez użytkownika zabezpieczonego adresu URL, użytkownik po udanym zalogowaniu się trafi na docelową stronę. Jeżeli jednak użytkownik zażąda strony logowania bezpośrednio przez podanie jej adresu URL, po udanym logowaniu domyślnie trafi do katalogu głównego ze ścieżki kontekstu (czyli pod adres http://localhost:8080/board/). Jeśli w deskryptorze wdrażania nie zdefiniowałeś strony powitalnej, możesz chcieć, aby użytkownik po zalogowaniu się był kierowany pod domyślny docelowy adres URL. ....
Jeżeli używasz domyślnej strony logowania utworzonej przez platformę Spring Security, po nieudanej próbie logowania platforma wyświetla ponownie stronę logowania oraz komunikat o błędzie. Jeśli jednak utworzysz niestandardową stronę logowania, będziesz musiał ustawić atrybut authentication-failure-url i określić w nim adres URL otwierany w wyniku nieudanego logowania. Możesz na przykład ponownie kierować użytkownika do niestandardowej strony logowania i dodać do adresu parametr żądania error.
174
5.2. LOGOWANIE SIĘ DO APLIKACJI SIECIOWYCH
....
Na stronie logowania trzeba sprawdzać, czy podano parametr żądania error. Jeśli wystąpił błąd, należy wyświetlić komunikat o błędzie. Wykorzystywany jest wtedy atrybut sesji SPRING_SECURITY_LAST_EXCEPTION, w którym zapisany jest ostatni wyjątek zgłoszony dla danego użytkownika. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> Zaloguj się Błąd logowania. Przyczyna: ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message} ...
Usługa wylogowywania Usługa wylogowywania pozwala określić sposób obsługi żądania wylogowania. Usługę tę można skonfigurować za pomocą elementu . ...
Domyślnie ta usługa jest łączona z adresem URL /j_spring_security_logout, dlatego można dodać do stron prowadzący pod ten adres odnośnik, aby umożliwić wylogowanie. Warto zauważyć, że ten adres można zmienić w atrybucie logout-url elementu . ">Wyloguj się
Domyślnie użytkownik po wylogowaniu jest kierowany do strony głównej ze ścieżki kontekstu, jednak czasem pożądane jest wyświetlanie użytkownikom innej strony. Ten efekt można uzyskać w następujący sposób: ...
Logowanie anonimowe Usługę logowania anonimowego można skonfigurować za pomocą elementu . Pozwala on określić nazwę i uprawnienia anonimowego użytkownika (wartości domyślne tych ustawień to anonymousUser i ROLE_ANONYMOUS).
175
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
...
Obsługa funkcji zapamiętywania użytkowników Obsługę funkcji zapamiętywania użytkowników można skonfigurować za pomocą elementu . Domyślnie określa on przy użyciu tokena nazwę użytkownika, hasło, czas przechowywania zapamiętanych danych i prywatny klucz. Token ten jest przechowywany w pliku cookie w przeglądarce użytkownika. Gdy użytkownik ponownie otworzy daną aplikację sieciową, wykryje ona token, co pozwoli automatycznie zalogować daną osobę. ...
Statyczne tokeny tego typu prowadzą do problemów z bezpieczeństwem, ponieważ mogą zostać przechwycone przez hakerów. Platforma Spring Security obsługuje dynamiczne tokeny, co pozwala zwiększyć poziom bezpieczeństwa, jednak wymaga utrwalania tokenów w bazie danych. Szczegółowe informacje na temat stosowania dynamicznych tokenów do zapamiętywania użytkowników znajdziesz w dokumentacji platformy Spring Security.
5.3. Uwierzytelnianie użytkowników Problem Gdy użytkownik próbuje zalogować się do aplikacji w celu uzyskania dostępu do zabezpieczonych zasobów, trzeba go uwierzytelnić i przyznać mu odpowiednie uprawnienia.
Rozwiązanie W platformie Spring Security za uwierzytelnianie odpowiadają połączeni w łańcuch dostawcy uwierzytelniania. Jeśli jeden z dostawców uwierzytelni użytkownika, dana osoba może się zalogować do aplikacji. Jeżeli któryś z dostawców wykryje, że użytkownik jest zablokowany lub podał nieprawidłowe dane, albo gdy żaden z dostawców nie potrafi uwierzytelnić danej osoby, próba logowania kończy się niepowodzeniem. Platforma Spring Security obsługuje wiele sposobów uwierzytelniania użytkowników i udostępnia wbudowane implementacje dostawców. Dostawców możesz łatwo skonfigurować za pomocą wbudowanych elementów XML-a. Większość typowych dostawców uwierzytelnia użytkowników na podstawie repozytorium przechowującego szczegółowe informacje o użytkownikach. Repozytorium to może znajdować się na przykład w pamięci aplikacji, w relacyjnej bazie danych lub w repozytorium LDAP. Przy przechowywaniu informacji o użytkownikach w repozytorium należy unikać zapisywania haseł w postaci zwykłego tekstu, ponieważ aplikacja staje się wtedy podatna na ataki hakerów. Zamiast tego w repozytorium należy umieszczać hasła w zaszyfrowanej formie. Zwykle do szyfrowania używa się jednokierunkowej funkcji haszującej. Gdy użytkownik wprowadzi hasło, należy zastosować tę samą funkcję i porównać wynik z danymi zapisanymi w repozytorium. Spring Security obsługuje kilka algorytmów szyfrujących (w tym MD5 i SHA) oraz udostępnia wbudowane enkodery haseł związane z tymi algorytmami. Jeśli aplikacja pobiera z repozytorium informacje o użytkowniku przy każdej próbie logowania, może to prowadzić do spadku wydajności. Wynika to z tego, że repozytorium jest zwykle przechowywane zdalnie i w odpowiedzi na żądanie musi wykonać odpowiednie zapytania. Dlatego Spring Security obsługuje zapisywanie informacji o użytkownikach w pamięci lokalnej, co pozwala uniknąć kosztów wykonywania zdalnych zapytań.
176
5.3. UWIERZYTELNIANIE UŻYTKOWNIKÓW
Jak to działa? Uwierzytelnianie użytkowników na podstawie definicji przechowywanych w pamięci Jeśli liczba użytkowników aplikacji jest niewielka, a ponadto nie trzeba często modyfikować informacji na ich temat, można zdefiniować te dane w pliku konfiguracyjnym platformy Spring Security. Wtedy dane te są wczytywane do pamięci aplikacji.
Dane użytkowników możesz zdefiniować w elementach zagnieżdżonych w elemencie . Dla każdego użytkownika można określić nazwę, hasło, status i zestaw przyznanych uprawnień. Użytkownik o statusie disabled nie może zalogować się do aplikacji. Spring Security pozwala też zapisać dane o użytkownikach w zewnętrznym pliku z właściwościami (na przykład w pliku /WEB-INF/users.properties).
Następnie należy utworzyć odpowiedni plik właściwości i zdefiniować w nim dane o użytkownikach. Dane te powinny mieć formę właściwości. admin=secret,ROLE_ADMIN,ROLE_USER user1=1111,ROLE_USER user2=2222,disabled,ROLE_USER
Każda właściwość w tym pliku reprezentuje określone dane o użytkowniku. Kluczem właściwości jest nazwa, a wartość składa się z kilku rozdzielonych przecinkami części. Pierwszą częścią jest hasło. Druga część jest opcjonalna i określa status; domyślnie status to enabled. Dalsze części to przyznane użytkownikowi uprawnienia.
Uwierzytelnianie użytkowników na podstawie bazy danych Dane o użytkownikach zwykle lepiej jest zapisać w bazie danych, co pozwala na łatwe zarządzanie nimi. Spring Security zapewnia wbudowaną obsługę zapytań o dane użytkowników zapisane w bazie. Domyślnie zapytania dotyczą danych o użytkownikach (w tym uprawnień) i mają następującą postać SQL-a: SELECT username, password, enabled FROM users WHERE username = ? SELECT username, authority FROM authorities WHERE username = ?
Aby platforma Spring Security pobierała dane o użytkownikach za pomocą takich instrukcji SQL-a, trzeba utworzyć w bazie odpowiednie tabele. W bazie Apache Derby można je utworzyć w schemacie board za pomocą poniższych instrukcji SQL-a: 177
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
CREATE TABLE USERS ( USERNAME VARCHAR(10) NOT NULL, PASSWORD VARCHAR(32) NOT NULL, ENABLED SMALLINT, PRIMARY KEY (USERNAME) ); CREATE TABLE AUTHORITIES ( USERNAME VARCHAR(10) NOT NULL, AUTHORITY VARCHAR(10) NOT NULL, FOREIGN KEY (USERNAME) REFERENCES USERS );
Następnie na potrzeby testów można umieścić w tych tabelach przykładowe dane o użytkownikach. Dane do dwóch utworzonych tabel są pokazane w tabelach 5.1 i 5.2. Tabela 5.1. Testowe dane o użytkownikach dla tabeli USERS USERNAME
PASSWORD
ENABLED
Admin
Secret
1
user1
1111
1
user2
2222
0
Tabela 5.2. Testowe dane o użytkownikach dla tabeli AUTHORITIES USERNAME
AUTHORITY
Admin
ROLE_ADMIN
Admin
ROLE_USER
user1
ROLE_USER
user2
ROLE_USER
Aby platforma Spring Security miała dostęp do tych tabel, należy zadeklarować źródło danych (na przykład w pliku board-service.xml) używane do nawiązywania połączeń z bazą danych. Uwaga By nawiązać połączenie z bazą danych Apache Derby, potrzebne są klienckie pliki .jar tego serwera, a także obsługa interfejsu JDBC w Springu. Jeśli używasz Mavena, dodaj do projektu następujące zależności: org.apache.derby derbyclient 10.4.2.0 org.springframework spring-jdbc ${spring.version}
178
5.3. UWIERZYTELNIANIE UŻYTKOWNIKÓW
Ostatni krok polega na skonfigurowaniu dostawcy uwierzytelniania, który kieruje do bazy danych zapytania o dane użytkowników. Aby skonfigurować dostawcę, wystarczy dodać element z referencją do źródła danych.
Jednak w niektórych sytuacjach programista ma własne repozytorium zdefiniowane w istniejącej już bazie danych. Załóżmy, że tabele utworzono za pomocą poniższych instrukcji SQL-a i że wszyscy użytkownicy z tabeli MEMBER mają status enabled. CREATE TABLE MEMBER ( ID BIGINT USERNAME VARCHAR(10) PASSWORD VARCHAR(32) PRIMARY KEY (ID) );
NOT NULL, NOT NULL, NOT NULL,
CREATE TABLE MEMBER_ROLE ( MEMBER_ID BIGINT NOT NULL, ROLE VARCHAR(10) NOT NULL, FOREIGN KEY (MEMBER_ID) REFERENCES MEMBER );
Ponadto przyjmijmy, że w bazie zapisane są dane o użytkownikach przedstawione w tabelach 5.3 i 5.4. Tabela 5.3. Istniejące dane o użytkownikach zapisane w tabeli MEMBER ID
USERNAME
PASSWORD
1
Admin
Secret
2
user1
1111
Tabela 5.4. Istniejące dane o użytkownikach zapisane w tabeli MEMBER_ROLE MEMBER_ID
ROLE
1
ROLE_ADMIN
1
ROLE_USER
2
ROLE_USER
Na szczęście Spring Security obsługuje też niestandardowe instrukcje SQL-a pozwalające pobierać dane o użytkownikach z istniejących baz danych. Instrukcje przeznaczone do pobierania danych o użytkownikach i uprawnień można podać w atrybutach users-by-username-query i authorities-by-username-query.
179
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
authorities-by-username-query= "SELECT member.username, member_role.role as authorities FROM member, member_role WHERE member.username = ? AND member.id = member_role.member_id" />
Szyfrowanie haseł Do tego miejsca dane o użytkownikach były zapisywane z hasłami w postaci zwykłego tekstu. Jednak to podejście powoduje zagrożenie atakami ze strony hakerów. Dlatego hasła przed ich zapisaniem należy zaszyfrować. Spring Security udostępnia kilka algorytmów do szyfrowania haseł. Możesz na przykład zastosować do tego jednokierunkowy algorytm haszujący MD5 (ang. Message-Digest Algorithm 5). Uwaga Możliwe, że będziesz potrzebował narzędzia do wyznaczenia skrótów MD5 haseł. Jednym z takich narzędzi jest Jacksum. Możesz je pobrać ze strony http://sourceforge.net/projects/jacksum/ i wypakować do wybranego katalogu. Następnie wywołaj poniższe polecenie, aby wyznaczyć skrót odpowiadający podanemu tekstowi: java -jar jacksum.jar -a md5 -q "txt:secret"
Teraz możesz zachować zaszyfrowane hasła w repozytorium z danymi użytkowników. Jeśli przechowujesz dane w pamięci, możesz zapisać zaszyfrowane hasła w atrybutach password. Następnie skonfiguruj element . W tym celu podaj w nim w atrybucie hash algorytm haszujący.
Enkoder haseł można ustawić także dla repozytorium użytkowników zapisanego w bazie danych.
W tabelach bazy należy oczywiście zapisać zaszyfrowane hasła, a nie ich tekstowe odpowiedniki. Ilustruje to tabela 5.5. Tabela 5.5. Zapisywane w tabeli USERS testowe dane użytkowników z zaszyfrowanymi hasłami USERNAME
PASSWORD
ENABLED
Admin
5ebe2294ecd0e0f08eab7690d2a6ee69
1
user1
b59c67bf196a4758191e42f76670ceba
1
user2
934b535800b1cba8f96a5d72f72f1611
0
180
5.3. UWIERZYTELNIANIE UŻYTKOWNIKÓW
Uwierzytelnianie użytkowników za pomocą repozytorium LDAP Spring Security obsługuje też uwierzytelnianie użytkowników za pomocą repozytorium LDAP. Najpierw trzeba przygotować dane o użytkownikach przeznaczone do zapisania w takim repozytorium. Przygotuj takie dane w formacie LDIF (ang. LDAP Data Interchange Format). Jest to standardowy format tekstowy służący do importowania i eksportowania danych z katalogów LDAP. Utwórz przykładowy plik users.ldif zawierający poniższe dane: dn: dc=springrecipes,dc=com objectClass: top objectClass: domain dc: springrecipes dn: ou=groups,dc=springrecipes,dc=com objectclass: top objectclass: organizationalUnit ou: groups dn: ou=people,dc=springrecipes,dc=com objectclass: top objectclass: organizationalUnit ou: people dn: uid=admin,ou=people,dc=springrecipes,dc=com objectclass: top objectclass: uidObject objectclass: person uid: admin cn: admin sn: admin userPassword: secret dn: uid=user1,ou=people,dc=springrecipes,dc=com objectclass: top objectclass: uidObject objectclass: person uid: user1 cn: user1 sn: user1 userPassword: 1111 dn: cn=admin,ou=groups,dc=springrecipes,dc=com objectclass: top objectclass: groupOfNames cn: admin member: uid=admin,ou=people,dc=springrecipes,dc=com dn: cn=user,ou=groups,dc=springrecipes,dc=com objectclass: top objectclass: groupOfNames cn: user member: uid=admin,ou=people,dc=springrecipes,dc=com member: uid=user1,ou=people,dc=springrecipes,dc=com
Nie martw się, jeśli nie rozumiesz przedstawionego pliku LDIF. Prawdopodobnie nie będziesz musiał często używać tego formatu do definiowania danych do repozytorium LDAP, ponieważ większość serwerów LDAP umożliwia konfigurowanie ustawień za pomocą interfejsu graficznego. Pokazany plik users.ldif zawiera następujące elementy:
181
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
domyślną domenę LDAP (dc=springrecipes,dc=com), jednostki organizacyjne groups i people służące do przechowywania grup i użytkowników, użytkowników admin i user1 o hasłach secret i 1111, grupę admin (zawierającą użytkownika admin) i grupę user (obejmującą użytkowników admin i user1). Na potrzeby testów możesz zainstalować na lokalnym komputerze serwer LDAP, aby zapisać w nim repozytorium użytkowników. By ułatwić sobie instalowanie i konfigurowanie, wybierz serwer OpenDS (http://www.opends.org/). Jest to oparty na Javie otwarty silnik usług katalogowych obsługujący protokół LDAP. Uwaga Serwer OpenDS ma dwa rodzaje interfejsów instalacyjnych. Jeden z nich działa w wierszu poleceń, a drugi ma interfejs graficzny. W tym przykładzie używany jest interfejs działający w wierszu poleceń, dlatego pobierz plik ZIP z serwerem, wypakuj go do wybranego katalogu (na przykład C:\OpenDS-2.2.0), a następnie uruchom skrypt setup w tym katalogu. C:\OpenDS-2.2.0>setup --cli OpenDS Directory Server 2.2.0 Please wait while the setup program initializes... What would you like to use as the initial root user DN for the Directory Server? [cn=Directory Manager]: Please provide the password to use for the initial root user: ldap Please re-enter the password for confirmation: ldap On which port would you like the Directory Server to accept connections from LDAP clients? [1389]: On which port would you like the Administration Connector to accept connections? [4444]: What do you wish to use as the base DN for the directory data? [dc=example,dc=com]:dc=springrecipes,dc=com Options for populating the database: 1) 2) 3) 4)
Only create the base entry Leave the database empty Import data from an LDIF file Load automatically-generated sample data
Enter choice [1]: 3 Please specify the path to the LDIF file containing the data to import: users.ldif Do you want to enable SSL? (yes / no) [no]: Do you want to enable Start TLS? (yes / no) [no]: Do you want to start the server when the configuration is completed? (yes / no) [yes]: Enable OpenDS to run as a Windows Service? (yes / no) [no]: What 1) 2) 3)
182
would you like to do? Setup the server with the parameters above Provide the setup parameters again Cancel the setup
5.3. UWIERZYTELNIANIE UŻYTKOWNIKÓW
Enter choice [1]: Configuring Directory Server ..... Done. Importing LDIF file users.ldif ....... Done. Starting Directory Server ........ Done. Do you want to start the server when the configuration is completed? (yes / no) [yes]:
Zauważ, że na tym serwerze LDAP główny użytkownik i powiązane z nim hasło to cn=DirectoryManager i ldap. Później będziesz potrzebował tych danych do nawiązania połączenia z serwerem. Po uruchomieniu serwera LDAP możesz skonfigurować platformę Spring Security, by uwierzytelniała użytkowników na podstawie zapisanego na tym serwerze repozytorium.
Uwaga Aby uwierzytelniać użytkowników na podstawie repozytorium LDAP, musisz umieścić w parametrze classpath projekt Springa wykorzystujący to repozytorium. Jeśli używasz Mavena, dodaj do projektu Mavena poniższą zależność: org.springframework.ldap spring-ldap 1.3.0.RELEASE
...
Należy skonfigurować element w celu zdefiniowania, jak aplikacja ma wyszukiwać użytkowników w repozytorium LDAP. Możesz tu za pomocą kilku atrybutów określić filtry, a także uwzględnianą przy przeszukiwaniu listę grup i użytkowników. Wartości atrybutów muszą być zgodne ze strukturą katalogów repozytorium. Przy ustawieniach zastosowanych w przedstawionym kodzie platforma Spring Security będzie wyszukiwać w jednostce organizacyjnej people użytkownika o określonym identyfikatorze oraz będzie przeszukiwać grupy użytkowników z jednostki organizacyjnej groups. Spring Security Spring Security automatycznie przypisuje do każdej grupy uprawnienia ROLE_przyrostek. Ponieważ na serwerze OpenDS do kodowania haseł użytkowników domyślnie używany jest algorytm SSHA (ang. Salted Secure Hash Algorithm), w elemencie należy ustawić algorytm haszujący {sha}. Zauważ, że wartość ta różni się od ustawienia sha i jest specyficzna dla kodowania haseł z serwera LDAP. Element musi określać definicję serwera LDAP, która informuje o tym, jak należy nawiązywać połączenia z serwerem LDAP. Można określić tu nazwę głównego użytkownika i jego hasło, aby połączyć się z serwerem LDAP działającym na lokalnym komputerze.
183
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
Zapisywanie danych o użytkownikach w pamięci podręcznej Zarówno element , jak i element umożliwiają obsługę zapisywania danych o użytkownikach w pamięci podręcznej. Przede wszystkim trzeba wybrać używaną implementację obsługi pamięci podręcznej. Ponieważ Spring i Spring Security mają wbudowaną obsługę narzędzia Ehcache (http://ehcache.sourceforge.net/), możesz wybrać je jako implementację pamięci podręcznej i utworzyć dla niego plik konfiguracyjny (ehcache.xml z katalogu podanego w parametrze classpath) o następującej zawartości:
Uwaga Aby zastosować narzędzie Ehcache do zapisywania obiektów w pamięci podręcznej, trzeba dodać bibliotekę Ehcache 1.7.2 do parametru classpath. Jeśli używasz Mavena, dodaj do projektu Mavena poniższą zależność: net.sf.ehcache 1.7.2 ehcache-core
W przedstawionym pliku z ustawieniami narzędzia Ehcache zdefiniowane są dwa rodzaje konfiguracji pamięci podręcznej. Jeden dotyczy ustawień domyślnych, a drugi jest powiązany z zapisywaniem w pamięci podręcznej danych o użytkownikach. Jeśli używana jest konfiguracja pamięci podręcznej danych o użytkownikach, w pamięci są zapisywane informacje o maksymalnie 100 osobach. Gdy ten limit zostanie przekroczony, dane o nadmiarowych użytkownikach będą zapisywane na dysku. Informacje wygasają po 10 minutach bezczynności użytkownika lub, jeśli dana osoba jest aktywna, po godzinie od ich utworzenia. Aby w platformie Spring Security włączyć zapisywanie danych o użytkownikach w pamięci podręcznej, należy ustawić atrybut cache-ref w elemencie lub . W tym atrybucie trzeba podać obiekt typu UserCache. Dla narzędzia Ehcache platforma Spring Security udostępnia odpowiednią implementację tego interfejsu, EhCacheBasedUserCache, w której trzeba ustawić obiekt pamięci Ehcache. ... ...
184
5.4. PODEJMOWANIE DECYZJI Z ZAKRESU KONTROLI DOSTĘPU
W Springu obiekt pamięci Ehcache można utworzyć za pomocą ziarna EhCacheFactoryBean. W tym celu należy podać menedżer i nazwę pamięci podręcznej. Spring udostępnia też ziarno EhCacheManagerFactoryBean, które służy do tworzenia menedżera pamięci Ehcache na podstawie pliku konfiguracyjnego. Domyślnie wczytywany jest plik ehcache.xml (zlokalizowany w katalogu głównym z parametru classpath). Ponieważ menedżera pamięci Ehcache mogą używać także inne komponenty usługowe, należy go zdefiniować w pliku board-service.xml.
5.4. Podejmowanie decyzji z zakresu kontroli dostępu Problem W procesie uwierzytelniania aplikacja przyznaje zestaw uprawnień użytkownikom, którzy pomyślnie przeszli ten proces. Gdy użytkownik próbuje uzyskać dostęp do określonego zasobu aplikacji, musi ona ustalić, czy ten zasób jest dostępny dla osoby o przyznanych uprawnieniach (lub innych cechach).
Rozwiązanie Decyzja z zakresu kontroli dostępu polega na ustaleniu, czy użytkownik jest uprawniony do dostępu do danego zasobu aplikacji. Podejmowana jest na podstawie statusu użytkownika, a także natury i atrybutów dostępu zasobu. W platformie Spring Security decyzje z zakresu kontroli dostępu są podejmowane przez specjalne menedżery, które muszą zawierać implementację interfejsu AccessDecisionManager. Możesz samodzielnie utworzyć takie menedżery (wystarczy napisać implementację tego interfejsu), jednak Spring Security udostępnia trzy wygodne menedżery działające w modelu głosowania. Ich opis znajdziesz w tabeli 5.6. Tabela 5.6. Dostępne w platformie Spring Security menedżery do podejmowania decyzji z zakresu kontroli dostępu Menedżer podejmowania decyzji o dostępie Kiedy dostęp jest przyznawany? AffirmativeBased
Gdy przynajmniej jeden głosujący optuje za przyznaniem dostępu.
ConsensusBased
Gdy większość głosujących optuje za przyznaniem dostępu.
UnanimousBased
Gdy wszyscy głosujący wstrzymują się od głosu lub optują za przyznaniem dostępu (żaden głosujący nie może odmówić dostępu).
185
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
Wszystkie te menedżery wymagają skonfigurowania grupy jednostek głosujących, optujących za przyznaniem lub odmową dostępu. W każdej takiej jednostce trzeba zaimplementować interfejs AccessDecisionVoter. Głosujący może optować za przyznaniem lub odmową dostępu do zasobu albo wstrzymać się od głosu. Głosy są reprezentowane przez stałe ACCESS_GRANTED, ACCESS_DENIED i ACCESS_ABSTAIN zdefiniowane w interfejsie AccessDecisionVoter. Jeśli nie jest ustawiony żaden menedżer, Spring Security domyślnie automatycznie konfiguruje menedżer typu AffirmativeBased ze skonfigurowanymi dwoma opisanymi poniżej jednostkami głosującymi. Jednostka RoleVoter podejmuje decyzje na podstawie ról użytkowników. Przetwarza tylko atrybuty dostępu w formie ROLE_przedrostek, przy czym człon przedrostek można dowolnie zmodyfikować. Jeśli użytkownik ma rolę potrzebną do uzyskania dostępu do zasobu, jednostka ta głosuje za przyznaniem dostępu. Gdy użytkownik nie ma żadnej z wymaganych ról, jednostka optuje za odmową dostępu. Jeżeli zasób nie ma żadnego atrybutu dostępu w postaci ROLE_przedrostek, jednostka wstrzymuje się od głosu. Jednostka AuthenticatedVoter podejmuje decyzje na podstawie poziomu uwierzytelnienia użytkownika. Przetwarza ona tylko atrybuty dostępu IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED i IS_AUTHENTICATED_ANONYMOUSLY. Ta jednostka głosuje za przyznaniem dostępu, jeśli poziom użytkownika jest wyższy od wymaganego lub mu równy (wcześniej wymieniono poziomy w kolejności od najwyższego do najniższego).
Jak to działa? Domyślnie Spring Security automatycznie konfiguruje menedżer podejmowania decyzji o dostępie, jeśli sam go nie ustawisz. Menedżer domyślny jest odpowiednikiem menedżera zdefiniowanego za pomocą poniższej konfiguracji ziarna:
Ten domyślny menedżer i powiązane z nim jednostki głosujące są odpowiednim rozwiązaniem w większości typowych sytuacji wymagających sprawdzania uprawnień. Jeśli jednak masz inne potrzeby, możesz utworzyć własne menedżery lub jednostki głosujące. W większości przypadków wystarczy przygotować niestandardowe jednostki głosujące. Możesz na przykład dodać jednostkę podejmującą decyzję na podstawie adresów IP użytkowników: package com.apress.springrecipes.board.security; import org.springframework.security.core.Authentication; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.access.AccessDecisionVoter; import java.util.Collection; public class IpAddressVoter implements AccessDecisionVoter { public static final String IP_PREFIX = "IP_"; public static final String IP_LOCAL_HOST = "IP_LOCAL_HOST"; public boolean supports(ConfigAttribute attribute) { return attribute.getAttribute() != null
186
5.4. PODEJMOWANIE DECYZJI Z ZAKRESU KONTROLI DOSTĘPU
&& attribute.getAttribute().startsWith(IP_PREFIX); } public boolean supports(Class clazz) { return true; } public int vote(Authentication authentication, Object object, Collection configList) { if (!(authentication.getDetails() instanceof WebAuthenticationDetails)) { return ACCESS_DENIED; } WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails(); String address = details.getRemoteAddress(); int result = ACCESS_ABSTAIN; for (ConfigAttribute config : configList) { result = ACCESS_DENIED; if (IP_LOCAL_HOST.equals(config.getAttribute())) { if (address.equals("127.0.0.1") || address.equals("0:0:0:0:0:0:0:1")) { return ACCESS_GRANTED; } } } return result; } }
Zauważ, że ta jednostka przetwarza tylko atrybuty dostępu w formie IP_przedrostek. W tej wersji obsługiwany jest tylko atrybut dostępu IP_LOCAL_HOST. Jeśli użytkownik korzysta z przeglądarki o adresie IP równym 127.0.0.1 lub 0:0:0:0:0:0:0:1 (ta ostatnia wartość jest zwracana przez linuksowe stacje robocze bez dostępu do sieci), jednostka „uzna”, że należy przyznać dostęp do zasobu. W przeciwnym razie jednostka zagłosuje za odmową dostępu. Jeśli dany zasób nie ma atrybutu dostępu w formie IP_przedrostek, jednostka wstrzyma się od głosu. Następnie należy zdefiniować niestandardowy menedżer powiązany z nową jednostką głosującą. Jeśli definiujesz menedżer w pliku board-security.xml, musisz dodać przedrostek beans, ponieważ schematem domyślnym używanym w tym pliku jest schemat security.
Teraz załóżmy, że chcesz umożliwić użytkownikom komputera, na którym działa dany kontener WWW (czyli administratorom serwera), usuwanie wiadomości bez logowania. W tym celu trzeba w elemencie konfiguracyjnym ustawić nowy menedżer podejmowania decyzji o dostępie i dodać atrybut dostępu IP_LOCAL_HOST do wzorca adresów URL /messageDelete.htm*.
187
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
...
Gdy teraz otworzysz forum z poziomu komputera lokalnego, nie będziesz musiał logować się jako administrator, aby móc usunąć zamieszczone wiadomości.
5.5. Zabezpieczanie wywołań metod Problem Zastępnikiem (lub uzupełnieniem) zabezpieczania dostępu do adresów URL w warstwie sieciowej jest zabezpieczanie wywołań metod w warstwie usług. Na przykład gdy jeden kontroler musi wywoływać różne metody z warstwy usług, można precyzyjnie kontrolować te wywołania.
Rozwiązanie Spring Security umożliwia deklaratywne zabezpieczanie wywołań metod. Jedną z technik jest osadzenie elementu w definicji ziarna, co pozwala zabezpieczyć jego metody. Inna możliwość to skonfigurowanie globalnego elementu , co powoduje zabezpieczenie różnych metod pasujących do wyrażeń z punktami przecięcia języka AspectJ. Możesz też opatrzyć adnotacją @Secured metody zadeklarowane w interfejsie ziarna lub w klasie z implementacją, a następnie włączyć dla tych metod zabezpieczenia w elemencie .
Jak to działa? Zabezpieczanie metod w wyniku zagnieżdżenia interceptora Pierwsza technika zabezpieczania metod ziarna polega na osadzeniu elementu w definicji ziarna. Możesz na przykład zabezpieczyć metody ziarna messageBoardService zdefiniowane w pliku board-service.xml. Ponieważ wspomniany element jest zdefiniowany w schemacie security, trzeba zaimportować ten schemat.
188
5.5. ZABEZPIECZANIE WYWOŁAŃ METOD
...
W elemencie ziarna można podać różne elementy , aby określić atrybuty dostępu powiązane z metodami tego ziarna. Można jednocześnie wskazać więcej metod; w tym celu należy podać wzorzec nazw metod zawierający symbol wieloznaczny. Jeśli chcesz zastosować niestandardowy menedżer podejmowania decyzji o dostępie, możesz wskazać go w atrybucie access-decision-manager-ref.
Zabezpieczanie metod za pomocą punktów przecięcia Drugim rozwiązaniem jest zdefiniowanie globalnych punktów przecięcia w elemencie . W ten sposób można zabezpieczyć metody za pomocą wyrażeń z punktami przecięcia języka AspectJ, zamiast osadzać interceptor zabezpieczeń w każdym ziarnie, którego metody tego wymagają. Element należy skonfigurować w pliku board-security.xml, co pozwala scentralizować ustawienia zabezpieczeń. Ponieważ domyślna przestrzeń nazw w tym pliku to security, nie trzeba bezpośrednio określać przedrostka wspomnianego elementu. W atrybucie access-decision-manager-ref możesz podać niestandardowy menedżer podejmowania decyzji o dostępie.
Aby przetestować to podejście, musisz usunąć dodany wcześniej element .
Zabezpieczanie metod za pomocą adnotacji Trzecia technika zabezpieczania metod polega na opatrzeniu ich adnotacją @Secured. Możesz na przykład dodać tę adnotację do metod klasy MessageBoadServiceImpl i jako wartość tej adnotacji (ta wartość jest typu String[]) podać atrybuty dostępu. package com.apress.springrecipes.board.service; ... import org.springframework.security.access.annotation.Secured;
189
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
public class MessageBoardServiceImpl implements MessageBoardService { ... @Secured({"ROLE_USER", "ROLE_GUEST"}) public List listMessages() { ... } @Secured("ROLE_USER") public synchronized void postMessage(Message message) { ... } @Secured({"ROLE_ADMIN", "IP_LOCAL_HOST"}) public synchronized void deleteMessage(Message message) { ... } @Secured({"ROLE_USER", "ROLE_GUEST"}) public Message findMessageById(Long messageId) { return messages.get(messageId); } }
Następnie w elemencie trzeba włączyć zabezpieczenia dla metod opatrzonych adnotacją @Secured.
5.6. Obsługa zabezpieczeń w widokach Problem Czasem programista chce wyświetlać w widokach aplikacji sieciowej informacje związane z uwierzytelnianiem, na przykład nazwę użytkownika i przyznane uprawnienia. Ponadto widoki powinny być wyświetlane warunkowo na podstawie uprawnień poszczególnych osób.
Rozwiązanie W plikach JSP można napisać skryptlety JSP, aby pobierać informacje o uwierzytelnianiu i uprawnieniach za pomocą interfejsu API platformy Spring Security, ale jest to mało wydajne rozwiązanie. Spring Security udostępnia bibliotekę znaczników JSP, która pozwala obsługiwać zabezpieczenia w widokach JSP. Ta biblioteka obejmuje znaczniki umożliwiające pokazywanie informacji o uwierzytelnieniu, a także warunkowe wyświetlanie zawartości widoków na podstawie uprawnień użytkowników.
Jak to działa? Wyświetlanie informacji o uwierzytelnieniu Załóżmy, że chcesz wyświetlić nazwę użytkownika i przyznane mu uprawnienia w nagłówku strony z listą wiadomości (jest to strona messageList.jsp). Przede wszystkim trzeba zaimportować definicję biblioteki znaczników platformy Spring Security.
190
5.6. OBSŁUGA ZABEZPIECZEŃ W WIDOKACH
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %> Lista wiadomości Witaj! ...
Znacznik daje dostęp do obiektu Authentication bieżącego użytkownika, co pozwala wyświetlić właściwości tego obiektu. W atrybucie property można podać nazwę właściwości lub ścieżkę do niej. Na przykład właściwość name służy do wyświetlania nazwy użytkownika. Oprócz bezpośredniego wyświetlania właściwości związanych z uwierzytelnianiem można za pomocą tego znacznika zapisać wartość właściwości w zmiennej JSP. Nazwę tej zmiennej można podać w atrybucie var. Możesz na przykład zapisać wartości właściwości authorities (zawiera ona przyznane użytkownikowi uprawnienia) w zmiennej JSP authorities i wyświetlić je jedna po drugiej za pomocą znacznika . Ponadto korzystając z atrybutu scope, można określić zasięg zmiennej.
Warunkowe wyświetlanie zawartości widoków Jeśli chcesz warunkowo wyświetlać zawartość widoku na podstawie uprawnień użytkownika, zastosuj znacznik . W oparciu o uprawnienia można na przykład wyświetlać autorów wiadomości. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %> Lista wiadomości ... Autor ${message.author} ...
191
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
...
Aby wewnętrzny fragment był wyświetlany tylko wtedy, gdy użytkownik ma przyznane wszystkie określone uprawnienia, trzeba podać te uprawnienia w atrybucie ifAllGranted. Jeżeli fragment ma być widoczny, gdy użytkownik ma dowolne uprawnienia z listy, należy wymienić je w atrybucie ifAnyGranted. Autor ${message.author}
Wewnętrzny fragment można też wyświetlać, jeśli użytkownik nie ma żadnego z listy uprawnień podanych w atrybucie ifNotGranted. Autor ${message.author}
5.7. Zabezpieczanie obiektów domenowych Problem Czasem skomplikowane wymogi z zakresu bezpieczeństwa wymagają obsługi zabezpieczeń na poziomie obiektów domenowych. To oznacza, że w każdym takim obiekcie należy ustawić inne atrybuty dostępu dla różnych uwierzytelnianych jednostek.
Rozwiązanie Spring Security udostępnia moduł ACL, który pozwala na przypisanie do każdego obiektu domenowego listy kontroli dostępu (ang. Access Control List — ACL). Lista ACL określa tożsamość obiektu, a także zestaw pozycji kontroli dostępu (ang. Access Control Entry — ACE), z których każda składa się z dwóch podstawowych części. Są to: Uprawnienia. Uprawnienia w pozycjach ACE są reprezentowane za pomocą maski bitowej, w której każdy bit dotyczy uprawnień konkretnego rodzaju. W klasie BasePermissions zdefiniowanych jest pięć podstawowych uprawnień. Mają one postać stałych: READ (bit 0 lub liczba całkowita 1), WRITE (bit 1 lub liczba całkowita 2), CREATE (bit 2 lub liczba całkowita 4), DELETE (bit 3 lub liczba całkowita 8) i ADMINISTRATION (bit 4 lub liczba całkowita 16). Możesz też zdefiniować własne uprawnienia przy użyciu pozostałych, niewykorzystanych bitów. Identyfikator SID (ang. Security Identity). Każda pozycja ACE obejmuje uprawnienia dla konkretnego identyfikatora SID. Taki identyfikator może określać uwierzytelnianą jednostkę (PrincipalSid) lub rolę (GrantedAuthoritySid), którą należy powiązać z uprawnieniami. Spring Security obok definicji modelu opartego na listach ACL udostępnia interfejsy API służące do wczytywania i ustawiania tego modelu, a ponadto zapewnia wydajne implementacje tych interfejsów zgodne z technologią JDBC. Aby uprościć korzystanie z list ACL, Spring Security udostępnia narzędzia (na przykład jednostki głosujące i znaczniki JSP), które pozwalają stosować listy ACL w sposób spójny z innymi mechanizmami przeznaczonymi do zabezpieczania aplikacji. 192
5.7. ZABEZPIECZANIE OBIEKTÓW DOMENOWYCH
Jak to działa? Konfigurowanie usług opartych na listach ACL Spring Security oferuje wbudowaną obsługę przechowywania danych z list ACL w relacyjnej bazie danych i dostępu do nich za pomocą interfejsu JDBC. W tym celu trzeba przede wszystkim utworzyć w bazie poniższe tabele na dane z listy ACL. CREATE TABLE ACL_SID( ID BIGINT SID VARCHAR(100) PRINCIPAL SMALLINT PRIMARY KEY (ID), UNIQUE (SID, PRINCIPAL) );
NOT NULL GENERATED BY DEFAULT AS IDENTITY, NOT NULL, NOT NULL,
CREATE TABLE ACL_CLASS( ID BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, CLASS VARCHAR(100) NOT NULL, PRIMARY KEY (ID), UNIQUE (CLASS) ); CREATE TABLE ACL_OBJECT_IDENTITY( ID BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, OBJECT_ID_CLASS BIGINT NOT NULL, OBJECT_ID_IDENTITY BIGINT NOT NULL, PARENT_OBJECT BIGINT, OWNER_SID BIGINT, ENTRIES_INHERITING SMALLINT NOT NULL, PRIMARY KEY (ID), UNIQUE (OBJECT_ID_CLASS, OBJECT_ID_IDENTITY), FOREIGN KEY (PARENT_OBJECT) REFERENCES ACL_OBJECT_IDENTITY, FOREIGN KEY (OBJECT_ID_CLASS) REFERENCES ACL_CLASS, FOREIGN KEY (OWNER_SID) REFERENCES ACL_SID ); CREATE TABLE ACL_ENTRY( ID BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, ACL_OBJECT_IDENTITY BIGINT NOT NULL, ACE_ORDER INT NOT NULL, SID BIGINT NOT NULL, MASK INTEGER NOT NULL, GRANTING SMALLINT NOT NULL, AUDIT_SUCCESS SMALLINT NOT NULL, AUDIT_FAILURE SMALLINT NOT NULL, PRIMARY KEY (ID), UNIQUE (ACL_OBJECT_IDENTITY, ACE_ORDER), FOREIGN KEY (ACL_OBJECT_IDENTITY) REFERENCES ACL_OBJECT_IDENTITY, FOREIGN KEY (SID) REFERENCES ACL_SID );
W platformie Spring Security zdefiniowane są interfejsy API i wydajne implementacje interfejsu JDBC pozwalające na dostęp do danych z list ACL zapisanych w przedstawionych tabelach. Z tej przyczyny rzadko trzeba pobierać dane z list ACL bezpośrednio z bazy danych. Ponieważ każdy obiekt domenowy może mieć własną listę ACL, czasem w aplikacji jest wiele takich list. Na szczęście Spring Security obsługuje zapisywanie obiektów ACL w pamięci podręcznej. Także tu można używać narzędzia Ehcache i tworzyć w pliku ehcache.xml (zlokalizowanym w katalogu głównym z parametru classpath) nowe konfiguracje na potrzeby zapisywania list ACL w pamięci podręcznej.
193
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
...
Następnie należy skonfigurować na potrzeby aplikacji usługi związane z listami ACL. Jednak ponieważ Spring Security nie umożliwia konfigurowania modułu ACL za pomocą konfiguracji opartej na schemacie XML-a, moduł ten trzeba skonfigurować przy użyciu zwykłych ziaren Springa. Domyślna przestrzeń nazw w pliku board-security.xml to security, dlatego niewygodne jest konfigurowanie modułu ACL w tym pliku za pomocą standardowych elementów XML-a z przestrzeni nazw beans. Utwórz więc odrębny plik z konfiguracją ziaren, board-acl.xml, w którym znajdą się ustawienia związane z listami ACL. Lokalizację tego pliku dodaj do deskryptora wdrażania. ... contextConfigLocation /WEB-INF/board-service.xml /WEB-INF/board-security.xml /WEB-INF/board-acl.xml
W pliku z konfiguracją modułu ACL głównym ziarnem jest usługa ACL. W Spring Security dostępne są dwa interfejsy (AclService i MutableAclService), które definiują działanie tej usługi. AclService odpowiada za operacje służące do wczytywania list ACL. MutableAclService to interfejs pochodny od AclService, zawierający operacje, które pozwalają tworzyć, aktualizować i usuwać listy ACL. Jeśli w aplikacji potrzebne jest tylko wczytywanie takich list, można wybrać implementację interfejsu AclService (na przykład JdbcAclService). W przeciwnym razie należy zastosować implementację interfejsu MutableAclService (na przykład JdbcMutableAclService).
194
5.7. ZABEZPIECZANIE OBIEKTÓW DOMENOWYCH
Główną definicją ziarna w tym pliku jest usługa ACL. Jest nią tu egzemplarz klasy JdbcMutableAclService, umożliwiający zarządzanie listami ACL. Konstruktor tej klasy wymaga podania trzech argumentów. Pierwszym jest źródło danych używane do nawiązywania połączeń z bazą przechowującą dane z list ACL. Takie źródło danych należy wcześniej zdefiniować w pliku board-service.xml, aby można było wskazać je w konfiguracji (zakładamy przy tym, że tabele ACL utworzyłeś w tej samej bazie danych). Trzeci argument to egzemplarz pamięci podręcznej dla list ACL. Jako używaną na zapleczu implementację pamięci podręcznej można wykorzystać tu narzędzie Ehcache. Drugi argument (sidIdentityQuery) to strategia wyszukiwania danych w usłudze ACL. Zauważ, że jeśli używasz bazy HSQLDB, nie trzeba podawać właściwości sidIdentityQuery. Wynika to z tego, że domyślnie wykorzystywana jest właśnie ta baza. Jeżeli używasz innej bazy (takiej jak Apache Derby), należy podać odpowiadającą jej wartość. Jedyną implementacją strategii wyszukiwania dostępną w Spring Security jest BasicLookupStrategy. Wykonuje ona podstawowe wyszukiwanie za pomocą standardowych instrukcji zgodnych z SQL-em. Jeśli chcesz wykorzystać zaawansowane mechanizmy bazy danych, aby przyspieszyć wyszukiwanie, możesz utworzyć własną implementację strategii wyszukiwania. Wymaga to zaimplementowania interfejsu LookupStrategy. Egzemplarz klasy BasicLookupStrategy także wymaga podania źródła danych i egzemplarza pamięci podręcznej. Ponadto w konstruktorze tej klasy trzeba zdefiniować argument typu AclAuthorizationStrategy. Obiekt tego typu określa, czy uwierzytelniana jednostka ma uprawienia do zmiany wybranych właściwości z listy ACL. Zwykle każda kategoria właściwości jest powiązana z wymaganymi uprawnieniami. W przedstawionej konfiguracji tylko użytkownik o roli ROLE_ADMIN może zmienić właściciela listy ACL, informacje z wpisów ACE, a także inne szczegóły. Usługa JdbcMutableAclService zawiera standardowe instrukcje SQL-a służące do zarządzania danymi z list ACL w relacyjnych bazach danych. Jednak te instrukcje są niezgodne z niektórymi bazami. Trzeba na przykład zmodyfikować instrukcję sprawdzania tożsamości na potrzeby bazy Apache Derby. 195
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
Zarządzanie listami ACL obiektów domenowych W działających na zapleczu usługach i obiektach DAO można zarządzać listami ACL obiektów domenowych za pomocą wcześniej zdefiniowanej usługi ACL, którą można dodać dzięki wstrzykiwaniu zależności. W rozwijanym forum należy utworzyć listę ACL dla zamieszczanej wiadomości i usuwać taką listę w momencie kasowania wiadomości. package com.apress.springrecipes.board.service; ... import org.springframework.security.acls.model.MutableAcl; import org.springframework.security.acls.model.MutableAclService; import org.springframework.security.acls.domain.BasePermission; import org.springframework.security.acls.model.ObjectIdentity; import org.springframework.security.acls.domain.ObjectIdentityImpl; import org.springframework.security.acls.domain.GrantedAuthoritySid; import org.springframework.security.acls.domain.PrincipalSid; import org.springframework.security.access.annotation.Secured; import org.springframework.transaction.annotation.Transactional; public class MessageBoardServiceImpl implements MessageBoardService { ... private MutableAclService mutableAclService; public void setMutableAclService(MutableAclService mutableAclService) { this.mutableAclService = mutableAclService; } @Transactional @Secured("ROLE_USER") public synchronized void postMessage(Message message) { ... ObjectIdentity oid = new ObjectIdentityImpl(Message.class, message.getId()); MutableAcl acl = mutableAclService.createAcl(oid); acl.insertAce(0, BasePermission.ADMINISTRATION, new PrincipalSid(message.getAuthor()), true); acl.insertAce(1, BasePermission.DELETE, new GrantedAuthoritySid("ROLE_ADMIN"), true); acl.insertAce(2, BasePermission.READ, new GrantedAuthoritySid("ROLE_USER"), true); mutableAclService.updateAcl(acl); } @Transactional @Secured({"ROLE_ADMIN", "IP_LOCAL_HOST"}) public synchronized void deleteMessage(Message message) { ... ObjectIdentity oid = new ObjectIdentityImpl(Message.class, message.getId()); mutableAclService.deleteAcl(oid, false); } }
Gdy użytkownik zamieszcza wiadomość, należy utworzyć nową listę ACL i podać identyfikator wiadomości jako tożsamość tej listy ACL. Gdy użytkownik usuwa wiadomość, trzeba usunąć powiązaną z nią listę ACL. Dla nowych wiadomości na liście ACL umieszczane są trzy wpisy ACE określające następujące uprawnienia:
196
5.7. ZABEZPIECZANIE OBIEKTÓW DOMENOWYCH
autor wiadomości może nią zarządzać, użytkownik o roli ROLE_ADMIN może usunąć wiadomość, użytkownik o roli ROLE_USER może odczytać wiadomość. Klasa JdbcMutableAclService wymaga, aby w wywołujących ją metodach była włączona obsługa transakcji. Dzięki temu instrukcje SQL-a można wykonywać w ramach transakcji. Dlatego dwie metody służące do zarządzania listami ACL są opatrzone adnotacją @Transactional, a w pliku board-service.xml zdefiniowane są menedżer transakcji i element . Aby można było zarządzać listami ACL, należy też pamiętać o wstrzyknięciu usługi ACL do usługi obsługującej forum. ...
Podejmowanie decyzji z zakresu kontroli dostępu na podstawie list ACL Jeśli dla każdego obiektu domenowego dostępna jest lista ACL, można wykorzystać te listy do podejmowania decyzji z zakresu kontroli dostępu do metod związanych z tymi obiektami. Na przykład gdy użytkownik spróbuje usunąć zamieszczoną wiadomość, można sprawdzić powiązaną z nią listę ACL i ustalić, czy dana osoba ma uprawnienia potrzebne do wykonania tej operacji. Spring Security udostępnia klasę AclEntryVoter, która umożliwia zdefiniowanie jednostki głosującej podejmującej decyzje na podstawie list ACL. Poniższa jednostka głosująca z pliku board-acl.xml uczestniczy w głosowaniu, jeśli dana metoda ma atrybut dostępu ACL_MESSAGE_DELETE, a typ argumentu tej metody to Message. Jeżeli bieżący użytkownik ma uprawnienia ADMINISTRATION lub DELETE na liście ACL dla obiektu domenowego reprezentującego wiadomość, będzie mógł usunąć daną wiadomość. ...
Po skonfigurowaniu jednostki głosującej trzeba dodać ją do menedżera decyzji o dostępie. Ponieważ jednostka głosująca bazująca na listach ACL nie może podejmować decyzji opartych na żądaniach HTTP, nie należy umieszczać jej w globalnym menedżerze decyzji o dostępie, gdyż menedżer ten jest używany dla elementu . Zamiast tego należy skonfigurować inny menedżer, związany z wywołaniami metod (tu jest to menedżer aclAccessDecisionManager), i dodać do niego jednostkę głosującą opartą na listach ACL. W pliku board-security.xml zmodyfikuj element , aby zastosować nowy menedżer do zabezpieczenia wywołań metod.
Po skonfigurowaniu jednostki głosującej i menedżera decyzji o dostępie ostatnim krokiem pozostaje ustawienie atrybutu dostępu ACL_MESSAGE_DELETE metody deleteMessage(). package com.apress.springrecipes.board.service; ... import org.springframework.security.access.annotation.Secured; public class MessageBoardServiceImpl implements MessageBoardService { ... @Transactional @Secured("ACL_MESSAGE_DELETE") public synchronized void deleteMessage(Message message) { ... } }
Po ustawieniu tego atrybutu tylko użytkownik o uprawnieniu ADMINISTRATION (domyślnie jest to autor wiadomości) lub DELETE (domyślnie jest to administrator o roli ROLE_ADMIN) do wiadomości podanej jako argument będzie mógł usunąć tę wiadomość. Jeśli chcesz ukryć odnośnik Usuń przy wiadomości, gdy bieżący użytkownik nie jest uprawniony do jej usunięcia, możesz umieścić odnośnik w znaczniku . Zawartość tego znacznika jest wyświetlana warunkowo w zależności od listy ACL danego obiektu domenowego. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %> Lista wiadomości
198
5.7. ZABEZPIECZANIE OBIEKTÓW DOMENOWYCH
... ...
Znacznik powoduje sprawdzenie listy ACL określonego obiektu domenowego w celu ustalenia, czy bieżący użytkownik ma potrzebne uprawnienia. Zawartość znacznika jest wyświetlana tylko wtedy, gdy użytkownik ma przynajmniej jedno z wymaganych uprawnień. Zauważ, że w tym znaczniku uprawnienia są podane za pomocą liczb całkowitych odpowiadających wartościom masek bitowych. Wartości 8 i 16 reprezentują uprawnienia DELETE i ADMINISTRATION.
Obsługa obiektów domenowych zwracanych przez metody Spring Security udostępnia dostawców uruchamianych po wywołaniu (ang. after invocation provider). Jednostki te obsługują zwrócone z metod obiekty domenowe zgodnie z listami ACL tych obiektów. Jeśli metoda zwraca jeden obiekt domenowy, można zarejestrować egzemplarz dostawcy AclEntryAfterInvocationProvider i sprawdzać, czy bieżący użytkownik ma uprawnienia potrzebne do uzyskania dostępu do tego obiektu. Jeżeli użytkownik nie ma wymaganych uprawnień, zgłaszany jest wyjątek, co zapobiega zwróceniu danego obiektu. Jeśli metoda zwraca kolekcję obiektów domenowych, można zarejestrować egzemplarz dostawcy AclEntryAfterInvocationCollectionFilteringProvider, aby filtrować zwracaną kolekcję na podstawie list ACL jej elementów. Obiekty domenowe, do których użytkownik nie ma uprawnień, są wtedy usuwane z kolekcji przed jej zwróceniem do metody wywołującej. ...
199
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
Aby w platformie Spring Security zarejestrować niestandardowego dostawcę uruchamianego po wywołaniu, wystarczy umieścić w definicji ziarna element . Ten element jest zdefiniowany w schemacie security, dlatego najpierw trzeba zaimportować ten schemat. Teraz można dodać atrybuty dostępu AFTER_ACL_COLLECTION_READ i AFTER_ACL_READ (będą one obsługiwane przez opisanych wcześniej dostawców uruchamianych po wywołaniu) dla metod listMessages() i findMessageById(). package com.apress.springrecipes.board.service; ... import org.springframework.security.access.annotation.Secured; public class MessageBoardServiceImpl implements MessageBoardService { ... @Secured({"ROLE_USER", "ROLE_GUEST", "AFTER_ACL_COLLECTION_READ"}) public List listMessages() { ... } @Secured({"ROLE_USER", "ROLE_GUEST", "AFTER_ACL_READ"}) public Message findMessageById(Long messageId) { ... } }
Podsumowanie Z tego rozdziału dowiedziałeś się, jak zabezpieczać aplikacje za pomocą platformy Spring Security 3.0. Można ją wykorzystać do zabezpieczania dowolnych aplikacji Javy, najbardziej przydatna jest jednak w aplikacjach sieciowych. W obszarze bezpieczeństwa ważne są kwestie uwierzytelniania, uprawnień i kontroli dostępu, dlatego powinieneś dobrze je zrozumieć. Często trzeba zabezpieczać ważne adresy URL przez zablokowanie nieuprawnionego dostępu do nich. Spring Security pomaga zrobić to w deklaratywny sposób. Platforma ta obsługuje zabezpieczenia za pomocą filtrów serwletów, które można skonfigurować w prostych elementach XML-a. Jeśli w danej aplikacji sieciowej wymagane są proste i typowe zabezpieczenia, można ustawić atrybut auto-config w elemencie http. Spring Security automatycznie skonfiguruje wtedy podstawowe usługi z zakresu bezpieczeństwa.
200
PODSUMOWANIE
Spring Security obsługuje różne sposoby logowania się użytkowników do aplikacji sieciowych. Są to na przykład logowanie oparte na formularzach i podstawowe uwierzytelnianie HTTP. Dostępna jest też usługa logowania anonimowego, dzięki której można traktować użytkowników anonimowych w taki sam sposób jak wszystkich innych. Mechanizm zapamiętywania użytkowników pozwala aplikacji zapamiętać tożsamość danej osoby między sesjami przeglądarki. W platformie Spring Security dostępnych jest wiele wbudowanych implementacji dostawców uwierzytelniania, a także sposobów uwierzytelniania użytkowników. Można na przykład uwierzytelniać użytkowników na podstawie definicji zapisanych w pamięci, relacyjnej bazy danych lub repozytorium LDAP. W repozytorium zawsze powinieneś przechowywać hasła w zaszyfrowanej postaci, ponieważ hasła w formie zwykłego tekstu mogą zostać przechwycone przez hakerów. Spring Security obsługuje też lokalne zapisywanie danych o użytkownikach w pamięci podręcznej, co pozwala uniknąć kosztów związanych z wykonywaniem zdalnych zapytań. Decyzje dotyczące tego, czy użytkownik ma uprawnienia do dostępu do danego zasobu, są podejmowane przez specjalne menedżery. Spring Security udostępnia trzy menedżery podejmowania decyzji z zakresu kontroli dostępu, w których wykorzystuje się głosowanie. Wszystkie te menedżery wymagają grupy jednostek głosujących, które należy skonfigurować pod kątem podejmowania decyzji z zakresu kontroli dostępu. Spring Security umożliwia zabezpieczanie wywołań metod w deklaratywny sposób. Można albo umieścić interceptor zabezpieczeń w definicji ziarna, albo dopasowywać metody za pomocą wyrażeń z punktem przecięcia języka AspectJ lub adnotacji. Spring Security pozwala też pokazywać w widokach JSP informacje o uwierzytelnieniu użytkownika i warunkowo wyświetlać zawartość widoków na podstawie uprawnień użytkownika. Spring Security udostępnia moduł ACL, który umożliwia utworzenie dla każdego obiektu domenowego listy ACL na potrzeby kontroli dostępu. Za pomocą wydajnych interfejsów API platformy Spring Security można wczytywać listy ACL poszczególnych obiektów domenowych i zarządzać tymi listami. Interfejsy te są implementowane za pomocą technologii JDBC. Spring Security udostępnia ponadto jednostki głosujące i znaczniki JSP, dzięki którym można w sposób spójny korzystać z list ACL i z innych mechanizmów z obszaru bezpieczeństwa.
201
ROZDZIAŁ 5. BEZPIECZEŃSTWO W SPRINGU
202
ROZDZIAŁ 6
Integrowanie Springa z innymi platformami do tworzenia aplikacji sieciowych W tym rozdziale zobaczysz, jak zintegrować Spring z kilkoma popularnymi platformami do tworzenia aplikacji sieciowych. Te platformy to: Struts, JSF i DWR. Dający duże możliwości kontener IoC oraz rozbudowane mechanizmy dla aplikacji korporacyjnych sprawiają, że Spring dobrze nadaje się do implementowania warstw usług i utrwalania danych w aplikacjach Javy EE. Jednak w warstwie prezentacji można wykorzystać jedną z wielu różnych platform. Dlatego często pojawia się konieczność zintegrowania Springa z wybraną aplikacją do tworzenia aplikacji sieciowych. W procesie integracji najważniejszy jest dostęp z poziomu tych platform do ziaren zadeklarowanych w kontenerze IoC. Apache Struts (http://struts.apache.org/) to popularna otwarta platforma do tworzenia aplikacji sieciowych oparta na wzorcu projektowym MVC. Platforma Struts jest wykorzystywana w wielu projektach sieciowych przez członków społeczności skupionej wokół Javy, dlatego ma liczne grono użytkowników. Warto zauważyć, że mechanizmy obsługi tej platformy dostępne w Springu są przeznaczone tylko dla wersji Struts 1.x. Wynika to z tego, że w wersji Struts 2 platforma ta została połączona z platformą WebWork. Dzięki temu w Springu można bardzo łatwo skonfigurować akcje platformy Struts, używając kontenera IoC jako fabryki obiektów platformy Struts 2. JSF (czyli JavaServer Faces, http://java.sun.com/javaee/javaserverfaces/) to doskonała platforma do tworzenia aplikacji sieciowych oparta na komponentach i sterowaniu zdarzeniami. Jest ona częścią specyfikacji Javy EE. W JSF można używać bogatego zestawu standardowych komponentów, a także tworzyć niestandardowe komponenty wielokrotnego użytku. W platformie JSF warstwa prezentacji jest dobrze oddzielona od interfejsu użytkownika, ponieważ kod warstw można umieścić w ziarnach zarządzanych. Dzięki podejściu opartemu na komponentach i popularności platforma JSF jest obsługiwana w wielu środowiskach IDE udostępniających projektowanie wizualne. DWR (czyli Direct Web Remoting, http://getahead.org/dwr) to biblioteka, która pozwala wykorzystać w aplikacjach sieciowych mechanizmy Ajaksa (ang. Asynchronous JavaScript and XML). Ta biblioteka umożliwia wywoływanie obiektów Javy po stronie serwera za pomocą uruchamianego w przeglądarce kodu w JavaScripcie. Pozwala też dynamicznie aktualizować fragmenty stron internetowych bez konieczności odświeżania całych stron. Po zakończeniu lektury tego rozdziału będziesz potrafił zintegrować Spring z aplikacjami sieciowymi utworzonymi za pomocą serwletów i technologii JSP oraz popularnych platform, takich jak: Struts, JSF i DWR.
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
6.1. Dostęp do Springa w dowolnych aplikacjach sieciowych Problem Programista chce używać w aplikacji sieciowej ziaren zadeklarowanych w kontenerze IoC niezależnie od tego, z której platformy korzysta.
Rozwiązanie Aplikacja sieciowa może wczytać kontekst aplikacji Springa. Wymaga to zarejestrowania odbiornika serwletów ContextLoaderListener. Ten odbiornik przechowuje wczytany kontekst aplikacji w kontekście serwletu aplikacji sieciowej. Później serwlet (lub dowolny obiekt mający dostęp do kontekstu serwletu) może używać kontekstu aplikacji Springa za pomocą metody narzędziowej.
Jak to działa? Załóżmy, że chcesz utworzyć aplikację sieciową do określania mierzonej w kilometrach odległości między dwoma miastami. Najpierw trzeba zdefiniować poniższy interfejs usługi: package com.apress.springrecipes.city; public interface CityService { public double findDistance(String srcCity, String destCity); }
Dla uproszczenia można zaimplementować ten interfejs za pomocą odwzorowania Javy, w którym zapisane są dane o odległościach. Kluczami tego odwzorowania powinny być miasta początkowe, a wartościami — odwzorowania zawierające docelowe miasta i odległości między nimi a źródłową miejscowością. package com.apress.springrecipes.city; ... public class CityServiceImpl implements CityService { private Map> distanceMap; public void setDistanceMap(Map> distanceMap) { this.distanceMap = distanceMap; } public double findDistance(String srcCity, String destCity) { Map destinationMap = distanceMap.get(srcCity); if (destinationMap == null) { throw new IllegalArgumentException("Nie znaleziono miasta początkowego"); } Double distance = destinationMap.get(destCity); if (distance == null) { throw new IllegalArgumentException("Nie znaleziono miasta docelowego"); } return distance; } }
204
6.1. DOSTĘP DO SPRINGA W DOWOLNYCH APLIKACJACH SIECIOWYCH
Następnie należy utworzyć przedstawioną poniżej strukturę katalogów aplikacji sieciowej. Ponieważ aplikacja wymaga dostępu do kontenera IoC, w katalogu WEB-INF/lib trzeba umieścić potrzebne pliki .jar Springa. Jeśli używasz Mavena, poszukaj w rozdziale 1. informacji o tym, jak dodać do parametru classpath odpowiednie pliki .jar. city/ WEB-INF/ classes/ lib/-*.jar jsp/ distance.jsp applicationContext.xml web.xml
W pliku z konfiguracją ziarna Springa zapisz na stałe dane o odległościach między kilkoma miastami. Dane te umieść w elemencie . Nadaj plikowi nazwę applicationContext.xml i umieść go w katalogu WEB-INF.
W deskryptorze wdrażania (czyli w pliku web.xml) należy zarejestrować dostępny w Springu odbiornik serwletów ContextLoaderListener, aby w momencie uruchamiania programu wczytywał kontekst aplikacji Springa do kontekstu serwletu. Lokalizacja pliku z konfiguracją ziarna jest ustalana na podstawie odpowiedniego parametru kontekstu, contextConfigLocation. Można podać w nim kilka plików z konfiguracją ziaren. Poszczególne pliki należy rozdzielić przecinkami lub spacjami. contextConfigLocation /WEB-INF/applicationContext.xml org.springframework.web.context.ContextLoaderListener
205
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
Domyślna lokalizacja, w której odbiornik szuka pliku z konfiguracją ziarna, jest identyczna z lokalizacją wskazaną w kodzie (/WEB-INF/applicationContext.xml), dlatego zastosowany parametr można pominąć. Aby umożliwić użytkownikom sprawdzanie odległości między miastami, trzeba utworzyć plik JSP z odpowiednim formularzem. Nazwij ten plik distance.jsp i umieść go w katalogu WEB-INF/jsp, aby uniemożliwić bezpośredni dostęp do niego. Formularz musi zawierać dwa pola, w których użytkownik może wpisać miasta początkowe i docelowe. Na formularzu znajduje się też komórka przeznaczona na odległość między podanymi miejscowościami. Odległości między miastami
Potrzebny jest też serwlet do przetwarzania żądań podania odległości. Gdy do serwletu trafia żądanie HTTP GET, serwlet wyświetla formularz. Później, gdy formularz jest przesyłany za pomocą metody POST, serwlet znajduje odległość między dwoma podanymi miastami i wyświetla ją na formularzu. Uwaga Aby tworzyć aplikacje sieciowe używające interfejsu Servlet API, trzeba dodać ten interfejs. Jeśli korzystasz z Mavena, dodaj do projektu poniższą zależność: javax.servlet servlet-api 2.5 package com.apress.springrecipes.city.servlet; ... import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
206
6.1. DOSTĘP DO SPRINGA W DOWOLNYCH APLIKACJACH SIECIOWYCH
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; public class DistanceServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { forward(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String srcCity = request.getParameter("srcCity"); String destCity = request.getParameter("destCity"); WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext( getServletContext()); CityService cityService = (CityService) context.getBean("cityService"); double distance = cityService.findDistance(srcCity, destCity); request.setAttribute("distance", distance); forward(request, response); } private void forward(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher = request.getRequestDispatcher("WEB-INF/jsp/distance.jsp"); dispatcher.forward(request, response); } }
Ten serwlet w celu określenia odległości potrzebuje dostępu do ziarna cityService zadeklarowanego w kontenerze IoC. Ponieważ w kontekście serwletu jest zapisany kontekst aplikacji Springa, można przekazać do metody WebApplicationContextUtils.getRequiredWebApplicationContext() kontekst serwletu i pobrać potrzebny kontekst aplikacji. Następnie należy dodać deklarację serwletu do pliku web.xml i powiązać serwlet z wzorcem adresu URL /distance. ... distance com.apress.springrecipes.city.servlet.DistanceServlet distance /distance
Teraz możesz umieścić nową aplikację sieciową w kontenerze WWW (na przykład na serwerze Apache Tomcat 6.x). Domyślnie serwer Tomcat oczekuje na żądania w porcie 8080, dlatego jeśli umieścisz aplikację w ścieżce city, będziesz mógł po uruchomieniu programu uzyskać do niego dostęp za pomocą poniższego adresu URL: http://localhost:8080/city/distance
207
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
6.2. Używanie Springa w serwletach i filtrach Problem Specyfikacja serwletu obejmuje serwlety i filtry. Serwlety obsługują żądania i odpowiedzi oraz odpowiadają za generowanie danych wyjściowych i operacje na nadchodzących żądaniach. Filtry reagują na stan poszczególnych żądań i odpowiedzi przed zadziałaniem docelowego serwletu i po wykonaniu przez niego zadania. Filtry działają tu tak samo jak w programowaniu aspektowym — pozwalają przechwytywać i modyfikować wywołania metod. Filtry można dodać na dowolnym poziomie do istniejących serwletów. Można więc wielokrotnie wykorzystać filtry, aby w dowolnym serwlecie udostępnić ogólne mechanizmy (takie jak kompresję danych w formacie gzip). Potrzebne elementy deklaruje się w pliku web.xml. Kontener serwletu wczytuje plik z konfiguracją i tworzy egzemplarze serwletów i filtrów oraz zarządza cyklem ich życia w kontenerze. Ponieważ za cykl życia odpowiada kontener serwletów, a nie Spring, dostęp do usługi kontenera Springa (na przykład za pomocą tradycyjnego wstrzykiwania zależności i programowania aspektowego) jest trudny. Możesz wykorzystać klasę WebApplicationContextUtils do wyszukiwania i pobierania potrzebnych zależności, jednak tracisz w ten sposób korzyści związane ze wstrzykiwaniem zależności, ponieważ kod musi wtedy bezpośrednio pobierać obiekty (rozwiązanie to nie jest lepsze od zastosowania na przykład interfejsu JNDI). Dlatego warto pozwolić Springowi na obsługę wstrzykiwania zależności i zarządzanie cyklem życia ziaren.
Rozwiązanie Jeśli chcesz zaimplementować narzędzie podobne do filtrów, a jednocześnie zachować pełny dostęp do używanych w Springu mechanizmów cyklu życia i wstrzykiwania zależności, zastosuj klasę DelegatingFilterProxy. Jeżeli zamierzasz zaimplementować rozwiązanie podobne do serwletu, a przy tym zachować pełny dostęp do używanych w Springu mechanizmów cyklu życia i wstrzykiwania zależności, zastosuj klasę HttpRequestHandler Servlet. Wymienione klasy konfiguruje się w pliku web.xml, jednak mogą one delegować swoje zadania do ziarna skonfigurowanego w kontekście aplikacji Springa.
Jak to działa? Serwlety Wróćmy do wcześniejszego przykładu. Załóżmy, że chcesz zmodyfikować serwlet w taki sposób, aby móc wykorzystać mechanizmy kontekstu aplikacji i konfigurację Springa. Umożliwia to klasa HttpRequestHandlerServlet. Korzysta się z niej w nieco pośredni sposób — należy skonfigurować egzemplarz klasy org.springframework.web. context.support.HttpRequestHandlerServlet w pliku web.xml, a następnie nadać nazwę temu egzemplarzowi. Serwlet przyjmuje nazwę skonfigurowaną w pliku web.xml i szuka w głównym kontekście aplikacji Springa odpowiedniego ziarna. Jeśli potrzebne ziarno istnieje i zawiera implementację interfejsu HttpRequestHandler, serwlet deleguje wszystkie żądania do tego ziarna. W tym celu wywoływana jest metoda handleRequest. Najpierw należy napisać ziarno z implementacją interfejsu org.springframework.web.HttpRequestHandler. Istniejący serwlet DistanceServlet zastąp w tym celu obiektem POJO z implementacją tego interfejsu. Nowy kod powinien działać tak samo jak jego wcześniejsza wersja, trzeba go tylko uporządkować w nowy sposób. Definicję potrzebnego obiektu POJO znajdziesz poniżej. package com.apress.springrecipes.city.servlet; import com.apress.springrecipes.city.CityService; import org.springframework.web.HttpRequestHandler; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest;
208
6.2. UŻYWANIE SPRINGA W SERWLETACH I FILTRACH
import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class DistanceHttpRequestHandler implements HttpRequestHandler { private CityService cityService; public void setCityService(final CityService cityService) { this.cityService = cityService; } @Override public void handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { if (request.getMethod().toUpperCase().equals("POST")) { String srcCity = request.getParameter("srcCity"); String destCity = request.getParameter("destCity"); double distance = cityService.findDistance(srcCity, destCity); request.setAttribute("distance", distance); } forward(request, response); } private void forward(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher = request.getRequestDispatcher ("WEB-INF/jsp/distance.jsp"); dispatcher.forward(request, response); } }
Teraz trzeba odpowiednio skonfigurować ziarno w pliku applicationContext.xml.
209
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
W tym rozwiązaniu skonfigurowano ziarno i wstrzyknięto referencję do egzemplarza klasy CityServiceImpl. Zwróć uwagę na identyfikator ziarna (distance), ponieważ będzie on potrzebny do skonfigurowania serwletu w pliku web.xml. contextConfigLocation /WEB-INF/applicationContext.xml org.springframework.web.context.ContextLoaderListener distance org.springframework.web.context.support.HttpRequestHandlerServlet distance /distance
Z ziarna można korzystać tak samo jak w poprzedniej aplikacji. Cały kod, w którym używany jest punkt końcowy /distance, nadal będzie działał. Aby się o tym przekonać, uruchom przeglądarkę i otwórz stronę http://localhost:8080/distance?srcCity=New%20York&destCity=London.
Filtry Platforma Spring udostępnia podobny mechanizm dla filtrów. Dalej pokazana jest konfiguracja prostego filtra, który przechodzi po atrybutach przychodzących żądań i wyświetla te atrybuty. Wykorzystywany jest tu pomocniczy obiekt typu CityServiceRequestAuditor, którego zadaniem jest wyliczanie parametrów żądania (jak łatwo sobie wyobrazić, omawiany filtr można zastosować na przykład do wysyłania danych do dziennika systemowego, do agenta monitorującego Splunk™ lub technologii JMX). Poniżej znajdziesz kod klasy CityServiceRequestAuditor. package com.apress.springrecipes.city; import java.util.Map; public class CityServiceRequestAuditor { public void log(Map attributes) { for (String k : attributes.keySet()) { System.out.println(String.format("%s=%s", k, attributes.get(k))); } } }
210
6.2. UŻYWANIE SPRINGA W SERWLETACH I FILTRACH
W przykładzie dotyczącym serwletu obiekt typu HttpRequestHandlerServlet delegował zadania do innego obiektu, który zawierał implementację interfejsu HttpRequestHandler. Docelowy obiekt był znacznie prostszy od zwykłego serwletu. Jednak klasa javax.servlet.Filter nie umożliwia istotnego uproszczenia interfejsu, dlatego zadania delegowane są do implementacji filtra skonfigurowanej za pomocą Springa. Kod implementacji filtra jest przedstawiony poniżej. package com.apress.springrecipes.city.filter; import com.apress.springrecipes.city.CityServiceRequestAuditor; import javax.servlet.*; import java.io.IOException; import java.util.Map; /** * Ta klasa ma przechwytywać żądania kierowane do obiektu {@link * com.apress.springrecipes.city.CityServiceImpl} i rejestrować je */ public class CityServiceRequestFilter implements Filter { private CityServiceRequestAuditor cityServiceRequestAuditor; @Override public void init(final FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(final ServletRequest scervletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { Map parameterMap = servletRequest.getParameterMap(); this.cityServiceRequestAuditor.log(parameterMap); filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } public void setCityServiceRequestAuditor(final CityServiceRequestAuditor cityServiceRequestAuditor) { this.cityServiceRequestAuditor = cityServiceRequestAuditor; } }
Klasa ta do działania wymaga między innymi ziarna typu CityServiceRequestAuditor. Można je wstrzyknąć i skonfigurować w kontekście aplikacji Springa pod przedstawionym wcześniej kodem. …
211
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
Teraz wystarczy skonfigurować w pliku web.xml egzemplarz klasy org.springframework.web.filter. DelegatingFilterProxy Springa. Przedstawiona tu konfiguracja wiąże filtr ze skonfigurowanym wcześniej serwletem distance. … cityServiceRequestFilter org.springframework.web.filter.DelegatingFilterProxy cityServiceRequestFilter distance
Zwróć uwagę na pewną cechę — atrybut filter-name służy do określania, które ziarno z głównego kontekstu aplikacji Springa należy znaleźć, aby delegować do niego zadania. Może zauważyłeś w tym pewną nadmiarowość. Interfejs filtra udostępnia dwie metody zarządzania cyklem życia ( init i destroy), choć kontekst Springa też obsługuje zarządzanie cyklem życia ziaren. Domyślnie klasa DelegatingFilterProxy nie deleguje wywołań metod cyklu życia do ziarna. Zamiast tego używane są mechanizmy zarządzania cyklem życia dostępne w Springu (InitializingBean, @PostConstruct i inne dla inicjowania oraz DisposableBean, @PreDestroy i inne dla usuwania). Jeśli chcesz, aby Spring automatycznie wywoływał metody cyklu życia, w pliku web.xml ustaw dla filtra w elemencie init-param parametr targetFilterLifecycle na true. cityServiceRequestFilter org.springframework.web.filter.DelegatingFilterProxy targetFilterLifecycle true
6.3. Integrowanie Springa z platformą Struts 1.x Problem Programista chce z poziomu aplikacji sieciowej utworzonej za pomocą platformy Apache Struts 1.x uzyskać dostęp do ziaren zadeklarowanych w kontenerze IoC.
212
6.3. INTEGROWANIE SPRINGA Z PLATFORMĄ STRUTS 1.X
Rozwiązanie W aplikacji opartej na platformie Struts można wczytać kontekst aplikacji Springa w wyniku zarejestrowania odbiornika serwletów ContextLoaderListener. Następnie można korzystać z kontekstu aplikacji w kontekście serwletu w taki sam sposób jak w zwykłej aplikacji sieciowej. Spring zapewnia jednak lepsze, dostosowane do platformy Struts mechanizmy dostępu do kontekstu aplikacji. Po pierwsze, Spring umożliwia wczytanie kontekstu aplikacji w wyniku zarejestrowania wtyczki platformy Struts w pliku konfiguracyjnym tej platformy. Wtedy dla tego kontekstu aplikacji jednostką nadrzędną automatycznie jest kontekst aplikacji wczytany przez odbiornik serwletów, dzięki czemu można korzystać z ziaren zadeklarowanych w tym nadrzędnym kontekście. Po drugie, Spring udostępnia klasę ActionSupport. Jest to klasa pochodna od klasy Action oferująca wygodną metodę getWebApplicationContext(), która zapewnia dostęp do kontekstu aplikacji Springa. Po trzecie, w akcjach platformy Struts można wstrzykiwać ziarna Springa za pomocą mechanizmów wstrzykiwania zależności. Wymogiem wstępnym jest przy tym zadeklarowanie ziarna w kontekście aplikacji Springa. Następnie Struts musi zażądać od Springa znalezienia potrzebnych ziaren.
Jak to działa? Zaimplementujmy teraz za pomocą platformy Apache Struts aplikację sieciową do wyszukiwania odległości między miastami. Zacznij od utworzenia dla aplikacji sieciowej przedstawionej dalej struktury katalogów. Uwaga Przy tworzeniu aplikacji sieciowych przy użyciu platformy Struts 1.3 należy podać lokalizację tej platformy w parametrze classpath. Ponadto aby można było wykorzystać w Springu obsługę platformy Struts, trzeba dodać do parametru classpath odpowiednią zależność. Jeśli używasz Mavena, dodaj do projektu Mavena poniższe zależności: org.springframework spring-struts ${spring.version} struts struts org.apache.struts struts-core 1.3.10
Zauważ, że wykluczana jest wersja platformy Struts powiązana z biblioteką spring-struts. W zamian dodawana jest nowsza wersja, której ścieżka do repozytorium Mavena jest inna niż dla wersji powiązanej z biblioteką spring-struts. city/ WEB-INF/ classes/ lib/*-jar jsp/ distance.jsp applicationContext.xml struts-config.xml web.xml
213
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
W deskryptorze wdrażania (czyli w pliku web.xml) aplikacji Strutsa trzeba zarejestrować serwlet Strutsa ActionServlet, który będzie obsługiwał żądania. Ten serwlet można powiązać z wzorcem adresów URL *.do. action org.apache.struts.action.ActionServlet action *.do
Wczytywanie kontekstu aplikacji Springa do aplikacji Strutsa Istnieją dwa sposoby wczytywania kontekstu aplikacji Springa do aplikacji Strutsa. Pierwszy polega na zarejestrowaniu w pliku web.xml odbiornika serwletów ContextLoaderListener. Jest to uniwersalne rozwiązanie, które stosowano wcześniej do wczytywania kontekstu aplikacji Springa do aplikacji Strutsa. Wspomniany odbiornik domyślnie wczytuje plik /WEB-INF/applicationContext.xml jako plik z konfiguracją ziarna Springa, dlatego nie trzeba bezpośrednio wskazywać lokalizacji tego pliku. org.springframework.web.context.ContextLoaderListener ...
Inne podejście polega na zarejestrowaniu wtyczki ContextLoaderPlugin Strutsa w pliku konfiguracyjnym Strutsa struts-config.xml. Domyślnie wtyczka ta wczytuje plik z konfiguracją ziarna na podstawie nazwy egzemplarza klasy ActionServlet zarejestrowanego w pliku web.xml. Do tej nazwy dodawany jest przyrostek –servlet.xml, co tu w efekcie daje nazwę action-servlet.xml. Jeśli chcesz wczytać inny plik z konfiguracją ziarna, podaj jego nazwę we właściwości contextConfigLocation. ...
Jeśli obie konfiguracje są jednocześnie dostępne, kontekst aplikacji Springa wczytany za pomocą wtyczki Strutsa automatycznie używa jako jednostki nadrzędnej kontekstu aplikacji wczytanej z wykorzystaniem odbiornika serwletów. Zwykle usługi biznesowe należy deklarować w kontekście aplikacji wczytywanym
214
6.3. INTEGROWANIE SPRINGA Z PLATFORMĄ STRUTS 1.X
przez odbiornik serwletów, natomiast komponenty sieciowe warto umieścić w odrębnym kontekście aplikacji, ładowanym za pomocą wtyczki Strutsa. Na razie pomijamy ustawienia tej wtyczki.
Dostęp do kontekstu aplikacji Springa w akcjach Strutsa Struts pomaga wiązać wartości pól formularza HTML-a z właściwościami ziarna w momencie przesyłania formularza. Najpierw należy utworzyć klasę formularza (dziedziczącą po klasie ActionForm) i umieścić w niej dwie właściwości, reprezentujące miasta początkowe i docelowe. package com.apress.springrecipes.city.struts; import org.apache.struts.action.ActionForm; public class DistanceForm extends ActionForm { private String srcCity; private String destCity; // Gettery i settery ... }
Następnie należy utworzyć plik JSP z formularzem, w którym użytkownicy mogą podawać początkowe i docelowe miasta. Ten formularz i jego pola zdefiniuj za pomocą dostępnej w Strutsie biblioteki znaczników. Dzięki temu pola będą automatycznie wiązane z właściwościami ziarna. Plik JSP nazwij distance.jsp i umieść go w katalogu WEB-INF/jsp, aby uniemożliwić bezpośredni dostęp do niego. <%@ taglib prefix="html" uri="http://struts.apache.org/tags-html" %> Odległości między miastami
W Strutsie każde żądanie sieciowe jest przetwarzane przez akcję (obiekt klasy pochodnej od klasy Action). Czasem akcje Strutsa potrzebują dostępu do ziaren Springa. Aby uzyskać dostęp do kontekstu aplikacji
215
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
wczytanego za pomocą odbiornika serwletów ContextLoaderListener, można wykorzystać statyczną metodę WebApplicationContextUtils.getRequiredWebApplicationContext(). Istnieje jednak lepszy sposób na uzyskanie dostępu do kontekstu aplikacji Springa w akcjach Strutsa — można utworzyć klasę pochodną od klasy ActionSupport. ActionSupport to klasa pochodna od klasy Action udostępniająca wygodną metodę getWebApplicationContext(), która umożliwia korzystanie z kontekstu aplikacji Springa. Ta metoda najpierw próbuje zwrócić kontekst aplikacji zwrócony przez wtyczkę ContextLoaderPlugin. Jeśli taki kontekst nie istnieje, metoda próbuje zwrócić kontekst nadrzędny (czyli kontekst aplikacji wczytany przez odbiornik ContextLoaderListener). package com.apress.springrecipes.city.struts; ... import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import import import import
org.apache.struts.action.ActionForm; org.apache.struts.action.ActionForward; org.apache.struts.action.ActionMapping; org.springframework.web.struts.ActionSupport;
public class DistanceAction extends ActionSupport { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { if (request.getMethod().equals("POST")) { DistanceForm distanceForm = (DistanceForm) form; String srcCity = distanceForm.getSrcCity(); String destCity = distanceForm.getDestCity(); CityService cityService = (CityService) getWebApplicationContext().getBean("cityService"); double distance = cityService.findDistance(srcCity, destCity); request.setAttribute("distance", distance); } return mapping.findForward("success"); } }
W pliku konfiguracyjnym Strutsa, struts-config.xml, należy zadeklarować ziarna formularza, a także akcje. Te ostatnie trzeba powiązać z aplikacją.
216
6.3. INTEGROWANIE SPRINGA Z PLATFORMĄ STRUTS 1.X
Teraz można umieścić aplikację w kontenerze WWW i uzyskać do niej dostęp pod adresem URL http://localhost:8080/city/distance.do.
Deklarowanie akcji Strutsa w pliku z konfiguracją ziaren Springa Oprócz aktywnego wyszukiwania ziaren Springa w akcjach Strutsa za pomocą kontekstu aplikacji Springa można też zastosować wstrzykiwanie zależności w celu wstrzyknięcia ziaren Springa do akcji Strutsa. Wtedy akcja Strutsa nie musi dziedziczyć po klasie ActionSupport — wystarczy, że będzie dziedziczyć po klasie Action. package com.apress.springrecipes.city.struts; ... import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import import import import
org.apache.struts.action.Action; org.apache.struts.action.ActionForm; org.apache.struts.action.ActionForward; org.apache.struts.action.ActionMapping;
public class DistanceAction extends Action { private CityService cityService; public void setCityService(CityService cityService) { this.cityService = cityService; } public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { if (request.getMethod().equals("POST")) { ... double distance = cityService.findDistance(srcCity, destCity); request.setAttribute("distance", distance); } return mapping.findForward("success"); } }
Jednak aby było możliwe wstrzykiwanie zależności, akcją musi zarządzać Spring. Potrzebną deklarację można umieścić w pliku applicationContext.xml lub w innym pliku z konfiguracją ziaren wczytywanym w klasie ContextLoaderPlugin. Żeby lepiej oddzielić usługi biznesowe od komponentów sieciowych, warto umieścić tę deklarację w pliku action-servlet.xml w katalogu WEB-INF. Plik ten będzie domyślnie wczytywany przez klasę ContextLoaderPlugin, ponieważ używany egzemplarz klasy ActionServlet nosi nazwę action.
Nazwa ziarna akcji musi być identyczna z nazwą podaną w atrybucie path elementu action w pliku struts-config.xml. Ponieważ atrybut id elementu nie może zawierać znaku /, należy zastosować atrybut name. W pliku konfiguracyjnym można wskazywać ziarna zadeklarowane w nadrzędnym kontekście aplikacji, wczytywanym przez odbiornik ContextLoaderListener.
217
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
W pliku struts-config.xml trzeba zarejestrować egzemplarz klasy ContextLoaderPlugin, aby wczytywał przedstawiony wcześniej pik z konfiguracją ziarna. ... ...
Ponadto trzeba zarejestrować procesor żądań Strutsa, DelegatingRequestProcessor, aby żądał od Strutsa wyszukania akcji w kontekście aplikacji Springa. Wyszukiwanie odbywa się na podstawie dopasowywania ścieżki akcji do nazwy ziarna. Po zarejestrowaniu procesora żądań nie trzeba już ustawiać atrybutu type akcji. Czasem zarejestrowany jest już inny procesor żądań, dlatego nie można zarejestrować procesora DelegatingRequestProcessor. W takiej sytuacji można podać jako typ akcji DelegatingActionProxy, co pozwala uzyskać potrzebny efekt.
6.4. Integrowanie Springa z platformą JSF Problem Programista chce w aplikacji sieciowej utworzonej za pomocą platformy JSF mieć dostęp do ziaren zadeklarowanych w kontenerze IoC.
Rozwiązanie W aplikacji JSF można uzyskać dostęp do kontekstu aplikacji Springa w taki sam sposób jak w każdej aplikacji sieciowej (czyli w wyniku zarejestrowania odbiornika serwletów ContextLoaderListener i używania go w kontekście serwletu). Jednak dzięki podobieństwu między modelami ziaren w platformach Spring i JSF można bardzo łatwo zintegrować te narzędzia. Wymaga to zarejestrowania dostępnego w Springu mechanizmu przekształcania zmiennych JSF. Takim mechanizmem jest DelegatingVariableResolver (dla wersji JSF 1.1) i SpringBean FacesELResolver (dla wersji JSF 1.2 i nowszych). Potrafią one przekształcać zmienne JSF na ziarna Springa. Ponadto można nawet zadeklarować zarządzane ziarna JSF w pliku z konfiguracją ziaren Springa, co pozwala przechowywać wszystkie używane ziarna w jednym miejscu.
218
6.4. INTEGROWANIE SPRINGA Z PLATFORMĄ JSF
Jak to działa? Załóżmy, że chcesz użyć platformy JSF do zaimplementowania aplikacji sieciowej określającej odległości między miastami. Najpierw utwórz przedstawioną dalej strukturę katalogów aplikacji sieciowej. Uwaga Aby zacząć tworzyć aplikację sieciową za pomocą platformy JSF, potrzebujesz biblioteki z implementacją tej platformy. Możesz wykorzystać implementację JSF-RI (ang. JSF Reference Implementation) lub jej odpowiednik rozwijany przez niezależną firmę. Jeśli korzystasz z Mavena, dodaj do projektu Mavena następujące zależności: javax.servlet servlet-api 2.5 javax.faces jsf-api 1.2_13 javax.servlet jstl 1.1.2 jsf-impl javax.faces 1.2_13 provided org.apache.myfaces.core myfaces-api 1.2.8 org.apache.myfaces.core myfaces-impl 1.2.8 city/ WEB-INF/ classes/ lib/-*.jar applicationContext.xml faces-config.xml web.xml distance.jsp
219
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
W deskryptorze wdrażania (czyli w pliku web.xml) aplikacji JSF trzeba zarejestrować serwlet JSF FacesServlet, który będzie obsługiwał żądania sieciowe. Ten serwlet możesz powiązać z wzorcem adresów URL *.faces. Aby przy uruchamianiu programu wczytywać kontekst aplikacji Springa, zarejestruj odbiornik serwletów ContextLoaderListener. org.springframework.web.context.ContextLoaderListener contextConfigLocation /WEB-INF/applicationContext.xml org.apache.myfaces.webapp.StartupServletContextListener org.springframework.web.context.request.RequestContextListener faces javax.faces.webapp.FacesServlet faces *.faces
Ważnym aspektem platformy JSF jest możliwość oddzielenia logiki warstwy prezentacji od interfejsów użytkownika. W tym celu warstwa prezentacji jest zapisywana w jednym lub kilku ziarnach zarządzanych JSF. Na potrzeby funkcji określającej odległość między miastami można utworzyć poniższą klasę ziarna zarządzanego JSF, DistanceBean: package com.apress.springrecipes.city.jsf; ... public class DistanceBean { private private private private
String srcCity; String destCity; double distance; CityService cityService;
public String getSrcCity() { return srcCity; }
220
6.4. INTEGROWANIE SPRINGA Z PLATFORMĄ JSF
public String getDestCity() { return destCity; } public double getDistance() { return distance; } public void setSrcCity(String srcCity) { this.srcCity = srcCity; } public void setDestCity(String destCity) { this.destCity = destCity; } public void setCityService(CityService cityService) { this.cityService = cityService; } public void find() { distance = cityService.findDistance(srcCity, destCity); } }
W tym ziarnie zdefiniowane są cztery właściwości. Ponieważ strona musi wyświetlać właściwości srcCity, destCity i distance, należy zdefiniować getter dla każdej z nich. Użytkownik może wprowadzić wartości właściwości srcCity i destCity, tak więc potrzebny jest dla nich także setter. Używane na zapleczu ziarno CityService jest wstrzykiwane za pomocą settera. Gdy wywoływana jest metoda find() ziarna, uruchamia ono działającą na zapleczu usługę, która określa odległość między dwoma miastami. Wartość ta jest zapisywana we właściwości distance, aby można ją było później wyświetlić. Następnie w katalogu głównym kontekstu aplikacji sieciowej utwórz plik distance.jsp. Trzeba umieścić go w tym katalogu, ponieważ gdy serwlet FacesServlet otrzyma żądanie, powiąże je z plikiem JSP o nazwie z żądania. Na przykład jeśli użytkownik zażąda adresu URL /distance.faces, serwlet FacesServlet wczyta plik /distance.jsp. <%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %> <%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %> Odległości między miastami Miasto początkowe Miasto docelowe Odległość
221
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
Ten plik JSP zawiera komponent , który pozwala użytkownikom wprowadzać miasta początkowe i docelowe. Pola przeznaczone na te miasta są zdefiniowane za pomocą dwóch komponentów , których wartości są wiązane z właściwościami ziarna zarządzanego JSF. Odległość jest zdefiniowana za pomocą komponentu , ponieważ wartość ta jest przeznaczona tylko do odczytu. Należy też zdefiniować komponent , którego kliknięcie spowoduje wykonanie akcji po stronie serwera.
Przetwarzanie ziaren Springa w JSF Plik konfiguracyjny JSF faces-config.xml w katalogu WEB-INF umożliwia skonfigurowanie reguł poruszania się po aplikacji i ziaren zarządzanych JSF. W omawianej prostej aplikacji znajduje się tylko jedna strona, dlatego nie trzeba konfigurować reguł poruszania się. Wystarczy skonfigurować przedstawione wcześniej ziarno DistanceBean. Poniżej przedstawiony jest plik konfiguracyjny dostosowany do platformy JSF 1.1. org.springframework.web.jsf.DelegatingVariableResolver distanceBean com.apress.springrecipes.city.jsf.DistanceBean request cityService #{cityService}
Poniżej znajduje się ten sam plik skonfigurowany pod kątem klasy SpringBeanFacesELResolver, dostępnej od wersji JSF 1.2. org.springframework.web.jsf.el.SpringBeanFacesELResolver distanceBean com.apress.springrecipes.city.jsf.DistanceBean request
222
6.5. INTEGROWANIE SPRINGA Z PLATFORMĄ DWR
cityService #{cityService}
Dla ziarna DistanceBean ustawiony jest zasięg request, co oznacza, że dla każdego żądania tworzony jest nowy egzemplarz ziarna. Zauważ, że dzięki zarejestrowaniu ziarna określającego zmienne, DelegatingVariable Resolver (lub SpringBeanFacesELResolver), można łatwo wskazać ziarno zadeklarowane w kontekście aplikacji Springa. Można je podać jako zmienną JSF w postaci ${nazwaZiarna}. Jednostka przetwarzająca zmienne najpierw próbuje znaleźć standardową zmienną JSF. Jeśli jest to niemożliwe, w kontekście aplikacji Springa szukane jest ziarno o nazwie identycznej z podaną nazwą zmiennej. Teraz możesz umieścić aplikację w kontenerze WWW i uzyskać do niej dostęp pod adresem http://localhost:8080/city/distance.faces.
Deklarowanie ziaren zarządzanych JSF w pliku z konfiguracją ziarna Springa Dzięki zarejestrowaniu egzemplarza klasy DelegatingVariableResolver można wskazywać ziarna zadeklarowane w Springu w ziarnach zarządzanych JSF. Jednak w tym podejściu ziarna są zarządzane przez dwa kontenery — JSF i Springa. Lepsze rozwiązanie polega na scentralizowaniu zarządzania ziarnami w kontenerze IoC. Dlatego usuń deklarację ziarna zarządzanego z pliku konfiguracyjnego JSF i dodaj poniższą deklarację ziarna Springa w pliku applicationContext.xml.
Aby ustawić zasięg request w kontekście aplikacji Springa, należy zarejestrować w deskryptorze wdrażania odbiornik RequestContextListener. org.springframework.web.context.ContextLoaderListener org.springframework.web.context.request.RequestContextListener ...
6.5. Integrowanie Springa z platformą DWR Problem Programista chce uzyskać w aplikacji sieciowej utworzonej za pomocą platformy DWR dostęp do ziaren zadeklarowanych w kontenerze IoC.
223
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
Rozwiązanie Platforma DWR współdziała ze Springiem i umożliwia udostępnianie ziaren Springa na potrzeby zdalnych wywołań. Używany jest przy tym kreator spring. Ponadto platforma DWR 2.0 udostępnia schemat XML-a, który umożliwia skonfigurowanie platformy DWR w pliku z konfiguracją ziarna Springa. Wystarczy więc za pomocą znacznika określić, które ziarna mają być dostępne na potrzeby zdalnych wywołań. Nie trzeba przy tym stosować pliku konfiguracyjnego platformy DWR.
Jak to działa? Załóżmy, że chcesz zaimplementować za pomocą platformy DWR aplikację sieciową do określania odległości między miastami. Aplikacja ta ma obsługiwać technologię Ajax. Najpierw należy utworzyć przedstawioną dalej strukturę katalogów aplikacji sieciowej. Uwaga Aby utworzyć aplikację sieciową za pomocą platformy DWR, trzeba umieścić pliki tej platformy w katalogu WEB-INF/lib. Jeśli używasz Mavena, dodaj do projektu Mavena poniższe zależności: org.directwebremoting dwr 2.0.3 city/ WEB-INF/ classes/ lib/-*.jar applicationContext.xml dwr.xml web.xml distance.html
W deskryptorze wdrażania (czyli w pliku web.xml) aplikacji opartej na platformie DWR trzeba zarejestrować serwlet DWR DwrServlet, aby obsługiwał ajaksowe żądania stron. Ten serwlet możesz powiązać z wzorcem adresów URL /dwr/*. Aby przy uruchamianiu programu wczytywać kontekst aplikacji Springa, należy zarejestrować odbiornik serwletów ContextLoaderListener. org.springframework.web.context.ContextLoaderListener dwr org.directwebremoting.servlet.DwrServlet
224
6.5. INTEGROWANIE SPRINGA Z PLATFORMĄ DWR
dwr /dwr/*
W aplikacji opartej na platformie DWR potrzebny jest plik konfiguracyjny określający, które obiekty mają być dostępne w zdalnych wywołaniach w JavaScripcie. Domyślnie plikiem konfiguracyjnym jest wczytywany przez serwlet DwrServlet plik dwr.xml z katalogu WEB-INF.
Ten plik konfiguracyjny platformy DWR udostępnia klasę CityServiceImpl na potrzeby zdalnych wywołań w JavaScripcie. Kod źródłowy tej klasy jest generowany dynamicznie w pliku CityService.js. Kreator new jest najczęściej używanym kreatorem platformy DWR. Przy każdym wywołaniu tworzy on nowy egzemplarz danej klasy. Zdalnie wywoływać można tylko metodę findInstance() wspomnianej klasy.
Udostępnianie ziaren Springa na potrzeby zdalnych wywołań Kreator new platformy DWR przy każdym wywołaniu tworzy nowy egzemplarz danej klasy. Jeśli chcesz udostępniać ziarna z kontekstu aplikacji Springa na potrzeby zdalnych wywołań, możesz zastosować kreator spring i wskazać nazwę udostępnianego ziarna.
Teraz można napisać stronę przeznaczoną do określania odległości między miastami. Gdy używasz Ajaksa, strony nie trzeba odświeżać tak jak tradycyjnych stron internetowych. Dlatego możesz utworzyć statyczną stronę HTML (na przykład distance.html w katalogu głównym aplikacji sieciowej). Odległości między miastami
Gdy użytkownik kliknie przycisk Znajdź, wywoływana jest funkcja find() JavaScriptu. Prowadzi to do zgłoszenia ajaksowego żądania metody CityService.findDistance(), do której są przekazywane wartości z pól określających początkowe i docelowe miasta. Gdy nadejdzie ajaksowa odpowiedź, aplikacja wyświetli odległość w elemencie distance. Aby to rozwiązanie działało, trzeba dołączyć biblioteki JavaScriptu generowane dynamicznie przez platformę DWR. Teraz można umieścić aplikację w kontenerze WWW i uzyskać dostęp do niej pod adresem URL http://localhost:8080/city/distance.html.
Konfigurowanie platformy DWR w pliku z konfiguracją ziaren Springa Platformę DWR 2.0 można skonfigurować bezpośrednio w pliku z konfiguracją ziaren Springa. Zanim jednak będzie można to zrobić, trzeba w deskryptorze wdrażania zastąpić zarejestrowany wcześniej serwlet DwrServlet serwletem DwrSpringServlet. ... dwr org.directwebremoting.spring.DwrSpringServlet
W pliku z konfiguracją ziarna Springa, applicationContext.xml, można skonfigurować platformę DWR za pomocą elementów XML-a zdefiniowanych w schemacie przeznaczonym dla tej platformy. Najpierw należy zadeklarować element , aby włączyć platformę DWR w Springu. Następnie dla każdego ziarna, które ma być dostępne na potrzeby zdalnych wywołań, należy dodać element z informacjami konfiguracyjnymi analogicznymi do tych z pliku dwr.xml.
226
PODSUMOWANIE
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.directwebremoting.org/schema/spring-dwr http://www.directwebremoting.org/schema/spring-dwr-2.0.xsd"> ...
Teraz można usunąć plik dwr.xml, ponieważ wszystkie ustawienia z niego znalazły się w pliku z konfiguracją ziarna Springa.
Podsumowanie Z tego rozdziału dowiedziałeś się, jak zintegrować Spring z aplikacjami sieciowymi napisanymi w technologii Servlet/JSP i przy użyciu popularnych platform, takich jak: Struts, JSF i DWR. Najważniejszym aspektem tej integracji jest dostęp w wymienionych platformach do ziaren zadeklarowanych w kontenerze IoC Springa. W standardowych aplikacjach sieciowych niezależnie od używanej platformy można zarejestrować udostępniany przez Spring odbiornik serwletów ContextLoaderListener. Pozwala on wczytać kontekst aplikacji Springa do kontekstu serwletu w danej aplikacji sieciowej. Później serwlet lub dowolny inny obiekt korzystający z kontekstu serwletu mają dostęp do kontekstu aplikacji Springa dzięki specjalnej metodzie narzędziowej. W aplikacjach sieciowych utworzonych za pomocą platformy Struts kontekst aplikacji Springa można wczytać także w wyniku zarejestrowania wtyczki Strutsa. Dla tego kontekstu aplikacji jednostką nadrzędną automatycznie staje się kontekst aplikacji wczytany przez odbiornik serwletów. Spring udostępnia klasę ActionSupport, która ma wygodną metodę pozwalającą na dostęp do kontekstu aplikacji Springa. Możesz też deklarować akcje platformy Struts w kontekście aplikacji Springa, co prowadzi do wstrzyknięcia ziaren Springa. W platformie JSF możesz zarejestrować jednostkę przetwarzającą zmienne, DelegatingVariableResolver, aby łączyła zmienne platformy JSF z ziarnami Springa. Ponadto można zadeklarować ziarna zarządzane platformy JSF w pliku z konfiguracją ziaren Springa, co pozwala umieścić wszystkie używane ziarna w jednym miejscu. Platforma DWR umożliwia udostępnianie ziaren Springa na potrzeby zdalnych wywołań. Służy do tego kreator spring. W wersji DWR 2.0 dostępny jest schemat XML-a dla Springa, który umożliwia skonfigurowanie platformy DWR w pliku z konfiguracją ziaren Springa.
227
ROZDZIAŁ 6. INTEGROWANIE SPRINGA Z INNYMI PLATFORMAMI DO TWORZENIA APLIKACJI SIECIOWYCH
228
ROZDZIAŁ 7
Platforma Spring Web Flow
W tym rozdziale nauczysz się, jak używać platformy Spring Web Flow (jest to jeden z podprojektów związanych ze Springiem) do zarządzania przepływem sterowania w aplikacjach sieciowych. Sposób korzystania z tej platformy w wersji 2.0 znacznie się zmienił w porównaniu z wersją 1.0. Platforma Spring Web Flow 2.0 jest prostsza od wersji 1.0 i zawiera wiele usprawnień zgodnych z podejściem „konwencja zamiast konfiguracji”. W tym rozdziale omawiana jest tylko platforma Spring Web Flow 2.0. Zauważ, że wymaga ona korzystania z platformy Spring 2.5.4 lub nowszej. Ponadto Spring Web Flow 2.0.8 i nowsze wersje mają konfigurację nieco odmienną od wcześniejszych wydań. W tym rozdziale omawiana jest konfiguracja z wersji 2.0.8. W tradycyjnych aplikacjach sieciowych programiści często zarządzają przepływem sterowania programowo, dlatego trudno jest zarządzać przepływem i ponownie go wykorzystać. Platforma Spring Web Flow udostępnia język definiowania przepływu sterowania, co pozwala oddzielić przepływ sterowania od logiki prezentacji i zapewnia wiele możliwości konfiguracyjnych. Dzięki temu przepływ sterowania można łatwo modyfikować i ponownie wykorzystać. Spring Web Flow jest zgodna nie tylko z projektem Spring Web MVC, ale też z projektem Spring Portlet MVC i innymi platformami do tworzenia aplikacji sieciowych (takimi jak Struts i JSF). Po zakończeniu lektury tego rozdziału będziesz potrafił tworzyć proste aplikacje sieciowe oparte na platformie Spring MVC i technologii JSF, w których do zarządzania przepływem sterowania służy Spring Web Flow. W tym rozdziale opisane są tylko podstawowe funkcje i konfiguracja platformy Spring Web Flow. Więcej szczegółów na jej temat znajdziesz w dokumentacji.
7.1. Zarządzanie prostym przepływem sterowania za pomocą platformy Spring Web Flow Problem Programista chce zarządzać prostym przepływem sterowania w aplikacji Spring MVC za pomocą platformy Spring Web Flow.
Rozwiązanie Platforma Spring Web Flow umożliwia przedstawianie operacji w interfejsie użytkownika z wykorzystaniem przepływów sterowania. Przepływ sterowania można zdefiniować albo w Javie, albo w XML-u. Powszechnie stosuje się definicje oparte na XML-u, ponieważ język ten daje duże możliwości i jest bardzo popularny.
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
Można też łatwo zmodyfikować takie definicje bez konieczności ponownego kompilowania kodu. Ponadto środowisko Spring IDE współdziała z platformą Spring Web Flow i udostępnia edytor graficzny przeznaczony do tworzenia definicji opartych na XML-u. Definicja przepływu sterowania obejmuje jeden lub więcej stanów, z których każdy odpowiada krokowi w przepływie. Spring Web Flow udostępnia kilka rodzajów stanów, na przykład: stan widoku, stan akcji, stan podejmowania decyzji, stan podprzepływu i stan końcowy. Po zakończeniu wykonywania zadań dla danego stanu zgłaszane jest zdarzenie. Obejmuje ono jednostkę źródłową, identyfikator zdarzenia i czasem atrybuty. Dla każdego stanu istnieje zero lub więcej przejść, w których zwrócony identyfikator zdarzenia jest łączony z następnym stanem. Gdy użytkownik uruchomi nowy przepływ sterowania, Spring Web Flow potrafi automatycznie wykryć początkowy stan przepływu (czyli stan bez przejść z wcześniejszych stanów), dlatego nie trzeba bezpośrednio go określać. Przepływ może zakończyć się w jednym ze zdefiniowanych stanów końcowych. To powoduje oznaczenie przepływu jako zakończonego i zwolnienie zajmowanych zasobów.
Jak to działa? Załóżmy, że chcesz zbudować system internetowy dla biblioteki. Pierwszym jego ekranem powinna być strona powitalna. Należy umieścić na niej dwa odnośniki. Gdy użytkownik kliknie odnośnik Dalej, system ma wyświetlić stronę z wprowadzającymi informacjami o bibliotece. Na tej stronie powinien znajdować się następny odnośnik Dalej, prowadzący do strony z menu. Jeśli użytkownik na stronie powitalnej kliknie Pomiń, system powinien pominąć wprowadzenie i od razu wyświetlić menu. Ten przepływ sterowania jest przedstawiony na rysunku 7.1. W tym przykładzie zobaczysz, jak utworzyć aplikację za pomocą platformy Spring MVC i wykorzystać Spring Web Flow do zarządzania przepływem sterowania.
Rysunek 7.1. Przepływ sterowania dla strony powitalnej Na stronie z wprowadzeniem można wyświetlić dni, w które wypadają święta — biblioteka jest wtedy zamknięta. Informacje o tych dniach pobierane są z działającej na zapleczu usługi o następującym interfejsie: package com.apress.springwebrecipes.library.service; ... public interface LibraryService { public List getHolidays(); }
Na potrzeby testów można zapisać daty wybranych świąt w kodzie z implementacją tej usługi. package com.apress.springwebrecipes.library.service; ... public class LibraryServiceImpl implements LibraryService { public List getHolidays() { List holidays = new ArrayList(); holidays.add(new GregorianCalendar(2007, 11, 25).getTime());
230
7.1. ZARZĄDZANIE PROSTYM PRZEPŁYWEM STEROWANIA ZA POMOCĄ PLATFORMY SPRING WEB FLOW
holidays.add(new GregorianCalendar(2008, 0, 1).getTime()); return holidays; } }
Konfigurowanie aplikacji Spring MVC korzystającej z platformy Spring Web Flow By utworzyć system biblioteki za pomocą platform Spring MVC i Spring Web Flow, najpierw przygotuj przedstawioną dalej strukturę katalogów. Uwaga Aby zarządzać przepływem sterowania przy użyciu platformy Spring Web Flow, trzeba wskazać w parametrze classpath dystrybucję tej platformy (w wersji 2.0.8). Jeśli używasz Mavena, dodaj do projektu Mavena następującą zależność: org.springframework.webflow spring-faces 2.0.8.RELEASE library/ WEB-INF/ classes/ flows/ welcome/ introduction.jsp menu.jsp welcome.jsp welcome.xml lib/-*jar library-service.xml library-servlet.xml library-webflow.xml web.xml
Spring Web Flow umożliwia stosowanie języka wyrażeń w definicjach przepływów sterowania. Język ten pozwala na dostęp do modelu danych i wywoływanie usług działających na zapleczu. W platformie Spring Web Flow dostępne są języki wyrażeń Unified EL (używany w JSF 1.2 i JSP 2.1) oraz OGNL (ang. Object-Graph Navigation Language; stosuje się go na przykład w Tapestry, WebWork i w innych platformach). Składnia obu tych języków jest bardzo podobna, a w zakresie podstawowych wyrażeń (dostępu do właściwości i wywołań metod) nawet identyczna. Wyrażenia przedstawione w tym rozdziale są poprawne w obu wspomnianych językach, dlatego możesz stosować dowolny z nich. W środowisku JSF zaleca się używanie języka Unified EL, ponieważ można wtedy stosować ten sam język w definicjach przepływów i w widokach JSF. Jednak programiści korzystający wcześniej z platformy Spring Web Flow 1.0 mogą preferować język OGNL, który w tej wersji był jedynym dostępnym językiem wyrażeń. Spring Web Flow potrafi wykrywać biblioteki JBoss EL (jest to domyślna implementacja języka Unified EL) i OGNL dostępne w parametrze classpath. Możesz dołączyć jedną z nich (ale nie obie jednocześnie), dodając odpowiedni plik JAR do tej ścieżki. Uwaga Jeśli w platformie Spring Web Flow chcesz używać Unified EL jako języka wyrażeń, możesz zastosować bibliotekę JBoss EL (w wersji 2.0.0 GA) lub OGNL (w wersji 2.6.9). Jeżeli korzystasz z Mavena, dodaj do projektu Mavena jedną z podanych poniżej zależności: org.jboss.seam jboss-el
231
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
2.0.0.GA
lub: ognl ognl 2.6.9
Jeśli zdecydujesz się na korzystanie z biblioteki JBoss EL, dodaj do projektu Mavena repozytorium JBoss: http://repository.jboss.org/maven2/ jboss JBoss Repository
Tworzenie plików konfiguracyjnych W deskryptorze wdrażania (czyli w pliku web.xml) należy zarejestrować odbiornik ContextLoaderListener, aby w momencie uruchamiania programu wczytywał główny kontekst aplikacji. Należy też zarejestrować serwlet DispatcherServlet platformy Spring MVC na potrzeby rozdzielania żądań. Z tym serwletem można powiązać wzorzec adresów URL /flow/*, by serwlet obsługiwał wszystkie żądania ze ścieżką rozpoczynającą się od członu flow. contextConfigLocation /WEB-INF/library-service.xml /WEB-INF/library-security.xml org.springframework.web.context.ContextLoaderListener springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy springSecurityFilterChain /*
232
7.1. ZARZĄDZANIE PROSTYM PRZEPŁYWEM STEROWANIA ZA POMOCĄ PLATFORMY SPRING WEB FLOW
library org.springframework.web.servlet .DispatcherServlet library /flows/*
Odbiornik ContextLoaderListener wczytuje główny kontekst aplikacji z pliku konfiguracyjnego wskazanego w parametrze contextConfigLocation. Tu jest to plik library-service.xml. W tym pliku zadeklarowane są ziarna warstwy usług. W przepływie sterowania dla strony powitalnej można zadeklarować w tym pliku usługę, która zwraca daty świąt państwowych.
Ponieważ egzemplarz klasy DispatcherServlet podany w deskryptorze wdrażania ma nazwę library, w katalogu WEB-INF należy utworzyć plik library-servlet.xml. Umieść w nim następujący kod:
Aby oddzielić od siebie konfiguracje platform Spring MVC i Spring Web Flow, można umieścić ustawienia tej ostatniej w innym pliku (na przykład library-webflow.xml) i zaimportować go w pliku library-servlet.xml. Następnie utwórz poniższy plik:
233
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
…
Klasa FlowHandlerMapping tworzy ścieżki URL na podstawie identyfikatorów zarejestrowanych przepływów sterowania. Zwracany jest obiekt typu FlowHandler, który wywołuje przepływ sterowania zdefiniowany jako flowRegistry. Następnie należy zarejestrować definicje przepływów sterowania w rejestrze. Wymaga to podania lokalizacji danych przepływów. Pierwszy człon nazwy pliku określonej w definicji przepływu (na przykład welcome dla pliku welcome.xml) jest domyślnie używany jako identyfikator przepływu. Jeśli chcesz zastosować inny identyfikator, podaj go w atrybucie id.
Tworzenie definicji przepływów sterowania Spring Web Flow udostępnia oparty na XML-u język definicji przepływów sterowania. Poprawność kodu w tym języku można sprawdzać na podstawie definicji XSD. Ponadto język ten jest obsługiwany w środowisku Spring IDE oraz w pakiecie STS (ang. SpringSource Tool Suite). Teraz możesz zdefiniować w pliku /WEB-INF/flows/welcome/welcome.xml przepływ sterowania dla strony powitalnej.
W tym przepływie sterowania dla strony powitalnej zdefiniowane są trzy stany widoku: welcome, introduction i menu. Stan widoku powoduje wyświetlenie użytkownikowi widoku. W platformie Spring MVC widokowi odpowiada zwykle strona JSP. Domyślnie stan widoku powoduje wyświetlenie strony JSP, której nazwa zaczyna się od identyfikatora danego stanu i kończy się rozszerzeniem .jsp. Do tej strony powinna prowadzić ta sama ścieżka co do definicji przepływu. Jeśli chcesz wyświetlić inny widok, musisz podać jego logiczną nazwę w atrybucie view i zdefiniować jednostkę określającą widoki z platformy Spring MVC, aby znajdowała odpowiedni widok. 234
7.1. ZARZĄDZANIE PROSTYM PRZEPŁYWEM STEROWANIA ZA POMOCĄ PLATFORMY SPRING WEB FLOW
Do uruchamiania akcji dla stanu widoku przed jego wyświetleniem służy element . Spring Web Flow umożliwia wywoływanie metod za pomocą wyrażeń w językach Unified EL i OGNL. Więcej informacji o tych językach znajdziesz w artykule „Unified Expression Language” (http://java.sun.com/products/jsp/ reference/techart/unifiedEL.html) i w dokumentacji języka OGNL (http://www.ognl.org/). Zastosowane tu wyrażenie jest poprawne zarówno w języku Unified EL, jak i w OGNL. Wyrażenie to wywołuje metodę getHolidays() ziarna libraryService i zapisuje wynik w zmiennej holidays o zasięgu żądania. Diagram przepływu sterowania dla strony powitalnej jest przedstawiony na rysunku 7.2.
Rysunek 7.2. Diagram przepływu sterowania dla strony powitalnej
Tworzenie widoków Teraz trzeba utworzyć pliki JSP dla wymienionych wcześniej trzech stanów widoku. Te pliki można umieścić w tym samym katalogu, w którym znajduje się definicja przepływu sterowania, i nadać im nazwy odpowiadające nazwom stanów widoku. Dzięki temu pliki będą domyślnie wczytywane. Zacznij od pliku welcome.jsp. Witaj Witaj! Dalej Pomiń
W tym pliku JSP znajdują się dwa odnośniki zgłaszające zdarzenia. Identyfikatory tych zdarzeń to next i skip. W platformie Spring Web Flow identyfikator można podać albo jako wartość parametru żądania _eventId (na przykład _eventId=next), albo jako człon nazwy parametru żądania, której przedrostkiem jest _eventId (na przykład _eventId_next). W tym drugim przypadku wartość parametru nie ma znaczenia. Ponadto aby uruchomić przepływ sterowania, adresy URL muszą zaczynać się od zmiennej ${flowExecutionUrl}. Zmienna ta jest przetwarzana przez platformę Spring Web Flow w czasie wykonywania programu. Następnie utwórz stronę introduction.jsp wyświetlającą daty świąt państwowych. Są one wczytywane przed wyświetleniem widoku przez akcję podaną w elemencie . <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> Wprowadzenie
235
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
Daty świąt Dalej
W ostatnim kroku utwórz plik menu.jsp. Ten widok jest bardzo prosty, ponieważ z odpowiadającego mu stanu nie ma przejść do innych stanów. Menu Menu
Teraz możesz umieścić aplikację z przepływem sterowania w kontenerze WWW (na przykład na serwerze Apache Tomcat 6.0 lub Jetty). Domyślnie serwery Tomcat i Jetty (i wtyczka Maven Jetty skonfigurowana na potrzeby tego kodu) oczekują na żądania kierowane do portu 8080. Dlatego jeśli umieścisz aplikację w ścieżce mvc, będziesz mógł uruchomić przepływ sterowania dla strony powitalnej za pomocą poniższego adresu URL (ponieważ adresy URL z członem flows w ścieżce są kierowane do serwletu DispatcherServlet): http://localhost:8080/mvc/flows/welcome
7.2. Modelowanie przepływów sterowania za pomocą różnych rodzajów stanów Problem Programista chce przedstawić różne rodzaje operacji w interfejsie użytkownika jako przepływy sterowania wykonywane w platformie Spring Web Flow.
Rozwiązanie W platformie Spring Web Flow każdemu krokowi w przepływie sterowania odpowiada stan. Stan może być powiązany z dowolną liczbą przejść do następnych stanów określanych na podstawie identyfikatora zdarzenia. Spring Web Flow udostępnia kilka wbudowanych rodzajów stanów służących do tworzenia przepływów sterowania. Można też zdefiniować własne rodzaje stanów. W tabeli 7.1 znajdziesz opis wbudowanych rodzajów stanów z platformy Spring Web Flow.
Jak to działa? Załóżmy, że chcesz utworzyć przepływ sterowania umożliwiający użytkownikom biblioteki wyszukiwanie książek. Najpierw użytkownik musi podać na stronie z kryteriami cechy książki. Jeśli do wprowadzonych kryteriów pasuje kilka książek, wszystkie pozycje są wyświetlane na stronie z listą wyników. Na tej stronie użytkownik może wybrać książkę, aby zapoznać się z informacjami o niej na stronie ze szczegółami. Jeżeli do kryteriów pasuje tylko jedna książka, należy przejść bezpośrednio do strony ze szczegółami (bez wyświetlania listy). Przepływ sterowania dla wyszukiwania książek jest przedstawiony na rysunku 7.3.
236
7.2. MODELOWANIE PRZEPŁYWÓW STEROWANIA ZA POMOCĄ RÓŻNYCH RODZAJÓW STANÓW
Tabela 7.1. Wbudowane typy stanów z platformy Spring Web Flow Typ stanu
Opis
Stan widoku
Wyświetla widok w ramach przepływu sterowania (na przykład wyświetla informacje i pobiera dane od użytkownika). Przepływ sterowania jest wstrzymywany do momentu zgłoszenia zdarzenia, które powoduje wznowienie przepływu. Takim zdarzeniem może być na przykład kliknięcie odnośnika lub przesłanie formularza.
Stan akcji
Wykonuje akcje z przepływu, na przykład aktualizuje bazę danych lub pobiera informacje w celu ich wyświetlenia.
Stan podejmowania decyzji
Przetwarza wyrażenie logiczne, aby ustalić, do którego stanu należy przejść.
Stan podprzepływu
Uruchamia następny przepływ jako podprzepływ bieżącego. Podprzepływ po zakończeniu działania zwraca sterowanie do nadrzędnego przepływu.
Stan końcowy
Kończy przepływ. Wszystkie zmienne z zasięgu przepływu stają się wtedy nieprawidłowe.
Rysunek 7.3. Przepływ sterowania dla wyszukiwania książek Przede wszystkim należy utworzyć klasę domeny, Book. Trzeba w niej zaimplementować interfejs Serializable, ponieważ potrzebne będzie utrwalanie egzemplarzy tej klasy w sesjach. package com.apress.springwebrecipes.library.domain; ... public class Book implements Serializable { private private private private
String isbn; String name; String author; Date publishDate;
// Konstruktory, gettery i settery ... }
Następnie utwórz klasę BookCriteria. Jej egzemplarze posłużą do wiązania pól formularza. Także w tej klasie z podanej wcześniej przyczyny należy zaimplementować interfejs Serializable. package com.apress.springwebrecipes.library.domain; ... public class BookCriteria implements Serializable { private String keyword;
237
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
private String author; // Gettery i settery ... }
W warstwie usług zaprojektuj interfejs usługi obsługującej wyszukiwanie książek dla warstwy prezentacji. package com.apress.springwebrecipes.library.service; ... public interface BookService { public List search(BookCriteria criteria); public Book findByIsbn(String isbn); }
Na potrzeby testów zapisz na stałe w kodzie kilka książek i zaimplementuj metodę search() przeznaczoną do szukania książek (odbywa się to przez sprawdzanie wszystkich pozycji). package com.apress.springwebrecipes.library.service; ... public class BookServiceImpl implements BookService { private Map books; public BookServiceImpl() { books = new HashMap(); books.put("0001", new Book("0001", "Spring Framework", "Ray", new GregorianCalendar(2007, 0, 1).getTime())); books.put("0002", new Book("0002", "Spring Web MVC", "Paul", new GregorianCalendar(2007, 3, 1).getTime())); books.put("0003", new Book("0003", "Spring Web Flow", "Ray", new GregorianCalendar(2007, 6, 1).getTime())); } public List search(BookCriteria criteria) { List results = new ArrayList(); for (Book book : books.values()) { String keyword = criteria.getKeyword().trim(); String author = criteria.getAuthor().trim(); boolean keywordMatches = keyword.length() > 0 && book.getName().contains(keyword); boolean authorMatches = book.getAuthor().equals(author); if (keywordMatches || authorMatches) { results.add(book); } } return results; } public Book findByIsbn(String isbn) { return books.get(isbn); } }
Aby usługa wyszukiwania książek była dostępna we wszystkich przepływach sterowania, należy zadeklarować ją w pliku z konfiguracją warstwy usług (czyli w pliku library-service.xml), wczytywanym na potrzeby głównego kontekstu aplikacji.
238
7.2. MODELOWANIE PRZEPŁYWÓW STEROWANIA ZA POMOCĄ RÓŻNYCH RODZAJÓW STANÓW
Teraz możesz zacząć tworzenie przepływu związanego z wyszukiwaniem książek. Najpierw podaj lokalizację definicji przepływu w elemencie flow-registry w pliku library-webflow.xml i utwórz plik XML z tą definicją. ...
Następnie przejdź do stopniowego tworzenia przepływu. Wykorzystaj do tego różne rodzaje stanów dostępnych w platformie Spring Web Flow.
Definiowanie stanów widoku Zgodnie z wymaganiami przepływu związanego z wyszukiwaniem książek najpierw trzeba utworzyć stan widoku, aby wyświetlać formularz, w którym użytkownik może podać cechy szukanej pozycji. W platformie Spring Web Flow można utworzyć akcję (odpowiada ona kontrolerowi z platformy Spring MVC) do obsługi żądań dotyczących przepływu. Spring Web Flow udostępnia klasę FormAction, która pomaga w obsłudze formularzy. Przy przetwarzaniu skomplikowanych formularzy można utworzyć pochodną od niej klasę i dodać w niej własny kod. W przypadku prostych formularzy można bezpośrednio zastosować klasę FormAction i ustawić jej właściwości (na przykład klasę obiektu formularza, edytory właściwości i mechanizmy sprawdzania poprawności). Następnie można za pomocą tej klasy zdefiniować w pliku library-webflow.xml akcję formularza, która posłuży do obsługi formularza z cechami książek.
Akcja formularza może wiązać pola formularza z właściwościami obiektu formularza o tych samych nazwach. Najpierw jednak trzeba określić we właściwości formObjectClass klasę, której akcja użyje do tworzenia egzemplarzy formularza. Aby przekształcić wartości pól formularza na odpowiedni typ danych, trzeba w akcji zarejestrować niestandardowe edytory właściwości. Możesz utworzyć obiekt rejestrujący edytory właściwości i podać go we właściwości propertyEditorRegistrar. package com.apress.springwebrecipes.library.web; ... import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.propertyeditors.CustomDateEditor; public class PropertyEditors implements PropertyEditorRegistrar { public void registerCustomEditors(PropertyEditorRegistry registry) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); registry.registerCustomEditor(Date.class, new CustomDateEditor( dateFormat, true)); } }
Gdy akcja formularza jest już gotowa, można zdefiniować pierwszy stan widoku, służący do obsługi formularza z cechami książek. Aby jak najszybciej przetestować przepływ, możesz od razu wyświetlać wyniki wyszukiwania na stronie z listą książek — bez sprawdzania liczby znalezionych pozycji. Przepływ związany z wyszukiwaniem książek zdefiniuj w pliku /WEBINF/flows/bookSearch/bookSearch.xml.
Ten stan widoku przed wyświetleniem strony wywołuje metodę setupForm() zdefiniowanej wcześniej akcji formularza bookCriteriaAction. Ta metoda tworzy egzemplarz wskazanej klasy formularza. Gdy użytkownik prześle formularz o identyfikatorze zdarzenia search, nastąpi przejście do stanu bookList, w którym są wyświetlane wszystkie wyniki wyszukiwania. Przed przejściem do nowego stanu trzeba wywołać metodę bindAndValidate() akcji formularza, aby powiązać wartości pól z właściwościami obiektu formularza, a następnie sprawdzić poprawność tego obiektu za pomocą służących do tego obiektów (jeśli są zarejestrowane). Następnie należy wywołać używaną na zapleczu usługę, by znalazła książki pasujące do powiązanego obiektu z kryteriami. Wyniki są zapisywane w zmiennej books o zasięgu przepływu, dzięki czemu można ich używać także w innych stanach. Zmienne o zasięgu przepływu są przechowywane w sesji, dlatego muszą implementować interfejs Serializable. Następnie utwórz dla tego widoku nowy stan. Zapisz go w pliku JSP o nazwie odpowiadającej danemu stanowi widoku. Plik ten umieść w odpowiedniej lokalizacji (/WEB-INF/flows/bookSearch/bookCriteria.jsp), dzięki czemu będzie on automatycznie wczytywany. <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> Cechy szukanej książki
Ten plik JSP zawiera formularz zdefiniowany za pomocą znaczników Springa. Formularz jest powiązany z obiektem o nazwie bookCriteria, który jest generowany automatycznie na podstawie nazwy klasy obiektu formularza (BookCriteria). Na formularzu znajduje się przycisk typu submit, którego kliknięcie prowadzi do zgłoszenia zdarzenia o identyfikatorze search. 240
7.2. MODELOWANIE PRZEPŁYWÓW STEROWANIA ZA POMOCĄ RÓŻNYCH RODZAJÓW STANÓW
Drugi stan widoku w omawianym przepływie służy do wyświetlania wyników wyszukiwania. Ten stan powoduje wyświetlenie widoku z listą wszystkich wyników w tabeli. Wraz z wynikami dostępne są odnośniki pozwalające wyświetlić szczegółowe informacje o książkach. Gdy użytkownik kliknie jeden z tych odnośników, zgłaszane jest zdarzenie select, które prowadzi do przejścia do stanu bookDetails (w tym stanie wyświetlane są szczegóły dotyczące książki).
W odnośniku należy podać numer ISBN książki jako parametr żądania. Dzięki temu działająca na zapleczu usługa może znaleźć książkę i zapisać dane na jej temat w zmiennej book o zasięgu przepływu. Widok związany z tym stanem należy umieścić w pliku WEB-INF/flows/bookSearch/bookList.jsp, dzięki czemu będzie automatycznie wczytywany. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> Lista książek ISBN Tytuł Autor Data wydania ${book.isbn} ${book.name} ${book.author}
W każdym wierszu tabeli kolumna z numerem ISBN zawiera odnośnik, który powoduje zgłoszenie zdarzenia select z tym numerem jako parametrem żądania.
Ostatni stan widoku w opisywanym przepływie służy do wyświetlania szczegółowych informacji o wybranej książce. Na razie ten stan nie prowadzi do żadnych innych stanów.
Widok powiązany z tym stanem zapisz w pliku /WEB-INF/flows/bookSearch/bookDetails.jsp, dzięki czemu będzie automatycznie wczytywany.
241
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> Informacje o książce ISBN ${book.isbn} Tytuł ${book.name} Autor ${book.author} Data wydania
Teraz można zainstalować tę aplikację i pod adresem http://localhost:8080/mvc/flows/bookSearch przetestować uproszczony przepływ sterowania. Diagram dla obecnej wersji tego przepływu sterowania jest przedstawiony na rysunku 7.4.
Rysunek 7.4. Diagram przepływu sterowania dla procesu wyszukiwania książek obejmujący tylko stany widoku
Definiowanie stanów akcji Akcję związaną z wyszukiwaniem można umieścić w stanie widoku bookCriteria, ale nie będzie jej można wtedy ponownie wykorzystać dla innych stanów wymagających wyszukiwania książek. Akcje przeznaczone do wielokrotnego użytku najlepiej jest umieszczać w niezależnych stanach akcji. Stan akcji obejmuje jedną lub więcej akcji wykonywanych w ramach przepływu sterowania. Podane akcje są uruchamiane w zadeklarowanej 242
7.2. MODELOWANIE PRZEPŁYWÓW STEROWANIA ZA POMOCĄ RÓŻNYCH RODZAJÓW STANÓW
kolejności. Jeśli akcja zwróci obiekt Event o identyfikatorze pasującym do przejścia, natychmiast ma miejsce przejście (dalsze akcje nie są wtedy wykonywane). Wykonanie wszystkich akcji bez zwrócenia takiego obiektu skutkuje wykonaniem przejścia success. Aby umożliwić powtórne wykorzystanie kodu, akcję wyszukiwania książek warto umieścić w stanie searchBook. Następnie należy zmodyfikować przejście ze stanu bookCriteria, tak aby prowadziło do nowego stanu. ...
Diagram obecnej wersji omawianego przepływu sterowania jest przedstawiony na rysunku 7.5.
Rysunek 7.5. Diagram przepływu sterowania dla wyszukiwania książek — wersja ze stanem akcji
Definiowanie stanów podejmowania decyzji Teraz spełnisz wspomniany na początku wymóg — jeśli wyników wyszukiwania jest więcej niż jeden, należy wyświetlić je na stronie z listą wyników, natomiast gdy wynik jest tylko jeden, aplikacja ma od razu przechodzić do strony ze szczegółowymi informacjami o książce. Aby uzyskać ten efekt, potrzebny jest stan podejmowania decyzji, który sprawdza wartość wyrażenia logicznego w celu wybrania przejścia. ...
243
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
result="flowScope.books" /> ...
Przejście success w stanie searchBook zmieniono na checkResultSize, czyli na stan decyzji, który sprawdza, czy zwrócony został dokładnie jeden wynik. Jeśli tak jest (wartość true), następuje przejście do stanu akcji extractResult, co prowadzi do zapisania pierwszego (i jedynego) wyniku w zmiennej book o zasięgu przepływu. W przeciwnym razie aplikacja przechodzi do stanu bookList, który wyświetla listę wszystkich wyników wyszukiwania. Diagram przepływu dla obecnej wersji rozwiązania jest przedstawiony na rysunku 7.6.
Rysunek 7.6. Diagram przepływu sterowania — wersja ze stanem podejmowania decyzji
Definiowanie stanów końcowych Podstawowe zadanie w przepływie sterowania dla wyszukiwania książek zostało wykonane. Możliwe jednak, że trzeba udostępnić odnośnik Nowe wyszukiwanie zarówno na stronie z listą książek, jak i na stronie ze szczegółowymi informacjami o wybranej pozycji, aby umożliwić rozpoczęcie nowego wyszukiwania. Jedno z rozwiązań to dodanie do obu tych stron poniższego odnośnika: Nowe wyszukiwanie
244
7.2. MODELOWANIE PRZEPŁYWÓW STEROWANIA ZA POMOCĄ RÓŻNYCH RODZAJÓW STANÓW
Jak widać, ten odnośnik powoduje zgłoszenie zdarzenia newSearch. Jednak zamiast przechodzić do pierwszego stanu widoku (bookCriteria), lepiej jest zdefiniować stan końcowy, który powoduje ponowne rozpoczęcie całego przepływu sterowania. Taki stan końcowy powoduje unieważnienie wszystkich zmiennych o zasięgu przepływu, co pozwala zwolnić zajęte zasoby. ...
Domyślnie stan końcowy prowadzi do ponownego uruchomienia danego przepływu i przejścia do stanu początkowego. Można jednak uruchomić w zamian inny stan; wtedy w atrybucie view stanu końcowego należy podać po przedrostku flowRedirect nazwę nowego stanu. Diagram obecnej wersji przepływu sterowania znajdziesz na rysunku 7.7.
Rysunek 7.7. Diagram przepływu sterowania dla wyszukiwania książek — wersja ze stanem końcowym
245
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
Definiowanie stanów podprzepływów Załóżmy, że istnieje inny przepływ sterowania, który także wymaga wyświetlania szczegółowych informacji o książkach. W celu powtórnego wykorzystania kodu można umieścić stan bookDetails w nowym przepływie sterowania, który w innych przepływach będzie wywoływany jako podprzepływ. Zacznij od podania lokalizacji definicji przepływu w elemencie flow-registry w pliku library-webflow.xml i utworzenia pliku XML z definicją nowego przepływu: ...
Następnie przenieś stan widoku bookDetails do nowego przepływu i w tym samym katalogu umieść plik bookDetails.jsp. Ponieważ stan bookDetails obejmuje przejście do stanu newSearch, należy zdefiniować ten ostatni jako stan końcowy nowego przepływu.
Obiekt reprezentujący wyświetlaną książkę jest przekazywany do tego przepływu jako parametr wejściowy i zapisywany pod nazwą book. Obiekt ten należy zapisać w zmiennej book o zasięgu przepływu. Zauważ, że zasięg ten obejmuje tylko dany przepływ. Następnie zdefiniuj w przepływie bookSearch stan podprzepływu, który uruchamia przepływ bookDetails w celu wyświetlenia szczegółowych informacji o książce.
W tej definicji do podprzepływu bookDetails przekazywany jest parametr wejściowy w postaci zmiennej book (jest to zmienna o zasięgu przepływu z przepływu bookSearch). Gdy przepływ bookDetails zakończy działanie i przejdzie do stanu newSearch, nastąpi przejście do tego stanu także w przepływie nadrzędnym (tu jest to przejście do stanu końcowego newSearch w przepływie bookSearch). Diagram obecnej wersji przepływu sterowania jest przedstawiony na rysunku 7.8.
246
7.3. ZABEZPIECZANIE PRZEPŁYWÓW STEROWANIA W APLIKACJACH SIECIOWYCH
Rysunek 7.8. Diagram przepływu sterowania dla wyszukiwania książek — wersja z podprzepływem
7.3. Zabezpieczanie przepływów sterowania w aplikacjach sieciowych Problem Programista chce zabezpieczyć w aplikacji określone przepływy, tak aby mieli do nich dostęp tylko uprawnieni użytkownicy.
Rozwiązanie Spring Web Flow umożliwia integrację z platformą Spring Security, dlatego można łatwo zabezpieczać przepływy sterowania w aplikacjach sieciowych za pomocą tej ostatniej. Poprawnie skonfigurowana platforma Spring Security pozwala zabezpieczyć przepływ, stan lub przejście. Trzeba w tym celu dodać element z ustawionymi wymaganymi atrybutami dostępu.
247
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
Jak to działa? Aby zabezpieczyć przepływ za pomocą platformy Spring Security, najpierw należy skonfigurować filtr DelegatingFilterProxy w deskryptorze wdrażania (czyli w pliku web.xml). Ten filtr deleguje filtrowanie żądań HTTP do filtra zdefiniowanego w platformie Spring Security. Uwaga Aby stosować platformę Spring Security w aplikacji sieciowej z przepływem sterowania, trzeba dodać tę platformę do parametru classpath. Jeśli używasz Mavena, dodaj do projektu Mavena poniższą zależność: org.springframework.security spring-security-core 3.0.2.RELEASE contextConfigLocation /WEB-INF/library-service.xml /WEB-INF/library-security.xml ... springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy springSecurityFilterChain /* ...
Ponieważ konfiguracja zabezpieczeń powinna być stosowana do całej aplikacji, należy umieścić ją w jednym miejscu, w pliku /WEBINF/library-security.xml, i wczytywać ten plik do głównego kontekstu aplikacji. Utwórz ten plik i umieść w nim następujący kod:
248
7.4. UTRWALANIE OBIEKTÓW W PRZEPŁYWACH STEROWANIA W APLIKACJACH SIECIOWYCH
W tej konfiguracji dla platformy Spring Security włączane jest ustawienie auto-config w elemencie . Zapewnia to domyślną usługę logowania za pomocą formularzy, usługę logowania anonimowego itd. Zdefiniowane są też dwa konta na potrzeby testów. W pliku z konfiguracją przepływu (czyli w pliku library-webflow.xml) trzeba w elemencie flow-executor zarejestrować odbiornik związany z wykonywaniem przepływu, SecurityFlowExecutionListener, aby włączyć platformę Spring Security dla przepływów sterowania w aplikacji sieciowej. ...
Platforma Spring Security jest już skonfigurowana pod kątem aplikacji sieciowej z przepływem sterowania. Teraz możesz zabezpieczyć przepływ; w tym celu umieść element w definicji przepływu. Zabezpiecz na przykład przepływ bookSearch zdefiniowany w pliku /WEB-INF/flows/bookSearch/ bookSearch.xml. ...
W atrybucie attributes możesz podać dowolną liczbę atrybutów dostępu wymaganych do korzystania z danego przepływu sterowania (poszczególne atrybuty trzeba rozdzielić przecinkami). Domyślnie dostęp do przepływu ma użytkownik, który posiada dowolne z wymaganych uprawnień. Można też sprawić, aby przepływ był dostępny tylko dla użytkowników mających wszystkie podane uprawnienia. W tym celu ustaw atrybut match elementu na all. Jeśli teraz zainstalujesz aplikację i przetestujesz przepływ sterowania dla procesu wyszukiwania książek, będziesz musiał najpierw zalogować się do aplikacji. Za pomocą elementu możesz też zabezpieczyć konkretny stan lub określone przejście.
7.4. Utrwalanie obiektów w przepływach sterowania w aplikacjach sieciowych Problem W wielu sytuacjach w różnych stanach przepływu sterowania trzeba tworzyć i aktualizować trwałe obiekty. Zgodnie z naturą przepływów sterowania zmiany wprowadzone w tych obiektach nie powinny być zapisywane w bazie do momentu dojścia do końcowego stanu danego przepływu. Wtedy można albo zatwierdzić wszystkie zmiany w ramach transakcji, albo je odrzucić (jeśli przepływ zakończył się niepowodzeniem lub został anulowany). Można zachowywać kontekst utrwalania między różnymi stanami przepływu, jednak jest to niewydajne podejście.
249
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
Rozwiązanie Spring Web Flow może automatycznie zarządzać kontekstem utrwalania dostępnym w różnych stanach przepływu. Programista może korzystać z zarządzanego kontekstu za pomocą zmiennej o zasięgu przepływu udostępnianej przez platformę Spring Web Flow. W wersji 2.0 tej platformy obsługiwane są technologie JPA i Hibernate. Aby platforma Spring Web Flow zarządzała kontekstami utrwalania na potrzeby przepływów sterowania, trzeba w elemencie flow-executor zarejestrować odbiornik związany z wykonywaniem takich przepływów. Przy stosowaniu technologii JPA jest to odbiornik typu JpaFlowExecutionListener, natomiast technologia Hibernate współdziała z odbiornikiem HibernateFlowExecutionListener. Obie te klasy znajdują się w pakiecie org.springframework.webflow.persistence. Gdy nowy przepływ rozpoczyna działanie, odbiornik tworzy nowy kontekst utrwalania (menedżer encji JPA lub sesję Hibernate) i wiąże go z zasięgiem przepływu. Następnie w różnych stanach przepływu można utrwalać obiekty w kontekście utrwalania. Należy też zdefiniować stan końcowy, który albo zatwierdza zmiany, albo je ignoruje.
Jak to działa? Załóżmy, że chcesz utworzyć przepływ sterowania umożliwiający czytelnikom wypożyczanie książek z biblioteki. Dane o wypożyczeniach mają być zapisywane w bazie danych zarządzanej przez Apache Derby lub inny silnik bazodanowy. Można wykorzystać interfejs JPA do utrwalania rekordów i Hibernate jako silnik powiązany z tym interfejsem. Zacznij od zdefiniowania klasy encji BorrowingRecord opatrzonej adnotacjami JPA. Uwaga Aby wykorzystać Hibernate jako silnik dla interfejsu JPA, musisz dodać biblioteki Hibernate 3, Hibernate 3 EntityManager, JPA API i Ehcache. Ponieważ klasa EntityManager z Hibernate wymaga biblioteki Javassist, trzeba dodać także tę bibliotekę do parametru classpath. By zastosować silnik bazodanowy Apache Derby, dodaj element dla klienckiego pliku .jar tego silnika. Jeśli używasz Mavena, dodaj do modelu POM poniższe deklaracje: javax.persistence persistence-api 1.0 org.hibernate hibernate-entitymanager 3.4.0.GA net.sf.ehcache ehcache javax.transaction jta org.apache.derby derbyclient 10.4.2.0
250
7.4. UTRWALANIE OBIEKTÓW W PRZEPŁYWACH STEROWANIA W APLIKACJACH SIECIOWYCH
package com.apress.springwebrecipes.library.domain; ... import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class BorrowingRecord implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private private private private
String isbn; Date borrowDate; Date returnDate; String reader;
// Gettery i settery ... }
Następnie utwórz plik persistence.xml z konfiguracją JPA. Umieść go w katalogu META-INF z katalogu głównego z parametru classpath.
W tym pliku konfiguracyjnym wystarczy zdefiniować jednostkę utrwalania library. Dane dostawcy JPA zostaną skonfigurowane w kontekście aplikacji Springa.
Konfigurowanie JPA w kontekście aplikacji Springa W pliku z konfiguracją warstwy usług (czyli w pliku library-service.xml) można skonfigurować fabrykę menedżerów encji JPA. Wymaga to określenia źródła danych i adaptera dostawcy JPA (można w nim skonfigurować informacje o danym dostawcy JPA). Ponadto trzeba skonfigurować menedżer transakcji JPA, który będzie zarządzał takimi transakcjami. Szczegółowy opis konfigurowania JPA znajdziesz w rozdziale 17. ...
251
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
Konfigurowanie JPA na potrzeby platformy Spring Web Flow Aby platforma Spring Web Flow mogła zarządzać kontekstami utrwalania przepływów sterowania, trzeba w elemencie flow-executor zarejestrować odbiornik związany z wykonywaniem przepływów. Ponieważ tu używany jest interfejs JPA, w pliku library-webflow.xml należy zarejestrować odbiornik JpaFlowExecutionListener. ... ...
W konstruktorze odbiornika JpaFlowExecutionListener trzeba podać fabrykę menedżerów encji JPA i menedżer transakcji (elementy te zostały skonfigurowane w warstwie usług). Za pomocą atrybutu criteria można filtrować nazwy przepływów, które odbiornik ma uwzględniać. Podane nazwy należy rozdzielić przecinkami. Wartością domyślną jest gwiazdka, która oznacza uwzględnianie wszystkich przepływów.
Używanie JPA w przepływach sterowania Zdefiniuj teraz przepływ dla procesu wypożyczania książek z biblioteki. Zacznij od zarejestrowania definicji nowego przepływu w elemencie flow-registry. ...
Pierwszy stan z tego przepływu wyświetla formularz, w którym użytkownicy biblioteki mogą wpisać informacje o wypożyczanych pozycjach. Dane te są wiązane z obiektem formularza typu BorrowingRecord. W pliku library-webflow.xml możesz zdefiniować akcję przeznaczoną do obsługi tego formularza.
252
7.4. UTRWALANIE OBIEKTÓW W PRZEPŁYWACH STEROWANIA W APLIKACJACH SIECIOWYCH
W pliku z definicją przepływu (/WEB-INF/flows/borrowBook/borrowBook.xml) trzeba zdefiniować element , aby zażądać od platformy Spring Web Flow zarządzania kontekstem utrwalania dla każdego egzemplarza przepływu.
Ten przepływ obejmuje dwa stany widoku i dwa stany końcowe. Stan borrowForm wyświetla użytkownikom formularz, w którym można wpisać dane na temat wypożyczanych książek. Dane te są wiązane z obiektem borrowingRecord o zasięgu przepływu. Nazwa tego obiektu jest tworzona na podstawie nazwy jego klasy, BorrowingRecord. Jeśli użytkownik przejdzie dalej, nastąpi zmiana stanu na borrowReview. Ten stan wyświetla wprowadzone informacje i pozwala na ich zatwierdzenie. Jeśli użytkownik potwierdzi wpisane dane, obiekt formularza w zasięgu przepływu zostanie utrwalony za pomocą zarządzanego kontekstu utrwalania i nastąpi przejście do stanu końcowego confirm. Ten stan ma atrybut commit ustawiony na true, dlatego powoduje zatwierdzenie zmian w bazie danych. Jednak w każdym z utworzonych stanów widoku użytkownik może anulować proces wypożyczania, co spowoduje przejście do stanu końcowego cancel, w którym zmiany są ignorowane. Diagram przepływu sterowania dla procesu wypożyczania książek jest przedstawiony na rysunku 7.9.
253
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
Rysunek 7.9. Diagram przepływu sterowania dla procesu wypożyczania książek Ostatni krok polega na utworzeniu widoków dla dwóch stanów widoku. Dla stanu borrowForm utwórz plik borrowForm.jsp i umieść go w katalogu /WEB-INF/flows/borrowBook/, dzięki czemu plik ten będzie wczytywany automatycznie. <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> Formularz do wypożyczania książek
Dla stanu borrowReview utwórz plik borrowReview.jsp i umieść go we wspomnianym wcześniej katalogu. Plik ten posłuży do potwierdzania informacji o wypożyczanych książkach.
254
7.5. INTEGROWANIE PLATFORMY SPRING WEB FLOW Z TECHNOLOGIĄ JSF
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> Potwierdzanie danych
Teraz możesz zainstalować aplikację i przetestować przepływ dla procesu wypożyczania książek. Aby sprawdzić działanie przepływu, przejdź pod adres URL http://localhost:8080/library/flows/borrowBook.
7.5. Integrowanie platformy Spring Web Flow z technologią JSF Problem Platforma Spring Web Flow domyślnie wyświetla widoki za pomocą technologii z platformy Spring MVC (czyli JSP i Tiles). Możliwe jednak, że chcesz wykorzystać przy tworzeniu widoków bogaty zestaw komponentów interfejsu użytkownika z technologii JSF lub za pomocą platformy Spring Web Flow zarządzać przepływami sterowania w istniejących aplikacjach JSF. W obu przypadkach trzeba zintegrować platformę Spring Web Flow z technologią JSF. 255
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
Rozwiązanie Platforma Spring Web Flow udostępnia dwa podmoduły (Spring Faces i Spring JavaScript), które upraszczają stosowanie platformy JSF i JavaScriptu w Springu. Podmoduł Spring Faces integruje Spring z platformą JSF 1.2 i jej nowszymi wersjami, dzięki czemu można wykorzystywać komponenty interfejsu użytkownika z platformy JSF w platformach Spring MVC i Spring Web Flow. Ten podmoduł obsługuje wyświetlanie widoków JSF w platformie Spring Web Flow, a także udostępnia liczne funkcje integrowania platform JSF i Spring Web Flow. Spring JavaScript to abstrakcyjna platforma JavaScript, w której jako narzędzie do tworzenia interfejsu użytkownika używany jest pakiet Dojo JavaScript (http://www.dojotoolkit.org/). Spring Faces udostępnia zestaw komponentów do sprawdzania poprawności po stronie klienta. Współdziałają one ze standardowymi komponentami JSF przeznaczonymi do wprowadzania danych i są oparte na platformie Spring JavaScript. Komponenty z platformy Spring Faces są dostępne jako znaczniki Facelets, dlatego trzeba używać systemu Facelets jako technologii tworzenia widoków JSF.
Jak to działa? Wyświetlanie widoków JSF w platformie Spring Web Flow Teraz spróbuj ponownie zaimplementować widoki przepływu związanego z wypożyczaniem książek. Tym razem zastosuj platformę JSF. Aby móc wykorzystywać dostępne w platformie Spring Faces komponenty do sprawdzania poprawności z platformy JSF, trzeba utworzyć widoki JSF za pomocą systemu Facelets. Zacznij od skonfigurowania serwletu JSF FacesServlet w pliku web.xml. Uwaga Aby zintegrować technologie JSF i Facelets z platformą Spring Web Flow, trzeba dodać podmoduł Spring Faces do parametru classpath. Potrzebne są też: implementacja JSF, biblioteka Facelets i implementacja języka wyrażeń. Jeśli używasz Mavena, dodaj do projektu Mavena następujące zależności: org.springframework.webflow spring-faces 2.0.8.RELEASE com.sun.facelets jsf-facelets 1.1.15.B1 org.apache.myfaces.core myfaces-impl 1.2.7 javax.servlet jstl 1.2 javax.servlet.jsp jsp-api 2.1
256
7.5. INTEGROWANIE PLATFORMY SPRING WEB FLOW Z TECHNOLOGIĄ JSF
org.jboss.seam jboss-el 2.0.0.GA javax.el el-api ... faces javax.faces.webapp.FacesServlet
Zauważ, że ten serwlet jest rejestrowany tylko na potrzeby inicjowania aplikacji sieciowej opartej na platformie JSF. Nie będzie on służył do obsługi żądań kierowanych do przepływu sterowania, dlatego nie trzeba dodawać definicji . Jeśli jednak chcesz wykorzystać starszą technikę obsługi żądań JSF, musisz podać tę definicję. W pliku konfiguracyjnym JSF (czyli w pliku faces-config.xml z katalogu WEB-INF) trzeba skonfigurować obiekt obsługi widoków JSF, FaceletViewHandler, aby włączyć obsługę systemu Facelets. Skonfiguruj też obiekt typu SpringBeanFacesELResolver, by umożliwić dostęp do ziaren Springa z poziomu języka wyrażeń platformy JSF. com.sun.facelets.FaceletViewHandler org.springframework.web.jsf.el .SpringBeanFacesELResolver
W pliku library-webflow.xml trzeba w atrybucie flow-registry określić usługę tworzenia przepływów JSF, która zastąpi domyślne usługi tworzenia przepływów Spring MVC. Dzięki temu usługa tworzenia przepływów JSF będzie generować widoki JSF dla przepływów sterowania. Ponadto należy skonfigurować obiekt ViewResolver, aby przetwarzał widoki oparte na systemie Facelets zgodnie ze zdefiniowanymi zasadami. Te zasady określają, które widoki pojawią się po wystąpieniu poszczególnych stanów.
257
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
…
Usługi tworzenia przepływów JSF używają wewnętrznie fabryki widoków JSF, która domyślnie wczytuje stronę Facelets o nazwie składającej się z nazwy stanu widoku i rozszerzenia .xhtml. Przed utworzeniem stron Facelets dla stanów borrowForm i borrowReview można zdefiniować szablon stron, aby ujednolicić układ aplikacji sieciowej. Szablon ten możesz zapisać w pliku /WEB-INF/template.xhtml. Biblioteka
W tym szablonie zdefiniowane są dwa obszary — title i content. Na stronach opartych na tym szablonie należy umieścić w tych obszarach odpowiednie treści. Teraz utwórz plik /WEB-INF/flows/borrowBook/borrowForm.xhtml dla stanu borrowForm (plik ten będzie wczytywany automatycznie). 258
7.5. INTEGROWANIE PLATFORMY SPRING WEB FLOW Z TECHNOLOGIĄ JSF
Formularz do wypożyczania książek ISBN Data wypożyczenia Data zwrotu Czytelnik
Na tej stronie używane są standardowe komponenty JSF (na przykład form, outputLabel, inputText i commandButton). Powodują one powstanie formularza wiążącego wartości pól z obiektem formularza. Akcja uruchamiana za pomocą przycisku poleceń jest powiązana z identyfikatorem zdarzenia z platformy Spring Web Flow, który powoduje odpowiednie przejście. Teraz utwórz plik /WEB-INF/flows/borrowBook/borrowReview.xhtml dla stanu borrowReview. Potwierdzanie danych ISBN
259
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
Data wypożyczenia Data zwrotu Czytelnik
Akcje i odbiorniki ActionListener Do poruszania się w tradycyjnych aplikacjach JSF służą odnośniki prowadzące bezpośrednio do zasobu lub uruchamiające akcje (na przykład za pomocą elementu commandLink). Akcja to metoda używanego na zapleczu ziarna, która przetwarza dane (na przykład w reakcji na przesłanie formularza), a następnie zwraca wartość typu String. Ta wartość jest wiązana z określonymi skutkami nawigacji opisanymi w pliku faces-config.xml. Gdy używasz platformy Spring Web Flow, automatycznie zarządza ona wiązaniem zwróconych łańcuchów znaków ze skutkami nawigacji (nie trzeba wykorzystywać do tego pliku faces-config.xml). Dzieje się tak nawet wtedy, gdy wartości typu String są zwracane przez akcje. Jest kilka powodów, dla których ma to znaczenie. W platformie Spring Web Flow można użyć nazwy przejścia w parametrach akcji (lub odbiornika actionListener). Wtedy gdy kliknięcie przycisku ma prowadzić do wykonania danej akcji, można wykorzystać identyfikator zdarzenia do uruchomienia przepływu i użyć w nim wyrażenia do wywoływania funkcji Javy. To rozwiązanie w większości sytuacji działa dobrze, jednak w wywoływanych metodach niedostępny jest kontekst FacesContext. Jest wiele powodów, dla których może on być przydatny w akcjach. Jeśli stosujesz sprawdzanie poprawności kombinacji pól lub stanu przed uruchomieniem przepływu, powinieneś wykorzystać starszy, standardowy styl wywoływania metod i zwracać w nich wartość typu String (określa skutki nawigacji) lub null (pozwala zatrzymać obsługę nawigacji). To podejście umożliwia używanie nawigacji z platformy Spring Web Flow oraz korzystanie z akcji do wywoływania metod i wykonywania operacji. Jeśli następnie okaże się, że należy przejść do innej strony, można to zrobić w zwykły sposób. W tym rozwiązaniu przycisk commandButton, którego akcja jest powiązana z przejściem z platformy Spring Web Flow:
przyjmuje następującą postać:
W metodzie formSubmitted umieść standardowy kod: public String formSubmitted (){ FacesContext fc = FacesContext.getCurrentInstance(); // Wartość jest różna od null // Wykonywanie dowolnych operacji lub ustawianie stanu return "proceed" ; }
260
7.5. INTEGROWANIE PLATFORMY SPRING WEB FLOW Z TECHNOLOGIĄ JSF
Używanie komponentów JSF z platformy Spring Faces Zanim zaczniesz używać komponentów z platformy Spring Faces, musisz w deskryptorze wdrażania zarejestrować serwlet ResourceServlet. Jest on udostępniany przez platformę Spring JavaScript i zapewnia dostęp do statycznych zasobów z plików JAR. Omawiane komponenty pobierają za pomocą tego serwletu statyczne zasoby w JavaScripcie i CSS-ie z platformy Spring JavaScript. ... resources org.springframework.js.resource.ResourceServlet resources /resources/*
Spring Faces udostępnia zestaw komponentów służących do sprawdzania po stronie klienta poprawności standardowych komponentów JSF, które umożliwiają użytkownikom wprowadzanie danych. Komponenty do sprawdzania poprawności mają postać znaczników systemu Facelets i są zdefiniowane w bibliotece znaczników platformy Spring Faces. Dlatego najpierw trzeba dodać tę bibliotekę w elemencie głównym. Poniżej pokazano, jak włączyć sprawdzanie poprawności po stronie klienta dla komponentów formularza używanego do wypożyczania książek (/WEB-INF/flows/borrowBook/borrowForm.xhtml). Formularz przeznaczony do wypożyczania książek ISBN Data wypożyczenia Data zwrotu
261
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
Czytelnik
Zastosowane tu komponenty umożliwiają sprawdzanie poprawności komponentów inputText po stronie klienta. Zauważ, że komponent clientDateValidator dodatkowo udostępnia kontrolkę wyboru daty dla powiązanego pola na dane wejściowe. Kliknięcie przycisku poleceń umieszczonego w komponencie validateAllInClick prowadzi do uruchomienia wszystkich zastosowanych na danej stronie komponentów sprawdzających poprawność. Kontrolują one, czy powiązane z nimi pola są poprawne. Należy też wybrać motyw z pakietu Dojo, który ma posłużyć do wyświetlania komponentów. Możesz na przykład ustawić w szablonie template.xhtml motyw tundra w elemencie . Biblioteka
7.6. Korzystanie z platformy RichFaces w platformie Spring Web Flow Problem W poprzedniej recepturze zobaczyłeś, jak wykorzystać zaawansowane komponenty z platformy Spring Faces. Większość tych komponentów jest oparta na platformie Dojo (przeznaczonej dla języka JavaScript). Komponenty te są bardzo dobre, jednak czasem potrzebne są bardziej rozbudowane rozwiązania, takie jak platforma RichFaces.
Rozwiązanie Można zintegrować platformę Spring Web Flow z platformą RichFaces. Ta ostatnia udostępnia dwie biblioteki, które mają odmienne przeznaczenie. Jedna z tych bibliotek, Ajax4JSF, pozwala wzbogacić istniejące komponenty i elementy stron o mechanizmy Ajaksa. Druga, RichFaces, zawiera zestaw zaawansowanych komponentów z wbudowaną obsługą Ajaksa.
262
7.6. KORZYSTANIE Z PLATFORMY RICHFACES W PLATFORMIE SPRING WEB FLOW
Jak to działa? Ponieważ za pomocą Ajaksa można przekierowywać użytkowników i ponownie wyświetlać wybrane fragmenty stron, ważne jest, aby dobrze zintegrować stosowane rozwiązania z platformą Spring Web Flow. W opisywanym tu rozwiązaniu wszystkie zadania z zakresu nawigacji są delegowane do platformy Spring Web Flow, a ta zarządza stanem obiektów powiązanych z przepływami. Dlatego w interfejsach API bibliotek przeznaczonych do integrowania z platformą Spring Web Flow znajdują się specjalne haczyki. Jeden z nich (dla platformy RichFaces) jest od razu gotowy do użycia.
Konfigurowanie platformy RichFaces pod kątem platformy JSF Aby skonfigurować platformę RichFaces, trzeba wprowadzić kilka zmian w pliku web.xml. Jest tak niezależnie od tego, czy używana jest platforma Spring Web Flow. Większość przedstawionego poniżej kodu pliku web.xml powinna być dla Ciebie zrozumiała, ponieważ powtórzyliśmy go, aby utworzyć kompletny, działający przykład. Ilustruje on konfigurowanie platform Spring Web Flow, RichFaces, Facelets i JSF (w postaci implementacji MyFaces Apache’a). richfaces-swf-application org.springframework.web.context .ContextLoaderListener org.springframework.web.context .request.RequestContextListener contextConfigLocation /WEB-INF/spring/web-application-context.xml org.ajax4jsf.VIEW_HANDLERS com.sun.facelets.FaceletViewHandler org.apache.myfaces.webapp .StartupServletContextListener RichFaces Filter richfaces org.ajax4jsf.Filter richfaces faces REQUEST FORWARD INCLUDE
263
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
richfaces SwfServlet REQUEST FORWARD INCLUDE faces javax.faces.webapp.FacesServlet 1 SwfServlet org.springframework.web.servlet .DispatcherServlet contextConfigLocation 2 Resource Servlet org.springframework.js.resource .ResourceServlet 0 Resource Servlet /resources/* SwfServlet /swf/* faces *.xhtml /faces/*
W kodzie znajdują się definicje zarówno serwletu Spring Web Flow, jak i serwletu JSF. Ponadto skonfigurowano filtr RichFaces, aby obsługiwał żądania przeznaczone dla obu tych serwletów. Następny krok polega na skonfigurowaniu platformy Spring Web Flow pod kątem używanej biblioteki. Prawie całą konfigurację można pozostawić w pierwotnej postaci. Wyjątkiem jest to, że trzeba określić w platformie Spring Web Flow, jak ma obsługiwać ajaksowe żądania. W tym celu skonfiguruj egzemplarz adaptera FlowHandlerAdapter w pliku library-webflow.xml. Dodaj poniższy fragment w końcowej części pliku (tuż przed końcowym znacznikiem zamykającym element beans).
264
7.6. KORZYSTANIE Z PLATFORMY RICHFACES W PLATFORMIE SPRING WEB FLOW
Na tym etapie prawie wszystko jest już gotowe. Ostatni niuans to dodanie aktualizowania fragmentów stron za pomocą Ajaksa. Gdy stosujesz biblioteki RichFaces i Ajax4JSF, akcje mogą zmieniać stan po stronie serwera, a także wyświetlać fragmenty stron po stronie klienta. Zobacz, jak zaktualizować licznik przy użyciu Ajaksa. Kliknięcie przycisku powoduje tu zwiększenie liczby. W przepływie sterowania użytkownik jest kierowany do pierwotnego widoku, w którym ponownie wyświetlana jest zaktualizowana wartość. Jeśli korzystasz z biblioteki RichFaces, kod powinien wyglądać tak: Aktualizowanie licznika
Kliknięcie odnośnika powoduje przesłanie na serwer nazwy metody z danego stanu przepływu (jako wartości action elementu commandLink), co prowadzi do przejścia dalej w przepływie sterowania. Tu stan obejmuje elementy transition i render. W momencie przejścia sprawdzana jest wartość wyrażenia (co skutkuje uruchomieniem kodu aktualizującego licznik), a następnie fragment strony, który należy ponownie wyświetlić, jest przesyłany do klienta. Po stronie klienta RichFaces ponownie wyświetla komponenty o podanych identyfikatorach, a także komponenty a4j:outputPanel, których atrybut ajaxRendered ma wartość true (przy czym komponenty te są pomijane, jeśli ustawiony jest atrybut limitToList). Odbiega to nieco od sposobu działania platformy SpringFaces w Spring Web Flow, gdzie w trakcie nawigacji trzeba bezpośrednio określić, jakie elementy mają zostać ponownie wyświetlone.
Platforma Spring Web Flow udostępnia zmienną kontekstu, flowRenderFragments, którą Ajax4JSF może obserwować. Pozwala to ponownie wyświetlać fragmenty stron określone w ramach przejścia i uniknąć dzięki temu konfliktów z platformą Spring Web Flow. Przedstawiony wcześniej element commandLink należy
265
ROZDZIAŁ 7. PLATFORMA SPRING WEB FLOW
zmodyfikować, tak aby wykorzystać w nim nowe przejście. W efekcie powstaje niegeneryczne rozwiązanie, w którym cała logika związana z przejściami i nawigacją jest pod kontrolą platformy Spring Web Flow (czyli tam, gdzie jej miejsce).
Podsumowanie Z tego rozdziału dowiedziałeś się, jak zarządzać przepływem sterowania w aplikacjach sieciowych za pomocą platformy Spring Web Flow. Zacząłeś od utworzenia definicji przepływu w tej platformie. Takie definicje składają się z jednego lub więcej stanów. Dostępne są stany: widoku, akcji, podejmowania decyzji, podprzepływu i końcowe. Po zakończeniu wykonywania zadań w danym stanie zgłaszane jest zdarzenie. Zawiera ono źródło i identyfikator zdarzenia, a czasem także atrybuty. Każdy stan może też obejmować przejścia, które łączą zwracane identyfikatory zdarzeń z kolejnymi stanami. Następnie dowiedziałeś się, jak zabezpieczać przepływy platformy Spring Web Flow. Platformę tę można zintegrować z platformą Spring Security. Dzięki temu można łatwo zabezpieczać przepływy sterowania za pomocą tej platformy. Odpowiednio skonfigurowana platforma Spring Security umożliwia zabezpieczanie przepływów, stanów lub przejść. Wymaga to dodania elementu z podanymi potrzebnymi atrybutami dostępu. Zobaczyłeś też, jak w platformie Spring Web Flow rozwiązany jest problem utrwalania danych. Od wersji 2.0 platforma ta obsługuje technologie JPA i Hibernate, co pozwala na łatwy dostęp do kontekstu utrwalania w różnych stanach przepływu sterowania. Można wtedy korzystać z zarządzanego kontekstu utrwalania za pomocą zmiennej o zasięgu przepływu udostępnianej przez platformę Spring Web Flow. Na końcu dowiedziałeś się, że Spring Web Flow nie tylko współdziała z dostępnymi w platformie Spring MVC technologiami wyświetlania widoków (JSP i Tiles), ale też pozwala przy odpowiedniej konfiguracji na stosowanie komponentów JSF i RichFaces w widokach używanych w przepływach sterowania.
266
ROZDZIAŁ 8
Platforma Spring MVC
W tym rozdziale dowiesz się, jak tworzyć aplikacje sieciowe za pomocą platformy Spring MVC. Spring MVC to jeden z najważniejszych modułów platformy Spring. Jest on oparty na rozbudowanym kontenerze IoC Springa i korzysta z wielu mechanizmów tego kontenera, co pozwala uprościć konfigurację. MVC (ang. Model-View-Controller) to popularny wzorzec z zakresu projektowania interfejsów użytkownika. Pozwala on oddzielić logikę biznesową od interfejsu użytkownika w wyniku wyodrębnienia w aplikacji ról modelu, widoku i kontrolera. Modele odpowiadają za przechowywanie danych aplikacji wyświetlanych w widokach. Widoki mają jedynie wyświetlać te dane i nie powinny obejmować logiki biznesowej. Kontrolery odpowiadają za odbieranie żądań od użytkowników i wywoływanie działających na zapleczu usług wykonujących operacje biznesowe. Po zakończeniu pracy usługi te mogą zwracać do widoków dane przeznaczone do wyświetlenia. Kontrolery odbierają te dane i przygotowują modele, które można wyświetlić w widokach. Wzorzec MVC przede wszystkim umożliwia oddzielenie warstwy biznesowej od interfejsu użytkownika, dzięki czemu każdą z tych warstw można niezależnie modyfikować bez wpływania na inne elementy. W aplikacjach opartych na platformie Spring MVC modele zwykle zawierają obiekty domenowe przetwarzane przez warstwę usług i utrwalane w warstwie utrwalania. Widoki to zazwyczaj szablony JSP napisane z wykorzystaniem biblioteki JSTL (ang. Java Standard Tag Library). Jednak widoki można też definiować jako pliki PDF, pliki Excela, usługi sieciowe typu REST, a nawet interfejsy typu Flex (te ostatnie często określa się mianem aplikacji RIA — ang. Rich Internet Application). Po zakończeniu lektury tego rozdziału będziesz potrafił pisać aplikacje sieciowe w Javie za pomocą platformy Spring MVC. Poznasz też standardowe rodzaje kontrolerów i widoków tej platformy, a także sposoby stosowania adnotacji do tworzenia kontrolerów w Springu 3.0. Ponadto zrozumiesz podstawowe zasady korzystania z platformy Spring MVC, które posłużą jako wprowadzenie do bardziej zaawansowanych zagadnień omawianych w dalszych rozdziałach.
8.1. Tworzenie prostej aplikacji sieciowej za pomocą platformy Spring MVC Problem Dalej zobaczysz, jak utworzyć prostą aplikację sieciową przy użyciu platformy Spring MVC. Dzięki temu poznasz podstawowe techniki i konfigurację tej platformy.
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Rozwiązanie Głównym komponentem w platformie Spring MVC jest kontroler. W najprostszych aplikacjach opartych na tej platformie kontroler to jedyny serwlet, jaki trzeba skonfigurować w deskryptorze wdrażania web.xml Javy. Kontroler platformy Spring MVC (nazywany też serwletem przekierowań — ang. dispatcher servlet) jest implementacją jednego z podstawowych wzorców projektowych Javy EE firmy Sun. Jest to wzorzec kontroler frontonu. Taki serwlet działa w platformie Spring MVC jak kontroler frontonu, co oznacza, że każde żądanie sieciowe musi przejść przez ten kontroler i że zarządza on całym procesem obsługi żądań. Gdy żądanie sieciowe trafia do aplikacji opartej na platformie Spring MVC, jako pierwszy odbiera je kontroler. Następnie kontroler przetwarza różne komponenty skonfigurowane w kontekście aplikacji sieciowej Springa lub adnotacje obecne w samym kontrolerze. Jest to potrzebne do obsłużenia żądania. Rysunek 8.1 przedstawia podstawowy przebieg obsługi żądań w platformie Spring MVC.
Rysunek 8.1. Podstawowy przebieg obsługi żądań w platformie Spring MVC Aby zdefiniować klasę kontrolera w Springu 3.0, trzeba opatrzyć ją adnotacją @Controller. W porównaniu z kontrolerami w innych platformach lub we wcześniejszych wersjach Springa tu nie trzeba implementować żadnego specyficznego dla platformy interfejsu ani dziedziczyć po określonej klasie bazowej. W wersjach Springa starszych niż 3.0 jedna z klas (na przykład AbstractController) służyła do dodania do klasy operacji serwletu przekierowań. Począwszy od wersji 2.5, serwlety przekierowań można definiować za pomocą adnotacji. W Springu 3.0 wspomniana rodzina klas została uznana za przestarzałą; zamiast niej należy tworzyć klasy z adnotacjami. Gdy klasa z adnotacją @Controller (czyli klasa kontrolera) otrzyma żądanie, szuka odpowiedniej metody w celu jego obsłużenia. Wymaga to, aby klasa kontrolera wiązała każde żądanie z metodą obsługi. Służą do tego odwzorowania metod obsługi. By uzyskać pożądany efekt, metody w klasie kontrolera należy opatrzyć adnotacjami @RequestMapping, dzięki czemu metody te będą traktowane jak metody obsługi. Sygnatury metod obsługi mogą mieć dowolną postać (tak jak w każdej standardowej klasie). Możesz nadać takiej metodzie wybraną nazwę, a także zdefiniować różne argumenty. Metody obsługi mogą też zwracać dowolne wartości (typu String lub wartość void) dostosowane do zadania wykonywanego w aplikacji. W dalszej części książki natrafisz na różne argumenty metod obsługi opatrzonych adnotacją @RequestMapping. Poniżej znajduje się tylko fragmentaryczna lista dozwolonych typów argumentów:
268
8.1. TWORZENIE PROSTEJ APLIKACJI SIECIOWEJ ZA POMOCĄ PLATFORMY SPRING MVC
HttpServletRequest i HttpServletResponse, dowolnego typu parametry żądania opatrzone adnotacją @RequestParam, dowolnego typu atrybuty modelu opatrzone adnotacją @ModelAttribute, umieszczone w przychodzącym żądaniu wartości z plików cookie opatrzone adnotacją @CookieValue, Map i ModelMap (pozwalają dodawać atrybuty do modelu w metodach obsługi), Errors i BindingResult (umożliwiają metodzie obsługi dostęp do wiązania i wyników procesu sprawdzania poprawności z obiektu polecenia), SessionStatus (umożliwia metodzie obsługi generowanie powiadomień o zakończeniu przetwarzania sesji).
Gdy klasa kontrolera wybierze odpowiednią metodę obsługi, przekazuje do niej żądanie. Zwykle kod kontrolera w celu obsłużenia żądania wywołuje działającą na zapleczu usługę. Ponadto kod metody obsługi często dodaje dane do różnych argumentów wejściowych (typu HttpServletRequest, Map, Errors lub SessionStatus) używanych w przepływie sterowania w platformie Spring MVC lub usuwa z nich dane. Metoda obsługi po zakończeniu przetwarzania żądania przekazuje sterowanie do widoku, który jest reprezentowany jako wartość zwracana przez tę metodę. Ta wartość nie reprezentuje implementacji widoku (na przykład pliku user.jsp lub report.pdf), ale widok logiczny (na przykład user lub raport; zwróć uwagę na brak rozszerzenia pliku). Zapewnia to programistom większą swobodę. Wartość zwracana przez metodę obsługi jest typu String (reprezentuje wtedy logiczną nazwę widoku) lub jest równa void (wówczas na podstawie nazwy metody obsługi lub kontrolera ustalana jest domyślna logiczna nazwa widoku). W kontekście przekazywania informacji z kontrolera do widoku nie ma znaczenia, czy metoda obsługi zwraca logiczną nazwę widoku (typu String), czy wartość void. Argumenty wejściowe metody obsługi i tak są dostępne w widoku. Na przykład jeśli metoda obsługi jako parametry wejściowe przyjmuje obiekty Map i SessionStatus, a następnie modyfikuje ich zawartość, te same obiekty są dostępne także w widoku zwróconym przez metodę obsługi. Gdy klasa kontrolera otrzymuje widok, na podstawie logicznej nazwy widoku określa konkretną implementację (na przykład plik user.jsp lub report.pdf). Używany jest do tego mechanizm określający widoki, czyli skonfigurowane w kontekście aplikacji sieciowej ziarno z implementacją interfejsu ViewResolver. Ziarno to odpowiada za zwrócenie konkretnej implementacji widoku (pliku HTML, JSP, PDF lub innego) odpowiadającej logicznej nazwie widoku. Gdy klasa kontrolera określi już implementację widoku na podstawie jego nazwy, zgodnie z tą implementacją wyświetla obiekty (typu HttpServletRequest, Map, Errors lub SessionStatus) przekazane przez metodę obsługi. Widok odpowiada za wyświetlanie użytkownikom obiektów dodanych w kodzie metody obsługi.
Jak to działa? Załóżmy, że chcesz utworzyć system rezerwowania kortów na hali sportowej. Dla tej aplikacji należy utworzyć sieciowy interfejs użytkownika, aby umożliwić użytkownikom dokonywanie rezerwacji przez internet. Aplikacja ta ma być oparta na platformie Spring MVC. Zacznij od utworzenia w podpakiecie domain poniższych klas domenowych. package com.apress.springrecipes.court.domain; ... public class Reservation { private private private private private
String courtName; Date date; int hour; Player player; SportType sportType;
// Konstruktory, gettery i settery ...
269
ROZDZIAŁ 8. PLATFORMA SPRING MVC
} package com.apress.springrecipes.court.domain; public class Player { private String name; private String phone; // Konstruktory, gettery i settery ... } package com.apress.springrecipes.court.domain; public class SportType { private int id; private String name; // Konstruktory, gettery i settery ... }
Następnie należy zdefiniować przedstawiony poniżej interfejs usługi. Umieść go w podpakiecie service. W ten sposób udostępnisz usługi rezerwowania kortów warstwie prezentacji. package com.apress.springrecipes.court.service; ... public interface ReservationService { public List query(String courtName); }
W wersji produkcyjnej aplikacji w implementacji tego interfejsu należy utrwalać dane w bazie. Jednak dla uproszczenia tu rekordy z rezerwacjami przechowywane są na liście, która zawiera kilka zapisanych na stałe rezerwacji (dodanych na potrzeby testów). package com.apress.springrecipes.court.service; ... public class ReservationServiceImpl implements ReservationService { public static final SportType TENNIS = new SportType(1, "Tenis"); public static final SportType SOCCER = new SportType(2, "Piłka nożna"); private List reservations; public ReservationServiceImpl() { reservations = new ArrayList(); reservations.add(new Reservation("Tenis nr 1", new GregorianCalendar(2008, 0, 14).getTime(), 16, new Player("Jerzy", "Brak"), TENNIS)); reservations.add(new Reservation("Tenis nr 2", new GregorianCalendar(2008, 0, 14).getTime(), 20, new Player("Agnieszka", "Brak"), TENNIS)); } public List query(String courtName) { List result = new ArrayList(); for (Reservation reservation : reservations) {
270
8.1. TWORZENIE PROSTEJ APLIKACJI SIECIOWEJ ZA POMOCĄ PLATFORMY SPRING MVC
if (reservation.getCourtName().equals(courtName)) { result.add(reservation); } } return result; } }
Konfigurowanie aplikacji opartej na platformie Spring MVC Następnie należy utworzyć układ aplikacji Spring MVC. Aplikacje sieciowe tworzone za pomocą tej platformy zwykle konfiguruje się tak samo jak standardowe aplikacje sieciowe Javy. Wyjątkiem jest konieczność dodania kilku plików konfiguracyjnych i bibliotek związanych z platformą Spring MVC. Specyfikacja Javy EE określa poprawną strukturę katalogów dla aplikacji sieciowej Javy zapisanej w archiwum Web Archive (czyli w pliku WAR). Na przykład w katalogu WEB-INF trzeba umieścić deskryptor wdrażania web.xml. Pliki klas i pliki JAR aplikacji sieciowej powinny się znaleźć w katalogach WEB-INF/classes i WEB-INF/lib. W systemie rezerwacji kortów należy utworzyć przedstawioną dalej strukturę katalogów. Zauważ, że wyróżniono w niej pliki konfiguracyjne specyficzne dla Springa. Uwaga Aby utworzyć aplikację sieciową za pomocą platformy Spring MVC, musisz dodać do parametru classpath wszystkie standardowe zależności Springa (więcej informacji na ten temat znajdziesz w rozdziale 1.), a także zależności dla platform Spring Web i Spring MVC. Jeśli używasz Mavena, dodaj do projektu Mavena następujące zależności: org.springframework spring-webmvc ${spring.version} org.springframework spring-web ${spring.version} court/ css/ images/ WEB-INF/ classes/ lib/*.jar jsp/ welcome.jsp reservationQuery.jsp court-service.xml court-servlet.xml web.xml
Pliki spoza katalogu WEB-INF są bezpośrednio dostępne (pod adresami URL) dla użytkowników, dlatego pliki CSS i pliki graficzne znajdują się poza tym katalogiem. Gdy używasz platformy Spring MVC, pliki JSP pełnią funkcję szablonów. Są wczytywane przez platformę w celu dynamicznego generowania treści, dlatego te pliki należy umieścić w katalogu WEB-INF, aby uniemożliwić bezpośredni dostęp do nich. Jednak niektóre serwery aplikacji nie pozwalają na wczytywanie plików z katalogu WEB-INF wewnętrznie przez aplikację sieciową. W takiej sytuacji jedynym rozwiązaniem jest umieszczenie plików JSP poza katalogiem WEB-INF.
271
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Tworzenie plików konfiguracyjnych Deskryptor wdrażania web.xml to najważniejszy plik konfiguracyjny w aplikacjach sieciowych Javy. W tym pliku definiuje się serwlety aplikacji i powiązane z nimi żądania sieciowe. W aplikacji Spring MVC wystarczy zdefiniować jeden egzemplarz serwletu DispatcherServlet. Działa on jak kontroler frontonu takiej aplikacji. W razie potrzeby można zdefiniować więcej takich egzemplarzy. W dużych aplikacjach przydatne może być stosowanie kilku egzemplarzy serwletu DispatcherServlet. Pozwala to powiązać poszczególne egzemplarze z określonymi adresami URL. W efekcie zarządzanie kodem jest łatwiejsze, a różni członkowie zespołu mogą pracować nad logiką aplikacji bez przeszkadzania sobie nawzajem. Court Reservation System court org.springframework.web.servlet.DispatcherServlet 1 court /
W tym deskryptorze wdrażania zdefiniowany jest serwlet typu DispatcherServlet. Jest to podstawowa klasa serwletów platformy Spring MVC. Klasa ta przyjmuje żądania sieciowe i kieruje je do odpowiednich metod obsługi. Nazwa dodanego egzemplarza to court, a powiązany jest on ze wszystkimi adresami URL z ukośnikiem (/), gdzie ukośnik reprezentuje katalog główny. Zauważ, że można też ustawiać bardziej precyzyjne wzorce adresów URL. W dużej aplikacji sensowniejsze może być powiązanie poszczególnych wzorców z różnymi serwletami. Tu dla uproszczenia wszystkie adresy URL z tworzonej aplikacji trafiają do jednego serwletu court. Nazwa serwletu DispatcherServlet pozwala też określić, jaki plik z konfiguracją platformy Spring MVC należy wczytać. Domyślnie wyszukiwany jest plik o nazwie składającej się z nazwy serwletu i przyrostka –servlet.xml. Nazwę pliku konfiguracyjnego można określić bezpośrednio w parametrze contextConfigLocation serwletu. Przy pokazanych wcześniej ustawieniach serwlet court domyślnie będzie wczytywał plik konfiguracyjny court-servlet.xml. Powinien to być standardowy plik z konfiguracją ziaren Springa: ...
Później można skonfigurować platformę Spring MVC za pomocą zestawu ziaren Springa zadeklarowanych w tym pliku. Możesz też zadeklarować inne komponenty aplikacji, na przykład obiekty odpowiedzialne za dostęp do danych, a także obiekty usługowe. Łączenie w jednym pliku konfiguracyjnym ziaren z różnych warstw nie jest jednak dobrym rozwiązaniem. Zamiast tego powinieneś utworzyć dla każdej warstwy odrębny plik z konfiguracją ziaren — na przykład plik court-persistence.xml dla warstwy utrwalania danych i plik court-service.xml dla warstwy usług. W pliku court-service.xml można umieścić poniższy obiekt usługowy:
272
8.1. TWORZENIE PROSTEJ APLIKACJI SIECIOWEJ ZA POMOCĄ PLATFORMY SPRING MVC
Aby Spring mógł wczytać pliki konfiguracyjne inne niż court-servlet.xml, trzeba zdefiniować w pliku web.xml odbiornik serwletów typu ContextLoaderListener. Domyślnie wczytuje on plik z konfiguracją ziaren WEB-INF/applicationContext.xml, jednak w parametrze kontekstu contextConfigLocation można też wskazać inny plik. Aby podać kilka plików konfiguracyjnych, oddziel ich lokalizacje przecinkami lub odstępami. contextConfigLocation /WEB-INF/court-service.xml org.springframework.web.context.ContextLoaderListener ...
Zauważ, że odbiornik ContextLoaderListener wczytuje określone pliki z konfiguracją ziaren do głównego kontekstu aplikacji, natomiast każdy egzemplarz serwletu DispatcherServlet wczytuje takie pliki do własnego kontekstu aplikacji i traktuje główny kontekst aplikacji jako nadrzędny. Dlatego kontekst wczytany przez każdy egzemplarz serwletu DispatcherServlet ma dostęp do ziaren zadeklarowanych w głównym kontekście aplikacji (i może nawet zastąpić te ziarna), przy czym nie działa to w drugą stronę. Konteksty wczytane przez różne egzemplarze serwletu DispatcherServlet nie mają do siebie dostępu.
Aktywowanie skanowania adnotacji platformy Spring MVC Przed utworzeniem kontrolerów aplikacji trzeba skonfigurować aplikację sieciową w taki sposób, aby klasy były sprawdzane pod kątem adnotacji @Controller i @RequestMapping. Tylko wtedy klasy będą mogły pełnić funkcję kontrolerów. Najpierw (by Spring automatycznie wykrywał adnotację) włącz za pomocą elementu funkcję skanowania komponentów w Springu. Ponadto potrzebne są dodatkowe instrukcje w kontekście aplikacji sieciowej. Wynika to z tego, że adnotacja @RequestMapping platformy Spring MVC wiąże żądania adresów URL z klasami kontrolerów i odpowiadającymi im metodami obsługi. Aby to rozwiązanie zadziałało, trzeba w kontekście aplikacji sieciowej zarejestrować egzemplarze typów DefaultAnnotationHandlerMapping i AnnotationMethodHandlerAdapter. Te egzemplarze przetwarzają adnotacje @RequestMapping na poziomie klasy (pierwszy) i metody (drugi). By włączyć obsługę kontrolerów opartych na adnotacjach, umieść poniższą konfigurację w pliku court-servlet.xml.
273
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Zwróć uwagę na element . Atrybut base-package ma w nim wartość com.apress. springrecipes.court.web. Jest to pakiet używany w opisanym dalej kontrolerze platformy Spring MVC. Dalej w kontekście aplikacji sieciowej domyślnie rejestrowane są klasy ziaren DefaultAnnotationHandler Mapping i AnnotationMethodHandlerAdapter. Po przygotowaniu podstawowego pliku z konfiguracją kontekstu na potrzeby skanowania adnotacji platformy Spring MVC można przejść do tworzenia klasy kontrolera, a także do uzupełniania konfiguracji w pliku court-servlet.xml.
Tworzenie kontrolerów platformy Spring MVC Klasą kontrolera opartą na adnotacjach może być dowolna klasa. Nie musi ona implementować żadnego konkretnego interfejsu ani dziedziczyć po określonej klasie bazowej. Wystarczy dodać do danej klasy adnotację @Controller. W kontrolerze można zdefiniować jedną lub kilka metod na potrzeby obsługi jednej lub kilku akcji. W sygnaturach metod obsługi można wykorzystać różne argumenty. Adnotację @RequestMapping można zastosować na poziomie klasy albo na poziomie metody. Pierwsze z omawianych tu podejść polega na powiązaniu konkretnego wzorca adresów URL z klasą kontrolera, a następnie określonych żądań HTTP z każdą z metod obsługi. package com.apress.springrecipes.court.web; ... import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; import org.springframework.ui.Model; @Controller @RequestMapping("/welcome") public class WelcomeController { @RequestMapping(method = RequestMethod.GET) public String welcome(Model model) { Date today = new Date(); model.addAttribute("today", today); return "welcome"; } }
Ten kontroler tworzy obiekt typu java.util.Date, aby pobrać bieżącą datę, a następnie dołącza ją do obiektu wejściowego typu Model jako atrybut, co pozwala wyświetlić datę w docelowym widoku. Ponieważ aktywowałeś już skanowanie adnotacji dla zadeklarowanego w pliku court-servlet.xml pakietu com.apress.springrecipes.court.web, po zainstalowaniu programu adnotacje klasy kontrolera zostaną wykryte. Adnotacja @Controller oznacza, że dana klasa jest kontrolerem platformy Spring MVC. Adnotacja @RequestMapping jest ciekawsza, ponieważ zawiera właściwości i można ją zadeklarować na poziomie klasy lub na poziomie metody. Pierwsza wartość używana w przedstawionej klasie, ("/welcome"), służy do określenia adresów URL obsługiwanych przez dany kontroler. Oznacza to, że wszystkie żądania adresu URL /welcome będą obsługiwane przez klasę WelcomeController. Gdy klasa kontrolera otrzyma żądanie, deleguje je do zadeklarowanej w niej domyślnej metody obsługi żądań HTTP GET. Dzieje się tak, ponieważ każde początkowe żądanie adresu URL jest typu HTTP GET. 274
8.1. TWORZENIE PROSTEJ APLIKACJI SIECIOWEJ ZA POMOCĄ PLATFORMY SPRING MVC
Dlatego gdy kontroler obsługuje żądanie adresu URL /welcome, deleguje je do domyślnej metody obsługi żądań HTTP GET. Adnotacja @RequestMapping(method = RequestMethod.GET) służy do ustawienia metody welcome jako domyślnej metody obsługi żądań HTTP GET w kontrolerze. Warto wspomnieć, że jeśli nie zadeklarowano domyślnej metody tego rodzaju, zgłaszany jest wyjątek ServletException. Dlatego ważne jest, aby w kontrolerze platformy Spring MVC określić przynajmniej adres URL i domyślną metodę obsługi żądań HTTP GET. Podejście to można nieco zmodyfikować i zadeklarować w adnotacji @RequestMapping na poziomie metody obie wartości — adres URL i domyślną metodę obsługi żądań HTTP GET. Taka deklaracja jest pokazana poniżej: @Controller public class WelcomeController { @RequestMapping(value = "/welcome", method=RequestMethod.GET) public String welcome(Model model) { …
Ta ostatnia deklaracja działa tak samo jak wcześniejsza. Atrybut value określa adres URL, z którym powiązana jest dana metoda obsługi, a atrybut method ustawia tę metodę jako domyślną metodę obsługi żądań HTTP GET. Ten ostatni kontroler ilustruje podstawowe zasady konfigurowania platformy Spring MVC. Jednak typowy kontroler może (na potrzeby przetwarzania danych biznesowych) wywoływać działające na zapleczu usługi. Na przykład kontroler sprawdzający rezerwacje określonego kortu może wyglądać tak: package com.apress.springrecipes.court.web; ... import com.apress.springrecipes.court.domain.Reservation; import com.apress.springrecipes.court.service.ReservationService; import import import import
org.springframework.beans.factory.annotation.Autowired; org.springframework.stereotype.Controller; org.springframework.web.bind.annotation.RequestMapping; org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.ui.Model; @Controller @RequestMapping("/reservationQuery") public class ReservationQueryController { private ReservationService reservationService; @Autowired public void ReservationQueryController(ReservationService reservationService) { this.reservationService = reservationService; } @RequestMapping(method = RequestMethod.GET) public void setupForm() { } @RequestMapping(method = RequestMethod.POST) public String sumbitForm(@RequestParam("courtName") String courtName, Model model) { List reservations = java.util.Collections.emptyList(); if (courtName != null) { reservations = reservationService.query(courtName); }
275
ROZDZIAŁ 8. PLATFORMA SPRING MVC
model.addAttribute("reservations", reservations); return "reservationQuery"; } }
Ten kontroler także wymaga adnotacji @Controller określającej, że dana klasa jest kontrolerem platformy Spring MVC. Nowością jest tu adnotacja @Autowired dodana do konstruktora klasy. Pozwala ona tworzyć w konstruktorze pola na podstawie deklaracji z plików z konfiguracją aplikacji (czyli z pliku court-service.xml). Dlatego ostatni kontroler próbuje na przykład znaleźć ziarno reservationService, aby utworzyć pole o tej samej nazwie. Jak może sobie przypominasz, w pliku court-service.xml zdefiniowane jest ziarno usługowe reservationService. Dzięki temu ziarno usługowe można wstrzyknąć do klasy kontrolera i automatycznie przypisać do odpowiedniego pola. Bez stosowania adnotacji @Autowired ziarno usługowe mógłbyś wstrzyknąć bezpośrednio w pliku konfiguracyjnym court-servlet.xml za pomocą poniższego kodu:
Adnotacja @Autowired pozwala więc zaoszczędzić czas, ponieważ dzięki niej nie trzeba wstrzykiwać właściwości za pomocą instrukcji XML-a. W kodzie klasy kontrolera znajduje się też instrukcja @Request Mapping("/reservationQuery"). Określa ona, że dany kontroler powinien obsługiwać żądania adresu URL /reservationQuery. Jak wcześniej wspomniano, kontroler po otrzymaniu żądania szuka domyślnej metody obsługi żądań HTTP GET. Ponieważ publiczna metoda void setupForm() jest opatrzona odpowiednią adnotacją @RequestMapping, kontroler ją wywołuje. Zauważ, że w tej metodzie — inaczej niż w poprzedniej domyślnej metodzie obsługi żądań HTTP GET — nie ma parametrów wejściowych ani kodu. Ponadto metoda zwraca wartość void. Oznacza to dwie rzeczy. Ponieważ metoda nie ma parametrów wejściowych ani kodu, widok jedynie wyświetla dane zapisane na stałe w szablonie (czyli w pliku JSP); kontroler nie dodaje żadnych danych. Ponadto zwracana wartość void powoduje, że używana jest domyślna nazwa widoku ustalana na podstawie żądanego adresu URL. Dlatego dla adresu URL /reservationQuery wykorzystywany jest widok o nazwie reservationQuery. Druga metoda obsługi jest opatrzona adnotacją @RequestMapping(method = RequestMethod.POST). Na pozór używanie dwóch metod obsługi z ustawionym na poziomie klasy jednym adresem URL /reservation Query jest dziwne, jednak wytłumaczenie jest proste. Jedna z metod jest wywoływana dla żądań HTTP GET, a druga — dla żądań HTTP POST tego samego adresu. Większość żądań HTTP w aplikacjach sieciowych jest typu GET, a żądania POST są zgłaszane głównie przy przesyłaniu przez użytkowników formularzy HTML. Dlatego w opisanym dalej widoku aplikacji jedna z metod jest wywoływana przy początkowym wczytywaniu formularza HTML (żądania HTTP GET), natomiast druga — przy przesyłaniu tego formularza (żądania HTTP POST). Przyjrzyj się domyślnej metodzie obsługi żądań HTTP POST i zwróć uwagę na dwa parametry wejściowe. Pierwszy z nich, zadeklarowany jako @RequestParam("courtName") String courtName, służy do pobierania parametru courtName z żądania. Tu żądanie HTTP POST ma postać /reservationQuery?courtName=. Dlatego wspomniana deklaracja sprawia, że przekazana wartość jest dostępna w metodzie jako zmienna courtName. Drugi zadeklarowany parametr, Model, służy do zdefiniowania obiektu, w którym dane są przekazywane do docelowego widoku. Metoda obsługi używa pola reservationService z kontrolera do wykonania zapytania, w którym występuje zmienna courtName. Wyniki tego zapytania są przypisywane do obiektu Model, który jest później wyświetlany w docelowym widoku. Zauważ, że metoda zwraca widok o nazwie reservationQuery. Ta metoda może też zwracać wartość void (tak jak domyślna metoda obsługi żądań HTTP GET) i być powiązana z tym samym widokiem domyślnym reservationQuery na podstawie adresu URL z żądania. Oba rozwiązania działają tak samo. Skoro już wiesz, jak wygląda struktura kontrolerów platformy Spring MVC, pora przejść do widoków, do których metody obsługi z kontrolera przekazują wyniki.
276
8.1. TWORZENIE PROSTEJ APLIKACJI SIECIOWEJ ZA POMOCĄ PLATFORMY SPRING MVC
Tworzenie widoków JSP Spring MVC udostępnia różnego rodzaju widoki dostosowane do rozmaitych technologii warstwy prezentacji. Obsługiwane formaty to: JSP, HTML, PDF, XLS (arkusze Excela), XML, JSON, Atom, RSS, JasperReports, a także inne implementacje widoków opracowane przez niezależnych programistów. W aplikacjach Spring MVC widokami są najczęściej szablony JSP napisane z wykorzystaniem biblioteki JSTL. Gdy serwlet DispatcherServlet (zdefiniowany w pliku web.xml aplikacji) przyjmuje zwróconą przez metodę obsługi nazwę widoku, wiąże logiczną nazwę widoku z implementacją widoku w celu jego wyświetlenia. Może na przykład skonfigurować ziarno InternalResourceViewResolver (w tej aplikacji należy to zrobić w pliku court-servlet.xml) z kontekstu aplikacji sieciowej, aby nazwy widoków były wiązane z plikami JSP z katalogu /WEB-INF/jsp/.
Przy ostatniej konfiguracji logiczny widok reservationQuery jest łączony z implementacją widoku z pliku /WEB-INF/jsp/reservationQuery.jsp. Na tej podstawie można utworzyć poniższy szablon JSP dla kontrolera z metodą welcome. Nazwij plik z tym kodem welcome.jsp i umieść go w katalogu WEB-INF/jsp. <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> Witaj Witaj w systemie rezerwowania kortów Dzisiejsza data to: .
W tym szablonie JSP biblioteka znaczników fmt (jest to część biblioteki JSTL) służy do sformatowania atrybutu today modelu zgodnie ze wzorcem yyyy-MM-dd. Nie zapomnij umieścić definicji biblioteki znaczników fmt w początkowej części szablonu JSP. Następnie trzeba utworzyć inny szablon JSP dla kontrolera zapytań o rezerwację. Nazwij ten szablon reservationQuery.jsp, aby pasował do nazwy widoku. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> Zapytania o rezerwacje Nazwa kortu Data Godzina Gracz
277
ROZDZIAŁ 8. PLATFORMA SPRING MVC
${reservation.courtName} ${reservation.hour} ${reservation.player.name}
W tym szablonie JSP znajduje się formularz umożliwiający użytkownikom podanie nazwy kortu, który chcą sprawdzić. Dalej umieszczony jest znacznik , co pozwala w pętli sprawdzić atrybut reservations modelu w celu wygenerowania wynikowej tabeli.
Instalowanie aplikacji sieciowej Gorąco zachęcamy do tego, aby w trakcie tworzenia aplikacji sieciowej zainstalować lokalny serwer aplikacji Javy EE z kontenerem WWW umożliwiającym testowanie i debugowanie kodu. By umożliwić łatwe konfigurowanie i instalowanie aplikacji, tu jako kontener WWW używany jest serwer Apache Tomcat 6.0.x. Katalog instalacyjny tego kontenera WWW to katalog webapps. Domyślnie serwer Tomcat oczekuje na żądania w porcie 8080 i umieszcza aplikacje w kontekście o nazwie zgodnej z nazwą pliku WAR aplikacji. Dlatego jeśli umieścisz aplikację w pliku WAR o nazwie court.war, kontrolery strony powitalnej i zapytań o rezerwacje będzie można uruchomić za pomocą poniższych adresów URL: http://localhost:8080/court/welcome http://localhost:8080/court/reservationQuery
8.2. Wiązanie żądań za pomocą adnotacji @RequestMapping Problem Gdy serwlet DispatcherServlet otrzymuje żądanie sieciowe, próbuje przekierować je do różnych klas kontrolerów (czyli klas opatrzonych adnotacją @Controller). Przebieg przekierowywania zależy od różnych adnotacji @RequestMapping zadeklarowanych w klasie kontrolera i od umieszczonych w niej metod obsługi żądań. Za pomocą adnotacji @RequestMapping należy zdefiniować strategię wiązania żądań.
Rozwiązanie W aplikacjach Spring MVC żądania sieciowe są wiązane z metodami ich obsługi za pomocą adnotacji @RequestMapping umieszczanych w klasach kontrolera. Metody obsługi są wiązane z adresami URL na podstawie ścieżek określanych względem ścieżki kontekstu (czyli ścieżki kontekstu aplikacji sieciowej) i ścieżki serwletu (czyli ścieżki powiązanej z serwletem DispatcherServlet). Tak więc dla adresu URL http://localhost:8080/court/welcome pasująca ścieżka to /welcome, ponieważ ścieżka kontekstu to /court, natomiast ścieżka serwletu jest nieokreślona (została zadeklarowana jako / w pliku web.xml).
278
8.2. WIĄZANIE ŻĄDAŃ ZA POMOCĄ ADNOTACJI @REQUESTMAPPING
Jak to działa? Wiązanie żądań bezpośrednio w metodach Najprostszym sposobem stosowania adnotacji @RequestMapping jest dodanie ich bezpośrednio do metod obsługi. Aby ta technika zadziałała, musisz dodać taką adnotację do wszystkich metod obsługi i określić w każdej z tych adnotacji wzorzec adresów URL. Jeśli adnotacja @RequestMapping metody obsługi pasuje do adresu URL żądania, serwlet DispatcherServlet przekierowuje żądanie do danej metody w celu przetworzenia żądania. @Controller public class MemberController { private MemberService memberService; @Autowired public MemberController(MemberService memberService) { this.memberService = memberService; } @RequestMapping("/member/add") public String addMember(Model model) { model.addAttribute("member", new Member()); model.addAttribute("guests", memberService.list()); return "memberList"; } @RequestMapping(value={"/member/remove","/member/delete"}, method=RequestMethod.GET) public String removeMember( @RequestParam("memberName") String memberName) { memberService.remove(memberName); return "redirect:"; } }
Ostatni listing ilustruje, jak za pomocą adnotacji @RequestMapping powiązać każdą metodę obsługi z konkretnymi adresami URL. Do drugiej metody obsługi przypisano dwa adresy URL, dlatego jest ona wywoływana w wyniku przejścia pod adresy /member/remove i /member/delete. Domyślnie przyjmuje się, że wszystkie żądania adresów URL to żądania HTTP GET.
Wiązanie żądań na poziomie klasy Adnotację @RequestMapping można też dodać do klasy kontrolera. Dzięki temu w metodach obsługi można albo pominąć adnotacje @RequestMapping (tak jak w kontrolerze ReservationQueryController z receptury 8.1), albo precyzyjniej określić adresy URL w tych adnotacjach dołączonych do metod. Na potrzeby ogólnego dopasowywania adresów URL można w adnotacjach @RequestMapping stosować symbole wieloznaczne (*). Na poniższym listingu pokazano, jak wykorzystać symbole wieloznaczne w adresach URL w adnotacji @RequestMapping, a następnie precyzyjnie określić adresy URL w adnotacjach tego samego typu dodanych do metod obsługi. @Controller @RequestMapping("/member/*") public class MemberController { private MemberService memberService; @Autowired public MemberController(MemberService memberService) {
279
ROZDZIAŁ 8. PLATFORMA SPRING MVC
this.memberService = memberService; } @RequestMapping("add") public String addMember(Model model) { model.addAttribute("member", new Member()); model.addAttribute("guests", memberService.list()); return "memberList"; } @RequestMapping(value={"remove","delete"}, method=RequestMethod.GET) public String removeMember( @RequestParam("memberName") String memberName) { memberService.remove(memberName); return "redirect:"; } @RequestMapping("display/{user}") public String removeMember( @RequestParam("memberName") String memberName, @PathVariable("user") String user) { ... } @RequestMapping public void memberList() { ... } public void memberLogic(String memberName) { ... } }
Zauważ, że w adnotacji @RequestMapping na poziomie klasy podano adres URL z symbolem wieloznacznym, /member/*. To sprawia, że wszystkie żądania z członem /member/ w adresie URL są kierowane do metod obsługi tego kontrolera. W dwóch pierwszych metodach obsługi także występuje adnotacja @RequestMapping. Metoda addMember() jest wywoływana w odpowiedzi na zgłoszenie żądania HTTP GET adresu URL /member/add. Wywołanie metody removeMember() ma miejsce, gdy żądanie HTTP GET dotyczy adresu /member/remove lub /member/delete. W trzeciej metodzie obsługi wartość adnotacji @RequestMapping jest podawana za pomocą specjalnego zapisu {path_variable}. Dzięki temu wartość z adresu URL można przekazać jako dane wejściowe do metody obsługi. Zauważ, że w metodzie obsługi znajduje się deklaracja argumentu @PathVariable("user") String user. Dlatego gdy nadejdzie żądanie w postaci member/display/piotrek, metoda obsługi będzie miała dostęp do zmiennej user o wartości piotrek. Ta technika pozwala uniknąć manipulowania obiektem żądania i jest przydatna zwłaszcza przy projektowaniu usług sieciowych typu REST. Czwarta metoda także jest opatrzona adnotacją @RequestMapping, ale nie ma podanego adresu URL. Ponieważ na poziomie klasy używany jest adres URL z symbolem wieloznacznym (/member/*), ta metoda obsługi jest wykonywana dla wszystkich adresów zgodnych z podanym wzorcem, takich jak /member/abcdefg lub /member/randomroute. Zwróć uwagę na zwracaną wartość void, która sprawia, że ta metoda obsługi jest domyślnie powiązana z widokiem o nazwie identycznej z nazwą tej metody (czyli memberList). Ostatnia metoda, memberLogic, nie jest opatrzona adnotacjami @RequestMapping. Oznacza to, że jest to metoda narzędziowa klasy. Takie metody nie wpływają na działanie platformy Spring MVC.
280
8.2. WIĄZANIE ŻĄDAŃ ZA POMOCĄ ADNOTACJI @REQUESTMAPPING
Wiązanie żądań na podstawie typu żądań HTTP W adnotacjach @RequestMapping domyślnie przyjmuje się, że wszystkie przychodzące żądania są typu HTTP GET. W większości aplikacji sieciowych jest to najczęściej stosowany rodzaj żądań. Jeśli jednak przychodzące żądania są innego typu, trzeba go bezpośrednio określić w adnotacji @RequestMapping: @RequestMapping(method = RequestMethod.POST) public String submitForm(@ModelAttribute("member") Member member, BindingResult result, Model model) { ... }
Ta ostatnia metoda pełni funkcję domyślnej metody obsługi wszystkich żądań HTTP POST skierowanych do danej klasy kontrolera. Adresy URL powiązane z tą klasą należy określić w adnotacji @RequestMapping na poziomie klasy. Oczywiście można też zastosować atrybut value, aby bezpośrednio określić adresy URL powiązane z daną metodą obsługi. @RequestMapping(value= "processUser" method = RequestMethod.POST) public String submitForm(@ModelAttribute("member") Member member, BindingResult result, Model model) { ... }
To, czy trzeba określać typ żądań HTTP obsługiwanych przez poszczególne metody, zależy od tego, z jakimi jednostkami i w jaki sposób komunikuje się kontroler. Przeglądarki internetowe większość operacji wykonują za pomocą żądań HTTP GET i HTTP POST. Jednak inne urządzenia lub aplikacje (na przykład usługi sieciowe typu REST) mogą wymagać obsługi innych rodzajów żądań HTTP. Łącznie jest osiem różnych typów żądań HTTP: HEAD, GET, POST, PUT, DELETE, TRACE, OPTIONS i CONNECT. Obsługa ich wszystkich wykracza jednak poza zakres kontrolera MVC, ponieważ serwer WWW, a także żądająca jednostka muszą obsługiwać takie żądania. Większość żądań HTTP jest typu GET lub POST, dlatego rzadko (jeśli w ogóle) będziesz musiał implementować obsługę dodatkowych rodzajów żądań.
Gdzie podziały się rozszerzenia z adresów URL (na przykład .html lub .jsp)? Może zauważyłeś, że w adresach URL podawanych w adnotacjach @RequestMapping nie ma żadnych rozszerzeń plików (na przykład .html lub .jsp). Jest to poprawne rozwiązanie zgodne z modelem MVC, choć nie jest powszechnie stosowane. Kontrolera nie należy wiązać z rozszerzeniem określonego rodzaju, wyznaczającym technologię używaną do tworzenia widoków (na przykład HTML lub JSP). To dlatego kontrolery zwracają widoki logiczne, a dopasowywane adresy URL są deklarowane bez rozszerzeń. Ponieważ obecnie aplikacje często udostępniają te same treści w różnych formatach (takich jak XML, JSON, PDF lub XLS), mechanizm określający widoki powinien zbadać podane w żądaniu rozszerzenie (jeśli klient w ogóle je wpisał) i ustalić, której technologii tworzenia widoków użyć. W tym krótkim wprowadzeniu zobaczyłeś, jak ustawić mechanizm określania widoków w pliku z konfiguracją modelu MVC (*-servlet.xml) w taki sposób, aby wiązał widoki logiczne z plikami JSP. Nie wymaga to stosowania w adresach URL rozszerzeń nazw plików (na przykład .jsp). Z dalszych receptur dowiesz się, że w platformie Spring MVC wykorzystuje się to samo podejście z adresami URL bez rozszerzeń, aby udostępniać treści za pomocą różnych technologii tworzenia widoków.
281
ROZDZIAŁ 8. PLATFORMA SPRING MVC
8.3. Przechwytywanie żądań przy użyciu interceptorów przetwarzania Problem Za pomocą filtrów serwletów zdefiniowanych w interfejsie Servlet API można dodać wstępne i końcowe przetwarzanie każdego żądania sieciowego przed jego obsłużeniem przez serwlet i po wykonaniu tego zadania. Załóżmy, że chcesz skonfigurować podobne filtry w kontekście aplikacji sieciowej Springa, aby wykorzystać możliwości kontenera. Ponadto czasem przydatne jest wstępne i końcowe przetwarzanie żądań sieciowych obsługiwanych przez metody obsługi platformy Spring MVC. Pozwala to manipulować atrybutami zwracanego przez te metody modelu przed przekazaniem go do widoków.
Rozwiązanie Platforma Spring MVC umożliwia przechwytywanie żądań sieciowych na potrzeby wstępnego i końcowego przetwarzania. Służą do tego interceptory przetwarzania (ang. handler interceptors). Takie interceptory konfiguruje się w kontekście aplikacji sieciowej Springa, dlatego mogą one korzystać ze wszystkich mechanizmów kontenera i zadeklarowanych w nim ziaren. Interceptor przetwarzania można zarejestrować dla konkretnych adresów URL, co pozwala przechwytywać żądania kierowane pod te adresy. W każdym interceptorze przetwarzania trzeba zaimplementować interfejs HandlerInterceptor. Ten interfejs obejmuje trzy wywoływane zwrotnie metody, które należy zaimplementować: preHandle(), postHandle() i afterCompletion(). Dwie z tych metod są wywoływane przed obsłużeniem żądania przez metodę obsługi (preHandle()) i po wykonaniu tego zadania (postHandle()). Metoda postHandle() zapewnia też dostęp do zwróconego obiektu ModelAndView, co pozwala manipulować atrybutami modelu. Metoda afterCompletion() jest wywoływana po całkowitym zakończeniu przetwarzania żądania (czyli po wyświetleniu widoku).
Jak to działa? Załóżmy, że chcesz zmierzyć czas przetwarzania każdego żądania sieciowego przez metody obsługi i wyświetlać użytkownikom wyniki pomiaru w widokach. W tym celu możesz utworzyć interceptor przetwarzania: package com.apress.springrecipes.court.web; ... import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; public class MeasurementInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { long startTime = System.currentTimeMillis(); request.setAttribute("startTime", startTime); return true; } public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { long startTime = (Long) request.getAttribute("startTime"); request.removeAttribute("startTime");
282
8.3. PRZECHWYTYWANIE ŻĄDAŃ PRZY UŻYCIU INTERCEPTORÓW PRZETWARZANIA
long endTime = System.currentTimeMillis(); modelAndView.addObject("handlingTime", endTime - startTime); } public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
W metodzie preHandle() tego interceptora rejestrowany jest czas początkowy, który zostaje zapisany w atrybucie żądania. Ta metoda powinna zwrócić true, co pozwala serwletowi DispatcherServlet kontynuować obsługiwanie żądania. Gdy zwracana jest inna wartość, serwlet DispatcherServlet „uznaje”, że metoda obsłużyła już żądanie (wtedy serwlet bezpośrednio zwraca odpowiedź do użytkownika). Później, w metodzie postHandle(), należy wczytać czas początkowy z atrybutu żądania i porównać go z bieżącym czasem. Dzięki temu można obliczyć czas obsługi żądania i dodać go do modelu przekazywanego do widoku. Ponieważ w metodzie afterCompletion() nie trzeba wykonywać żadnych zadań, można pozostawić ją pustą. Gdy implementujesz interfejs, musisz zaimplementować wszystkie metody, nawet jeśli niektóre z nich nie są potrzebne. Dlatego lepszym rozwiązaniem jest utworzenie klasy pochodnej od klasy adaptera interceptora. W klasie adaptera znajdują się domyślne implementacje wszystkich metod interceptora. Wystarczy przesłonić tylko potrzebne metody. package com.apress.springrecipes.court.web; ... import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; public class MeasurementInterceptor extends HandlerInterceptorAdapter { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ... } public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { ... } }
Interceptor przetwarzania jest zarejestrowany w ziarnie DefaultAnnotationHandlerMapping. To ziarno stosuje interceptory do wszystkich klas opatrzonych adnotacją @Controller. We właściwości interceptors można podać kilka interceptorów (typem tej właściwości jest tablica). ...
283
ROZDZIAŁ 8. PLATFORMA SPRING MVC
...
Następnie można wyświetlić zarejestrowany czas w pliku welcome.jsp, aby sprawdzić, czy interceptor poprawnie działa. Ponieważ kontroler WelcomeController nie ma wielu zadań do wykonania, możliwe, że czas obsługi będzie równy 0 milisekund. Jeśli tak się stanie, dodaj do klasy instrukcję sleep, by otrzymać dłuższy czas obsługi. <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> Witaj ... Czas obsługi (w milisekundach): ${handlingTime}
Korzystanie z ziarna DefaultAnnotationHandlerMapping w pokazanej tu postaci ma pewną wadę — interceptor jest przypisywany do każdej klasy opatrzonej adnotacją @Controller. Jeśli istnieje kilka kontrolerów, można określić, do których z nich należy zastosować interceptory. W tym celu trzeba zdefiniować niestandardowy interceptor przetwarzania. Na szczęście jest on potrzebny tak często, że powstał projekt ułatwiający pracę w tym scenariuszu. Projekt spring-plugins Scotta Murphy’ego umożliwia stosowanie interceptorów do kontrolerów na podstawie adresów URL. Wspomniany projekt możesz pobrać ze strony http://code.google.com/p/springplugins/downloads/list. Po wykonaniu tego zadania i umieszczeniu odpowiedniego pliku JAR w katalogu /WEB-INF/lib aplikacji wystarczy dodać do ziarna DefaultAnnotationHandlerMapping konfigurację projektu: ...
... /reservationSummary*
284
8.4. OKREŚLANIE USTAWIEŃ REGIONALNYCH UŻYTKOWNIKÓW
Przede wszystkim dodane jest tu ziarno interceptora, summaryReportInterceptor. Struktura jego klasy jest identyczna ze strukturą klasy dla interceptora measurementInterceptor — w klasach obu tych interceptorów jest zaimplementowany interfejs HandlerInterceptor. Jednak nowy interceptor wykonuje operacje przeznaczone dla konkretnego kontrolera. Potrzebny efekt można uzyskać za pomocą ziarna org.springplugins.web.SelectedAnnotationHandlerMapping (pochodzi ono z projektu spring-plugins). Ziarno to, podobnie jak ziarno DefaultAnnotationHandlerMapping, ma właściwość interceptors, do której można przypisać listę ziaren interceptorów. Jednak SelectedAnnotation HandlerMapping, w odróżnieniu od drugiego z używanych ziaren, ma dodatkowo właściwość url. Do tej właściwości należy przypisać listę adresów URL, dla których mają zostać zastosowane podane interceptory. Przy tych ustawieniach interceptor measurementInterceptor jest stosowany do wszystkich kontrolerów opatrzonych adnotacją @Controller, natomiast interceptor summaryReportInterceptor działa tylko dla kontrolerów, które mają tę adnotację, a dodatkowo są powiązane z adresami URL zgodnymi ze wzorcem /reservationSummary*. Jest jeszcze jeden aspekt deklaracji interceptorów przetwarzania. Dotyczy on właściwości order. Zauważ, że w ziarnie DefaultAnnotationHandlerMapping znajduje się teraz instrukcja . Właściwość order pozwala określić pierwszeństwo ziaren interceptorów. Mniejsza wartość (na przykład 0) oznacza, że interceptor ma wyższy priorytet. Tu ziarno SelectedAnnotationHandlerMapping ma niższą wartość właściwości order, 0, dlatego ma wyższy priorytet niż ziarno DefaultAnnotationHandlerMapping. Proces przypisywania wartości właściwości order do ziarna interceptora przypomina ustawianie właściwości uruchomieniowych w serwletach w pliku web.xml.
8.4. Określanie ustawień regionalnych użytkowników Problem Aby aplikacja sieciowa obsługiwała umiędzynarodawianie, musi określać preferowane ustawienia regionalne użytkowników i wyświetlać treści zgodnie z nimi.
Rozwiązanie W aplikacjach Spring MVC ustawienia regionalne użytkownika można ustalić za pomocą służącej do tego klasy, w której trzeba zaimplementować interfejs LocaleResolver. Spring MVC udostępnia kilka implementacji tego interfejsu. Umożliwiają one określanie ustawień regionalnych według różnych kryteriów. Możesz też samodzielnie zaimplementować wspomniany interfejs i utworzyć własną klasę tego rodzaju. Aby zdefiniować klasę określającą ustawienia regionalne, zarejestruj ziarno typu LocaleResolver w kontekście aplikacji sieciowej. Nazwę ziarna należy ustawić na localeResolver, dzięki czemu serwlet DispatcherServlet automatycznie je wykryje. Zauważ, że w jednym serwlecie DispatcherServlet można zarejestrować tylko jedno ziarno tego typu.
285
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Jak to działa? Określanie ustawień regionalnych na podstawie nagłówka żądania HTTP W Springu domyślną klasą do określania ustawień regionalnych jest AcceptHeaderLocaleResolver. Ustala ona ustawienia regionalne na podstawie nagłówka accept-language z żądania HTTP. Nagłówek ten jest tworzony przez przeglądarkę użytkownika w oparciu o ustawienia regionalne systemu operacyjnego. Zauważ, że ta klasa do określania ustawień regionalnych nie może ich zmienić, ponieważ nie potrafi zmodyfikować ustawień systemu operacyjnego.
Określanie ustawień regionalnych na podstawie atrybutu sesji Inną klasą do określania ustawień regionalnych jest SessionLocaleResolver. Ustala ona ustawienia na podstawie zdefiniowanego atrybutu sesji użytkownika. Jeśli dany atrybut nie istnieje, ustawienia regionalne są określane zgodnie z nagłówkiem accept-language żądania HTTP.
W tej klasie możesz ustawić właściwość defaultLocale. Określa ona domyślne ustawienia regionalne, używane, jeśli dany atrybut nie istnieje. Zauważ, że ta klasa pozwala zmienić ustawienia regionalne użytkownika — w tym celu wystarczy zmodyfikować odpowiedni atrybut sesji.
Określanie ustawień regionalnych na podstawie pliku cookie Do ustalania ustawień regionalnych możesz też zastosować klasę CookieLocaleResolver. Wykorzystuje ona plik cookie z przeglądarki użytkownika. Jeśli taki plik nie istnieje, używane są domyślne ustawienia regionalne podane w nagłówku accept-language żądania HTTP.
Plik cookie używany przez tę klasę można zmodyfikować za pomocą właściwości cookieName i cookieMaxAge. Właściwość cookieMaxAge określa, przez ile sekund plik cookie pozostaje aktualny. Wartość -1 oznacza, że plik przestaje być aktualny od razu po zamknięciu przeglądarki.
Możesz też ustawić właściwość defaultLocale, używaną, jeśli w przeglądarce nie ma potrzebnego pliku cookie. Ta klasa pozwala zmienić ustawienia lokalne użytkownika w wyniku modyfikacji odpowiedniego pliku cookie.
Zmienianie ustawień regionalnych użytkownika Oprócz modyfikowania ustawień regionalnych użytkownika przez bezpośrednie wywołanie metody LocaleResolver.setLocale() można też zastosować interceptor LocaleChangeInterceptor do odwzorowań metod obsługi. Ten interceptor wykrywa, czy w bieżącym żądaniu HTTP znajduje się specjalny parametr. Nazwę tego parametru można ustawić za pomocą właściwości paramName interceptora. Jeśli dany parametr jest dostępny, interceptor na podstawie jego wartości zmienia ustawienia regionalne użytkownika. ...
286
8.5. PLIKI ZEWNĘTRZNE Z TEKSTEM DOSTOSOWANYM DO USTAWIEŃ REGIONALNYCH
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"> ...
...
Interceptor LocaleChangeInterceptor potrafi wykrywać parametry tylko dla tych odwzorowań metod obsługi, które to umożliwiają. Dlatego jeśli w kontekście aplikacji zdefiniowane jest więcej niż jedno odwzorowanie metod obsługi, trzeba zarejestrować taki interceptor, aby pozwolić użytkownikom na zmianę ustawień regionalnych w dowolnym adresie URL. Teraz w dowolnym adresie URL można zmienić ustawienia regionalne za pomocą parametru language. Na przykład dwa poniższe adresy URL powodują modyfikację ustawień regionalnych z angielskich na amerykańskie i polskie: http://localhost:8080/court/welcome?language=en_US http://localhost:8080/court/welcome?language=pl
Następnie w pliku welcome.jsp można wyświetlić ustawienia regionalne z obiektu odpowiedzi HTTP, aby sprawdzić konfigurację interceptora: <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> Witaj ... Ustawienia regionalne: ${pageContext.response.locale}
8.5. Pliki zewnętrzne z tekstem dostosowanym do ustawień regionalnych Problem Gdy tworzysz wielojęzyczną aplikację sieciową, musisz wyświetlać strony internetowe zgodnie z ustawieniami regionalnymi użytkownika. Niepożądane jest jednak tworzenie wielu wersji tych samych stron dla różnych ustawień regionalnych.
287
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Rozwiązanie Aby uniknąć tworzenia odmiennych wersji strony dla różnych ustawień regionalnych, powinieneś budować strony niezależne od tych ustawień. Jest to możliwe dzięki zewnętrznym plikom z tekstem dostosowanym do ustawień regionalnych. Spring potrafi określać odpowiednie komunikaty za pomocą źródeł komunikatów, czyli klas z implementacją interfejsu MessageSource. W plikach JSP należy stosować zdefiniowany w bibliotece Springa znacznik , aby określać tekst na podstawie podanego kodu.
Jak to działa? Aby zdefiniować źródło komunikatów, zarejestruj w kontekście aplikacji sieciowej ziarno typu MessageSource. Nazwę tego ziarna należy ustawić na messageSource, by serwlet DispatcherServlet mógł je automatycznie wykryć. Zauważ, że w jednym takim serwlecie można zarejestrować tylko jedno ziarno tego rodzaju. Klasa ResourceBundleMessageSource określa komunikaty z różnych pakietów zasobów przeznaczonych dla różnych ustawień regionalnych. Możesz zarejestrować ziarno tej klasy w pliku court-servlet.xml, aby wczytywać pakiety zasobów o podstawowej nazwie messages.
Następnie utwórz dwa pakiety zasobów, messages.properties i messages_de.properties, by zapisać w nich komunikaty dla domyślnych i niemieckich ustawień regionalnych. Te pakiety zasobów należy umieścić w katalogu głównym z parametru classpath. welcome.title=Witaj welcome.message=Witaj w systemie rezerwowania kortów welcome.title=Willkommen welcome.message=Willkommen zum Spielplatz-Reservierungssystem
Teraz w pliku JSP (na przykład welcome.jsp) można zastosować znacznik , aby określić komunikat podany za pomocą kodu. Taki znacznik jest automatycznie zastępowany komunikatem dostosowanym do ustawień regionalnych użytkownika. Zauważ, że znacznik ten jest zdefiniowany w bibliotece Springa, dlatego na początku pliku JSP trzeba ją zadeklarować. <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> ...
W znaczniku można określić tekst domyślny, wyświetlany w sytuacji, gdy nie można znaleźć komunikatu dla podanego kodu.
288
8.6. OKREŚLANIE WIDOKÓW NA PODSTAWIE NAZW
8.6. Określanie widoków na podstawie nazw Problem Po zakończeniu obsługiwania żądania metoda zwraca logiczną nazwę widoku. Wtedy serwlet DispatcherServlet przekazuje sterowanie do szablonu widoku, co prowadzi do wyświetlenia informacji. Należy zdefiniować strategię określania przez serwlet DispatcherServlet widoków na podstawie ich nazw logicznych.
Rozwiązanie W aplikacjach Spring MVC widoki są określane za pomocą jednego lub więcej ziaren zadeklarowanych w kontekście aplikacji sieciowej. W tych ziarnach trzeba zaimplementować interfejs ViewResolver, aby były automatycznie wykrywane przez serwlet DispatcherServlet. Spring MVC udostępnia kilka implementacji tego interfejsu, pozwalających określać widoki przy użyciu różnych strategii.
Jak to działa? Określanie widoków na podstawie nazwy i lokalizacji szablonu Podstawowa strategia określania widoków polega na bezpośrednim powiązaniu ich z nazwą i lokalizacją szablonu. Ziarno klasy InternalResourceViewResolver wiąże każdą nazwę widoku z katalogiem aplikacji na podstawie ustawionego przedrostka i przyrostka. Aby zarejestrować ziarno tego typu, zadeklaruj je w kontekście aplikacji sieciowej.
Ziarno typu InternalResourceViewResolver określa nazwy widoków welcome i reservationQuery w następujący sposób: welcome ' /WEB-INF/jsp/welcome.jsp reservationQuery ' /WEB-INF/jsp/reservationQuery.jsp
Typ widoków można ustawić za pomocą właściwości viewClass. Jeśli parametr classpath zawiera ścieżkę do biblioteki JSTL (plik jstl.jar), ziarna typu InternalResourceViewResolver domyślnie wiążą nazwy widoku z obiektami widoku typu JstlView. Klasa InternalResourceViewResolver jest prosta i potrafi określać tylko wewnętrzne widoki, które można przekazywać za pomocą klasy RequestDispatcher z interfejsu Servlet API (takim widokiem może być wewnętrzny plik JSP lub serwlet). Widoki pozostałych typów obsługiwanych przez platformę Spring MVC trzeba określać przy użyciu innych strategii.
Określanie widoków na podstawie pliku konfiguracyjnego w XML-u Inna strategia określania widoków polega na zadeklarowaniu ich jako ziaren Springa i określaniu na podstawie nazw tych ziaren. Ziarno widoku można zadeklarować w pliku konfiguracyjnym kontekstu aplikacji sieciowej, jednak lepiej jest umieścić je w odrębnym pliku. Klasa XmlViewResolver domyślnie wczytuje ziarna widoku z pliku /WEB-INF/views.xml, jednak za pomocą właściwości location można zmienić lokalizację używanego pliku. /WEB-INF/court-views.xml
289
ROZDZIAŁ 8. PLATFORMA SPRING MVC
W pliku konfiguracyjnym court-views.xml można zadeklarować każdy widok jako zwykłe ziarno Springa. W tym celu należy podać nazwę klasy i właściwości ziarna. Pozwala to zadeklarować widoki dowolnego typu (na przykład RedirectView, a nawet własne widoki).
Określanie widoków na podstawie pakietu zasobów Ziarna widoku można zadeklarować nie tylko w pliku konfiguracyjnym w XML-u, ale też w pakiecie zasobów. Klasa ResourceBundleViewResolver wczytuje ziarno widoku z pakietu zasobu dostępnego w katalogu głównym z parametru classpath. Zauważ, że za pomocą tej klasy można wczytywać ziarna widoku z innych pakietów zasobów, przeznaczonych dla różnych ustawień regionalnych.
Ponieważ w klasie ResourceBundleViewResolver ustawiono tu views jako podstawową nazwę widoku, domyślnym pakietem zasobów jest pakiet views.properties. Można w nim zadeklarować ziarna widoku, korzystając z właściwości. Deklaracja w tej postaci jest odpowiednikiem poniższej deklaracji ziarna w XML-u: welcome.(class)=org.springframework.web.servlet.view.JstlView welcome.url=/WEB-INF/jsp/welcome.jsp reservationQuery.(class)=org.springframework.web.servlet.view.JstlView reservationQuery.url=/WEB-INF/jsp/reservationQuery.jsp welcomeRedirect.(class)=org.springframework.web.servlet.view.RedirectView welcomeRedirect.url=welcome
Określanie widoków za pomocą kilku klas Jeśli w danej aplikacji sieciowej używanych jest wiele widoków, jedna strategia ich określania to często za mało. Klasa InternalResourceViewResolver zwykle pozwala określić większość wewnętrznych widoków JSP, jednak przeważnie istnieją też inne typy widoków, które trzeba określać przy użyciu klasy ResourceBundle ViewResolver. Wtedy można połączyć dwie strategie określania widoków. ...
290
8.7. WIDOKI I NEGOCJOWANIE TREŚCI
Przy jednoczesnym stosowaniu kilku strategii ważne jest, aby ustalić ich priorytet. W tym celu możesz ustawić właściwość order w ziarnach określających widoki. Mniejsza wartość oznacza wyższy priorytet. Zauważ, że najniższy priorytet powinno mieć ziarno InternalResourceViewResolver, ponieważ zawsze określa ono widok (niezależnie od tego, czy docelowy widok istnieje). Dlatego jeśli inne ziarna będą miały niższy priorytet, nie będą miały możliwości określenia widoku. Pakiet zasobów views.properties powinien zawierać tylko te widoki, których nie można określić za pomocą klasy InternalResourceViewResolver (czyli widoki otwierane w wyniku przekierowań). welcomeRedirect.(class)=org.springframework.web.servlet.view.RedirectView welcomeRedirect.url=welcome
Przedrostek w przekierowaniach Jeśli w kontekście aplikacji sieciowej skonfigurowane jest ziarno InternalResourceViewResolver, może ono określać widoki otwierane w wyniku przekierowań. W tym procesie w nazwie widoku używany jest przedrostek redirect. Dalsza część nazwy widoku jest traktowana jak przekierowany adres URL. Na przykład nazwa widoku redirect:welcome powoduje przekierowanie pod względny adres URL welcome. W nazwie widoku można też ustawić bezwzględny adres URL.
8.7. Widoki i negocjowanie treści Problem W przedstawionych kontrolerach używane są adresy URL bez rozszerzeń (na przykład welcome zamiast welcome.html lub welcome.pdf). Należy zaprojektować strategię tak, aby dla wszystkich żądań zwracana była poprawna treść odpowiedniego typu.
Rozwiązanie Gdy aplikacja sieciowa otrzymuje żądanie, zawiera ono zestaw właściwości, które umożliwiają platformie przetwarzania (tu jest nią Spring MVC) określenie odpowiednich treści i ich typu do zwrócenia nadawcy żądania. Dwie podstawowe właściwości to: rozszerzenie adresu URL podane w żądaniu, nagłówek Accept żądania HTTP. Na przykład jeśli żądanie dotyczy adresu URL /reservationSummary.xml, kontroler potrafi wykryć to rozszerzenie i skierować żądanie do logicznego widoku reprezentującego widok w formacie XML. Jednak czasem żądanie może dotyczyć adresu URL w postaci /reservationSummary. Czy należy je skierować do widoku XML-a czy do widoku HTML-a? Na podstawie adresu URL nie da się tego stwierdzić. Dlatego zamiast określać dla takich żądań widok domyślny, można zbadać nagłówek Accept żądania HTTP i ustalić, jaki widok będzie najlepszy.
291
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Sprawdzanie nagłówków Accept w kontrolerze bywa skomplikowane. Dlatego platforma Spring MVC obsługuje badanie nagłówków za pomocą klasy ContentNegotiatingViewResolver. Pozwala to delegować żądania do widoków na podstawie rozszerzenia pliku z adresu URL lub wartości nagłówka Accept.
Jak to działa? Pierwszą rzeczą, jaką trzeba uwzględnić w kontekście negocjacji treści w platformie Spring MVC, jest to, że mechanizm ten konfiguruje się jako ziarno określające widoki, podobnie jak ziarna przedstawione w poprzedniej recepturze. Ziarno do negocjowania treści w platformie Spring MVC jest klasy ContentNegotiatingViewResolver. Jednak zanim opiszemy działanie tego ziarna, warto pokazać, jak zintegrować je z innymi ziarnami określającymi. ... ... ... class="org.springframework.web.servlet.view. InternalResourceViewResolver"> ...
292
8.7. WIDOKI I NEGOCJOWANIE TREŚCI
Deklaracje ziaren określających z ostatniego listingu nieco różnią się od deklaracji z poprzedniej receptury. Wykorzystano tu język SpEL do określenia pierwszeństwa ziaren. Najwyższy priorytet ma tu ziarno ContentNegotiatingViewResolver. Atrybut order ma w nim określoną w języku SpEL wartość #{T(org. springframework.core.Ordered).HIGHEST_PRECEDENCE}. W dalszych ziarnach wartość atrybutu order jest ustawiona za pomocą podobnych deklaracji w języku SpEL: #{bean_name.order+1)}. W ten sposób można określić dla ziaren względne wartości tego atrybutu, zamiast zapisywać je w postaci liczb. Wróćmy teraz do ziarna określającego typu ContentNegotiatingViewResolver. W tej konfiguracji ziarno to ma najwyższy priorytet, co jest konieczne do poprawnego działania ziarna do negocjowania treści. Wynika to z tego, że to ziarno nie określa widoków samodzielnie, ale deleguje to zadanie do innych ziaren określających. Ponieważ ziarno określające, które nie określa widoków, to nieco zaskakujące rozwiązanie, warto wytłumaczyć cały proces na przykładzie. Załóżmy, że kontroler otrzymuje żądanie kierowane pod adres /reservationSummary.xml. Gdy metoda obsługi zakończy pracę, sterowanie jest przekazywane do widoku logicznego o nazwie reservation. Na tym etapie zaczynają działać ziarna określające platformy Spring MVC. Pierwszym z nich jest ziarno Content NegotiatingViewResolver, ponieważ ma najwyższy priorytet. Ziarno ContentNegotiatingViewResolver najpierw określa typ mediów dla żądania. Odbywa się to w następujący sposób: Ziarno porównuje rozszerzenie ze ścieżki z żądania (na przykład .html, .xml lub .pdf) z domyślnym typem mediów (na przykład text/html) ustawionym w sekcji mediaTypes ziarna ContentNegotiatingViewResolver. Jeśli w ścieżce z żądania znajduje się rozszerzenie, jednak w sekcji mediaTypes ziarna ContentNegotiating ViewResolver nie ma pasującego typu mediów, ziarno próbuje ustalić odpowiedni dla rozszerzenia typ na podstawie obiektu FileTypeMap z platformy Java Activation Framework. Jeżeli w ścieżce z żądania nie ma rozszerzenia, używany jest nagłówek Accept żądania HTTP. Dla żądań kierowanych pod adres /reservationSummary.xml typ mediów jest ustalany w kroku pierwszym i jest to typ application/xml. Jednak dla żądań kierowanych pod adres /reservationSummary typ mediów można ustalić dopiero w kroku trzecim. Nagłówek Accept żądania HTTP zawiera wartości w postaci Accept:text/html lub Accept:application/pdf. Pomagają one ziarnu określającemu ustalić typ mediów oczekiwany przez nadawcę żądania, jeśli w adresie URL nie ma podanego rozszerzenia. Na tym etapie ziarno określające ContentNegotiatingViewResolver ma typ mediów oraz widok logiczny o nazwie reservation. Na podstawie tych informacji sprawdzane są pozostałe ziarna określające (zgodnie z ustawionym porządkiem), aby ustalić, który widok najlepiej pasuje do logicznej nazwy i wykrytego typu mediów. Ten proces pozwala utworzyć zestaw widoków logicznych o tej samej nazwie, ale powiązanych z różnymi typami mediów (na przykład HTML, PDF lub XLS). Ziarno ContentNegotiatingViewResolver określa, który z tych typów najlepiej pasuje do żądania. W tym podejściu można jeszcze bardziej uprościć projekt kontrolera, ponieważ nie trzeba zapisywać na stałe widoków logicznych potrzebnych do tworzenia mediów danego typu (na przykład widoków pdfReservation, xlsReservation i htmlReservation). Zamiast tego wystarczy jeden widok, na przykład reservation, a ziarno ContentNegotiatingViewResolver określi najlepiej dopasowany typ mediów. Oto możliwe skutki tego procesu: Określony typ mediów to application/pdf. Jeśli ziarno o najwyższym priorytecie (czyli najniższej wartości atrybutu order) jest powiązane z widokiem logicznym reservation, ale nie obsługuje typu mediów application/pdf, nie następuje dopasowanie. Należy kontynuować proces wyszukiwania w pozostałych ziarnach określających. Określony typ mediów to application/pdf. Dopasowywane jest ziarno określające o najwyższym priorytecie (czyli najniższej wartości atrybutu order), które jest powiązane z widokiem logicznym reservation i obsługuje typ mediów application/pdf. Określony typ mediów to text/html. Istnieją cztery ziarna określające powiązane z widokiem logicznym reservation, ale widoki z dwóch ziaren o najwyższych priorytetach nie obsługują typu mediów text/html. Dopasowane zostaje następne ziarno — powiązane z widokiem obsługującym ten typ mediów.
293
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Opisany proces wyszukiwania widoków odbywa się automatycznie i obejmuje wszystkie ziarna określające skonfigurowane w aplikacji. Można też skonfigurować w ziarnie ContentNegotiatingViewResolver domyślne widoki i ziarna określające, aby uniknąć korzystania z konfiguracji spoza tego ziarna. W recepturze 8.13 opisany jest kontroler, który używa ziarna określającego ContentNegotiatingViewResolver do wybierania widoków aplikacji.
8.8. Wiązanie wyjątków z widokami Problem Gdy występuje nieznany wyjątek, serwer aplikacji zwykle wyświetla użytkownikowi irytujący ślad stosu wywołań. Użytkownicy nie mogą nic zrobić ze śladem stosu, dlatego skarżą się, że aplikacja nie jest przyjazna. Ponadto wyświetlanie śladu stosu prowadzi do zagrożeń z obszaru bezpieczeństwa, ponieważ powoduje ujawnienie wewnętrznej hierarchii wywołań metod. Choć plik web.xml aplikacji sieciowej można skonfigurować tak, aby wyświetlał przyjazne użytkownikom strony JSP w reakcji na błąd HTTP lub wyjątek, Spring MVC obsługuje bardziej niezawodny sposób zarządzania widokami powiązanymi z wyjątkami.
Rozwiązanie W aplikacji Spring MVC można w kontekście aplikacji sieciowej zarejestrować jedno lub kilka ziaren określających wyjątki, aby przetwarzały nieprzechwycone wyjątki. W tych ziarnach należy zaimplementować interfejs HandlerExceptionResolver, co pozwoli serwletowi DispatcherServlet automatycznie je wykrywać. Spring MVC udostępnia proste ziarno określające wyjątki, które umożliwia powiązanie poszczególnych kategorii wyjątków z widokami.
Jak to działa? Załóżmy, że usługa rezerwowania kortów zgłasza poniższy wyjątek, gdy rezerwacja jest niemożliwa: package com.apress.springrecipes.court.service; ... public class ReservationNotAvailableException extends RuntimeException { private String courtName; private Date date; private int hour; // Konstruktory i gettery ... }
Aby określić nieprzechwycony wyjątek, można napisać niestandardowe ziarno określające wyjątki. W tym ziarnie należy zaimplementować interfejs HandlerExceptionResolver. Zwykle poszczególne kategorie wyjątków warto powiązać z różnymi stronami błędu. Spring MVC udostępnia ziarno określające wyjątki, SimpleMappingExceptionResolver, na potrzeby konfigurowania powiązań wyjątków w kontekście aplikacji sieciowej. Możesz na przykład zarejestrować w pliku court-servlet.xml poniższe ziarno określające wyjątki: reservationNotAvailable
W tym ziarnie określającym wyjątki zdefiniowany jest widok logiczny reservationNotAvailable powiązany z wyjątkami ReservationNotAvailableException. Do widoku można w elementach przypisać dowolną liczbę klas wyjątków, w tym ogólną klasę wyjątków java.lang.Exception. Dzięki temu użytkownik zobaczy widok dostosowany do typu wyjątku. Ostatni element, , definiuje widok domyślny o nazwie error. Widok ten jest używany, gdy wystąpi wyjątek, którego klasy nie podano w elemencie exceptionMappings. Jeśli w kontekście aplikacji sieciowej skonfigurowane jest ziarno InternalResourceViewResolver, to gdy rezerwacja nie jest możliwa, pojawia się przedstawiona poniżej strona reservationNotAvailable.jsp: <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> Rezerwacja niemożliwa Rezerwacja kortu ${exception.courtName} dnia o godzinie ${exception.hour}:00 nie jest możliwa.
Na stronie błędu dostęp do wyjątku zapewnia zmienna ${exception}. Pozwala to wyświetlić użytkownikowi szczegółowe informacje na temat danego wyjątku. Dobrym zwyczajem jest definiowanie domyślnej strony błędu dla nieznanych wyjątków. Można zdefiniować w elemencie widok domyślny lub powiązać stronę z kluczem java.lang.Exception w ostatnim wpisie odwzorowania, aby dana strona pojawiała się, gdy nie dopasowano żadnych wcześniejszych wpisów. Następnie należy utworzyć pokazany poniżej plik JSP error.jsp z widokiem domyślnym. Błąd Wystąpił błąd. W celu uzyskania szczegółowych informacji prosimy o kontakt z administratorem.
295
ROZDZIAŁ 8. PLATFORMA SPRING MVC
8.9. Przypisywanie wartości w kontrolerze za pomocą adnotacji @Value Problem Załóżmy, że w trakcie tworzenia kontrolera nie chcesz na stałe zapisywać wartości pola. Zamiast tego chcesz przypisywać wartość dostępną w ziarnie lub pliku właściwości (czyli w pliku message.properties).
Rozwiązanie Adnotacja @Value pozwala ustawić wartość pola kontrolera za pomocą języka SpEL. Przy użyciu tej adnotacji i języka SpEL można sprawdzić ziarna z kontekstu aplikacji i pobrać z nich wartości potrzebne do zainicjowania pól kontrolera.
Jak to działa? Przyjmijmy, że używany jest prosty kontroler, służący tylko do wyświetlania przedstawionej poniżej strony JSP z informacjami o programie. O programie System rezerwowania kortów
Na stronach z informacjami o programie często umieszcza się adres e-mail umożliwiający skontaktowanie się z administratorem. Jednak ponieważ ten adres jest wyświetlany zwykle na wielu stronach, warto zapisać go w jednym miejscu, na przykład w pliku message.properties aplikacji. Dzięki temu gdy adres się zmieni, wystarczy zmodyfikować go w jednym pliku, a poprawki zostaną uwzględnione we wszystkich miejscach. Dodaj więc do pliku message.properties aplikacji poniższą właściwość: [email protected]
Następnie należy zmodyfikować plik about.jsp, tak aby wyświetlał atrybut email przekazany przez kontroler za pomocą atrybutu model: O programie System rezerwowania kortów ...
296
8.10. OBSŁUGIWANIE FORMULARZY ZA POMOCĄ KONTROLERÓW
Adres e-mail: ${email}
Po utworzeniu pliku about.jsp w katalogu /WEB-INF/jsp/ aplikacji należy utworzyć odpowiedni kontroler, by przekazywał atrybut email do nowego widoku. Pokazany poniżej kontroler AboutController za pomocą adnotacji @Value ustawia wartość pola email na podstawie danych z pliku message.properties aplikacji. package com.apress.springrecipes.court.web; ... import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.beans.factory.annotation.Value; @Controller( public class AboutController @Value("#{ messageSource.getMessage('admin.email',null,'en')}") private String email; @RequestMapping("/about") public String courtReservation(Model model) { model.addAttribute("email", email); return "about"; } }
Wartość przypisywana w adnotacji @Value to instrukcja w języku SpEL. Platforma rozpoznaje takie instrukcje, ponieważ mają one specjalną postać "#{ instrukcja w języku SpEL }". Tu obiekt messageSource to zadeklarowane w kontekście aplikacji ziarno typu org.springframework. context.support.ResourceBundleMessageSource, dające dostęp do pliku message.properties. Więcej informacji o ziarnach tego typu znajdziesz w recepturze 8.5. Do referencji do ziarna jest dołączony człon getMessage('admin.email',null,'en'). Oznacza on metodę klasy ziarna. Jest ona wywoływana z podanymi parametrami i zwraca wartość właściwości admin.email. Adnotacja @Value powoduje, że ta wartość jest automatycznie przypisywana do pola email. Dalej znajduje się jedyna metoda obsługi kontrolera. Parametrem wejściowym tej metody jest obiekt typu Model. W tej metodzie pole email jest przypisywane do atrybutu email modelu, dzięki czemu wartość jest później dostępna w powiązanym widoku. Wartość zwracana about określa widok logiczny, któremu tu odpowiada plik about.jsp. Ponieważ przy metodzie obsługi znajduje się adnotacja @RequestMapping("/about"), dostęp do kontrolera można uzyskać za pomocą poniższego adresu URL: http://localhost:8080/court/about
8.10. Obsługiwanie formularzy za pomocą kontrolerów Problem W aplikacji sieciowej często trzeba obsługiwać formularze. Kontroler musi wyświetlać formularz użytkownikowi oraz obsługiwać przesyłanie formularza. Obsługa formularzy bywa skomplikowanym zadaniem, wymagającym zastosowania dobrze dobranych technik.
297
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Rozwiązanie Gdy użytkownik wchodzi w interakcję z formularzem, kontroler musi zapewnić obsługę dwóch operacji. Najpierw użytkownik za pomocą żądania HTTP GET wymaga od kontrolera wyświetlenia formularza, co prowadzi do pokazania widoku z tym formularzem. Gdy formularz jest przesyłany, zgłaszane jest żądanie HTTP POST. Wtedy trzeba między innymi sprawdzić poprawność wartości pól i wykonać operacje biznesowe na danych wprowadzonych w formularzu. Jeśli obsługa formularza zakończy się powodzeniem, wyświetlany jest widok z informacjami o sukcesie. W przeciwnym razie ponownie pojawia się widok formularza z informacjami o błędach.
Jak to działa? Załóżmy, że chcesz umożliwić użytkownikom zarezerwowanie kortu za pomocą formularza. Aby lepiej poznać dane obsługiwane przez kontroler, najpierw zapoznaj się z przetwarzanym przez niego widokiem (czyli z formularzem).
Tworzenie widoków formularza Utwórz widok formularza reservationForm.jsp. Formularz należy utworzyć za pomocą biblioteki znaczników form Springa, ponieważ upraszcza to wiązanie danych, wyświetlanie komunikatów o błędach, a także ponowne wyświetlanie wcześniej wprowadzonych wartości po wykryciu błędów. <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> Formularz dokonywania rezerwacji
W elemencie Springa zadeklarowane są dwa atrybuty. Atrybut method="post" informuje, że formularz przy przesyłaniu powoduje zgłoszenie żądania HTTP POST. Atrybut modelAttribute="reservation" określa, że dane z formularza są wiązane z modelem reservation. Pierwszy atrybut powinien być Ci znany, ponieważ występuje w większości formularzy HTML-a. Drugi atrybut stanie się bardziej zrozumiały po opisie kontrolera obsługującego ten formularz. Pamiętaj, że znacznik przed przesłaniem do użytkownika jest przekształcany na standardowy kod w HTML-u, tak więc atrybut modelAttribute="reservation" nie jest przydatny dla przeglądarki, ale służy do wygenerowania docelowego formularza w HTML-u. Dalej znajduje się znacznik . Pozwala on zdefiniować miejsca, w których pojawią się informacje o błędach, jeśli formularz nie będzie zgodny z regułami ustawionymi w kontrolerze. Atrybut path="*" informuje, że wyświetlane mają być wszystkie błędy (tak działa symbol wieloznaczny *). Atrybut cssClass="error" pozwala ustawić klasę CSS używaną do formatowania wyświetlanych informacji o błędach. Potem dodano różne znaczniki formularza i odpowiadające im znaczniki . W znacznikach z tej grupy atrybut path określa pola formularza. Tu są to pola: courtName, date i hour. Znaczniki są powiązane z właściwościami modelu podanego w atrybucie modelAttribute. Właściwości są określane za pomocą atrybutu path. Te znaczniki wyświetlają pierwotną wartość pola, którą może być albo wartość powiązanej właściwości, albo wartość odrzucona z powodu wystąpienia błędu w procesie wiązania. Znaczniki trzeba stosować w znaczniku , definiującym formularz wiązany z modelem o nazwie podanej w atrybucie modelAttribute. Dalej znajduje się standardowy znacznik HTML-a , który generuje przycisk wysyłania i powoduje przesłanie danych na serwer. Na końcu umieszczony jest znacznik . Zamyka on formularz. Jeśli formularz i dane zostały poprawnie przetworzone, należy utworzyć widok z informacjami o sukcesie, aby poinformować użytkownika o zarezerwowaniu kortu. Służy do tego pokazany poniżej plik reservationSuccess.jsp. Powodzenie rezerwacji Kort został zarezerwowany.
Błędy mogą też wynikać z wpisania w formularzu nieprawidłowych wartości. Na przykład jeśli dane mają niewłaściwy format lub w godzinie pojawi się litera, kontroler powinien odrzucić wartość. Należy wtedy wygenerować listę kodów błędów odpowiadających błędom zwracanym do widoku formularza. Te wartości są umieszczane w znaczniku . Na przykład w odpowiedzi na błędne dane w polu date kontroler generuje następujące kody błędów: typeMismatch.command.date typeMismatch.date typeMismatch.java.util.Date typeMismatch
Jeśli zdefiniowane jest ziarno ResourceBundleMessageSource, można umieścić w pakiecie zasobów dla odpowiednich ustawień regionalnych (w pliku messages.properties domyślnych ustawień) poniższe komunikaty o błędach: typeMismatch.date=Błędny format daty typeMismatch.hour=Błędny format godziny
299
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Jeśli w trakcie przetwarzania danych z formularza wystąpi błąd, do użytkownika zwracane są kody błędów i odpowiadające im wartości. Skoro już znasz strukturę widoków związanych z formularzem, a także obsługiwane dane, pora przejść do kodu przetwarzającego przesłane informacje (o rezerwacji).
Tworzenie usługi przetwarzającej formularz To nie kontroler, ale usługa, której używa, przetwarza podane w formularzu dane o rezerwacji. Najpierw zdefiniuj metodę make() w interfejsie ReservationService. package com.apress.springrecipes.court.service; ... public interface ReservationService { ... public void make(Reservation reservation) throws ReservationNotAvailableException; }
Następnie zaimplementuj tę metodę. Powinna ona dodawać element Reservation do listy rezerwacji. Jeśli rezerwacje się powtarzają, metoda ma zgłaszać wyjątek ReservationNotAvailableException. package com.apress.springrecipes.court.service; ... public class ReservationServiceImpl implements ReservationService { ... public void make(Reservation reservation) throws ReservationNotAvailableException { for (Reservation made : reservations) { if (made.getCourtName().equals(reservation.getCourtName()) && made.getDate().equals(reservation.getDate()) && made.getHour() == reservation.getHour()) { throw new ReservationNotAvailableException( reservation.getCourtName(), reservation.getDate(), reservation.getHour()); } } reservations.add(reservation); } }
Po przeanalizowaniu dwóch elementów wchodzących w interakcje z kontrolerem — widoków formularza i klasy z usługą rezerwacji — pora utworzyć kontroler obsługujący formularz do rezerwowania kortów.
Tworzenie kontrolera formularza Kontroler używany do obsługi formularzy wymaga niemal tych samych adnotacji, które wykorzystano we wcześniejszych recepturach. Przejdźmy więc od razu do kodu. package com.apress.springrecipes.court.web; ... @Controller @RequestMapping("/reservationForm") @SessionAttributes("reservation") public class ReservationFormController extends SimpleFormController { private ReservationService reservationService; @Autowired public ReservationFormController() {
300
8.10. OBSŁUGIWANIE FORMULARZY ZA POMOCĄ KONTROLERÓW
this.reservationService = reservationService; } @RequestMapping(method = RequestMethod.GET) public String setupForm(Model model) { Reservation reservation = new Reservation(); model.addAttribute("reservation", reservation); return "reservationForm"; } @RequestMapping(method = RequestMethod.POST) public String submitForm( @ModelAttribute("reservation") Reservation reservation, BindingResult result, SessionStatus status) { reservationService.make(reservation); return "redirect:reservationSuccess"; } }
Ten kontroler rozpoczyna się od standardowej adnotacji @Controller, po której następuje adnotacja @RequestMapping zapewniająca dostęp do kontrolera za pomocą poniższego adresu URL: http://localhost:8080/court/reservationForm
Gdy wprowadzisz ten adres URL w przeglądarce, prześle ona do aplikacji sieciowej żądanie HTTP GET. To z kolei spowoduje wykonanie metody setupForm, która — zgodnie z adnotacją @RequestMapping — ma obsługiwać żądania tego typu. Parametrem wejściowym metody setupForm jest obiekt typu Model. Powoduje on przesłanie danych z modelu do formularza z widoku. W metodzie obsługi tworzony jest pusty obiekt typu Reservation, który zostaje dodany jako atrybut do obiektu Model kontrolera. Następnie kontroler zwraca sterowanie do widoku reservationForm, któremu tu odpowiada plik reservationForm.jsp z formularzem. Najważniejszym aspektem tej ostatniej metody jest dodanie pustego obiektu typu Reservation. Gdy przyjrzysz się formularzowi reservationForm.jsp, zauważysz, że w znaczniku zadeklarowany jest atrybut modelAttribute="reservation". To oznacza, że przy wyświetlaniu widoku formularz oczekuje, iż dostępny będzie obiekt reservation. Aby udostępnić ten obiekt, należy umieścić go w obiekcie typu Model z metody obsługi. Gdy dokładniej przyjrzysz się kodowi, zauważysz, że wartości atrybutu path w znacznikach odpowiadają nazwom pól obiektu typu Reservation. Ponieważ formularz jest wczytywany po raz pierwszy, oczywiste jest, że obiekt ten powinien być pusty. Innym aspektem, który należy omówić przed przejściem do drugiej metody obsługi z kontrolera, jest adnotacja @SessionAttributes("reservation") na początku deklaracji klasy kontrolera. Ponieważ formularz może zawierać błędy, niepożądane jest usuwanie danych wprowadzonych przez użytkownika przy każdym przesyłaniu formularza. Aby rozwiązać ten problem, adnotacja @SessionAttributes jest używana do zapisania w sesji użytkownika pola reservation. Dzięki temu przyszłe wywołania pola reservation będą dotyczyły tych samych danych. Nie ma przy tym znaczenia, czy formularz został przesłany dwa, czy więcej razy. Z tego samego powodu w całym kontrolerze tworzony i przypisywany do pola reservation jest tylko jeden obiekt typu Reservation. Po utworzeniu pustego obiektu tego typu (w metodzie obsługi żądań HTTP GET) wszystkie operacje są wykonywane właśnie na nim, ponieważ jest on przypisany do sesji użytkownika. Przejdźmy teraz do sytuacji, gdy formularz jest przesyłany po raz pierwszy. Po wypełnieniu pól użytkownik przesyła formularz, co prowadzi do zgłoszenia żądania HTTP POST. To z kolei powoduje wywołanie metody submitForm (zgodnie z wartością adnotacji @RequestMapping tej metody). Metoda submitForm ma trzy parametry wejściowe. Parametr @ModelAttribute("reservation") Reservation reservation pozwala używać obiektu reservation. Obiekt typu BindingResult to obiekt z danymi przesłanymi przez użytkownika. Obiekt typu SessionStatus jest używany wtedy, gdy potrzebny jest dostęp do sesji użytkownika. Na tym etapie metoda obsługi nie sprawdza poprawności danych ani nie korzysta z sesji użytkownika (do wykonywania tych zadań służą obiekty BindingResult i SessionStatus). Oba te mechanizmy zostaną omówione i dodane w dalszej części receptury. 301
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Jedyną operacją wykonywaną w opisywanej metodzie jest reservationService.make(reservation);. Powoduje ona wywołanie usługi rezerwacji dla bieżącego stanu obiektu reservation. Zwykle najpierw należy sprawdzić poprawność obiektów kontrolera, a dopiero potem wykonywać na nich operacje tego typu. Zauważ ponadto, że metoda obsługi zwraca widok redirect:reservationSuccess. Nazwą widoku jest reservationSuccess i odpowiada ona utworzonej wcześniej stronie reservationSuccess.jsp. Przedrostek redirect: w podanej nazwie pozwala uniknąć problemu ponownego przesłania formularza. Przy odświeżaniu strony z widokiem wyświetlanym po udanym przesłaniu formularza użyty wcześniej formularz jest ponownie przesyłany. Aby uniknąć tego problemu, można zastosować wzorzec projektowy przesyłanie-przekierowanie-pobieranie. Zgodnie z nim po udanym przesłaniu formularza nie należy bezpośrednio zwracać strony HTML, ale zastosować przekierowanie pod inny adres URL. Ten efekt można uzyskać dzięki poprzedzeniu nazwy widoku przedrostkiem redirect:.
Inicjowanie obiektu z atrybutami modelu i wstępne zapełnianie formularza wartościami Formularz jest zaprojektowany w taki sposób, aby umożliwić użytkownikom rezerwowanie kortów. Gdy jednak przyjrzysz się klasie domenowej Reservation, zauważysz, że w formularzu brakuje dwóch pól potrzebnych do utworzenia kompletnego obiektu rezerwacji. Jednym z nich jest pole player typu Player. Zgodnie z definicją klasy Player obiekt tego typu ma pola name i phone. Jak dodać pole player do widoku formularza i kontrolera? Przyjrzyj się najpierw widokowi formularza. Formularz do rezerwowania kortów
Dodano tu dwa nowe znaczniki reprezentujące pola obiektu typu Player. Ta deklaracja formularza jest prosta, ale trzeba jeszcze zmodyfikować kontroler. Pamiętaj, że jeśli używasz znaczników , widok oczekuje dostępu do przekazanych przez kontroler obiektów modelu pasujących do wartości path z tych znaczników. Choć metoda obsługi żądań HTTP GET z kontrolera zwraca do pokazanego widoku pusty obiekt typu Reservation, właściwość player ma w nim wartość null, co w trakcie wyświetlania formularza powoduje wyjątek. Aby rozwiązać ten problem, trzeba zainicjować pusty obiekt typu Player i przypisać go do zwracanego do widoku obiektu typu Reservation.
302
8.10. OBSŁUGIWANIE FORMULARZY ZA POMOCĄ KONTROLERÓW
@RequestMapping(method = RequestMethod.GET) public String setupForm( @RequestParam(required = false, value = "username") String username,Model model) { Reservation reservation = new Reservation(); reservation.setPlayer(new Player(username, null)); model.addAttribute("reservation", reservation); return "reservationForm"; }
W tym przypadku po utworzeniu pustego obiektu typu Reservation metoda setPlayer jest używana do przypisania do niego pustego obiektu typu Player. Zauważ też, że do utworzenia obiektu typu Player potrzebna jest wartość username. Jest ona pobierana z wartości wejściowej @RequestParam metody obsługi. Dzięki temu do tworzonego obiektu typu Player można przypisać konkretną wartość username przekazaną jako parametr żądania. W efekcie wartość ta zostaje przypisana do pola username formularza. Jeśli na przykład zgłoszone zostanie żądanie w następującej postaci: http://localhost:8080/court/reservationForm?username=Jerzy
metoda obsługi może pobrać parametr username i utworzyć obiekt typu Player, a następnie wstępnie zapełnić pole username formularza wartością Jerzy. Warto zauważyć, że w adnotacji @RequestParam przy parametrze username używana jest właściwość required=false, co sprawia, że żądanie zostanie przetworzone także wtedy, gdy nie będzie zawierać tego parametru.
Udostępnianie wstępnych danych w formularzu Gdy kontroler ma wyświetlić widok formularza, może przekazywać do niego pewne wstępne dane, na przykład elementy wyświetlane na liście opcji w HTML-u. Załóżmy, że chcesz umożliwić użytkownikowi wybór dyscypliny przy rezerwowaniu kortu. Jest ona zapisywana w ostatnim nieomówionym jeszcze polu klasy Reservation. Formularz do rezerwowania kortów
Znacznik służy do generowania listy rozwijanej z wartościami przekazanymi do widoku przez kontroler. Tak więc formularz przedstawia pole sportType jako zestaw elementów HTML-a, a nie jako stosowane wcześniej pola , w których użytkownik musi wpisać tekst.
303
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Przyjrzyj się też, w jaki sposób kontroler ustawia pole sportType jako atrybut modelu. Odbywa się to nieco inaczej niż we wcześniej opisanych polach. Zacznijmy od zdefiniowania w interfejsie ReservationService metody getAllSportTypes() służącej do pobierania wszystkich dostępnych dyscyplin: package com.apress.springrecipes.court.service; ... public interface ReservationService { ... public List getAllSportTypes(); }
Następnie można zaimplementować tę metodę w taki sposób, aby zwracała listę zapisanych na stałe wartości: package com.apress.springrecipes.court.service; ... public class ReservationServiceImpl implements ReservationService { ... public static final SportType TENNIS = new SportType(1, "Tenis"); public static final SportType SOCCER = new SportType(2, "Piłka nożna"); public List getAllSportTypes() { return Arrays.asList(new SportType[] { TENNIS, SOCCER }); } }
Gdy dostępny jest już kod zwracający zapisaną na stałe listę obiektów typu SportType, warto przyjrzeć się temu, jak kontroler przetwarza tę listę w celu zwrócenia jej do widoku formularza. package com.apress.springrecipes.court.service; ... @ModelAttribute("sportTypes") public List populateSportTypes() { return reservationService.getAllSportTypes(); } @RequestMapping(method = RequestMethod.GET) public String setupForm( @RequestParam(required = false, value = "username") String username, Model model) { Reservation reservation = new Reservation(); reservation.setPlayer(new Player(username, null)); model.addAttribute("reservation", reservation); return "reservationForm"; }
Zauważ, że metoda obsługi setupForm (zwracająca pusty obiekt Reservation do widoku formularza) nie wymaga zmian. Nowym fragmentem odpowiedzialnym za przekazywanie listy SportType jako atrybutu modelu do widoku formularza jest metoda opatrzona adnotacją @ModelAttribute("sportTypes"). Adnotacja @ModelAttribute służy do definiowania globalnych atrybutów modelu, dostępnych dla wszystkich docelowych widoków zwracanych przez metody obsługi. Działa to podobnie jak deklarowanie w metodach obsługi obiektu typu Model jako parametru wejściowego i przypisywanie do niego atrybutów dostępnych w docelowym widoku. Ponieważ metoda opatrzona adnotacją @ModelAttribute("sportTypes") zwraca wartość typu List i wywołuje metodę reservationService.getAllSportTypes(), zapisane na stałe obiekty TENNIS i SOCCER typu SportType są przypisywane do atrybutu sportTypes modelu. Ten atrybut jest używany w widoku formularza do zapełnienia listy rozwijanej ze znacznika .
304
8.10. OBSŁUGIWANIE FORMULARZY ZA POMOCĄ KONTROLERÓW
Wiązanie właściwości niestandardowych typów Gdy formularz jest przesyłany, kontroler wiąże wartości pól z właściwościami obiektu modelu (tu jest to obiekt typu Reservation) o analogicznych nazwach. Kontroler nie potrafi jednak przekształcić właściwości niestandardowego typu, chyba że programista wskaże odpowiednie edytory właściwości. Na przykład pole do wyboru dyscypliny przekazuje tylko identyfikator danego sportu. W ten sposób działają pola HTML-a. Dlatego trzeba za pomocą edytora właściwości przekształcić ten identyfikator na obiekt typu SportType. Przede wszystkim należy dodać do usługi ReservationService metodę getSportType(), aby na podstawie identyfikatora pobierała obiekt typu SportType: package com.apress.springrecipes.court.service; ... public interface ReservationService { ... public SportType getSportType(int sportTypeId); }
Na potrzeby testów można zaimplementować tę metodę za pomocą instrukcji switch-case. package com.apress.springrecipes.court.service; ... public class ReservationServiceImpl implements ReservationService { ... public SportType getSportType(int sportTypeId) { switch (sportTypeId) { case 1: return TENNIS; case 2: return SOCCER; default: return null; } } }
Następnie utwórz klasę SportTypeEditor, która ma przekształcać identyfikator dyscypliny na obiekt typu SportType. Ten edytor właściwości żąda od usługi ReservationService znalezienia dyscypliny. package com.apress.springrecipes.court.domain; ... import java.beans.PropertyEditorSupport; public class SportTypeEditor extends PropertyEditorSupport { private ReservationService reservationService; public SportTypeEditor(ReservationService reservationService) { this.reservationService = reservationService; } public void setAsText(String text) throws IllegalArgumentException { int sportTypeId = Integer.parseInt(text); SportType sportType = reservationService.getSportType(sportTypeId); setValue(sportType); } }
Po utworzeniu pomocniczej klasy SportTypeEditor potrzebnej do wiązania właściwości formularza z obiektem niestandardowej klasy SportType należy ustawić tę klasę w kontrolerze. Do tego w platformie Spring MVC służą niestandardowe klasy z implementacją interfejsu WebBindingInitializer.
305
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Utworzenie niestandardowej klasy z implementacją tego interfejsu pozwala ustawić w kontrolerze pomocnicze klasy używane do wiązania właściwości formularza z obiektami niestandardowych typów. Dotyczy to na przykład klasy SportTypeEditor i innych niestandardowych typów, takich jak Date. Choć wcześniej nie było mowy o polu z datą, jest z nim ten sam problem co z polem do wyboru dyscypliny. Użytkownik wpisuje w polu z datą tekst. Aby kontroler mógł przypisać dane tekstowe do pola date obiektu Reservation, trzeba powiązać pola z datami z obiektami typu Date. Ponieważ klasa Date należy do języka Java, nie musisz w tym celu tworzyć specjalnej klasy (podobnej do klasy SportTypeEditor); wymagana niestandardowa klasa jest dostępna w Springu. W kontrolerze trzeba więc powiązać klasy SportTypeEditor i Date. Na poniższym listingu przedstawiona jest potrzebna do tego klasa ReservationBindingInitializer z implementacją interfejsu WebBindingInitializer. package com.apress.springrecipes.court.web; ... import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.propertyeditors.CustomDateEditor; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.context.request.WebRequest; public class ReservationBindingInitializer implements WebBindingInitializer { private ReservationService reservationService; @Autowired public ReservationBindingInitializer(ReservationService reservationService) { this.reservationService = reservationService; } public void initBinder(WebDataBinder binder, WebRequest request) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class, new CustomDateEditor( dateFormat, true)); binder.registerCustomEditor(SportType.class, new SportTypeEditor( reservationService)); } }
Jedyne pole w tej klasie to reservationService i służy ono do uzyskania dostępu do ziarna ReservationService aplikacji. Zwróć uwagę na adnotację @Autowired, która powoduje wstrzyknięcie potrzebnego ziarna za pomocą konstruktora klasy. Metoda initBinder służy do wiązania klas Date i SportTypeEditor. Wcześniej jednak należy przygotować obiekt typu SimpleDateFormat, aby określić oczekiwany format pola z datą. Ponadto trzeba przy użyciu wywołania setLenient(false) ustawić ścisłe dopasowywanie do wzorca. Dalej znajdują się dwa wywołania metody registerCustomEditor. Jest to metoda obiektu typu WebDataBinder przekazanego jako parametr wejściowy metody initBinder. Pierwsze ze wspomnianych wywołań wiąże klasę Date z klasą CustomDateEditor. Klasa CustomDateEditor jest udostępniana przez platformę Spring i zapewnia te same funkcje co utworzona wcześniej klasa SportTypeEditor, przy czym działa dla obiektów typu Date. Parametrem wejściowym tej klasy są obiekty typu SimpleDateFormat, określające oczekiwany format daty i wartość logiczną informującą o tym, czy obiekt daty może być pusty (tu ta wartość logiczna to true). Drugie wywołanie wiąże klasę SportType z klasą SportTypeEditor. Ponieważ utworzyłeś klasę SportType Editor, powinieneś wiedzieć, że jej jedynym parametrem wejściowym jest ziarno typu ReservationService. Po utworzeniu klasy ReservationBindingInitializer trzeba ją zarejestrować w aplikacji. W tym celu zadeklaruj tę klasę jako właściwość ziarna typu AnnotationMethodHandlerAdapter.
306
8.10. OBSŁUGIWANIE FORMULARZY ZA POMOCĄ KONTROLERÓW
Dzięki tej ostatniej deklaracji każdy kontroler oparty na adnotacjach (czyli wszystkie klasy opatrzone adnotacją @Controller) będzie miał w metodach obsługi dostęp do tych samych edytorów właściwości.
Sprawdzanie poprawności danych z formularza Po przesłaniu formularza standardowo sprawdza się poprawność danych wprowadzonych przez użytkownika. Dopiero potem proces przesyłania danych można uznać za pomyślnie zakończony. Spring MVC obsługuje sprawdzanie poprawności za pomocą walidatorów, czyli klas z implementacją interfejsu Validator. Możesz napisać poniższy walidator, aby sprawdzał, czy wymagane pola formularza są uzupełnione i czy wpisana godzina rezerwacji jest poprawna w danym dniu roboczym lub wolnym od pracy. package com.apress.springrecipes.court.domain; ... import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; import org.springframework.stereotype.Component; @Component public class ReservationValidator implements Validator { public boolean supports(Class clazz) { return Reservation.class.isAssignableFrom(clazz); } public void validate(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "courtName", "required.courtName", "Nazwa kortu jest wymagana."); ValidationUtils.rejectIfEmpty(errors, "date", "required.date", "Data jest wymagana."); ValidationUtils.rejectIfEmpty(errors, "hour", "required.hour", "Godzina jest wymagana."); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "player.name", "required.playerName", "Nazwisko gracza jest wymagane."); ValidationUtils.rejectIfEmpty(errors, "sportType", "required.sportType", "Dyscyplina jest wymagana."); Reservation reservation = (Reservation) target; Date date = reservation.getDate(); int hour = reservation.getHour(); if (date != null) { Calendar calendar = Calendar.getInstance(); calendar.setTime(date); if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) { if (hour < 8 || hour > 22) { errors.reject("invalid.holidayHour", "Błędna godzina w dniu wolnym."); } } else { if (hour < 9 || hour > 21) { errors.reject("invalid.weekdayHour", "Błędna godzina w dniu roboczym."); }
307
ROZDZIAŁ 8. PLATFORMA SPRING MVC
} } } }
W tym walidatorze metody narzędziowe, na przykład rejectIfEmptyOrWhitespace() i rejectIfEmpty() z klasy ValidationUtils, są używane do sprawdzania poprawności danych z wymaganych pól formularza. Jeśli któreś z nich jest puste, metody generują błąd pola i wiążą go z danym polem. Drugim argumentem wspomnianych metod jest nazwa właściwości, natomiast trzeci i czwarty argument to kod błędu i domyślny komunikat o błędzie. Kod sprawdza też, czy podana godzina rezerwacji jest poprawna w dniu wolnym od pracy lub roboczym. Gdy dane są błędne, należy wywołać metodę reject(), aby utworzyć błąd obiektu wiązany z obiektem rezerwacji, a nie z konkretnym polem. Ponieważ klasa walidatora jest opatrzona adnotacją @Component, Spring próbuje utworzyć egzemplarz tej klasy jako ziarno. Nazwa ziarna (tu jest to reservationValidator) powstaje na podstawie nazwy klasy. Aby cały proces przebiegał poprawnie, trzeba aktywować skanowanie adnotacji w pakiecie z deklaracjami. Dodaj więc do pliku servlet-config.xml poniższy fragment:
Zamiast dodawać adnotację @Component, można ręcznie zarejestrować ziarno walidatora. W tym celu zastosuj poniższą składnię w pliku servlet-config.xml:
Ponieważ walidatory w trakcie sprawdzania poprawności mogą generować błędy, należy zdefiniować powiązane z kodami błędów komunikaty wyświetlane użytkownikom. Jeśli zdefiniowałeś obiekt klasy ResourceBundleMessageSource, możesz dodać poniższe komunikaty o błędach do pakietów zasobów powiązanych z odpowiednimi ustawieniami regionalnymi (dla domyślnych ustawień zapisz je w pliku messages.properties): required.courtName=Nazwa kortu jest wymagana. required.date=Data jest wymagana. required.hour=Godzina jest wymagana. required.playerName=Nazwisko gracza jest wymagane. required.sportType=Dyscyplina jest wymagana. invalid.holidayHour=Błędna godzina w dniu wolnym. invalid.weekdayHour=Błędna godzina w dniu roboczym.
Aby zastosować ten walidator, należy wprowadzić w kontrolerze następujące zmiany: package com.apress.springrecipes.court.service; ... private ReservationService reservationService; private ReservationValidator reservationValidator; @Autowired public ReservationFormController(ReservationService reservationService, ReservationValidator reservationValidator) { this.reservationService = reservationService; this.reservationValidator = reservationValidator; } @RequestMapping(method = RequestMethod.POST) public String submitForm( @ModelAttribute("reservation") Reservation reservation,
308
8.10. OBSŁUGIWANIE FORMULARZY ZA POMOCĄ KONTROLERÓW
BindingResult result, SessionStatus status) { reservationValidator.validate(reservation, result); if (result.hasErrors()) { model.addAttribute("reservation", reservation); return "reservationForm"; } else { reservationService.make(reservation); return "redirect:reservationSuccess"; } }
Pierwszym dodatkiem w tym kontrolerze jest pole ReservationValidator, zapewniające kontrolerowi dostęp do egzemplarza ziarna walidatora. Dzięki zastosowaniu adnotacji @Autowired ziarno typu ReservationValidator jest wstrzykiwane razem z istniejącym ziarnem typu ReservationService. Następna zmiana dotyczy metody obsługi żądań HTTP POST. Jest ona wywoływana za każdym razem, gdy użytkownik przesyła formularz. Teraz pierwszą operacją w tej metodzie jest wywołanie metody validate ziarna typu ReservationValidator. Parametrami tej ostatniej metody są obiekt typu Reservation oraz obiekt typu BindingResult, zawierający dane przesłane w formularzu przez użytkownika. Po zwróceniu sterowania przez metodę validate parametr result (jest to obiekt typu BindingResult) zawiera wyniki procesu sprawdzania poprawności. Dlatego dalej znajduje się instrukcja warunkowa oparta na wartości result.hasErrors(). Jeśli klasa walidatora wykryła błędy, ta wartość to true. Gdy w trakcie sprawdzania poprawności zostaje wykryty błąd, do obiektu typu Model z metody obsługi jest dodawany zmodyfikowany obiekt typu Reservation (w postaci zwróconej przez walidator), który można wyświetlić w docelowym widoku. Obiekt typu Reservation obejmuje komunikaty o błędach informujące użytkowników o problemach. Na końcu metoda obsługi zwraca widok reservationForm, który odpowiada wyjściowemu formularzowi. Użytkownik może więc ponownie przesłać informacje. Jeśli nie wystąpiły żadne błędy, następuje wywołanie dodające rezerwację (reservationService.make(reservation);), po czym użytkownik jest przekierowywany do widoku reservationSuccess wyświetlanego po udanym przesłaniu danych.
Wygasanie danych z sesji przechowywanych w kontrolerze Aby móc wielokrotnie przesyłać formularz i nie tracić przy tym wprowadzonych przez użytkownika danych, do kontrolera dodano adnotację @SessionAttributes. Dzięki temu między żądaniami zachowywana jest referencja do pola reservation zawierającego obiekt typu Reservation. Jednak po udanym przesłaniu formularza i dokonaniu rezerwacji nie ma sensu dalsze przechowywanie w sesji użytkownika obiektu typu Reservation. Jeśli go nie usuniesz, może się zdarzyć, że gdy użytkownik po krótkim czasie ponownie otworzy formularz, pojawią się dane ze starego obiektu tego typu. Wartości przypisywane za pomocą adnotacji @SessionAttributes usuwa się przy użyciu obiektu typu SessionStatus. Ten obiekt można przekazać jako parametr wejściowy do metod obsługi. Poniższy listing ilustruje, jak spowodować wygaśnięcie przechowywanych w kontrolerze danych z sesji. package com.apress.springrecipes.court.web; ... @Controller @RequestMapping("/reservationForm") @SessionAttributes("reservation") public class ReservationFormController { ... @RequestMapping(method = RequestMethod.POST) public String submitForm( @ModelAttribute("reservation") Reservation reservation, BindingResult result, SessionStatus status) { reservationValidator.validate(reservation, result); if (result.hasErrors()) { model.addAttribute("reservation", reservation);
309
ROZDZIAŁ 8. PLATFORMA SPRING MVC
return "reservationForm"; } else { reservationService.make(reservation); status.setComplete(); return "redirect:reservationSuccess"; } } }
Idealnym momentem na spowodowanie wygaśnięcia danych sesji jest chwila, gdy metoda obsługi doda już rezerwację (w wyniku wywołania metody reservationService.make(reservation);), ale tuż przed przekierowaniem użytkownika do strony z informacjami o powodzeniu. Aby uzyskać pożądany efekt, wystarczy wywołać metodę setComplete() obiektu typu SessionStatus. To naprawdę aż tak proste.
8.11. Obsługa wielu formularzy za pomocą kontrolerów formularzy kreatora Problem W aplikacjach sieciowych czasem potrzebne są skomplikowane, wielostronicowe formularze. Nazywa się je niekiedy formularzami kreatora, ponieważ użytkownicy muszą wypełniać strony jedna po drugiej, tak jak w kreatorach. Na potrzeby obsługi takich formularzy można utworzyć jeden lub więcej kontrolerów.
Rozwiązanie Ponieważ w formularzu kreatora jest wiele stron, trzeba zdefiniować zestaw widoków na potrzeby kontrolera takiego formularza. Ten kontroler powinien zarządzać stanem formularza w trakcie przechodzenia przez kolejne strony. Dla formularza kreatora — tak jak w zwykłym formularzu — można zastosować w kontrolerze jedną metodę obsługi związaną z przesyłaniem danych. Jednak aby móc odróżniać operacje wykonywane przez użytkownika, w każdym formularzu trzeba umieścić specjalny parametr żądania. Zwykle podaje się go w nazwie przycisku przesyłania. Parametr _finish powoduje zakończenie działania formularza kreatora. Parametr _cancel powoduje anulowanie wykonywania formularza kreatora. Parametr _targetx powoduje przejście do docelowej strony, gdzie x to indeks strony (liczony od zera). Na podstawie tych parametrów metoda obsługi w kontrolerze może określić, jakie operacje ma wykonać w zależności od stanu formularza i działań użytkownika.
Jak to działa? Załóżmy, że chcesz umożliwić użytkownikom cykliczne rezerwowanie kortu na daną godzinę. Zacznij od zdefiniowania w podpakiecie domain klasy PeriodicReservation. package com.apress.springrecipes.court.domain; ... public class PeriodicReservation { private private private private
310
String courtName; Date fromDate; Date toDate; int period;
8.11. OBSŁUGA WIELU FORMULARZY ZA POMOCĄ KONTROLERÓW FORMULARZY KREATORA
private int hour; private Player player; // Gettery i settery ... }
Następnie dodaj do interfejsu ReservationService metodę makePeriodic() umożliwiającą cykliczne rezerwowanie kortów. package com.apress.springrecipes.court.service; ... public interface ReservationService { ... public void makePeriodic(PeriodicReservation periodicReservation) throws ReservationNotAvailableException; }
W implementacji tej metody należy na podstawie obiektów typu PeriodicReservation wygenerować zestaw obiektów typu Reservation i przekazać każdą rezerwację do metody make(). W tej prostej aplikacji obsługa transakcji oczywiście nie jest potrzebna. package com.apress.springrecipes.court.service; ... public class ReservationServiceImpl implements ReservationService { ... public void makePeriodic(PeriodicReservation periodicReservation) throws ReservationNotAvailableException { Calendar fromCalendar = Calendar.getInstance(); fromCalendar.setTime(periodicReservation.getFromDate()); Calendar toCalendar = Calendar.getInstance(); toCalendar.setTime(periodicReservation.getToDate()); while (fromCalendar.before(toCalendar)) { Reservation reservation = new Reservation(); reservation.setCourtName(periodicReservation.getCourtName()); reservation.setDate(fromCalendar.getTime()); reservation.setHour(periodicReservation.getHour()); reservation.setPlayer(periodicReservation.getPlayer()); make(reservation); fromCalendar.add(Calendar.DATE, periodicReservation.getPeriod()); } } }
Tworzenie stron formularza kreatora Przyjmijmy, że chcesz wyświetlać użytkownikom podzielony na trzy strony formularz do cyklicznego rezerwowania kortów. Na każdej stronie znajdą się wybrane pola formularza. Pierwsza strona, reservationCourtForm.jsp, obejmuje tylko pole z nazwą rezerwowanego kortu. <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> Formularz do rezerwowania kortów
Formularz i pola na dane wejściowe z tej strony są zdefiniowane za pomocą znaczników i Springa. Te pola są wiązane z atrybutem modelu, reservation, i jego właściwościami. Używany jest też znacznik errors pozwalający wyświetlać użytkownikom komunikaty o błędnych danych w polach. Zauważ, że na stronie znajdują się dwa przyciski wysyłania. Nazwą przycisku Dalej musi być _target1. Ten przycisk informuje kontroler formularza kreatora, że należy przejść do drugiej strony (strony o indeksie 1 przy numerowaniu od zera). Przycisk Anuluj musi mieć nazwę _cancel. Informuje on kontroler, że należy anulować formularz. Używane jest też ukryte pole formularza pozwalające śledzić stronę, na której znajduje się użytkownik. Tu wartością tego pola jest 0. Druga strona to reservationTimeForm.jsp. Zawiera ona pola na datę i godzinę cyklicznych rezerwacji. <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> Podaj godzinę rezerwacji
Na tym formularzu znajdują się trzy przyciski wysyłania. Nazwami przycisków Wróć i Dalej muszą być _target0 i _target2. Informują one kontroler formularza kreatora o tym, że należy przejść do pierwszej i trzeciej
strony. Przycisk Anuluj powoduje anulowanie formularza. Ponadto na stronie znajduje się ukryte pole formularza służące do śledzenia strony, na której użytkownik się znajduje. Tu jest to strona 1. Trzecia strona to reservationPlayerForm.jsp. Zawiera ona pola z informacjami o graczu dokonującym cyklicznej rezerwacji kortów. <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> Podaj informacje o graczu
Ten formularz zawiera trzy przyciski. Przycisk Wróć nakazuje kontrolerowi formularza kreatora powrót do drugiej strony. Przycisk Zakończ musi mieć nazwę _finish. Ten przycisk żąda od kontrolera wysłania formularza. Przycisk Anuluj prowadzi do anulowania formularza. Ponadto na stronie znajduje się ukryte pole formularza służące do śledzenia strony, na której użytkownik się znajduje. Tu jest to strona 2.
Tworzenie kontrolera formularza kreatora Teraz utwórz kontroler formularza kreatora obsługujący formularz do dodawania rezerwacji cyklicznych. Ten kontroler, podobnie jak opisane wcześniej kontrolery MVC, zawiera dwie podstawowe metody obsługi — dla żądań HTTP GET i POST — a także wykorzystuje te same elementy (adnotacje, walidatory i sesje). W kontrolerze formularza kreatora wszystkie pola z różnych stron są wiązane z jednym obiektem typu Reservation (z atrybutu modelu) dostępnym dla różnych żądań z danej sesji użytkownika. package com.apress.springrecipes.court.web; ... @Controller @RequestMapping("/periodicReservationForm") @SessionAttributes("reservation") public class PeriodicReservationController { private ReservationService reservationService; @Autowired public PeriodicReservationController(ReservationService reservationService) { this.reservationService = reservationService; } @RequestMapping(method = RequestMethod.GET) public String setupForm(Model model) { PeriodicReservation reservation = new PeriodicReservation(); reservation.setPlayer(new Player()); model.addAttribute("reservation", reservation); return "reservationCourtForm" } @RequestMapping(method = RequestMethod.POST) public String submitForm( HttpServletRequest request, HttpServletResponse response, @ModelAttribute("reservation") PeriodicReservation reservation, BindingResult result, SessionStatus status, @RequestParam("_page") int currentPage, Model model) { Map pageForms = new HashMap(); pageForms.put(0,"reservationCourtForm");
314
8.11. OBSŁUGA WIELU FORMULARZY ZA POMOCĄ KONTROLERÓW FORMULARZY KREATORA
pageForms.put(1,"reservationTimeForm"); pageForms.put(2,"reservationPlayerForm"); if (request.getParameter("_cancel") != null) { // Powrót do bieżącej strony, ponieważ użytkownik kliknął przycisk Anuluj return (String)pageForms.get(currentPage); } else if (request.getParameter("_finish") != null) { // Użytkownik zakończył wypełnianie formularza — należy dodać rezerwację reservationService.makePeriodic(reservation); return "redirect:reservationSuccess"; } else { // Użytkownik kliknął przycisk Dalej lub Wróć (nazwa z członem _target) // Określanie docelowej strony int targetPage = WebUtils.getTargetPage(request, "_target", currentPage); // Jeśli targetPage ma wartość mniejszą niż na bieżącej stronie, // użytkownik kliknął przycisk Wróć if (targetPage < currentPage) { return (String)pageForms.get(targetPage); } // Użytkownik kliknął przycisk Dalej — należy zwrócić docelową stronę return (String)pageForms.get(targetPage); } } @ModelAttribute("periods") public Map periods() { Map periods = new HashMap(); periods.put(1, "Codziennie"); periods.put(7, "Co tydzień"); return periods; } }
Używane są tu niektóre mechanizmy zastosowane już w opisanym wcześniej kontrolerze Reservation FormController, dlatego nie warto ich szczegółowo omawiać. Oto krótkie przypomnienie: adnotacja @SessionAttributes powoduje umieszczenie obiektu reservation w sesji użytkownika, adnotacja @Autowired prowadzi do wstrzyknięcia ziarna do kontrolera, a metoda obsługi żądań HTTP GET przypisuje puste obiekty typu Reservation i Player w momencie wczytywania pierwszego widoku formularza. Jednak metoda obsługi żądań HTTP POST jest teraz bardziej skomplikowana, ponieważ musi przetwarzać trzy różne formularze. Zacznijmy od omówienia parametrów wejściowych tej metody. Te parametry wejściowe to między innymi standardowe obiekty typu HttpServletRequest i HttpServlet Response. Umożliwiają one metodzie obsługi dostęp do zawartości tych obiektów. W przedstawionych wcześniej metodach obsługi do podawania danych znajdujących się standardowo w tych obiektach używane były parametry w postaci @RequestParam, co jest krótszym rozwiązaniem. Także w tym kontrolerze możliwe jest pominięcie obiektów typu HttpServletRequest i HttpServletResponse oraz zastosowanie zamiast nich parametrów @RequestParam. Jednak tu pokazano, że w metodzie obsługi możliwy jest pełny dostęp do tych obiektów. Nazwy i składnia pozostałych parametrów wejściowych powinny być Ci znane, ponieważ występowały też we wcześniejszych kontrolerach. W metodzie obsługi zdefiniowany jest obiekt typu HashMap, który wiąże numery stron z nazwami widoków. Obiekt ten jest używany w różnych miejscach metody obsługi, ponieważ kontroler musi określać docelowe widoki w rozmaitych scenariuszach (na przykład w ramach sprawdzania poprawności i po kliknięciu przycisków Anuluj lub Dalej). Dalej znajduje się pierwsza instrukcja warunkowa, w której metoda sprawdza, czy obiekt HttpServlet Request zawiera parametr _cancel. Tę operację można też wykonać za pomocą parametru wejściowego w postaci @RequestParam("_cancel") String cancelButton. Jeśli żądanie obejmuje parametr _cancel, oznacza to, że użytkownik kliknął przycisk Anuluj na formularzu. Wtedy metoda obsługi zwraca sterowanie
315
ROZDZIAŁ 8. PLATFORMA SPRING MVC
do widoku odpowiadającego wartości zmiennej currentPage. Ta ostatnia zmienna jest przekazywana do tej metody jako parametr wejściowy. Następna instrukcja warunkowa sprawdza, czy obiekt HttpServletRequest zawiera parametr _finish. Jeśli tak jest, oznacza to, że użytkownik kliknął przycisk Zakończ. Wtedy metoda obsługi dodaje rezerwację (za pomocą wywołania reservationService.makePeriodic(reservation);) i kieruje użytkownika do widoku reservationSuccess. Gdy metoda obsługi wchodzi do następnego bloku instrukcji warunkowej, oznacza to, że użytkownik kliknął na jednym z formularzy przycisk Dalej lub Wróć. W tej sytuacji w obiekcie typu HttpServletRequest znajduje się parametr o nazwie _target. Jest tak, ponieważ każdy z przycisków Dalej i Wróć z formularza ma ten parametr. Za pomocą klasy WebUtils (jest to klasa narzędziowa z platformy Spring) pobierana jest wartość parametru _target. Możliwe wartości to: target0, target1 i target2, a użyta wartość jest skracana do postaci 0, 1 lub 2 reprezentującej docelową stronę. Po ustaleniu numeru docelowej strony i numeru bieżącej strony można określić, czy użytkownik kliknął przycisk Dalej, czy Wróć. Jeśli numer docelowej strony jest mniejszy niż strony bieżącej, oznacza to, że użytkownik kliknął przycisk Wróć. Jeżeli numer strony docelowej jest większy niż strony bieżącej, użytkownik musiał kliknąć przycisk Dalej. Na tym etapie nie jest oczywiste, po co metoda sprawdza, czy użytkownik kliknął przycisk Dalej, czy Wróć, ponieważ zawsze zwracany jest widok odpowiadający docelowej stronie. Powód takiego rozwiązania jest następujący: jeśli użytkownik kliknął przycisk Dalej, warto sprawdzić poprawność danych, natomiast po kliknięciu przycisku Wróć nie jest to konieczne. Stanie się to zrozumiałe w następnym kroku, po dodaniu do kontrolera mechanizmu sprawdzania poprawności danych. Na końcu znajduje się ostatnia metoda, opatrzona adnotacją @ModelAttribute("periods"). Jak pokazano we wcześniej omówionych kontrolerach, taka deklaracja umożliwia udostępnienie listy wartości w dowolnym docelowym widoku powiązanym z kontrolerem. Jeśli przyjrzysz się przedstawionemu wcześniej formularzowi ze strony reservationTimeForm.jsp, zauważysz, że wymaga on dostępu do określonego atrybutu modelu (o nazwie periods). Ponieważ klasa PeriodicReservationController jest opatrzona adnotacją @RequestMapping("/periodic ReservationForm"), dostęp do kontrolera można uzyskać za pomocą poniższego adresu URL: http://localhost:8080/court/periodicReservation
Sprawdzanie poprawności danych w formularzu kreatora W prostym kontrolerze formularza można sprawdzić cały obiekt modelu w jednym kroku, w momencie przesłania formularza. Jednak ponieważ kontroler formularza kreatora obsługuje kilka stron, trzeba sprawdzać każdą z nich po jej przesłaniu. Dlatego należy utworzyć poniższy walidator. Metoda validate() jest w nim podzielona na kilka precyzyjnych metod sprawdzających poprawność danych, z których każda bada pola z konkretnej strony. package com.apress.springrecipes.court.domain; import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; public class PeriodicReservationValidator implements Validator { public boolean supports(Class clazz) { return PeriodicReservation.class.isAssignableFrom(clazz); } public void validate(Object target, Errors errors) { validateCourt(target, errors); validateTime(target, errors); validatePlayer(target, errors); }
316
8.11. OBSŁUGA WIELU FORMULARZY ZA POMOCĄ KONTROLERÓW FORMULARZY KREATORA
public void validateCourt(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "courtName", "required.courtName", "Nazwa kortu jest wymagana."); } public void validateTime(Object target, Errors errors) { ValidationUtils.rejectIfEmpty(errors, "fromDate", "required.fromDate", "Data początkowa jest wymagana."); ValidationUtils.rejectIfEmpty(errors, "toDate", "required.toDate", "Data końcowa jest wymagana."); ValidationUtils.rejectIfEmpty(errors, "period", "required.period", "Okres jest wymagany."); ValidationUtils.rejectIfEmpty(errors, "hour", "required.hour", "Godzina jest wymagana."); } public void validatePlayer(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "player.name", "required.playerName", "Nazwisko gracza jest wymagane."); } }
Podobnie jak w poprzednim walidatorze, warto zwrócić uwagę na adnotację @Component, która powoduje automatyczne zarejestrowanie klasy walidatora jako ziarna. Po zarejestrowaniu tego ziarna wystarczy dodać walidator do kontrolera. Poniżej pokazane są zmiany, które trzeba wprowadzić w kontrolerze. package com.apress.springrecipes.court.domain; private ReservationService reservationService; private PeriodicReservationValidator validator; @Autowired public PeriodicReservationController(ReservationService reservationService, PeriodicReservationValidator validator) { this.reservationService = reservationService; this.validator = validator; } ... @RequestMapping(method = RequestMethod.POST) public String submitForm( HttpServletRequest request, HttpServletResponse response, @ModelAttribute("reservation") PeriodicReservation reservation, BindingResult result, SessionStatus status, @RequestParam("_page") int currentPage, Model model) { Map pageForms = new HashMap(); pageForms.put(0,"reservationCourtForm"); pageForms.put(1,"reservationTimeForm"); pageForms.put(2,"reservationPlayerForm"); if (request.getParameter("_cancel") != null) { // Powrót do bieżącego widoku, ponieważ użytkownik kliknął przycisk Anuluj return (String)pageForms.get(currentPage); } else if (request.getParameter("_finish") != null) { new PeriodicReservationValidator().validate(reservation, result); if (!result.hasErrors()) { reservationService.makePeriodic(reservation);
317
ROZDZIAŁ 8. PLATFORMA SPRING MVC
status.setComplete(); return "redirect:reservationSuccess"; } else { // Błędy return (String)pageForms.get(currentPage); } } else { int targetPage = WebUtils.getTargetPage(request, "_target", currentPage); // Jeśli targetPage ma wartość mniejszą niż dla bieżącej strony, // użytkownik kliknął przycisk Wróć if (targetPage < currentPage) { return (String)pageForms.get(targetPage); } // Użytkownik kliknął przycisk Dalej // Należy sprawdzić poprawność danych z określonej strony switch (currentPage) { case 0: new PeriodicReservationValidator(). validateCourt(reservation, result); break; case 1: new PeriodicReservationValidator(). validateTime(reservation, result); break; case 2: new PeriodicReservationValidator(). validatePlayer(reservation, result); break; } if (!result.hasErrors()) { // Brak błędów, należy zwrócić docelową stronę return (String)pageForms.get(targetPage); } else { // Wykryto błędy, należy zwrócić bieżącą stronę return (String)pageForms.get(currentPage); } } ... }
Pierwszym dodatkiem do kontrolera jest pole validator, do którego za pomocą konstruktora klasy przypisywany jest egzemplarz ziarna walidatora PeriodicReservationValidator. Dalej w kontrolerze znajdują się dwie referencje do dodanego walidatora. Pierwsza jest potrzebna wtedy, gdy użytkownik kończy przesyłanie formularza. Wówczas do walidatora są przekazywane obiekty typu Reservation i BindingResult. Jeśli walidator nie wykryje błędów, aplikacja dodaje rezerwację, resetuje sesję użytkownika i przekierowuje użytkownika do widoku reservationSuccess. Po wykryciu problemów użytkownik jest ponownie kierowany do formularza z bieżącego widoku w celu poprawienia błędów. Walidator jest stosowany w kontrolerze po raz drugi, gdy użytkownik kliknie na formularzu przycisk Dalej. Ponieważ powoduje to przejście do następnej strony, trzeba sprawdzić poprawność już wprowadzonych danych. Sprawdzenia wymagają trzy widoki, dlatego używana jest instrukcja case, która ustala wywoływaną metodę walidatora. Jeśli wykryto błędy, to po zwróceniu sterowania przez taką metodę użytkownik jest kierowany do widoku currentPage, gdzie może poprawić pomyłki. Jeżeli dane są poprawne, użytkownik trafia do widoku targetPage. Zauważ, że numery są powiązane ze stronami docelowymi w kolekcji HashMap w kontrolerze.
318
8.12. SPRAWDZANIE POPRAWNOŚCI ZIAREN ZA POMOCĄ ADNOTACJI (NA PODSTAWIE STANDARDU JSR-303)
8.12. Sprawdzanie poprawności ziaren za pomocą adnotacji (na podstawie standardu JSR-303) Problem Programista chce sprawdzać poprawność ziaren Javy w aplikacji sieciowej, używając adnotacji ze standardu JSR-303.
Rozwiązanie JSR-303 to specyfikacja dotycząca sprawdzania poprawności ziaren. Ma pozwolić na ustandaryzowanie sprawdzania poprawności ziaren Javy za pomocą adnotacji. We wcześniejszych przykładach zobaczyłeś, że Spring obsługuje doraźnie tworzone sposoby sprawdzania poprawności ziaren. Te techniki wymagają rozszerzenia jednej z klas Springa w celu utworzenia klasy walidatora dla konkretnego typu ziaren Javy. Standard JSR-303 ma umożliwiać stosowanie adnotacji bezpośrednio w klasach ziaren Javy. Dzięki temu reguły sprawdzania poprawności można ustawiać bezpośrednio w sprawdzanym kodzie zamiast w odrębnych klasach (na przykład w opisanych wcześniej klasach Springa).
Jak to działa? Przede wszystkim trzeba opatrzyć ziarno Javy potrzebnymi adnotacjami ze standardu JSR-303. Poniższy listing przedstawia klasę domenową Player z aplikacji do rezerwowania kortów. Do tej klasy dodano trzy adnotacje ze standardu JSR-303. package com.apress.springrecipes.court.domain; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import javax.validation.constraints.Pattern; public class Member { @NotNull @Size(min=2) private String name; @NotNull @Size(min = 9, max = 14) private String phone; @Pattern(regexp=".+@.+\\.[a-z]+") private String email; // Gettery i settery w celu zachowania zwięzłości zostały pominięte }
Do pola name są przypisane dwie adnotacje. Adnotacja @NotNull informuje, że pole nie może mieć wartości null, a adnotacja @Size pozwala określić wielkość pola — tu musi ono zawierać przynajmniej dwa znaki. Dla pola email zastosowano podobne ograniczenia, przy czym adnotacja @Size ma wartość (min = 9, max = 14). To oznacza, że pole musi zawierać przynajmniej 9, ale nie więcej niż 14 znaków. Pole email jest też opatrzone adnotacją @Pattern. Tu wartością tej adnotacji jest regexp=".+@.+\\.[a-z]+". To sprawia, że wartość przypisywana do pola email musi pasować do podanego wzorca, którym jest wyrażenie regularne reprezentujące adresy e-mail. Wiesz już, jak dodać do klasy ziarna Javy adnotacje ze standardu JSR-303. Teraz przyjrzyj się temu, jak wymusić w kontrolerze zgodność danych z adnotacjami walidatora. 319
ROZDZIAŁ 8. PLATFORMA SPRING MVC
package com.apress.springrecipes.court.web; import import import import
javax.validation.Validator; javax.validation.Validation; javax.validation.ValidatorFactory; javax.validation.ConstraintViolation;
@Controller @RequestMapping("/member/*") @SessionAttributes("guests") public class MemberController { private MemberService memberService; private static Validator validator; @Autowired public MemberController(MemberService memberService) { this.memberService = memberService; ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); validator = validatorFactory.getValidator(); } ... @RequestMapping(method = RequestMethod.POST) public String submitForm(@ModelAttribute("member") Member member, BindingResult result, Model model) { Set> violations = validator.validate(member); for (ConstraintViolation violation : violations) { String propertyPath = violation.getPropertyPath().toString(); String message = violation.getMessage(); // Dodawanie błędów związanych z adnotacjami JSR-303 do obiektu BindingResult // Dzięki temu Spring może wyświetlić informacje w widoku za pomocą obiektu // FieldError result.addError(new FieldError("member",propertyPath, "Błąd: "+ propertyPath + "(" + message + ")")); } if(!result.hasErrors()) { ... } else { ... } } }
Pierwszym dodatkiem do tego kontrolera jest pole validator typu javax.validation.Validator. Jednak — inaczej niż w stosowanej wcześniej specyficznej dla Springa technice sprawdzania poprawności — pole validator nie jest przypisane do żadnego ziarna. Zamiast tego używana jest klasa fabryczna javax.validation. ValidatorFactory. W ten sposób działa sprawdzanie poprawności oparte na standardzie JSR-303. Proces przypisywania obiektu tej klasy fabrycznej ma miejsce w konstruktorze kontrolera. Dalej w kontrolerze znajduje się metoda obsługi żądań HTTP POST. Obsługuje ona przesyłanie danych przez użytkownika. Ponieważ metoda obsługi oczekuje obiektu typu Person, który opatrzono adnotacjami ze standardu JSR-303, możliwe jest sprawdzenie poprawności danych. Pierwszy krok polega na utworzeniu kolekcji Set zawierającej elementy typu javax.validation. ConstraintViolation. Posłużą one do przechowywania informacji o błędach wykrytych w trakcie sprawdzania poprawności danych z obiektu typu Person. Wartość przypisana do kolekcji Set to efekt wywołania metody
320
8.13. TWORZENIE WIDOKÓW W FORMATACH XLS I PDF
validator.validate(member), która służy do uruchamiania procesu sprawdzania poprawności pola member (zawiera ono obiekt typu Person). Po zakończeniu sprawdzania poprawności uruchamiana jest pętla, która przechodzi po kolekcji Set i pobiera informacje o wszystkich błędach wykrytych w obiekcie typu Person. Ponieważ w tej kolekcji znajdują się
błędy specyficzne dla standardu JSR-303, trzeba pobrać z nich komunikaty o błędach i zapisać je w formacie odpowiednim dla platformy Spring MVC. Dzięki temu informacje o błędach można wyświetlić w zarządzanym przez Spring widoku w taki sam sposób, jakby zostały wygenerowane przez walidator ze Springa. Uwaga Aby zastosować w aplikacji sieciowej sprawdzanie poprawności ziaren zgodnie ze standardem JSR-303, trzeba dodać do parametru classpath zależność w postaci odpowiedniej implementacji. Jeśli używasz Mavena, dodaj do projektu Mavena poniższe zależności: javax.validation validation-api 1.0.0.GA org.hibernate hibernate-validator 4.0.0.GA
8.13. Tworzenie widoków w formatach XLS i PDF Problem Choć HTML to najczęściej stosowany format wyświetlania treści w sieci, czasem użytkownicy wolą, aby informacje z aplikacji sieciowej były eksportowane do XLS (formatu Excela) lub PDF. W Javie dostępnych jest kilka bibliotek, które pomagają w generowaniu plików w tych formatach. Jednak by móc korzystać z tych bibliotek bezpośrednio w aplikacji sieciowej, trzeba wygenerować pliki na zapleczu, a następnie zwrócić je jako załączniki binarne. W tym celu należy uwzględnić nagłówki odpowiedzi HTTP i strumienie wyjścia.
Rozwiązanie W Springu generowanie plików XLS i PDF jest zintegrowane z platformą MVC. Pliki tego rodzaju możesz traktować jak specjalne widoki, co pozwala w spójny sposób obsługiwać żądania sieciowe w kontrolerze i dodawać do modelu dane przekazywane do widoków w tych formatach. Dzięki temu nie musisz manipulować nagłówkami odpowiedzi HTTP i strumieniami wyjścia. Platforma Spring MVC obsługuje generowanie plików XLS za pomocą bibliotek Apache POI (http://poi.apache.org/) i JExcelAPI (http://jexcelapi.sourceforge.net/). Odpowiadające im klasy widoków to AbstractExcelView i AbstractJExcelView. Do generowania plików PDF służy biblioteka iText (http://www.lowagie.com/iText/) i odpowiadająca jej klasa widoku AbstractPdfView.
Jak to działa? Załóżmy, że użytkownicy chcą wygenerować raport z podsumowaniem informacji o rezerwacjach z danego dnia. Dostępna ma być możliwość uzyskania raportu w pliku Excela, PDF-ie lub podstawowym, HTML-owym formacie. Na potrzeby tego zadania należy zadeklarować w warstwie usług metodę, która zwraca wszystkie rezerwacje z danego dnia. 321
ROZDZIAŁ 8. PLATFORMA SPRING MVC
package com.apress.springrecipes.court.service; ... public interface ReservationService { ... public List findByDate(Date date); }
Następnie utwórz prostą implementację tej metody. W kodzie trzeba przejść po wszystkich dokonanych rezerwacjach. package com.apress.springrecipes.court.service; ... public class ReservationServiceImpl implements ReservationService { ... public List findByDate(Date date) { List result = new ArrayList(); for (Reservation reservation : reservations) { if (reservation.getDate().equals(date)) { result.add(reservation); } } return result; } }
Teraz możesz napisać prosty kontroler, który będzie pobierał parametr date z adresów URL. Parametr date jest obiektem z datą i należy go przekazać do warstwy usług w celu pobrania rezerwacji. Kontroler wymaga opisanego w recepturze 8.7 mechanizmu negocjowania treści. Dlatego zwraca jeden widok logiczny i pozwala takiemu mechanizmowi określić, czy raport ma mieć postać pliku Excela, dokumentu PDF czy domyślnej strony HTML. package com.apress.springrecipes.court.web; ... @Controller @RequestMapping("/reservationSummary*") public class ReservationSummaryController { private ReservationService reservationService; @Autowired public ReservationSummaryController(ReservationService reservationService) { this.reservationService = reservationService; } @RequestMapping(method = RequestMethod.GET) public String generateSummary( @RequestParam(required = true, value = "date") String selectedDate, Model model) { List reservations = java.util.Collections.emptyList(); try { Date summaryDate = new SimpleDateFormat("yyyy-MM-dd").parse(selectedDate); reservations = reservationService.findByDate(summaryDate); } catch (java.text.ParseException ex) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); ex.printStackTrace(pw); throw new ReservationWebException("Błędny format daty w podsumowaniu danych o rezerwacji",new Date(),sw.toString()); }
322
8.13. TWORZENIE WIDOKÓW W FORMATACH XLS I PDF
model.addAttribute("reservations",reservations); return "reservationSummary"; } }
Ten kontroler obejmuje tylko domyślną metodę obsługi żądań HTTP GET. Pierwsza operacja wykonywana w tej metodzie to utworzenie pustej listy obiektów typu Reservation, w których zapisywane są wyniki zwrócone przez usługę przetwarzającą rezerwacje. Dalej znajduje się blok try-catch, w którym kod próbuje utworzyć obiekt typu Date na podstawie parametru @RequestParam selectedDate, a także wywołuje na bazie tej daty usługę przetwarzającą rezerwacje. Jeśli tworzenie obiektu typu Date kończy się niepowodzeniem, zgłaszany jest niestandardowy wyjątek ReservationWebException Springa. Jeśli w bloku try-catch nie wystąpiły błędy, lista obiektów typu Reservation jest zapisywana w obiekcie typu Model kontrolera. Następnie metoda zwraca sterowanie do widoku reservationSummary. Zauważ, że kontroler zwraca jeden widok, choć obsługuje widoki w formatach PDF, XLS i HTML. Jest to możliwe dzięki obiektowi typu ContentNegotiatingViewResolver określającemu na podstawie jednej nazwy widoku, który z wielu widoków jest potrzebny. Więcej informacji na temat tego podejścia znajdziesz w recepturze 8.7.
Tworzenie widoków w Excelu Widok w Excelu można utworzyć za pomocą klasy pochodnej od klasy AbstractExcelView (biblioteka Apache POI) lub AbstractJExcelView (biblioteka JExcelAPI). W tym przykładzie używana jest klasa AbstractExcelView. W metodzie buildExcelDocument() można uzyskać dostęp do modelu przekazanego z kontrolera i do wstępnie utworzonego skoroszytu Excela. Zadaniem programisty jest zapełnienie skoroszytu danymi z modelu. Uwaga Aby móc generować w aplikacji sieciowej pliki Excela za pomocą biblioteki Apache POI, trzeba dodać związane z tą biblioteką zależności do parametru classpath. Jeśli używasz Mavena, dodaj do projektu Mavena poniższą zależność: org.apache.poi poi 3.0.2-FINAL package com.apress.springrecipes.court.web.view; ... import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.springframework.web.servlet.view.document.AbstractExcelView; public class ExcelReservationSummary extends AbstractExcelView { protected void buildExcelDocument(Map model, HSSFWorkbook workbook, HttpServletRequest request, HttpServletResponse response) throws Exception { List reservations = (List) model.get("reservations"); DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); HSSFSheet sheet = workbook.createSheet(); HSSFRow header = sheet.createRow(0); header.createCell((short) 0).setCellValue("Nazwa kortu"); header.createCell((short) 1).setCellValue("Data"); header.createCell((short) 2).setCellValue("Godzina");
323
ROZDZIAŁ 8. PLATFORMA SPRING MVC
header.createCell((short) 3).setCellValue("Nazwisko gracza"); header.createCell((short) 4).setCellValue("Telefon gracza"); int rowNum = 1; for (Reservation reservation : reservations) { HSSFRow row = sheet.createRow(rowNum++); row.createCell((short) 0).setCellValue(reservation.getCourtName()); row.createCell((short) 1).setCellValue( dateFormat.format(reservation.getDate())); row.createCell((short) 2).setCellValue(reservation.getHour()); row.createCell((short) 3).setCellValue( reservation.getPlayer().getName()); row.createCell((short) 4).setCellValue( reservation.getPlayer().getPhone()); } } }
W przedstawionym widoku Excela najpierw w skoroszycie tworzony jest arkusz. Do pierwszego wiersza arkusza kod dodaje nagłówki z raportu. Następnie kod przechodzi po liście rezerwacji i dla każdej z nich tworzy nowy wiersz. Ponieważ w kontrolerze znajduje się adnotacja @RequestMapping("/reservationSummary*"), a metoda obsługi wymaga w żądaniu parametru date, dostęp do tego widoku Excela można uzyskać za pomocą poniższego adresu URL: http://localhost:8080/court/reservationSummary.xls?date=2009-01-14
Tworzenie widoków PDF Aby otrzymać widok PDF, należy utworzyć klasę pochodną od klasy AbstractPdfView. W metodzie buildPdfDocument() można uzyskać dostęp do modelu przekazanego z kontrolera, a także wstępnie utworzyć dokument PDF. Zadaniem programisty jest zapełnienie dokumentu danymi z modelu. Uwaga Aby w aplikacji sieciowej generować pliki PDF za pomocą biblioteki iText, należy dodać ją do parametru classpath. Jeśli używasz Mavena, dodaj do projektu Mavena poniższą zależność: com.lowagie itext 2.0.8 package com.apress.springrecipes.court.web.view; ... import org.springframework.web.servlet.view.document.AbstractPdfView; import com.lowagie.text.Document; import com.lowagie.text.Table; import com.lowagie.text.pdf.PdfWriter; public class PdfReservationSummary extends AbstractPdfView { protected void buildPdfDocument(Map model, Document document, PdfWriter writer, HttpServletRequest request, HttpServletResponse response) throws Exception { List reservations = (List) model.get("reservations");
324
8.13. TWORZENIE WIDOKÓW W FORMATACH XLS I PDF
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); Table table = new Table(5); table.addCell("Nazwa kortu"); table.addCell("Data"); table.addCell("Godzina"); table.addCell("Nazwisko gracza"); table.addCell("Telefon gracza"); for (Reservation reservation : reservations) { table.addCell(reservation.getCourtName()); table.addCell(dateFormat.format(reservation.getDate())); table.addCell(Integer.toString(reservation.getHour())); table.addCell(reservation.getPlayer().getName()); table.addCell(reservation.getPlayer().getPhone()); } document.add(table); } }
Ponieważ w kontrolerze znajduje się adnotacja @RequestMapping("/reservationSummary*"), a metoda obsługi wymaga w żądaniu parametru date, dostęp do tego widoku PDF można uzyskać za pomocą poniższego adresu URL: http://localhost:8080/court/reservationSummary.pdf?date=2009-01-14
Tworzenie ziaren określających dla widoków XLS i PDF W recepturze 8.6 poznałeś różne strategie wiązania nazw widoków logicznych z konkretnymi implementacjami. Jedna z tych strategii polega na określaniu widoków z pakietu zasobów. To podejście dobrze nadaje się do wiązania nazw widoków logicznych z implementacjami w postaci klas dla formatów PDF lub XLS. Jeśli w kontekście aplikacji sieciowej ziarno typu ResourceBundleViewResolver jest skonfigurowane jako ziarno określające widoki, można zdefiniować widoki w pliku views.properties zapisanym w katalogu głównym z parametru classpath aplikacji sieciowej. Do pliku views.properties dodaj poniższy wpis, aby móc odwzorować klasę dla widoku XLS na nazwę widoku logicznego: reservationSummary.(class)= com.apress.springrecipes.court.web.view.ExcelReservationSummary
Ponieważ aplikacja wymaga negocjowania treści, ta sama nazwa widoku jest powiązana z różnymi technologiami generowania widoków. Ponadto w pliku views.properties nazwy nie mogą się powtarzać, dlatego trzeba utworzyć odrębny plik o nazwie secondaryviews.properties i powiązać w nim klasę widoku PDF z nazwą widoku logicznego, co pokazano poniżej: reservationSummary.(class)=com.apress.springrecipes.court.web.view.PdfReservationSummary
Zauważ, że ten plik (secondaryviews.properties) trzeba skonfigurować w odrębnym ziarnie określającym typu ResourceBundleViewResolver. Nazwa właściwości, reservationSummary, odpowiada nazwie widoku zwracanej przez kontroler. Ziarno określające ContentNegotiatingViewResolver musi na podstawie żądania ustalić, którą z klas należy zastosować. Następnie określona w ten sposób klasa generuje plik PDF lub XLS.
Tworzenie nazw plików PDF i XLS na podstawie daty Użytkownik żąda pliku PDF lub XLS za pomocą jednego z poniższych adresów URL: http://localhost:8080/court/reservationSummary.pdf?date=2009-01-14 http://localhost:8080/court/reservationSummary.xls?date=2009-02-24
325
ROZDZIAŁ 8. PLATFORMA SPRING MVC
Przeglądarka wyświetla wtedy użytkownikowi pytanie w postaci: „Czy zapisać plik reservationSummary.pdf?” lub „Czy zapisać plik reservationSummary.xls?”. Nazwa z tego pytania tworzona jest na podstawie żądanego adresu URL. Jednak ponieważ użytkownik może też podać w adresie URL datę, ciekawym rozwiązaniem będzie automatyczne generowanie pytań w postaci: „Czy zapisać plik ReservationSummary_2009_01_24.pdf?” lub „Czy zapisać plik ReservationSummary_2009_01_24.xls?”. Aby uzyskać taki efekt, można zastosować interceptor do modyfikowania adresów URL. Kod tego interceptora znajdziesz na poniższym listingu. package com.apress.springrecipes.court.web ... public class ExtensionInterceptor extends HandlerInterceptorAdapter { public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // W żądaniu występuje data, na podstawie której jest generowany raport String reportName = null; String reportDate = request.getQueryString(). replace("date=","").replace("-","_"); if(request.getServletPath().endsWith(".pdf")) { reportName= "ReservationSummary_" + reportDate + ".pdf"; } if(request.getServletPath().endsWith(".xls")) { reportName= "ReservationSummary_" + reportDate + ".xls"; } if (reportName != null) { // Ustawianie nagłówka HTTP Content-Disposition, aby użytkownik // zobaczył datę w nazwie pliku w oknie Zapisz jako response.setHeader("Content-Disposition", "attachment; filename="+reportName); } } }
Jeśli adres URL ma rozszerzenie .pdf lub .xls, interceptor pobiera cały ten adres. Po wykryciu takiego rozszerzenia należy utworzyć zwracaną nazwę pliku w postaci ReservationSummary_.<.pdf|.xls>. Aby mieć pewność, że użytkownik zobaczy w oknie zapisywania pliku nazwę w takiej formie, ustawiana jest ona w nagłówku HTTP Content-Disposition. By dodać ten interceptor i stosować go tylko do adresów URL odpowiadających kontrolerowi, który odpowiada za generowanie plików PDF i XLS, zapoznaj się z recepturą 8.3. Znajdziesz tam potrzebną konfigurację i dodatkowe informacje na temat klas interceptorów.
Negocjowanie treści i ustawianie nagłówków HTTP w interceptorze Choć ta aplikacja używa ziarna określającego ContentNegotiatingViewResolver do wybierania odpowiedniego widoku, proces modyfikowania docelowego adresu URL wykracza poza takie ziarno. Dlatego trzeba zastosować interceptor i ręcznie sprawdzać rozszerzenie żądania, a także ustawiać niezbędne nagłówki HTTP, aby zmodyfikować docelowy adres URL.
326
PODSUMOWANIE
Podsumowanie Z tego rozdziału dowiedziałeś się, jak utworzyć aplikację sieciową Javy za pomocą platformy Spring MVC. Głównym komponentem tej platformy jest klasa DispatcherServlet. Działa ona jak kontroler frontonu przekierowujący żądania do odpowiednich metod obsługi. W platformie Spring MVC kontrolerami są zwykle klasy Javy opatrzone adnotacją @Controller. Z różnych receptur dowiedziałeś się, jak wykorzystać także inne adnotacje używane w kontrolerach tej platformy. Te adnotacje to między innymi: @RequestMapping (określa docelowe adresy URL), @Autowired (automatycznie wstrzykuje referencje do ziaren) i @SessionAttributes (pozwala zapisywać obiekty w sesji użytkownika). Nauczyłeś się też stosować w aplikacji interceptory. Umożliwiają one modyfikowanie obiektów żądań i odpowiedzi w kontrolerze. Ponadto zobaczyłeś, w jaki sposób Spring MVC obsługuje przetwarzanie formularzy. Poznałeś między innymi sposób sprawdzania poprawności danych za pomocą walidatorów Springa i standardu JSR-303. Ponadto dowiedziałeś się, jak w platformie Spring MVC wykorzystywany jest język SpEL do upraszczania niektórych zadań z zakresu konfiguracji. Zobaczyłeś również, że Spring MVC obsługuje różne typy widoków związane z odmiennymi technologiami z warstwy prezentacji. Wiesz już też, w jaki sposób Spring obsługuje negocjowanie treści, co pozwala wybrać właściwy widok na podstawie rozszerzenia lub nagłówków HTTP z żądań.
327
ROZDZIAŁ 8. PLATFORMA SPRING MVC
328
ROZDZIAŁ 9
Usługi REST w Springu
Z tego rozdziału dowiesz się, jak Spring obsługuje technologię REST (ang. Representational State Transfer). Nazwę tę wymyślił w 2000 roku Roy Fielding (http://en.wikipedia.org/wiki/Roy_Fielding), a sama technologia okazała się ważnym elementem aplikacji sieciowych. Architektura REST jest oparta na protokole HTTP (ang. Hypertext Transfer Protocol) i zyskuje coraz większą popularność wśród twórców usług sieciowych. Usługi sieciowe stały się podstawą wielu procesów komunikacji między maszynami w sieci. Z uwagi na stosowanie w wielu organizacjach niespójnych rozwiązań (takich jak Java, Python, Ruby i .NET) konieczne stało się opracowanie technologii, która pozwoli na współdziałanie różnych środowisk. W jaki sposób aplikacja napisana w Pythonie ma używać informacji z aplikacji opartej na Javie? Jak aplikacja Javy ma pobierać dane z aplikacji zbudowanej za pomocą technologii .NET? Usługi sieciowe pozwalają rozwiązać takie problemy. Istnieją różne techniki tworzenia usług sieciowych. W aplikacjach sieciowych najczęściej stosuje się usługi sieciowe typu REST. Są one używane w niektórych spośród największych portali internetowych (takich jak Google i Yahoo!) do udostępniania informacji, do obsługi ajaksowych wywołań zgłaszanych przez przeglądarki, a także do dystrybucji danych z kanałów informacyjnych (na przykład RSS). Z tego rozdziału dowiesz się, jak używać technologii REST w aplikacjach Springa. Dzięki temu nauczysz się zarówno korzystać z danych, jak i udostępniać je za pomocą tego popularnego rozwiązania.
9.1. Publikowanie usług typu REST w Springu Problem Załóżmy, że chcesz opublikować usługę typu REST w Springu.
Rozwiązanie Przy projektowaniu usług typu REST w Springu masz dwie możliwości. Jedna z nich polega na publikowaniu danych aplikacji jako usługi typu REST, natomiast druga jest związana z dostępem w aplikacji do danych z niezależnej usługi tego typu. Ta receptura dotyczy publikowania danych aplikacji jako usługi typu REST. Z receptury 9.2 dowiesz się, jak uzyskać dostęp do danych z niezależnych usług tego rodzaju. Publikowanie danych aplikacji jako usługi typu REST jest związane z zastosowaniem adnotacji @RequestMapping i @PathVariable platformy Spring MVC. Jeśli opatrzysz metodę obsługi platformy Spring MVC tymi adnotacjami, aplikacja Springa będzie mogła publikować dane aplikacji jako usługi typu REST.
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
Ponadto Spring udostępnia zestaw mechanizmów do generowania danych w usługach typu REST. W tej recepturze poznasz najprostszy z tych mechanizmów, oparty na klasie MarshallingView Springa. W dalszych recepturach z tego rozdziału przedstawimy bardziej zaawansowane techniki z tej grupy.
Jak to działa? Publikowanie danych aplikacji sieciowej za pomocą usług typu REST (proces ten nazywa się też „tworzeniem punktu końcowego”) jest ściśle powiązane z platformą Spring MVC, którą poznałeś w rozdziale 8. Ponieważ Spring MVC korzysta z adnotacji @RequestMapping, którą należy dodać do metod obsługi i zastosować przy definiowaniu punktów dostępu (na przykład powiązanych z adresami URL), użycie tych adnotacji jest zalecanym sposobem definiowania punktu końcowego usługi typu REST. Na poniższym listingu przedstawiona jest klasa kontrolera platformy Spring MVC z metodą obsługi, która definiuje taki punkt końcowy. package com.apress.springrecipes.court.web; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import com.apress.springrecipes.court.domain.Member; @Controller public class RestMemberController { @RequestMapping("/members") public String getRestMembers(Model model) { // Zwraca widok membertemplate. Za pomocą ziarna określającego widok ten // jest łączony z powiązaną z klasą Member klasą szeregującą ze specyfikacji JAXB Member member = new Member(); member.setName("Jan Nowak"); member.setPhone("1-800-800-800"); member.setEmail("[email protected] "); model.addAttribute("member", member); return "membertemplate"; } }
Jeśli zastosujesz adnotację @RequestMapping("/members") do metody obsługi z kontrolera, punkt końcowy usługi typu REST będzie dostępny pod adresem http://[host_name]/[app-name]/members. Zanim przejdziemy do kodu tej ostatniej metody, warto wspomnieć o innych sposobach deklarowania punktów końcowych w usługach typu REST. Usługi tego rodzaju często mają parametry. Pozwala to ograniczyć lub przefiltrować dane. Na przykład żądanie w postaci http://[host_name]/[app-name]/member/353/ może służyć do pobierania informacji o osobie numer 353. Inny przykład to żądanie http://[host_name]/[app-name]/reservations/07-07-2010/, pozwalające pobrać rezerwacje z dnia 07-07-2010. Aby zastosować parametry w usługach typu REST w Springu, wykorzystaj adnotację @PathVariable. Tę adnotację należy dodać jako parametr wejściowy do metody obsługi (zgodnie z konwencjami z platformy Spring MVC), dzięki czemu parametry będą dostępne w kodzie tej metody. Poniższy fragment to metoda obsługi z usługi typu REST, w której to metodzie wykorzystano adnotację @PathVariable. Import org.springframework.web.bind.annotation.PathVariable; @RequestMapping("/member/{memberid}") public void getMember(@PathVariable("memberid") long memberID) { }
330
9.1. PUBLIKOWANIE USŁUG TYPU REST W SPRINGU
Zauważ, że wartość adnotacji @RequestMapping obejmuje człon {memberid}. Wartości ujęte w nawiasy klamrowe, {}, oznaczają, że parametry z adresu URL to zmienne. Zwróć też uwagę na to, że w metodzie obsługi używany jest parametr wejściowy @PathVariable("memberid") long memberID. Ta deklaracja wiąże wartość memberid z adresu URL ze zmienną o nazwie memberID, z której można korzystać w metodzie obsługi. Pokazana metoda obsługuje więc punkty końcowe usługi typu REST mające postać /member/353/ i /member/777/, a do zmiennej memberID przypisywane są wtedy wartości 353 i 777. W metodzie obsługi można wygenerować zapytania dotyczące osób o identyfikatorach 353 i 777 (zapisanych w zmiennej memberID), a następnie otrzymać potrzebne informacje. Choć omawiana metoda zwraca wartość void, dane zwracane przez używaną usługę są inne. Zgodnie z konwencjami stosowanymi w platformie Spring MVC metoda zwracająca wartość typu void jest wiązana z szablonem zwracającym dane. Przy definiowaniu punktów końcowych usług typu REST oprócz notacji {} można też zastosować symbol wieloznaczny, *. Po to rozwiązanie często sięga się wtedy, gdy projektanci decydują się na używanie opisowych adresów URL (nazywanych czasem przyjaznymi adresami URL) lub chcą wykorzystać techniki pozycjonowania i sprawić, aby adresy URL usługi były przyjazne dla wyszukiwarek. Poniżej pokazany jest fragment kodu usługi typu REST, w którym zastosowano symbol wieloznaczny. @RequestMapping("/member/*/{memberid}") public void getMember(@PathVariable("memberid") long memberID) { }
Tu dodanie symbolu wieloznacznego nie wpływa na działanie usługi typu REST, jednak metoda będzie obsługiwać żądania kierowane do punktów końcowych w postaci /member/Adam+Nowak/353/ lub /member/ Maria+Kowal/353/, co może mieć istotny wpływ na czytelność adresów dla użytkowników i pozycjonowanie strony. Warto też wspomnieć, że w definicjach metod obsługi dla punktów końcowych usług typu REST można zastosować wiązanie danych. Poniżej pokazany jest fragment kodu usługi typu REST, w którym wykorzystano tę technikę. @InitBinder public void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); } @RequestMapping("/reservations/{date}") public void getReservation(@PathVariable("date") Date resDate) { }
Teraz do żądania w postaci http://[host_name]/[app-name]/reservations/07-07-2010/ dopasowywana jest ostatnia metoda obsługi. Trafia do niej wartość 07-07-2010 w zmiennej resDate, którą można wykorzystać do przefiltrowania danych zwracanych przez omawianą usługę typu REST. Wróćmy do metody obsługi opatrzonej adnotacją @RequestMapping("/members"). Zauważ, że ta metoda tworzy obiekt typu Member, przypisywany następnie do obiektu typu Model. Dane, dzięki ich powiązaniu z obiektem typu Model z metody obsługi, stają się dostępne w widoku odpowiadającym tej metodzie. Jest to zgodne z konwencjami działania platformy Spring MVC. Tu danymi zwracanymi z usługi typu REST jest jeden zapisany na stałe obiekt typu Member. Bardziej zaawansowane usługi tego rodzaju mogą kierować zapytania do relacyjnych baz danych lub wywoływać metody statyczne. Wszystkie metody obsługi w platformie Spring MVC delegują zadania do widoków logicznych, które służą do wyświetlania treści przekazanych przez te metody. Te treści są ostatecznie kierowane do użytkowników zgłaszających żądania. W usługach typu REST użytkownicy zwykle oczekują, że zwracana treść będzie w formacie XML. Dlatego w platformie Spring MVC metody obsługi punktów końcowych usług typu REST są powiązane z widokami generującymi treść w tym formacie. W metodzie obsługi opatrzonej adnotacją @RequestMapping("/members") widać, że sterowanie trafia do widoku logicznego membertemplate. Widoki logiczne, zgodnie z konwencjami stosowanymi w platformie Spring MVC, są zdefiniowane w plikach *-servlet.xml aplikacji Springa. Na poniższym listingu przedstawiona jest definicja widoku logicznego membertemplate.
331
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
com.apress.springrecipes.court.domain.Member
Widok membertemplate jest typu MarshallingView. MarshallingView to klasa ogólnego przeznaczenia, która umożliwia wyświetlanie odpowiedzi za pomocą mechanizmu szeregującego. Szeregowanie to proces przekształcania obiektu zapisanego w pamięci na możliwe do przekazania dane. W omawianej sytuacji mechanizm szeregowania przekształca obiekt typu Member na dane w formacie XML. Mechanizm szeregowania używany w klasie MarshallingView, Jaxb2Marshaller, to jeden z zestawu XML-owych mechanizmów dostępnych w Springu. Inne mechanizmy tego rodzaju to: CastorMarshaller, JibxMarshaller, XmlBeansMarshaller i XStreamMarshaller. Mechanizmy szeregowania wymagają skonfigurowania. Tu zastosowano klasę Jaxb2Marshaller, ponieważ jest prosta i oparta na architekturze JAXB (ang. Java Architecture for XML Binding). Jeśli jednak wolisz korzystać z platformy Castor XML, łatwiejsza w użyciu może okazać się klasa CastorMarshaller, a jeżeli stosujesz bibliotekę XStream, wygodniej będzie Ci posłużyć się klasą XStreamMarshaller. Podobne uwagi dotyczą też pozostałych mechanizmów szeregowania. Klasę Jaxb2Marshaller trzeba skonfigurować za pomocą właściwości classesToBeBound lub contextPath. Gdy zastosujesz pierwszą z nich, przypisane do niej klasy będą określać strukturę obiektów przekształcanych na format XML. Na poniższym listingu pokazano, jak przypisać klasę Member do klasy Jaxb2Marshaller. package com.apress.springrecipes.court.domain; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement public class Member { private String name; private String phone; private String email; public String getEmail() { return email; } public String getName() { return name; } public String getPhone() { return phone; } public void setEmail(String email) { this.email = email; } public void setName(String name) { this.name = name; }
332
9.2. DOSTĘP DO USŁUG TYPU REST W SPRINGU
public void setPhone(String phone) { this.phone = phone; } }
Zauważ, że Member to klasa POJO opatrzona adnotacją @XmlRootElement. Ta adnotacja sprawia, że klasa Jaxb2Marshaller może wykrywać pola klas (obiektów) i przekształcać je na dane w formacie XML (na przykład name=Jan na jan , a [email protected] na [email protected] ). Warto przypomnieć, że po zgłoszeniu żądania adresu URL w postaci http://[host_name]//app-name]/ members.xml odpowiednia metoda obsługi tworzy obiekt typu Member, który jest następnie przekazywany do widoku logicznego membertemplate. Na podstawie definicji tego widoku mechanizm szeregowania przekształca obiekt typu Member na dane w formacie XML. Te dane są zwracane do jednostki żądającej ich od usługi typu REST. Dane zwracane przez tę usługę przedstawia poniższy listing: [email protected] Jan Kowal 1-800-800-800
Te XML-owe dane powstały w wyniku bardzo prostego procesu generowania odpowiedzi od usługi typu REST. W dalszych recepturach z tego rozdziału poznasz bardziej zaawansowane techniki, polegające na przykład na stosowaniu często używanych w usługach typu REST formatów: RSS, Atom i JSON. Jeśli dobrze przyjrzysz się punktowi końcowemu usługi typu REST (czyli przedstawionemu wcześniej adresowi URL), zauważysz rozszerzenie .xml. Jeżeli podasz inne rozszerzenie lub w ogóle je pominiesz, aplikacja może nie wywołać danej usługi. Jest to bezpośrednio związane ze sposobem określania widoków w platformie Spring MVC i nie ma nic wspólnego z samymi usługami typu REST. Ponieważ widok powiązany z metodą obsługi dla danej usługi typu REST zwraca stronę w formacie XML, jest uruchamiany dla rozszerzenia .xml. Dzięki temu ta sama metoda obsługi może obsługiwać wiele widoków. Wygodnym rozwiązaniem jest na przykład zwracanie tych samych informacji w formacie PDF dla żądań w postaci http://[host_name]/[app-name]/members.pdf, w formacie HTML dla żądań w postaci http:// [host_name]/[appname]/members.html i w formacie XML dla żądań kierowanych do usługi typu REST mających postać http://[host_name]/[appname]/members.xml. Co więc dzieje się z żądaniami bez rozszerzenia w adresie URL, takimi jak http://[host_name]/[app-name]/ members? W dużym stopniu zależy to od sposobu określania widoków w platformie Spring MVC. Udostępnia ona mechanizm negocjowania treści, który polega na określaniu docelowego widoku na podstawie rozszerzenia lub nagłówka HTTP żądania. Ponieważ żądania kierowane do usług typu REST mają zwykle nagłówki HTTP w postaci Accept: application/xml, platforma Spring MVC skonfigurowana pod kątem negocjowania treści może zwracać dane w formacie XML (z usługi typu REST) nawet dla żądań z tym nagłówkiem, które nie mają rozszerzenia. Ten sam mechanizm pozwala żądać danych w formacie HTML, PDF i XLS na podstawie nagłówków HTTP, przy czym rozszerzenia w żądaniach nie są wtedy konieczne. Omówienie negocjowania treści znajdziesz w rozdziale 8.
9.2. Dostęp do usług typu REST w Springu Problem Programista chce uzyskać dostęp do zewnętrznych usług typu REST (na przykład od Google’a, Yahoo lub partnera biznesowego), a następnie wykorzystać dane z usługi w aplikacji Springa.
333
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
Rozwiązanie Uzyskanie dostępu do niezależnych usług typu REST w aplikacjach Springa wymaga zastosowania klasy RestTemplate Springa. Klasa RestTemplate jest zaprojektowana według tych samych zasad co wiele innych klas z rodziny *Template (na przykład JdbcTemplate i JmsTemplate). Pozwala to uprościć pracę, ponieważ nawet złożone zadania są wykonywane za pomocą domyślnych operacji. Dzięki temu w aplikacjach Springa proces wywoływania usług typu REST i wykorzystywania zwróconych danych jest prosty.
Jak to działa? Przed omówieniem działania klasy RestTemplate warto przyjrzeć się cyklowi życia usług typu REST. Dzięki temu zrozumiesz zadania wykonywane przez tę klasę. Aby zapoznać się z cyklem życia usług typu REST, najlepiej jest wykorzystać przeglądarkę. Zacznij więc od uruchomienia takiego programu w komputerze. Pierwszą potrzebną rzeczą jest punkt końcowy usługi typu REST. Poniższy adres URL reprezentuje taki punkt udostępniany przez Yahoo. Ten punkt końcowy zwraca najnowsze informacje ze świata sportu. http://search.yahooapis.com/NewsSearchService/V1/newsSearch?appid= YahooDemo&query=sports&results=2&language=en
Struktura tego punktu końcowego obejmuje adres (zaczyna się on od członu http:// i kończy znakiem ?) oraz serię parametrów. Parametry rozpoczynają się po znaku ?, są rozdzielone symbolem & i składają się z klucza oraz podanej po znaku = wartości. Jeśli otworzysz w przeglądarce podany punkt końcowy usługi typu REST, przeglądarka zgłosi żądanie GET (jest to jedno z najczęściej używanych żądań HTTP obsługiwanych przez usługi typu REST). Po otrzymaniu odpowiedzi przez usługę przeglądarka wyświetli zwrócone dane: Toyota Recalls Will Slam Sports All the big sports are likely to see their sponsorship revenue go down as a result of the Toyota de http://blogs.forbes.com/sportsmoney/2010/02/toyota-recalls-will-slam-sports/ ...Pozostałe dane w celu zachowania zwięzłości zostały pominięte...
Jest to fragment dobrze uformowanych danych w formacie XML. Tak właśnie wygląda większość odpowiedzi od usług typu REST. Znaczenie danych zależy od konkretnej usługi. Tutaj znacznikami XML-a (, itd.) są definicje określone przez Yahoo, a dane tekstowe w poszczególnych znacznikach to informacje powiązane ze skierowanym do usługi żądaniem. Konsument usługi typu REST (czyli programista) musi znać strukturę danych (nazywaną czasem słownikiem) tej usługi, aby móc poprawnie przetwarzać informacje. Przedstawiona usługa typu REST korzysta z niestandardowego słownika, ale wiele takich usług posługuje się standardowymi słownikami (na przykład RSS), co pozwala ujednolicić przetwarzanie zwracanych przez usługi danych. Ponadto warto zauważyć, że niektóre usługi typu REST udostępniają kontrakty w języku WADL (ang. Web Application Description Language), co ułatwia wykrywanie danych i korzystanie z nich. Po zapoznaniu się za pomocą przeglądarki z cyklem życia usług typu REST można przejść do zastosowania klasy RestTemplate Springa i wykorzystania danych z usługi w aplikacji Springa.
334
9.2. DOSTĘP DO USŁUG TYPU REST W SPRINGU
Ponieważ klasa RestTemplate jest zaprojektowana pod kątem wywoływania usług typu REST, nie jest zaskoczeniem, że jej główne metody są ściśle powiązane z podstawowym aspektem tych usług, czyli metodami protokołu HTTP: HEAD, GET, POST, PUT, DELETE i OPTIONS. W tabeli 9.1 znajdziesz listę najważniejszych metod tej klasy. Tabela 9.1. Metody klasy RestTemplate powiązane z rodzajami żądań z protokołu HTTP Metoda
Opis
headForHeaders(String, Object…)
Wykonuje żądanie HEAD protokołu HTTP.
getForObject(String, Class, Object...)
Wykonuje żądanie GET protokołu HTTP.
postForLocation(String, Object, Object...)
Wykonuje żądanie POST protokołu HTTP dla obiektu.
postForObject(String, Object, Class, Object...)
Wykonuje żądanie POST protokołu HTTP dla klasy.
put(String, Object, Object...)
Wykonuje żądanie PUT protokołu HTTP.
delete(String, Object...)
Wykonuje żądanie DELETE protokołu HTTP.
optionsForAllow(String, Object...)
Wykonuje żądanie OPTIONS protokołu HTTP.
execute(String, HttpMethod, RequestCallback, ResponseExtractor, Object...)
Wykonuje dowolne żądanie protokołu HTTP z wyjątkiem operacji CONNECT.
W tabeli 9.1 widać, że nazwy metod klasy RestTemplate mają przedrostki w postaci nazw metod protokołu HTTP (HEAD, GET, POST, PUT, DELETE i OPTIONS). Ponadto execute to metoda ogólnego przeznaczenia, która pozwala wykonać dowolne żądanie HTTP (w tym rzadko stosowaną operację TRACE), choć nie działa dla metody CONNECT, ponieważ ta nie jest obsługiwana przez używaną w metodzie execute strukturę HttpMethod enum. Uwaga Zdecydowanie najczęściej stosowaną metodą HTTP w usługach typu REST jest GET, ponieważ służy ona do bezpiecznego pobierania informacji (bezpiecznego, czyli bez modyfikowania żadnych danych). Metody POST i DELETE są zaprojektowane na potrzeby modyfikowania informacji po stronie dostawcy, dlatego są rzadziej obsługiwane przez twórców usług typu REST. Gdy modyfikowanie danych jest konieczne, wielu dostawców decyduje się na użycie protokołu SOAP, który jest alternatywnym sposobem korzystania z usług typu REST.
Po zapoznaniu się z metodami klasy RestTemplate można przejść do wywołania tej samej usługi typu REST, którą wcześniej uruchomiłeś już za pomocą przeglądarki. Jednak tym razem posłuży do tego kod Javy ze Springa. Na poniższym listingu pokazana jest klasa kontrolera platformy Spring MVC z metodą obsługi, która korzysta z usługi typu REST i przekazuje otrzymane dane do standardowej strony HTML. package com.apress.springrecipes.court.web; import import import import
org.springframework.stereotype.Controller; org.springframework.ui.Model; org.springframework.web.bind.annotation.RequestMapping; org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.client.RestTemplate; @Controller public class RestNewsController { @Autowired protected RestTemplate restTemplate; @RequestMapping("/sportsnews") public String getYahooNews(Model model) {
335
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
// Zwraca widok newstemplate. Ziarno określające wiąże // ten widok ze stroną /WEB-INF/jsp/newstemplate.jsp String result = restTemplate.getForObject(" http://search.yahooapis.com/ NewsSearchService/V1/newsSearch?appid={appid}& query={query}&results={results}&language={language}", String.class, "YahooDemo","sports","2","en"); model.addAttribute("newsfeed", result); return "newstemplate"; } }
Ostrzeżenie Niektórzy dostawcy usług typu REST ograniczają dostęp do danych w zależności od źródła żądania. Odmowa dostępu odbywa się zwykle na podstawie danych zawartych w żądaniu (na przykład nagłówków HTTP lub adresów IP). Dlatego w niektórych sytuacjach dostawca może nie udostępnić danych nawet wtedy, gdy można z nich korzystać za pomocą innego medium (dane z usługi typu REST mogą być na przykład dostępne w przeglądarce, ale niedostępne przy próbie ich pobrania za pomocą aplikacji Springa). Zależy to od warunków korzystania z usługi określonych przez jej dostawcę.
W pierwszym wyróżnionym pogrubieniem wierszu zadeklarowana jest instrukcja importu potrzebna do korzystania w tworzonej klasie z klasy RestTemplate. Druga wyróżniona instrukcja reprezentuje tę samą klasę opatrzoną adnotacją @Autowired, dzięki czemu Spring może połączyć tę klasę z rozwiązaniem. Następny listing przedstawia konfigurację potrzebną w pliku konfiguracyjnym aplikacji sieciowej Springa do połączenia klasy RestTemplate.
Jeśli proces łączenia klas w Springu nie jest Ci znany, zachęcamy do lektury rozdziałów od 1. do 3. Opisane są w nich podstawowe mechanizmy działania tej platformy. Jeżeli nie znasz adnotacji @Autowired, zapoznaj się z rozdziałem 8. Znajdziesz w nim wprowadzenie do platformy Spring MVC, która jest opartą na tej adnotacji częścią Springa. Dalej znajduje się metoda obsługi opatrzona adnotacją @RequestMapping("/sportsnews"). Wykorzystano tu podstawowy sposób deklarowania metod w platformie Spring MCV. Ta metoda jest uruchamiana w odpowiedzi na żądanie skierowane pod adres http://[host_name]/[app-name]/sportsnews. W pierwszym wierszu omawianej metody obsługi znajduje się wywołanie metody getForObject z klasy RestTemplate. Metoda ta jest opisana w tabeli 9.1 i służy do wykonywania żądań GET protokołu HTTP, takich, jakie zgłaszają przeglądarki, aby otrzymać dane od usługi typu REST. Z metodą getForObject związane są dwa ważne aspekty: odpowiedź i parametry. Odpowiedź dla metody getForObject jest przypisywana do obiektu typu String. W tym obiekcie zapisywane są te same dane wyjściowe, które od usługi typu REST otrzymała przeglądarka (są to dane w formacie XML). Nawet jeśli nigdy nie przetwarzałeś XML-owych danych w Javie, prawdopodobnie wiesz, że pobieranie danych z obiektów typu String i manipulowanie nimi nie jest łatwe. Istnieją klasy lepiej niż ten typ dostosowane do przetwarzania danych w formacie XML (w tym danych z usług typu REST). Na razie zapamiętaj to. W dalszych recepturach poznasz lepsze sposoby wyodrębniania danych od usług typu REST i manipulowania nimi. Parametry przekazane do metody getForObject określają punkt końcowy usługi typu REST. Pierwszy parametr to adres URL (określa on punkt końcowy) i zestaw symboli zastępczych ujętych w nawias { }. Zauważ, że jest to ten sam adres URL co wpisywany w przeglądarce, jednak tu zamiast zapisanych na stałe wartości pojawiają się symbole zastępcze. W tym przykładzie każdy symbol zastępczy (na przykład {appid} i {query}) jest zastępowany jednym z pozostałych parametrów przekazanych do metody getForObject. Następny parametr reprezentuje klasę zwracanej wartości (String.class), natomiast pozostałe to wartości wstawiane w symbolach zastępczych (według kolejności — na przykład do symbolu {appid} przypisywana jest wartość YahooDemo, a do symbolu {query} wartość sports).
336
9.2. DOSTĘP DO USŁUG TYPU REST W SPRINGU
Choć to podejście umożliwia przekazywanie do usługi typu REST parametrów dla konkretnych wywołań (na przykład dla każdego użytkownika aplikacji sieciowej), przekazywanie zestawu obiektów typu String przy konieczności zachowania ich właściwego porządku może łatwo prowadzić do pomyłek. Różne opisane w tabeli 9.1 metody klasy RestTemplate umożliwiają też bardziej zwięzły sposób przekazywania parametrów. Można wykorzystać do tego kolekcje Javy. Tę technikę zastosowano w poniższym kodzie: Map params = new HashMap(); params.put("appid","YahooDemo"); params.put("query","sports"); params.put("results", "2"); params.put("language", "en"); String result = restTemplate.getForObject("http://search.yahooapis.com/ NewsSearchService/V1/newsSearch?appid={appid} &query={query}&results={results}&language={language}", String.class, params);
W tym fragmencie kodu wykorzystano klasę HashMap (jest to część platformy Java Collections) do utworzenia obiektu z parametrami dla usługi typu REST. Obiekt ten jest później przekazywany do metody getForObject klasy RestTemplate. Efekty przekazania zestawu parametrów typu String lub jednego parametru typu Map do różnych metod klasy RestTemplate są takie same. Wróćmy do dalszej części metody obsługi. Dane w formacie XML z usługi typu REST są przypisywane do obiektu typu String, a następnie wiązane z obiektem typu Model przy użyciu słowa kluczowego newsfeed. Dzięki temu w widoku powiązanym z tą metodą obsługi można uzyskać dostęp do danych i wyświetlić je. Tu metoda przekazuje sterowanie do widoku logicznego newstemplate. Ponieważ konfigurowanie widoków logicznych to operacja z zakresu platformy Spring MVC, to jeśli nie znasz tego zagadnienia, zajrzyj do rozdziału 8. Widok logiczny newstemplate jest ostatecznie wiązany z plikiem JSP, który potrafi wyświetlić zawartość XML-owych danych zwróconych przez usługę typu REST. Zgodnie z powiązaniami w platformie Spring MVC te dane są dostępne pod adresem http://[host_name]/[app-name]/ sportsnews.html. Potrzebny plik JSP jest pokazany na poniższym listingu. Wiadomości sportowe z serwisu Yahoo! Wiadomości sportowe z serwisu Yahoo!
Dane w formacie XML otrzymane z usługi typu REST są umieszczane w symbolu zastępczym ${newsfeed} dzięki kluczowi zdefiniowanemu w metodzie obsługi. Jak już wspominaliśmy, w dalszych recepturach znajdziesz omówienie bardziej zaawansowanych technik pobierania XML-owych danych zwróconych przez usługi typu REST i manipulowania takimi danymi. Przedstawione tu proste podejście polega na bezpośrednim wyświetleniu wszystkich danych na stronie HTML.
337
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
9.3. Publikowanie danych z kanałów informacyjnych RSS i Atom Problem Programista chce publikować w aplikacji Springa dane z kanałów RSS i Atom.
Rozwiązanie Kanały RSS i Atom stały się popularnym narzędziem publikowania informacji. Dostęp do tych kanałów zapewniają usługi typu REST, co oznacza, że utworzenie takiej usługi jest warunkiem wstępnym publikowania danych z tych kanałów. Oprócz dostępnej w Springu obsługi technologii REST można wykorzystać niezależne biblioteki zaprojektowane specjalnie z myślą o zarządzaniu kanałami RSS i Atom. Dzięki temu łatwiej jest publikować w usłudze typu REST tego rodzaju XML-owe dane. Dlatego w przykładzie używana jest otwarta biblioteka Project Rome, dostępna na stronie https://rome.dev.java.net/. Uwaga Biblioteka Project Rome wymaga biblioteki JDOM (dostępna na stronie http://www.jdom.org/). Jeśli używasz Mavena, dodaj do pliku pom.xml następującą zależność: org.jdom jdom 1.1
Wskazówka Choć kanały RSS i Atom często są opisywane jako kanały informacyjne, obecnie korzysta się z nich nie tylko do udostępniania najnowszych wiadomości. RSS i Atom służą też do publikowania informacji z blogów, wiadomości o pogodzie, podróżach i wielu innych rzeczach, przy czym dane mają format obsługiwany w wielu systemach (jest to format XML). Dlatego jeśli chcesz publikować jakiekolwiek informacje tak, aby były dostępne w różnych systemach, wykorzystanie kanałów RSS lub Atom jest doskonałym wyborem z powodu ich dużej popularności (są one obsługiwane w wielu aplikacjach i liczni programiści znają stosowaną w nich strukturę danych).
Jak to działa? Pierwszą rzeczą, jaką trzeba zrobić, jest ustalenie, jakie informacje mają być publikowane w kanałach RSS lub Atom. Informacje mogą znajdować się w relacyjnej bazie danych lub w pliku tekstowym; dostęp do nich można uzyskać za pomocą technologii JDBC lub ORM; mogą stanowić część ziarna Springa lub innej struktury itd. Omówienie tych zagadnień wykracza poza zakres tej receptury, dlatego załóżmy, że masz środki potrzebne do uzyskania dostępu do danych. Po ustaleniu publikowanych informacji trzeba zapisać je w strukturze odpowiedniej dla kanału RSS lub Atom. W tym celu przyda się biblioteka Project Rome. Jeśli nie znasz struktury danych z kanałów Atom, przeanalizuj poniższy kod zawierający informacje w tym formacie: Przykładowe dane z kanału informacyjnego
338
9.3. PUBLIKOWANIE DANYCH Z KANAŁÓW INFORMACYJNYCH RSS I ATOM
2010-08-31T18:30:02Z Jan Kowal urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6 Roboty sterowane danymi z kanału Atom wpadły w szał urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a 2010-08-31T18:30:02Z Tekst informacji.
Poniższy fragment ilustruje strukturę danych z kanału RSS: Przykładowe dane z kanału RSS Przykładowe dane z kanału RSS http://www.example.org/link.htm Mon, 28 Aug 2006 11:12:55 -0400 Tue, 31 Aug 2010 09:00:00 -0400 -
Przykładowy element To przykładowy element http://www.example.org/link.htm 1102345 Tue, 31 Aug 2010 09:00:00 -0400
Jak można zaobserwować, kanały RSS i Atom zawierają zwykłe dane w formacie XML, w których za pomocą zestawu elementów można publikować informacje. Omówienie szczegółów struktury danych z tych kanałów zasługuje na odrębną książkę. Poniżej znajdziesz opis pewnych wspólnych cech obu tych formatów: Używana jest sekcja metadanych z opisem zawartości kanału (na przykład elementy i w formacie Atom oraz i w formacie RSS). Do przedstawiania informacji wykorzystywane są powtarzające się elementy (element w formacie Atom i element - w formacie RSS). Ponadto każdy z tych elementów zawiera zestaw podelementów, które służą do dalszego opisywania informacji. Istnieją różne wersje obu formatów. Wersje kanału RSS to: 0.90, 0.91 Netscape, 0.91 Userland, 0.92, 0.93, 0.94, 1.0 i 2.0. Kanał Atom ma wersje: 0.3 i 1.0. Biblioteka Project Rome umożliwia tworzenie sekcji z metadanymi i powtarzających się elementów, a także wybranie jednej z wcześniej wymienionych wersji. Można przy tym korzystać z informacji z kodu Javy (na przykład obiektów typu String, Map itd.). Skoro znasz już strukturę danych z kanałów RSS i Atom, a także przeznaczenie biblioteki Project Rome, przyjrzyj się kontrolerowi platformy Spring MVC służącemu do wyświetlania takich danych użytkownikom.
339
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
package com.apress.springrecipes.court.web; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import com.apress.springrecipes.court.feeds.TournamentContent; import import import import
com.apress.springrecipes.court.domain.Member; java.util.List; java.util.Date; java.util.ArrayList;
@Controller public class FeedController { @RequestMapping("/atomfeed") public String getAtomFeed(Model model) { List tournamentList = new ArrayList(); tournamentList.add(TournamentContent.generateContent("ATP", new Date(),"Australian Open","www.australianopen.com")); tournamentList.add(TournamentContent.generateContent("ATP", new Date(),"Roland Garros","www.rolandgarros.com")); tournamentList.add(TournamentContent.generateContent("ATP", new Date(),"Wimbledon","www.wimbledon.org")); tournamentList.add(TournamentContent.generateContent("ATP", new Date(),"US Open","www.usopen.org")); model.addAttribute("feedContent",tournamentList); return "atomfeedtemplate"; } @RequestMapping("/rssfeed") public String getRSSFeed(Model model) { List tournamentList = new ArrayList(); tournamentList.add(TournamentContent.generateContent("FIFA", new Date(),"World Cup","www.fifa.com/worldcup/")); tournamentList.add(TournamentContent.generateContent("FIFA", new Date(),"U-20 World Cup","www.fifa.com/u20worldcup/")); tournamentList.add(TournamentContent.generateContent("FIFA", new Date(),"U-17 World Cup","www.fifa.com/u17worldcup/")); tournamentList.add(TournamentContent.generateContent("FIFA", new Date(),"Confederations Cup","www.fifa.com/confederationscup/")); model.addAttribute("feedContent",tournamentList); return "rssfeedtemplate"; } }
W tym kontrolerze platformy Spring MVC znajdują się dwie metody obsługi. Jedna z nich to getAtomFeed(), powiązana z adresami URL w postaci http://[host_name]/[app-name]/atomfeed, natomiast druga to getRSSFeed(), powiązana z adresami URL w formie http://[host_name]/[app-name]/rssfeed. W obu tych metodach definiowana jest kolekcja typu List zawierająca obiekty typu TournamentContent (są to obiekty POJO). Następnie ta kolekcja jest przypisywana do obiektu typu Model z metody obsługi, dzięki czemu jest dostępna w zwracanym widoku. Zwracane widoki logiczne w poszczególnych metodach obsługi to atomfeedtemplate i rssfeedtemplate. Te widoki logiczne są zdefiniowane w XML-owym pliku konfiguracyjnym Springa:
340
9.3. PUBLIKOWANIE DANYCH Z KANAŁÓW INFORMACYJNYCH RSS I ATOM
Jak widać, każdy widok logiczny jest powiązany z klasą. W tych klasach trzeba umieścić kod niezbędny do utworzenia widoków dla formatów Atom i RSS. W rozdziale 8. to samo oparte na klasach podejście zastosowano do tworzenia widoków dla formatów PDF i XLS Excela. Na potrzeby widoków dla formatów Atom i RSS Spring udostępnia dwie specjalne klasy bazujące na bibliotece Project Rome. Są to klasy AbstractAtomFeedView i AbstractRssFeedView. Te klasy są podstawą do tworzenia danych dla kanałów Atom i RSS, przy czym nie trzeba szczegółowo znać tych formatów. Na poniższym listingu przedstawiona jest klasa AtomFeedView. Jest to klasa pochodna od klasy Abstract AtomFeedView, wykorzystywana w widoku logicznym atomfeedtemplate. package com.apress.springrecipes.court.feeds; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.sun.syndication.feed.atom.Feed; import com.sun.syndication.feed.atom.Entry; import com.sun.syndication.feed.atom.Content; import org.springframework.web.servlet. view.feed.AbstractAtomFeedView; import import import import
java.util.Date; java.util.List; java.util.ArrayList; java.util.Map;
public class AtomFeedView extends AbstractAtomFeedView { protected void buildFeedMetadata(Map model, Feed feed, HttpServletRequest request) { feed.setId("tag:tennis.org"); feed.setTitle("Grand Slam Tournaments"); List tournamentList = (List)model. get("feedContent"); for (TournamentContent tournament : tournamentList) { Date date = tournament.getPublicationDate(); if (feed.getUpdated() == null || date.compareTo(feed.getUpdated()) > 0) { feed.setUpdated(date); } } } protected List buildFeedEntries(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { List tournamentList = (List)model.get("feedContent"); List entries = new ArrayList(tournamentList.size()); for (TournamentContent tournament : tournamentList) { Entry entry = new Entry(); String date = String.format("%1$tY-%1$tm-%1$td", tournament.getPublicationDate()); entry.setId(String.format("tag:tennis.org,%s:%d", date,
341
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
tournament.getId())); entry.setTitle(String.format("%s – Data dodania: %s", tournament.getName(), tournament.getAuthor())); entry.setUpdated(tournament.getPublicationDate()); Content summary = new Content(); summary.setValue(String.format("%s - %s", tournament.getName(),tournament.getLink())); entry.setSummary(summary); entries.add(entry); } return entries; } }
W tej klasie zwróć uwagę przede wszystkim na importowanie z pakietu com.sun.syndication.feed.atom kilku klas biblioteki Project Rome. Ponadto klasa ta dziedziczy po klasie AbstractAtomFeedView Springa. Dzięki temu jedyną rzeczą, jaką trzeba zrobić, jest dodanie kodu dwóch metod dziedziczonych po klasie AbstractAtomFeedView: buildFeedMetadata i buildFeedEntries. Metoda buildFeedMetadata ma trzy parametry wejściowe. Obiekt typu Map reprezentuje informacje używane do generowania danych przypisywanych w metodzie obsługi (tu wykorzystywana jest kolekcja List zawierająca obiekty typu TournamentContent). Obiekt typu Feed z biblioteki Project Rome służy do manipulowania danymi, a obiekt typu HttpServletRequest jest używany do manipulowania żądaniem HTTP. W metodzie buildFeedMetadata znajduje się kilka wywołań setterów obiektu typu Feed (metod setId, setTitle, setUpdated itd.). W dwóch wywołaniach znajdują się zapisane na stałe łańcuchy znaków, natomiast wartości w pozostałych są określane w trakcie przechodzenia po danych (po obiekcie typu Map). Wszystkie wywołania powodują przypisanie metadanych w formacie Atom. Uwaga Jeśli chcesz przypisać inne wartości do sekcji metadanych w formacie Atom lub określić konkretną wersję tego formatu, zapoznaj się z interfejsem API biblioteki Project Rome. Wersją domyślną jest Atom 1.0.
Metoda buildFeedEntries także ma trzy parametry wejściowe: obiekt typu Map reprezentuje dane przypisywane w metodzie obsługi (tu używana jest kolekcja List zawierająca obiekty typu TournamentContent). Obiekt typu HttpServletRequest jest potrzebny do manipulowania żądaniami HTTP, a obiekt typu HttpServletResponse umożliwia manipulowanie odpowiedzią HTTP. Warto też zauważyć, że metoda buildFeedEntries zwraca obiekty typu List. Tu zawierają one obiekty typu Entry, oparte na klasie z biblioteki Project Rome i obejmujące powtarzające się elementy z kanału Atom. W metodzie buildFeedEntries używany jest obiekt typu Map, co pozwala pobrać obiekt feedContent przypisany w metodzie obsługi. Następnie program tworzy pustą kolekcję List obiektów typu Entry. Dalej kod przechodzi w pętli po zawartości obiektu feedContent (obejmuje on kolekcję List z obiektami typu TournamentContent), tworzy dla każdego elementu obiekt typu Entry i przypisuje go do nadrzędnej listy obiektów typu Entry. Po zakończeniu wykonywania pętli metoda zwraca zapełnioną listę obiektów typu Entry. Uwaga Jeśli chcesz przypisać więcej wartości w sekcji powtarzających się elementów w kanale Atom, zapoznaj się z interfejsem API biblioteki Project Rome.
Po dodaniu opisanej właśnie klasy (oraz przedstawionego wcześniej kontrolera platformy Spring MVC) zażądanie adresu w postaci http://[host_name]/[app-name]/atomfeed.atom (lub http://[host_name]/atomfeed.xml) spowoduje zwrócenie następującej odpowiedzi: Grand Slam Tournaments tag:tennis.org
342
9.3. PUBLIKOWANIE DANYCH Z KANAŁÓW INFORMACYJNYCH RSS I ATOM
2010-03-04T20:51:50Z Australian Open - Posted by ATP tag:tennis.org,2010-03-04:0 2010-03-04T20:51:50Z Australian Open - www.australianopen.com Roland Garros - Posted by ATP tag:tennis.org,2010-03-04:1 2010-03-04T20:51:50Z Roland Garros - www.rolandgarros.com Wimbledon - Posted by ATP tag:tennis.org,2010-03-04:2 2010-03-04T20:51:50Z Wimbledon - www.wimbledon.org US Open - Posted by ATP tag:tennis.org,2010-03-04:3 2010-03-04T20:51:50Z US Open - www.usopen.org
Teraz przyjrzyj się innej metodzie obsługi, getRSSFeed, z opisanego wcześniej kontrolera platformy Spring MVC. Metoda ta odpowiada za generowanie danych dla kanału RSS. Zauważ, że proces ten przebiega podobnie jak przy tworzeniu kanału Atom. Ta metoda obsługi też tworzy kolekcję List zawierającą obiekty typu TournamentContent. Ta kolekcja jest później przypisywana do obiektu typu Model metody obsługi, dzięki czemu jest dostępna w zwracanym widoku. Tym razem zwracany widok logiczny nosi nazwę rssfeedtemplate. Jak wcześniej wspomniano, ten widok logiczny jest wiązany z klasą RssFeedView. Tę klasę znajdziesz na poniższym listingu. Klasa RssFeedView dziedziczy po klasie AbstractRssFeedView. package com.apress.springrecipes.court.feeds; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.sun.syndication.feed.rss.Channel; import com.sun.syndication.feed.rss.Item; import org.springframework.web.servlet. view.feed.AbstractRssFeedView; import import import import
java.util.Date; java.util.List; java.util.ArrayList; java.util.Map;
public class RSSFeedView extends AbstractRssFeedView { protected void buildFeedMetadata(Map model, Channel feed, HttpServletRequest request) { feed.setTitle("Światowe imprezy piłkarskie"); feed.setDescription("Kalendarz mistrzostw świata w piłce nożnej"); feed.setLink("tennis.org"); List tournamentList = (List)model.
343
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
get("feedContent"); for (TournamentContent tournament : tournamentList) { Date date = tournament.getPublicationDate(); if (feed.getLastBuildDate() == null || date.compareTo(feed. getLastBuildDate()) > 0) { feed.setLastBuildDate(date); } } } protected List buildFeedItems(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { List tournamentList = (List)model.get ("feedContent"); List- items = new ArrayList
- (tournamentList.size()); for (TournamentContent tournament : tournamentList) { Item item = new Item(); String date = String.format("%1$tY-%1$tm-%1$td", tournament.get PublicationDate()); item.setAuthor(tournament.getAuthor()); item.setTitle(String.format("%s – Autor: %s", tournament.getName(), tournament.getAuthor())); item.setPubDate(tournament.getPublicationDate()); item.setLink(tournament.getLink()); items.add(item); } return items; } }
Pierwszą wartą uwagi cechą tej klasy jest importowanie kilku klas biblioteki Project Rome z pakietu com.sun.syndication.feed.rss. Ponadto klasa ta dziedziczy po klasie AbstractRssFeedView ze Springa. Dzięki temu trzeba tylko zaimplementować dwie odziedziczone po tej klasie metody: buildFeedMetadata i buildFeedItems. Metoda buildFeedMetadata przypomina metodę o tej samej nazwie utworzoną dla formatu Atom. Zauważ, że metoda buildFeedMetadata manipuluje obiektem typu Channel opartym na klasie z biblioteki Project Rome. Ten typ służy do tworzenia kanału RSS, natomiast używany wcześniej typ Feed jest przeznaczony do tworzenia kanału Atom. Wywołania setterów dla obiektu typu Channel (setTitle, setDescription, setLink itd.) wykorzystuje się do przypisywania metadanych do kanału RSS. Nazwa buildFeedItems jest inna niż nazwa analogicznej metody dla formatu Atom ( buildFeedEntries).
Wynika to z tego, że powtarzające się elementy w kanałach Atom są nazywane wpisami (ang. entries), natomiast w formacie RSS są to elementy (ang. items). Pomimo różnych nazw działanie tych metod jest podobne. W metodzie buildFeedItems używany jest obiekt typu Map w celu pobrania obiektu feedContent ustawionego w metodzie obsługi. Następnie kod przechodzi w pętli po zawartości obiektu feedContent (obejmuje on listę obiektów TournamentContent) i dla każdego elementu tworzy obiekt typu Item, który jest przypisywany do nadrzędnej listy elementów tego typu. Po zakończeniu działania pętli metoda zwraca zapełnioną listę obiektów typu Item. Uwaga Jeśli chcesz przypisać więcej wartości do sekcji metadanych i powtarzających się elementów w kanale RSS, a także określić konkretną wersję formatu RSS, zapoznaj się z interfejsem API biblioteki Project Rome. Wersja domyślna to RSS 2.0.
344
9.4. PUBLIKOWANIE DANYCH W FORMACIE JSON W USŁUGACH TYPU REST
Gdy po dodaniu tej klasy (i opisanego wcześniej kontrolera platformy Spring MVC) przejdziesz pod adres w postaci http://[host_name]/rssfeed.rss (lub http://[host_name]/rssfeed.xml), otrzymasz następującą odpowiedź: World Soccer Tournaments tennis.org FIFA World Soccer Tournament Calendar Thu, 04 Mar 2010 21:45:08 GMT -
World Cup - Posted by FIFA www.fifa.com/worldcup/ Thu, 04 Mar 2010 21:45:08 GMT FIFA -
U-20 World Cup - Posted by FIFA www.fifa.com/u20worldcup/ Thu, 04 Mar 2010 21:45:08 GMT FIFA -
U-17 World Cup - Posted by FIFA www.fifa.com/u17worldcup/ Thu, 04 Mar 2010 21:45:08 GMT FIFA -
Confederations Cup - Posted by FIFA www.fifa.com/confederationscup/ Thu, 04 Mar 2010 21:45:08 GMT FIFA
9.4. Publikowanie danych w formacie JSON w usługach typu REST Problem Programista chce publikować dane w formacie JSON (ang. JavaScript Object Notation) w aplikacji Springa.
Rozwiązanie JSON stał się jednym (obok RSS-a i Atoma) z ulubionych formatów danych w usługach typu REST. Jednak w odróżnieniu od innych formatów, w których najczęściej wykorzystuje się XML, JSON ma specjalną notację opartą na JavaScripcie. W tej recepturze oprócz dostępnej w Springu obsługi usług typu REST używana jest też klasa MappingJacksonJsonView. Jest to klasa Springa ułatwiająca publikowanie treści w formacie JSON.
345
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
Uwaga Klasa MappingJacksonJsonView wymaga biblioteki Jackson JSON (możesz ją pobrać ze strony http://wiki.fasterxml.com/JacksonDownload). Jeśli używasz Mavena, dodaj do projektu następującą zależność: org.codehaus.jackson jackson-mapper-asl 1.4.2
Po co publikować dane w formacie JSON? Jeśli w rozwijanej aplikacji Springa używasz Ajaksa, bardzo prawdopodobne jest, że utworzysz usługi typu REST publikujące dane w formacie JSON. Wynika to głównie z ograniczonych możliwości przetwarzania danych po stronie przeglądarki. Choć przeglądarki potrafią przetwarzać i pobierać dane z usług typu REST publikujących informacje w formacie XML, rozwiązanie to nie jest wydajne. Jeśli zamiast tego zdecydujesz się na publikowanie danych w formacie JSON (opartym na języku JavaScript, dla którego przeglądarki mają wbudowany interpreter), przetwarzanie i pobieranie danych będzie bardziej efektywne. Formaty RSS i Atom mają określone standardy, natomiast format JSON nie ma zdefiniowanej struktury, jedynie opisaną dalej składnię. Dlatego struktura danych w tym formacie jest ustalana we współpracy z członkami zespołu odpowiedzialnymi za projektowanie ajaksowych aspektów aplikacji.
Jak to działa? Przede wszystkim trzeba ustalić, jakie dane mają być publikowane w formacie JSON. Te dane mogą znajdować się w relacyjnej bazie lub w pliku tekstowym; dostęp do nich można uzyskać za pomocą technologii JDBC lub ORM; mogą stanowić część ziarna Springa lub innej struktury itd. Omówienie tych zagadnień wykracza poza zakres tej receptury, dlatego załóżmy, że masz środki potrzebne do uzyskania dostępu do danych. Jeśli nie znasz formatu JSON, zapoznaj się z przedstawionym poniżej fragmentem danych w tym formacie. { "glossary": { "title": "example glossary", "GlossDiv": { "title": "S", "GlossList": { "GlossEntry": { "ID": "SGML", "SortAs": "SGML", "GlossTerm": "Standard Generalized Markup Language", "Acronym": "SGML", "Abbrev": "ISO 8879:1986", "GlossDef": { "para": "Język metaznaczników używany do tworzenia języków znaczników (takich jak DocBook).", "GlossSeeAlso": ["GML", "XML"] }, "GlossSee": "markup" } } } } }
346
9.4. PUBLIKOWANIE DANYCH W FORMACIE JSON W USŁUGACH TYPU REST
Jak widać, dane w formacie JSON obejmują tekst i separatory w postaci {, }, [, ], : i ". Nie omawiamy tu szczegółowo poszczególnych separatorów, warto jednak wiedzieć, że w porównaniu z formatem XML składnia języka JSON ułatwia silnikom języka JavaScript dostęp do danych i manipulowanie nimi. Ponieważ z receptur 9.1 i 9.3 dowiedziałeś się już, jak publikować dane za pomocą usług typu REST, tu przechodzimy od razu do metody usługi potrzebnej w kontrolerze platformy Spring MVC do zarządzania tym procesem. @RequestMapping("/jsontournament") public String getJSON(Model model) { List tournamentList = new ArrayList(); tournamentList.add(TournamentContent.generateContent("FIFA", new Date(),"World Cup","www.fifa.com/worldcup/")); tournamentList.add(TournamentContent.generateContent("FIFA", new Date(),"U-20 World Cup","www.fifa.com/u20worldcup/")); tournamentList.add(TournamentContent.generateContent("FIFA", new Date(),"U-17 World Cup","www.fifa.com/u17worldcup/")); tournamentList.add(TournamentContent.generateContent("FIFA", new Date(),"Confederations Cup","www.fifa.com/confederationscup/")); model.addAttribute("feedContent",tournamentList); return "jsontournamenttemplate"; }
Ta metoda usługi jest powiązana z adresami URL w postaci http://[host_name]/[appname]/jsontournament. Owa metoda, podobnie jak metody obsługi używane w recepturze 9.3 do publikowania danych w formatach Atom i RSS, definiuje listę obiektów typu TournamentContent (są to obiekty POJO). Ta lista jest potem przypisywana do obiektu typu Model z metody obsługi, dzięki czemu dane są dostępne w zwracanym widoku. Tu nazwa zwracanego widoku logicznego to jsontournamenttemplate. Ten widok logiczny jest zdefiniowany w XML-owym pliku konfiguracyjnym Springa:
Zauważ, że widok logiczny jsontournamenttemplate jest powiązany z klasą MappingJacksonJsonView Springa. Ta klasa przekształca całą zawartość odwzorowania z modelu (ustawionego w metodzie obsługi) na format JSON. Jeśli teraz otworzysz adres URL w postaci http://[host_name]/jsontournament.json (lub http://[host_name]/ jsontournament.xml), otrzymasz następującą odpowiedź: { "handlingTime":1, "feedContent": [ {"link":"www.fifa.com/worldcup/", "publicationDate":1267758100256, "author":"FIFA", "name":"World Cup", "id":16}, {"link":"www.fifa.com/u20worldcup/", "publicationDate":1267758100256, "author":"FIFA", "name":"U-20 World Cup", "id":17}, {"link":"www.fifa.com/u17worldcup/", "publicationDate":1267758100256, "author":"FIFA", "name":"U-17 World Cup", "id":18}, {"link":"www.fifa.com/confederationscup/", "publicationDate":1267758100256,
347
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
"author":"FIFA", "name":"Confederations Cup", "id":19} ] }
Dostęp do danych w formacie JSON w usługach typu REST Choć JSON jest popularnym formatem publikowania danych w usługach typu REST, poza przeglądarkami stosunkowo rzadko pobiera się dane w tym formacie. Oznacza to, że bardziej prawdopodobne jest, że w aplikacji Springa będziesz publikował dane w formacie JSON, niż z nich korzystał. Technicznie możliwe jest uzyskanie dostępu do danych w tym formacie w aplikacji Springa (czyli po stronie serwera). Służą do tego niezależne biblioteki Javy, na przykład JSON-LIB (http://json-lib.sourceforge.net/). Jednak w aplikacjach Springa często lepiej jest pobierać dane z usług typu REST w formacie XML. Wynika to z wbudowanej obsługi XML-a w Javie. Ponadto format XML jest bardziej intuicyjny, a w aplikacjach przetwarzanie danych w tym formacie jest łatwiejsze niż w przeglądarkach.
9.5. Dostęp do usług typu REST zwracających skomplikowane odpowiedzi w formacie XML Problem Programista chce uzyskać dostęp do usług typu REST zwracających skomplikowane odpowiedzi w formacie XML. Pobrane dane mają być używane w aplikacji Springa.
Rozwiązanie Usługi typu REST stały się popularnym narzędziem publikowania informacji. Jednak struktura danych zwracanych przez niektóre usługi tego rodzaju jest skomplikowana. Choć klasa RestTemplate Springa potrafi wykonywać różne operacje na usługach typu REST, tak aby zwracane dane można było wykorzystać w aplikacjach Springa, przetwarzanie złożonych odpowiedzi w formacie XML wymaga zastosowania także innych technik. Są one oparte na strumieniach danych, współdziałaniu klasy HttpConverterMessage Springa z językiem XPath (jest to oparty na XML-u język zapytań używany do pobierania węzłów z dokumentów XML) oraz pomocniczych narzędziach (takich jak klasa XPathTemplate Springa).
Jak to działa? Ponieważ z receptury 9.2 dowiedziałeś się już, jak uzyskać dostęp do usług typu REST za pomocą klasy RestTemplate Springa, tu koncentrujemy się na przetwarzaniu skomplikowanych odpowiedzi w formacie XML z kanału RSS. Zacznijmy od dostępu do kanału RSS z prognozą pogody. Punkt końcowy tego kanału to: http://rss.weather.com/rss/national/rss_nwf_rss.xml?cm_ven=NWF&cm_cat=rss&par=NWF_rss. Na podstawie informacji z receptury 9.2 można utworzyć w kontrolerze platformy Spring MVC przedstawioną poniżej metodę obsługi zapewniającą dostęp do tego kanału RSS. @RequestMapping("/nationalweather") public String getWeatherNews(Model model) { // Zwraca widok nationalweathertemplate. Ziarno określające wiąże
348
9.5. DOSTĘP DO USŁUG TYPU REST ZWRACAJĄCYCH SKOMPLIKOWANE ODPOWIEDZI W FORMACIE XML
// ten widok z plikiem /WEB-INF/jsp/nationalweathertemplate.jsp String result = restTemplate.getForObject("http://rss.weather.com/rss/ national/rss_nwf_rss.xml?cm_ven={cm_ven}&cm_cat={cm_cat} &par={par}", String.class, "NWF","rss","NWF_rss"); model.addAttribute("nationalweatherfeed",result); return "nationalweathertemplate"; }
Metoda obsługi korzysta z metody getForObject z klasy RestTemplate i przypisuje zwracane dane w formacie XML do obiektu typu String, który następnie jest dodawany do obiektu typu Model z metody obsługi. Po dodaniu obiektu typu String metoda przekazuje sterowanie do widoku logicznego nationalweathertemplate, dzięki czemu dane w formacie XML można wyświetlić żądającej ich jednostce. Ten kod jest identyczny z kodem przedstawionym w recepturze 9.2. Jednak ponieważ tu występują bardziej skomplikowane dane w formacie RSS, wygodnie jest przypisać informacje z usługi typu REST do obiektu innego typu niż String. Dlaczego? Aby można było łatwiej pobierać dane z usługi typu REST i manipulować nimi. By w Springu pobierać dane z usługi typu REST do obiektu innego typu niż String, należy zapoznać się z interfejsem HttpConverterMessage. Wszystkie obiekty zwracane i przekazywane w metodach klasy RestTemplate (opisanych w tabeli 9.1) są przekształcane na komunikaty HTTP i w drugą stronę za pomocą klasy z implementacją tego interfejsu. Domyślne implementacje interfejsu HttpConverterMessage zarejestrowane w klasie RestTemplate to: ByteArrayHttpMessageConverter, StringHttpMessageConverter, FormHttpMessageConverter i SourceHttpMessage Converter. To oznacza, że można zrzutować dane z usługi typu REST na tablicę bajtów, tablicę łańcuchów znaków, dane formularza lub źródło danych. Wskazówka Można też pisać własne konwertery zgodne z interfejsem MarshallingHttpMessageConverter. Pozwala to zastosować niestandardowe klasy szeregujące. Używanie niestandardowych konwerterów wymaga zarejestrowania ich we właściwości messageConverters ziarna w aplikacji Springa. Ponadto za pomocą tej właściwości można zastąpić domyślne implementacje zarejestrowane w klasie RestTemplate.
Poniższy fragment kodu pokazuje, jak zrzutować dane z usługi typu REST na obiekt typu StreamSource (javax.xml.transform.stream.StreamSource), który jest implementacją interfejsu Source (javax.xml. transform.Source). StreamSource result = restTemplate.getForObject(" http://rss.weather.com/rss/national/rss_nwf_rss.xml? cm_ven={cm_ven}&cm_cat={cm_cat}&par={par}", StreamSource.class, "NWF","rss","NWF_rss");
Dzięki temu łatwiej jest pobierać dane z usługi typu REST i manipulować nimi, ponieważ można wykorzystać do tego dającą duże możliwości klasę StreamSource. Jeden ze sposobów pobierania danych za pomocą klasy StreamSource związany jest z późniejszym zapisaniem ich w klasie, która ułatwia manipulowanie nimi. Można posłużyć się w tym celu dostępnym w Javie interfejsem dla modelu DOM (ang. Document Object Model); jest to interfejs org.w3c.dom.Document. W tym podejściu dane od usługi typu REST można pobierać w precyzyjny sposób. Poniższy listing przedstawia metodę obsługi z platformy Spring MVC. Metoda ta pobiera dane od usługi typu REST za pomocą interfejsu Document i obiektu klasy StreamSource utworzonego przy użyciu klasy RestTemplate. StreamSource source = restTemplate.getForObject(" http://rss.weather.com/rss/national/rss_nwf_rss.xml? cm_ven={cm_ven}&cm_cat={cm_cat}&par={par}", StreamSource.class, "NWF","rss","NWF_rss"); // Definiowanie obiektu typu DocumentBuilderFactory DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setValidating(false);
349
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
dbf.setIgnoringComments(false); dbf.setIgnoringElementContentWhitespace(true); dbf.setNamespaceAware(true); // Definiowanie obiektu typu DocumentBuilder DocumentBuilder db = null; db = dbf.newDocumentBuilder(); // Definiowanie obiektu typu InputSource InputSource is = new InputSource(); is.setSystemId(source.getSystemId()); is.setByteStream(source.getInputStream()); is.setCharacterStream(source.getReader()); is.setEncoding("ISO-8859-1"); // Definiowanie dokumentu DOM W3C Document doc = db.parse(is); // Pobieranie elementów NodeList itemElements = doc.getElementsByTagName("item"); // Definiowanie list z tytułami wiadomości i odnośnikami do nich List feedtitles = new ArrayList(); List feedlinks = new ArrayList(); // Przechodzenie w pętli po wszystkich elementach int length = itemElements.getLength(); for ( int n = 0; n < length; ++n ) { NodeList childElements = itemElements.item(n).getChildNodes(); int lengthnested = childElements.getLength(); for ( int k = 0; k < lengthnested; ++k ) { if (childElements.item(k).getNodeName() == "title") { feedtitles.add(childElements.item(k).getChildNodes().item(0).getNodeValue()); } if (childElements.item(k).getNodeName() == "link") { feedlinks.add(childElements.item(k).getChildNodes().item(0).getNodeValue()); } } } // Lista na treść wiadomości List feedcontent = new ArrayList(); int titlelength = feedtitles.size(); // Przechodzenie w pętli po pobranych tytułach wiadomości i odnośnikach do nich for ( int x = 0; x < titlelength; ++x ) { feedcontent.add(new FeedContent((String)feedtitles.get(x),(String)feedlinks.get(x))); } // Zapisywanie typu, wersji i danych kanału w obiekcie model model.addAttribute("feedtype", doc.getDocumentElement().getNodeName()); model.addAttribute("feedversion", doc.getDocumentElement().getAttribute("version")); model.addAttribute("feedcontent",feedcontent); return "nationalweathertemplate";
Ostrzeżenie Przy przetwarzaniu danych w formacie XML od usług typu REST często występują problemy z kodowaniem. Większość danych w formacie XML jest poprzedzona instrukcjami w postaci lub . To oznacza, że dla danych zastosowano kodowanie UTF-8 lub ISO-8859-1. Czasem dane w ogóle nie obejmują znacznika określającego
350
9.5. DOSTĘP DO USŁUG TYPU REST ZWRACAJĄCYCH SKOMPLIKOWANE ODPOWIEDZI W FORMACIE XML
kodowanie. Takie instrukcje (lub ich brak) mogą utrudniać przetwarzanie danych przez ich odbiorcę. Błędy przetwarzania, na przykład Invalid byte 1 of 1-byte UTF-8 sequence, występują stosunkowo często, gdy informacje o kodowaniu są niezgodne z rzeczywistym kodowaniem. Aby poradzić sobie z tego rodzaju problemami, konieczne może być zastosowanie dla danych konkretnego kodowania lub bezpośrednie ustawienie kodowania w klasie przekształcającej dane. Pozwala to na ich poprawne przetwarzanie.
Przedstawione tu rozwiązanie obejmuje wiele etapów. Po zrzutowaniu danych od usługi typu REST na obiekt typu StreamSource tworzone są obiekty typów: DocumentBuilderFactory, DocumentBuilder i InputSource. Te ostatnie klasy to standardowe narzędzia do manipulowania danymi w formacie XML w aplikacjach Javy. Umożliwiają dostosowanie do własnych potrzeb procesu pobierania danych. Pozwalają na przykład określić kodowanie, sprawdzić poprawność danych i przekształcić je za pomocą pliku XSL. Przy użyciu tych klas dane od usługi typu REST są umieszczane w obiekcie typu Document. Gdy dane są już zapisane w obiekcie typu Document, kod kilkakrotnie je przetwarza. Używane są przy tym klasy i metody związane z modelem DOM (NodeList, Node, getChildNodes() itd.). Po zakończeniu pobierania danych trzy obiekty (feedtype, feedversion i feedcontent) są przypisywane do obiektu typu Model z metody obsługi. Pozwala to wyświetlić potrzebne wartości w zwracanym widoku. Tu zwracany widok odpowiada plikowi JSP, który wyświetla wartości obiektów użytkownikowi. Jak możesz się przekonać, dane od usługi typu REST można teraz pobrać w bardziej precyzyjny sposób niż z obiektu typu String Javy (to rozwiązanie zastosowano w recepturze 9.2). Inny sposób pobierania danych od usługi typu REST wymaga zastosowania języka XPath. Jest to język zapytań powiązany z formatem XML. Tak jak SQL służy do precyzyjnego pobierania danych z baz relacyjnych, XPath wykonuje to samo zadanie dla danych w formacie XML. Składnia języka XPath i scenariusze jego stosowania są złożone — w końcu jest to kompletny język. Dlatego tu omawiamy podstawy tego języka i sposoby integrowania go z usługami typu REST i ze Springiem. Więcej szczegółów na temat składni języka XPath i sposobów jego użytkowania znajdziesz w specyfikacji, dostępnej pod adresem http://www.w3.org/TR/xpath/. XPath (podobnie jak model DOM) jest obsługiwany w samej Javie. Poniższy listing przedstawia wcześniej stosowaną metodę obsługi z platformy Spring MVC, jednak tym razem w metodzie wykorzystano interfejs javax.xml.xpath.XPathExpression Javy. // Kod przekształcający dane od usługi typu REST na obiekt typu Document // pominięto w celu zachowania zwięzłości // Definiowanie obiektu typu Document Document doc = db.parse(is); // Definiowanie list na tytuły wiadomości i odnośniki do nich List feedtitles = new ArrayList(); List feedlinks = new ArrayList(); // Definiowanie obiektów na potrzeby języka XPath XPathFactory factory = XPathFactory.newInstance(); XPath xpath = factory.newXPath(); // Definiowanie wyrażenia XPath pobierającego tytuły XPathExpression titleexpr = xpath.compile("//item/title"); // Definiowanie wyrażenia XPath pobierającego odnośniki XPathExpression linkexpr = xpath.compile("//item/link"); // Przetwarzanie wyrażenia XPath Object titleresult = titleexpr.evaluate(doc, XPathConstants.NODESET); Object linkresult = linkexpr.evaluate(doc, XPathConstants.NODESET); // Przechodzenie w pętli po pobranych tytułach za pomocą modelu DOM NodeList titlenodes = (NodeList) titleresult; for (int i = 0; i < titlenodes.getLength(); i++) { feedtitles.add(titlenodes.item(i).getChildNodes().item(0).getNodeValue()); }
351
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
// Przechodzenie w pętli po pobranych odnośnikach za pomocą modelu DOM NodeList linknodes = (NodeList) linkresult; for (int j = 0; j < linknodes.getLength(); j++) { feedlinks.add(linknodes.item(j).getChildNodes().item(0).getNodeValue()); } // Umieszczanie wartości w obiekcie typu Model i zwracanie ich // do widoku pominięto w celu zachowania zwięzłości return "nationalweathertemplate";
Choć to podejście także wymaga przekształcenia danych od usługi typu REST na obiekt typu Document, warto zwrócić uwagę na to, że język XPath ułatwia pobieranie danych (w porównaniu z samym modelem DOM). W jednym miejscu wyrażenie XPath //item/title służy do pobrania wszystkich elementów zagnieżdżonych w elementach - . W innym miejscu wyrażenie XPath //item/link pozwala pobrać wszystkie elementy
zagnieżdżone w elementach - . Po pobraniu za pomocą języka XPath dane są rzutowane na obiekt typu NodeList modelu DOM, a ostatecznie zostają przypisane do obiektu typu Model z metody obsługi, dzięki czemu można je wyświetlić użytkownikowi. Oprócz wbudowanej klasy XPath Javy można też zastosować inne techniki związane z językiem XPath, używając klas Springa. Te rozwiązania są częścią projektu Spring XML. Uwaga Pliki JAR projektu Spring XML są udostępniane niezależnie od głównej platformy Spring. Można je pobrać ze strony
http://www.springsource.com/repository/app/bundle/version/download?name=org. spr ingframework.xml&version =1.5.9.A&type=binary. Jeśli używasz Mavena, dodaj do projektu następujące zależności: org.springframework.ws spring-xml 1.5.9 org.springframework.ws spring-oxm 1.5.9
Pierwsza technika, w której używane są klasy Springa i język XPath, wymaga zdefiniowania wyrażeń języka XPath w ziarnach Springa. Dzięki temu te wyrażenia można wykorzystać w wielu miejscach aplikacji. Ponadto można pominąć dodatkowe klasy związane z językiem XPath (na przykład XPathFactory i XPath), a zamiast tego zastosować dostępną w Springu metodę wstrzykiwania ziaren. Używane wcześniej wyrażenie języka XPath można zdefiniować w pliku konfiguracyjnym aplikacji Springa w następujący sposób:
Po zdefiniowaniu ziaren z wyrażeniami języka XPath można je wstrzyknąć do klasy kontrolera platformy Spring MVC i wykorzystać w metodzie obsługi. Odbywa się to w taki sam sposób, jakby wyrażenia były zadeklarowane za pomocą dostępnych w Javie klas związanych z językiem XPath (na przykład XPathFactory i XPath). Na poniższym listingu przedstawiony jest kontroler platformy Spring MVC zgodny z tym podejściem.
352
9.5. DOSTĘP DO USŁUG TYPU REST ZWRACAJĄCYCH SKOMPLIKOWANE ODPOWIEDZI W FORMACIE XML
@Autowired protected XPathExpression feedtitleExpression; @Autowired protected XPathExpression feedlinkExpression; // POCZĄTEK METODY OBSŁUGI // Kod do przekształcania danych od usługi typu REST na obiekt // typu Document pominięto w celu zachowania zwięzłości // Definiowanie obiektu typu Document Document doc = db.parse(is); // Definiowanie list z tytułami wiadomości i odnośnikami do nich List feedtitles = new ArrayList(); List feedlinks = new ArrayList(); List titlenodes = feedtitleExpression. evaluateAsNodeList(doc.getDocumentElement()); List linknodes = feedlinkExpression. evaluateAsNodeList(doc.getDocumentElement()); for (Node node : titlenodes) { feedtitles.add(node.getChildNodes().item(0).getNodeValue()); } for (Node node : linknodes) { feedlinks.add(node.getChildNodes().item(0).getNodeValue()); } // Umieszczanie wartości w obiekcie typu Model i zwracanie ich // do widoku pominięto w celu zachowania zwięzłości return "nationalweathertemplate";
Warto zauważyć, że dzięki zastosowaniu klas PathExpressionFactoryBean i XPathExpression Springa kod do pobierania danych od usługi typu REST stał się prostszy. Przede wszystkim ziarna z wyrażeniami XPath są wstrzykiwane do klasy za pomocą adnotacji @Autowired. Gdy ziarna są już dostępne, można je przetworzyć. W tym celu należy przekazać dane od usługi typu REST zapisane jako obiekt typu Document. Zwróć też uwagę na to, że wyniki przetwarzania listy evaluateAsNodeList (należy ona do klasy XPathExpression Springa) są rzutowane na listę obiektów typu Node, a nie na obiekt typu NodeList modelu DOM. Także ta technika upraszcza proces pobierania danych, ponieważ można zastosować skrócony zapis dla pętli Javy. Przy stosowaniu projektu Spring XML można też wykorzystać klasę NodeMapper. Służy ona do bezpośredniego odwzorowywania węzłów z dokumentu XML na obiekty Javy. Na potrzeby omówienia klasy NodeMapper załóżmy, że używane jest poniższe ziarno z wyrażeniem języka XPath. Ziarno to jest zdefiniowane w pliku konfiguracyjnym Springa.
W tej definicji ziarna atrybut value ma wartość //item. Ta wartość języka XPath oznacza, że należy pobrać wszystkie elementy - z danych w formacie XML. Elementy te reprezentują powtarzające się pozycje z kanału RSS i same obejmują inne elementy (na przykład
i ). Na poniższym listingu pokazano, jak używać klasy NodeMapper i przedstawionego wcześniej ziarna z wyrażeniem XPath w kontrolerze platformy Spring MVC.
353
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
@Autowired protected XPathExpression feeditemExpression; // POCZĄTEK METODY OBSŁUGI // Kod do przekształcania danych od usługi typu REST na obiekt // typu Document pominięto w celu zachowania zwięzłości // Definiowanie obiektu typu Document Document doc = db.parse(is); // Definiowanie list z tytułami wiadomości i odnośnikami do nich List feedtitles = new ArrayList(); List feedlinks = new ArrayList(); List feedcontent = feeditemExpression.evaluate(doc, new NodeMapper() { public Object mapNode(Node node, int nodeNum) throws DOMException { Element itemElement = (Element) node; Element titleElement = (Element) itemElement.getElementsByTagName("title").item(0); Element linkElement = (Element) itemElement.getElementsByTagName("link").item(0); return new FeedContent(titleElement.getTextContent(), linkElement.getTextContent()); } } ); // Ustawianie typu, wersji i zawartości kanału w obiekcie model model.addAttribute("feedtype", doc.getDocumentElement().getNodeName()); model.addAttribute("feedversion", doc.getDocumentElement().getAttribute("version")); model.addAttribute("feedcontent",feedcontent); return "nationalweathertemplate";
Ziarno z wyrażeniem XPath jest wstrzykiwane do klasy kontrolera za pomocą adnotacji @Autowired (tak jak w poprzednim przykładzie). Zauważ jednak, że do tego ziarna (feedItemExpression) przekazywane są obiekty typu Document i NodeMapper. Obiekt typu Document reprezentuje dane w formacie XML zrzutowane z obiektu typu StreamSource przy użyciu klasy RestTemplate. Tę samą technikę zastosowano wcześniej. Jednak obiekt typu NodeMapper wymaga dokładnego omówienia. Obiekt ten przechodzi w pętli po elementach pasujących do ziarna z wyrażeniem XPath. Tu są to elementy - . W każdym powtórzeniu pętli za pomocą obiektu typu Element z modelu DOM pobierane są zagnieżdżone w elementach
- wartości
i . Zauważ też, że po każdym powtórzeniu pętli jest zwracany obiekt POJO typu FeedContent z pobranymi wartościami. W efekcie wynikiem przetwarzania wyrażenia XPath (feedItemExpression) jest lista obiektów typu FeedContent, a nie lista obiektów typu Node modelu DOM. Dzięki temu dane można przypisać bezpośrednio do obiektu typu Model metody obsługi, zamiast wcześniej przetwarzać obiekty typu Node modelu DOM. Jak widać, zastosowanie klasy NodeMapper Springa pozwala dodatkowo skrócić proces precyzyjnego pobierania danych od usługi typu REST. Następne podejście oparte na klasach Springa i języku XPath polega na wykorzystaniu typu XPathTemplate. Spośród wszystkich metod pobierania danych od usług typu REST w aplikacjach Springa ta technika pozwala otrzymać zdecydowanie najkrótszy kod. Przede wszystkim trzeba zdefiniować ziarno typu XPathTemplate Springa i udostępnić je w kontrolerze platformy Spring MVC (za pomocą wstrzyknięcia). Potrzebna konfiguracja jest pokazana poniżej. Ten kod należy umieścić w pliku konfiguracyjnym Springa.
354
9.5. DOSTĘP DO USŁUG TYPU REST ZWRACAJĄCYCH SKOMPLIKOWANE ODPOWIEDZI W FORMACIE XML
Uwaga Jaxp13XPathTemplate to implementacja typu XPathTemplate. Inna dostępna implementacja to JaxenXPathTemplate. Obie znajdują się w projekcie Spring XML i możesz zastosować dowolną z nich. Różnica polega na tym, że jedna z implementacji jest oparta na bibliotece JAXP 1.3 (jest ona częścią głównej platformy Javy 5), a druga — na bibliotece Jaxen (jest to otwarta biblioteka do obsługi języka XPath w Javie).
Po zdefiniowaniu ziarna typu XPathTemplate Springa można za pomocą adnotacji @Autowired wstrzyknąć je do kontrolera platformy Spring MVC, a następnie wykorzystać w metodzie obsługi. Ilustruje to poniższy listing. @Autowired protected org.springframework.xml.xpath.AbstractXPathTemplate feedXPathTemplate; @RequestMapping("/nationalweather") public String getWeatherNews(Model model) { Source source = restTemplate.getForObject(" http://rss.weather.com/rss/national/rss_nwf_rss.xml? cm_ven={cm_ven}&cm_cat={cm_cat}&par={par}", Source.class, "NWF","rss","NWF_rss"); // Definiowanie list z tytułami wiadomości i odnośnikami do nich List feedtitles = new ArrayList(); List feedlinks = new ArrayList(); List feedcontent = feedXPathTemplate.evaluate("//item", source, new NodeMapper() { public Object mapNode(Node node, int nodeNum) throws DOMException { Element itemElement = (Element) node; Element titleElement = (Element) itemElement.getElementsByTagName("title").item(0); Element linkElement = (Element) itemElement.getElementsByTagName("link").item(0); return new FeedContent(titleElement.getTextContent(), linkElement.getTextContent()); } } ); // Bez obiektu typu Document (tylko zapisane na stałe typ i wersja kanału) model.addAttribute("feedtype","rss"); model.addAttribute("feedversion","2.0"); // Dodawanie zawartości kanału pobranej za pomocą ziarna typu XPathTemplate model.addAttribute("feedcontent",feedcontent); return "nationalweathertemplate"; }
Zwróć uwagę na odpowiedź z metody getForObject klasy RestTemplate. Inaczej niż wcześniej odpowiedź jest tu przypisywana do interfejsu Source, a nie do obiektu typu StreamSource. Wynika to z tego, że XPathTemplate działa dla danych w formacie XML zapisanych jako obiekt typu Source. Warto przy tym wspomnieć, że klasa StreamSource implementuje interfejs Source, dlatego te typy są ze sobą zgodne. Zauważ, że wywołanie metody evaluate obiektu feedXPathTemplate typu XPathTemplate ma trzy parametry wejściowe: //item (reprezentuje zapytanie w języku XPath pobierające wszystkie elementy - ), source (reprezentuje referencję do danych w formacie XML, których dotyczy zapytanie w języku XPath) i obiekt typu NodeMapper (służy do przejścia po wszystkich elementach pobranych za pomocą zapytania XPath). Metoda 355
ROZDZIAŁ 9. USŁUGI REST W SPRINGU
evaluate zwraca listę obiektów typu FeedContent. Dzięki temu dane można od razu przypisać do obiektu typu Model z metody obsługi. Jak widać, dzięki zastosowaniu klasy XPathTemplate Springa proces pobierania danych od usługi typu REST udało się jeszcze bardziej skrócić. Wynika to z tego, że można pominąć obiekt typu Document modelu
DOM i wewnątrzwierszowo zadeklarować wyrażenia w języku XPath. Choć z powodu zwięzłości technika oparta na klasie XPathTemplate Springa wydaje się lepsza od wcześniej opisanych rozwiązań, czasem ważne są dodatkowe możliwości przy przetwarzaniu danych z usług typu REST. W przedstawionych wcześniej bardziej ogólnych metodach przetwarzania danych w formacie XML dostępnych jest więcej technik manipulowania danymi. Można na przykład wykorzystać filtry, bezpośrednio określić kodowanie i zastosować arkusze XSL. W niektórych sytuacjach daje to większe możliwości niż stosowanie klasy XPathTemplate Springa. Warto też wspomnieć, że oprócz omówionych tu podejść (są to najczęściej stosowane techniki przetwarzania danych w formacie XML) dostępnych jest wiele bibliotek służących do używania XML-a w Javie. Niektóre z tych bibliotek to: JDOM, Castor XML i Project Rome. Jeśli już znasz którąś z nich, możesz wykorzystać ją zamiast opisanych tu rozwiązań lub w połączeniu z nimi.
Używanie biblioteki Project Rome do dostępu do usług typu REST (RSS i Atom) Z receptury 9.3 dowiedziałeś się, jak publikować kanały Atom i RSS za pomocą biblioteki Project Rome. Biblioteka ta pozwala też na dostęp do danych z tych kanałów w kodzie Javy. Poniższy fragment kodu ilustruje dostęp do usług typu REST za pomocą interfejsu API biblioteki Project Rome: URL feedSource = new URL("http://rss.weather.com/ rss/national/rss_nwf_rss.xml?cm_ven=NWF& cm_cat=rss&par=NWF_rss"); SyndFeedInput input = new SyndFeedInput(); SyndFeed feed = input.build(new XmlReader(feedSource));
Jak widać, biblioteka Project Rome pozwala wywołać żądanie HTTP GET do usługi typu REST i przypisać zwrócone dane do obiektu klasy SyndFeed, który umożliwia dalsze manipulowanie nimi za pomocą interfejsu API tej biblioteki. Choć w tym podejściu nie są używane wbudowane w Spring ogólne techniki przetwarzania usług typu REST, możesz zastosować tę metodę, jeśli lubisz korzystać z biblioteki Project Rome i potrzebujesz jedynie dostępu do kanałów RSS lub Atom.
Podsumowanie Z tego rozdziału dowiedziałeś się, jak za pomocą Springa tworzyć usługi typu REST i pobierać z nich dane. Usługi typu REST są ściśle powiązane z platformą Spring MVC, gdzie kontroler przekierowuje żądania skierowane do takich usług, a także umożliwia dostęp do zewnętrznych usług i wykorzystanie pobranych danych w kontekście aplikacji. Dowiedziałeś się, że w usługach typu REST używane są adnotacje z kontrolerów platformy Spring MVC. Adnotacja @RequestMapping służy do określania punktów końcowych usług, a adnotacja @PathVariable pozwala ustawić parametry dostępu wykorzystywane do filtrowania danych od usługi. Ponadto poznałeś klasy do szeregowania w Springu danych w formacie XML (na przykład klasę Jaxb2Marshaller), które umożliwiają przekształcanie obiektów aplikacji na takie dane i zwracanie ich w usługach typu REST. Znasz już też klasę RestTemplate Springa. Obsługuje ona różne metody protokołu HTTP, w tym HEAD, GET, POST, PUT i DELETE. Wszystkie one umożliwiają dostęp do zewnętrznych usług typu REST bezpośrednio w kontekście aplikacji Springa i wykonywanie operacji na takich usługach. Dowiedziałeś się również, jak za pomocą interfejsu API biblioteki Project Rome publikować kanały Atom i RSS w aplikacji Springa. Zobaczyłeś też, w jaki sposób w aplikacjach Springa publikować dane w formacie JSON. Ponadto poznałeś różne techniki dostępu do usług typu REST zwracających skomplikowane dane w formacie XML. Te techniki to: strumienie danych, język XPath, jednostki szeregujące oraz klasy HttpMessageConverter i XPathTemplate Springa. 356
ROZDZIAŁ 10
Spring i Flex
Nadeszły ciekawe czasy dla programistów aplikacji sieciowych. W pierwszej dekadzie obecnego wieku pojawiły się bogate aplikacje internetowe. Aplikacje internetowe w ich obecnej postaci są efektem wielu kompromisów, na jakie musieli zdecydować się programiści tworzący programy mające działać w internecie — ostatecznej platformie. Na początku lat 90. ubiegłego wieku, gdy najpopularniejsza była architektura klient-serwer, każda aplikacja komunikowała się z serwerem w inny sposób, a większość klientów działała tylko w określonym systemie operacyjnym. Pojawienie się Javy pozwoliło oddzielić klienta od systemu operacyjnego, jednak interfejsy między klientami a serwerami nadal miały różne postacie. Później pojawiły się internet i sieć WWW, a wraz z nimi protokół HTTP. Internetu nie zaprojektowano jako platformy do uruchamiania aplikacji. W jego początkach nie istniały rozwiązania standardowe na przykład dla użytkowników języków Visual Basic lub Delphi. Nad tym, by internet stał się platformą aplikacji, trzeba było się napracować. Obecnie internet jest największą platformą na świecie. Daje zupełną swobodę — możesz utworzyć dowolną aplikację, która wymaga połączenia z siecią. Oparta na internecie sieć WWW początkowo miała być tylko zbiorem odnośników tworzącym wielką bibliotekę informacji. Powstrzymaj się z jej krytyką — sieć WWW jest niezwykle przydatna. Aby aplikacje mogły w niej działać, trzeba było włożyć w nie dużo pracy i często odchodzić od pierwotnych wyobrażeń na temat sieci WWW. Programiści musieli się z tym pogodzić. Tam gdzie to potrzebne, uzupełniali luki. W z natury bezstanowej platformie utworzyli sesje. Dodali do języka SGML znaczniki opisujące warstwę prezentacji, w efekcie czego powstał język HTML. Dla niezgodnych ze sobą przeglądarek utworzyli warstwę abstrakcji obsługiwaną za pomocą bibliotek, które stosowali z fanatycznym zacięciem. Zmodyfikowali założenia znanego wzorca MVC i opracowali model 2 MVC (odmiana wzorca MVC używana w Strutsie i w wielu pierwszych platformach MVC Javy) oraz wzorzec MVP (ang. model-view-presenter, czyli model-widok-prezenter; jest on stosowany najczęściej w środowisku .NET). Zrobiono dużo, aby dostosować sieć WWW do potrzeb programistów, jednak wciąż niełatwo jest opanować wszystkie elementy działającej aplikacji sieciowej. Dobry programista potrafi korzystać ze wszystkich technologii związanych z internetem i budować w ten sposób efektowne aplikacje. Takie programy działają w spójny sposób, szybko pracują i dobrze wyglądają. Wymagają stosowania takich technologii jak: HTML, CSS, JavaScript, DOM, XML, JSON, REST i — oczywiście — język programowania używany po stronie serwera. Naprawdę dobry programista dąży ponadto do tworzenia aplikacji, które działają spójnie we wszystkich przeglądarkach. Prawdziwy mistrz (a raczej cały zespół programistów) może nawet spróbować całkowicie ukryć słabości używanej platformy. Wymaga to napisania kompilatora, który na podstawie kodu w Javie generuje kod w JavaScripcie, a także zmiany działania HTML-a pod kątem wymagań stawianych aplikacji. Pozostali programiści, którzy chcą po prostu przygotować działającą aplikację, mogą pozwolić innym na wykonanie takiej pracy. Nie da się uniknąć wszystkich problemów, jednak można poradzić sobie z zadaniami, dla których nie utworzono jeszcze odpowiedniej warstwy abstrakcyjnej. Nie można zrezygnować z protokołu HTTP — to w końcu dzięki niemu sieć WWW jest tak wartościową platformą. Język programowania używany
ROZDZIAŁ 10. SPRING I FLEX
po stronie serwera zapewnia obsługę sesji, dlatego nie trzeba „naprawiać” tego aspektu sieci WWW. Ten problem został już więc rozwiązany. Większość potrzebnych narzędzi jest już gotowa. Jedyny kłopotliwy aspekt dotyczy tworzenia aplikacji dostępnej dla użytkownika. Istnieją platformy do tworzenia aplikacji sieciowych, które wykonują wiele skomplikowanych operacji po stronie serwera. Dostępne są też biblioteki komponentów, które rozwiązują problem różnic między przeglądarkami. Są to jednak tylko proste rozwiązania, zaprojektowane w taki sposób, aby działały w wielu klientach, dlatego nie mogą wykraczać poza możliwości żadnego z nich. Obecnie (w roku 2010) programiści aplikacji sieciowych zmagają się z wieloma zadaniami, których nie potrafią (szybko) wykonać, a które dla programisty aplikacji typu klient-serwer z lat 90. ubiegłego wieku (czyli około 20 lat temu!) nie stanowiły żadnego problemu. Te zadania i aspekty to na przykład: dostęp do systemu plików, multimedia, zaawansowana grafika, utrwalanie danych w pamięci lokalnej, skórki itd. Co gorsze, choć aplikacje w przeglądarkach działają coraz szybciej, bardzo nieliczne z nich działają z prędkością skompilowanych programów. Ponadto techniki sprawdzania poprawności danych z formularza, komunikowania się z serwerem i wyświetlania są w każdej platformie tworzenia aplikacji sieciowych inne. Gdy firma Macromedia (obecnie przejęta przez Adobe) wprowadziła na rynek technologię Flash, zrobiła to w celu udostępnienia zaawansowanych animacji z platformy Macromedia Shockwave w internecie. Z czasem narzędzia do tworzenia animacji zostały rozbudowane do postaci zaawansowanego środowiska programowania. Jedynym brakującym elementem była obsługa aplikacji przetwarzających duże ilości danych (do tego wcześniej służył język Visual Basic). Nie było więc niczym dziwnym to, że w 2004 roku, gdy firma Macromedia udostępniła środowisko Flex z bogatym zestawem kontrolek powiązanych z danymi i pełną obsługą wywołań zdalnych, zaciekawiło to programistów używających Javy. Pojawiły się pierwsze osoby stosujące tę technologię, choć ich liczba nie była duża. Problem polegał na tym, że platforma była komercyjna i droga. Narzędzia, pakiet SDK i warstwa pośrednia także były kosztowne. Dlatego niewielu programistów zdecydowało się ryzykować inwestowanie w niesprawdzoną architekturę, jaką był Flex. Koszty początkowe były zbyt wysokie. W 2005 roku Adobe kupił firmę Macromedia i zaczął bezpłatnie udostępniać niektóre narzędzia. Jednym z nich był pakiet SDK platformy Flex, dzięki czemu można było kompilować aplikacje z poziomu wiersza poleceń. Flash udostępnia protokół binarny AMF (ang. Action Message Format), używany przez maszyny wirtualne Flasha do komunikowania się z serwerami. Ten protokół jest szybki i zwięzły — zwłaszcza w porównaniu z formatami JSON i SOAP. W ramach wielu projektów o otwartym dostępie do kodu źródłowego programiści zastosowali wsteczną inżynierię do tego protokołu i udostępnili podobne, zastępcze narzędzia dla ulubionych platform (takich jak PHP, Python i — w świecie Javy — GraniteDS). Jednak najlepszym rozwiązaniem okazała się dobra implementacja, taka jak w drogim pakiecie warstwy pośredniej Lifecycle Data Services (LDS) udostępnionym przez firmę Adobe. W 2007 roku najważniejsze elementy pakietu LDS stały się dostępne jako oprogramowanie o otwartym dostępie do kodu źródłowego pod nazwą BlazeDS (poszczególne komponenty są dostępne na różnych licencjach; sama technologia BlazeDS jest oferowana na licencji LGPL). Dostępne narzędzia (oparte na Eclipse środowisko Adobe Flex Builder) nadal są płatne, jednak można samodzielnie utworzyć ich zastępniki. Zwłaszcza środowisko IDEA firmy IntelliJ w wersjach 8 i 9 zapewnia dobrą obsługę technologii Flex i AIR. Ponadto można tworzyć całe bazujące na tych technologiach aplikacje za pomocą wiersza poleceń lub niezależnych narzędzi (takich jak projekt FlexMojos dla Mavena 2; umożliwia on tworzenie aplikacji Flex przy użyciu Mavena). Ponieważ sama platforma stała się bardziej otwarta, szybko pojawiły się też otwarte projekty związane ze środowiskiem Flex. Jeśli chcesz wykorzystać technologię BlazeDS do używania usług Springa, dostępne jest specjalne narzędzie, Spring BlazeDS Integration, opracowane w ramach współpracy między firmami SpringSource i Adobe. Dzięki temu narzędziu praca w Springu i udostępnianie usług są bardzo proste. Po stronie klienta projekt Spring ActionScript pozwala wykorzystać w języku ActionScript wiele udogodnień dostępnych dla programistów Javy w Springu. Projekt ten posłuży tu do omówienia wstrzykiwania zależności w języku ActionScript, łączenia zdalnych usług i łączenia usług z komponentami środowiska Flex.
358
10.1. WPROWADZENIE DO ŚRODOWISKA FLEX
10.1. Wprowadzenie do środowiska Flex Problem Platforma Flex jest bardzo rozbudowana, dlatego warto ją poznać na ogólnym poziomie. Dalej omawiamy, do czego służą poszczególne narzędzia.
Rozwiązanie Poznasz tu narzędzia i technologie związane z platformą Flex. Najpierw opisany jest pakiet SDK i dostępne narzędzia. Dalej znajdziesz omówienie technologii warstwy pośredniej i aktualnego stanu rozwoju samej platformy.
Jak to działa? Pierwszą rzeczą, jaką należy wiedzieć o platformie Flex, jest to, że pod względem technicznym jest ona biblioteką zaimplementowaną na podstawie maszyny wirtualnej Flasha. Jeśli niedawno zirytowała Cię animowana reklama, prawdopodobnie zetknąłeś się już z Flashem. We Flashu przebieg czasu jest związany ze zmianą klatek. W każdej sekundzie środowisko uruchomieniowe Flasha odświeża w stałych odstępach czasu zawartość ekranu. Jeśli używasz zwykłego kodu we Flashu, musisz pracować z klatkami animacji, ponieważ są one związane z czasem trwania animacji. We Flashu obszar, w którym wyświetlana jest animacja, to scena. Komponenty są dodawane do sceny i mają określony cykl życia — podobnie jak obiekty modelu DOM w przeglądarce. We Flashu używany jest język ActionScript 3.0. Jest to odmiana JavaScriptu inspirowana różnymi wersjami tego języka. W istniejącym kodzie prawdopodobnie natrafisz na dwie odmiany ActionScriptu. Są to: ActionScript 2, stosowany we Flashu 8, i ActionScript 3, używany od Flasha 9. Te wersje znacznie różnią się między sobą. ActionScript 2 jest bardziej podobny do JavaScriptu używanego w przeglądarkach, natomiast ActionScript 3 przypomina JScript.NET, C# lub Javy. ActionScript, w odróżnieniu od znanych większości programistów wersji JavaScriptu używanych w przeglądarkach, jest językiem kompilowanym. Strony z kodem w ActionScripcie mają rozszerzenie .as, a rozszerzeniem plików binarnych jest .swf. Za pomocą narzędzi ActionScriptu możesz tworzyć dołączane biblioteki, podobne do bibliotek .dll z systemu Windows lub bibliotek .so z Linuksa. Takie biblioteki ActionScriptu mają rozszerzenie .swc. To może wydawać się dziwne dla programistów Javy nieużywających języków C i C++ oraz platformy .NET; w Javie wszystkie pliki binarne mają rozszerzenie .jar. Możliwość zastosowania w bibliotece .jar „a” biblioteki .jar „b” nie jest zależna od dostępności tej ostatniej w czasie kompilacji. Dalej, gdy zaczniesz używać ActionScriptu w Springu, zobaczysz, że biblioteki tego języka są dostępne jako pliki .swc. Flex jest zaimplementowany jako biblioteka oparta na maszynie wirtualnej Flasha. Gdy zajrzysz do kodu źródłowego platformy Flex, zauważysz, że duża jego część jest napisana w ActionScripcie. Biblioteka ta ma wyjątkowy status, ponieważ w komputerach większości osób jest dostępna bezpośrednio w odtwarzaczu multimediów. Flash i Flex działają na stronie w przeglądarce w podobny sposób do apletów Javy. Aplikacji Fleksa nie można zainstalować niezależnie w menu programów systemu operacyjnego. Nie można też manipulować nimi w systemie plików (wyjątkiem jest na przykład przesyłanie plików, nad czym programista nie ma kontroli; jest to jednak nietypowy przypadek). Ponadto cykl życia aplikacji Fleksa jest ograniczony działaniem przeglądarki. Gdy przeglądarka kończy pracę, to samo dotyczy też Fleksa. Aby rozwiązać ten problem, firma Adobe udostępniła środowisko AIR. Jest ono rozbudowaną wersją Fleksa i ma interfejs API umożliwiający manipulowanie systemem plików, instalowanie aplikacji, ich automatyczne aktualizowanie itd. Środowisko uruchomieniowe AIR (podobnie jak Flex) jest udostępniane przez firmę Adobe. Instalowanie aplikacji w środowisku AIR przypomina bardziej instalowanie aplikacji typu Java Web Start niż apletów. Po napisaniu aplikacji Fleksa można bardzo łatwo zmodyfikować zewnętrzny kontener w taki sposób, aby uruchamiać program w środowisku AIR. Tak więc aplikacje Fleksa są jednocześnie aplikacjami środowiska AIR, przy czym nie działa to w drugą stronę.
359
ROZDZIAŁ 10. SPRING I FLEX
Podstawy tworzenia aplikacji Fleksa Flash zapewnia oparte na animacjach środowisko z obsługą osi czasu, importowania grafiki itd. (przy czym do obsługi zdarzeń służą skrypty w ActionScripcie), natomiast Flex to platforma oparta na kodzie. Dwa podstawowe komponenty aplikacji Fleksa to pliki ActionScriptu (z rozszerzeniem .as) i pliki .mxml. Pliki ActionScriptu mogą zawierać klasy, publiczne funkcje i zmienne oraz adnotacje. W takich plikach można umieścić dowolną liczbę klas publicznych. Składnia i działanie języka ActionScript powinny wyglądać znajomo dla każdego użytkownika Javy, C# lub Scali. MXML to odmiana formatu XML przeznaczona do opisywania komponentów interfejsu użytkownika i generowania modelu DOM. Skrypty w plikach w tym formacie można zapisywać wewnątrzwierszowo (ta technika jest stosowana dalej w prostych przykładach z tego rozdziału). Każdy znacznik w pliku MXML opisuje komponent lub obiekt rejestrowany w kontenerze. W aplikacjach Fleksa zewnętrzny znacznik to . W środowisku AIR tę funkcję pełni znacznik . Znaczniki te opisują kontenery, które potrafią zarządzać tworzeniem komponentów dla obiektu Stage Flasha. Obiekty ActionScriptu można tworzyć w plikach MXML lub w Springu. Zauważysz, że w niektórych sytuacjach wygodniejsze jest stosowanie kodu skryptów, a w innych — plików MXML. Możesz nawet utworzyć całą aplikację Fleksa za pomocą ActionScriptu (bez używania plików MXML). W trakcie kompilacji pliki MXML są najpierw przekształcane na wyrażenia w ActionScripcie w formacie pośrednim, a dopiero w nim są ostatecznie kompilowane. Pliki MXML obsługują ograniczoną wersję wiązania za pomocą języka wyrażeń (ograniczoną w porównaniu z obsługą języków wyrażeń w takich platformach jak Tapestry lub JSF). Komponenty Fleksa są łączone ze sobą przy użyciu języka wyrażeń, a także rozbudowanego mechanizmu obsługi zdarzeń. Przyjrzyj się prostemu plikowi MXML.
Ten plik reprezentuje najprostszą możliwą aplikację Fleksa. Po wczytaniu aplikacji i skonfigurowaniu wszystkich obiektów zgłaszane jest zdarzenie applicationComplete. We Fleksie, podobnie jak w przeglądarce, istnieją dwa sposoby odbierania zdarzeń: w wyniku ich programowego zarejestrowania i za pomocą atrybutów w pliku MXML, dołączanych do komponentów zgłaszających zdarzenia. Tu odbiornik dodano przy użyciu atrybutu applicationComplete znacznika mx:Application. W większości sytuacji subskrybowanie zdarzeń w plikach MXML jest bardzo wygodne, jednak czasem przydatne jest też programowe rejestrowanie odbiorników. Przyjrzyj się teraz następnemu przykładowi. Tu komunikat jest wyświetlany w efekcie naciśnięcia przycisku, a zdarzenie jest rejestrowane w ActionScripcie.
360
10.1. WPROWADZENIE DO ŚRODOWISKA FLEX
applicationComplete="applicationCompleteHandler(event)">
To już ostatni przykład — nie zamierzamy Cię zanudzać. W końcu to odmiana JavaScriptu! Przedstawiony wcześniej kod można zmodyfikować tak, aby zastosować funkcję anonimową:
W tych przykładach nie trzeba wykonywać żadnych specjalnych operacji w celu uzyskania dostępu do obiektu wygenerowanego na podstawie znacznika . Wystarczy wskazać ten obiekt za pomocą wartości atrybutu id — button. Nie trzeba stosować instrukcji w postaci document.getElementById(String), która byłaby potrzebna w przeglądarce. Teraz przyjrzyj się prostej klasie ActionScriptu. Dalej rozbudujemy tę klasę, jednak na razie warto zobaczyć, jak definiować zwykłe obiekty ActionScriptu (nazywane też Plain Old ActionScript Object).
361
ROZDZIAŁ 10. SPRING I FLEX
package com.apress.springwebrecipes.auction.model { public class Item { private var _sellerEmail:String; private var _basePrice:Number; private var _sold:Date; private var _description:String; private var _id:Number; public function get id():Number { return _id; } public function set id(value:Number):void { _id = value; } // ... } }
Ten kod powinien być zrozumiały. Najbardziej charakterystyczna jest składnia w stylu pakietów z języków C++ lub C# (klasy znajdują się wewnątrz deklaracji pakietu). Tu używana jest tylko jedna klasa, jednak można zdefiniować ich dowolną liczbę. Nie zamierzamy szczegółowo omawiać reguł widoczności; modyfikatory public i private działają w standardowy sposób. Następnym wartym uwagi zagadnieniem jest składnia właściwości używana w ActionScripcie. Zadeklarowana jest tu zmienna klasy prywatnej, _id. Jej typ to Number. Dalej zdefiniowane są dwie funkcje. Różnią się one nieco od wcześniej przedstawianych. Występują w nich słowa kluczowe get i set, natomiast nazwa obu funkcji jest taka sama. Język ułatwia w ten sposób definiowanie getterów i setterów. Podobnie wygląda to w ziarnach JavaBeans Javy, jednak tu użytkownik może manipulować właściwościami tak jak publicznymi zmiennymi egzemplarza. Przedstawioną klasę można wykorzystać w następujący sposób: package com.apress.springwebrecipes { import com.apress.springwebrecipes.auction.model.Item; public class ItemDemo { public function ItemDemo() { var item:Item = new Item(); item.id = 232; } } }
Narzędzia Zdecydowanie najczęściej używanym narzędziem do tworzenia aplikacji Fleksa jest środowisko Flex Builder firmy Adobe. To właśnie z niego będziemy korzystać w tym rozdziale. Flex Builder to środowisko IDE oparte na narzędziu Eclipse. To środowisko zapewnia obsługę kompilacji, debugowania i kreatorów w zakresie tworzenia aplikacji w ActionScripcie i Fleksie. Dostępny jest też edytor graficzny, jednak okazuje się, że nie jest on zbyt przydatny przy tworzeniu średnich i dużych aplikacji. Flex Builder umożliwia korzystanie także z innych narzędzi z pakietu Adobe Creative Suite, takich jak Flash Professional (jest ono przydatne do tworzenia aplikacji i komponentów opartych na osi czasu), Photoshop i Illustrator.
362
10.1. WPROWADZENIE DO ŚRODOWISKA FLEX
Zdaniem niektórych osób najcenniejszą cechą Fleksa jest możliwość integracji z innymi narzędziami firmy Adobe w ramach procesu pracy obejmującego projektantów i programistów. We Fleksie występują stany. Umożliwiają one przypisanie dowolnych nazw do grup komponentów i konfiguracji (służy do tego właściwość currentState) oraz aktywowanie stanu za pomocą silnika. W stanie można określić, że komponenty mają zostać dodane do ekranu lub usunięte z niego. Można też całkowicie zastąpić bieżącą zawartość okna. Możliwości są naprawdę duże. Stany służą także do opisywania stanu kontrolek na ekranie. Pakiet Adobe Creative Suite ułatwia eksportowanie zasobów z programów Photoshop lub Illustrator i wiązanie warstw zasobów ze stanami w komponencie Fleksa. Wyobraź sobie przycisk o stanach związanych z najechaniem na niego kursorem myszy, kliknięciem myszą, zwolnieniem kursora myszy, aktywowaniem itd. Do każdego z tych stanów można przypisać warstwę z pliku Photoshopa. Na przykład warstwa związana z aktywowaniem może powodować rozjaśnienie przycisku, warstwa związana z kliknięciem myszą — przyciemnienie kolorów itd. Istnieje też wiele innych podobnych udogodnień, przy czym wszystkie efekty można uzyskać także bez środowiska Flex Builder (za pomocą niezależnego kompilatora), choć trzeba wiedzieć, jak to zrobić. Najprostszym i najtańszym sposobem na rozpoczęcie pracy z Fleksem jest wykorzystanie pakietu SDK. Możesz go pobrać bezpłatnie z witryny firmy Adobe. W czasie powstawania tej książki najnowsza wersja pakietu SDK miała numer 3.5. Można ją pobrać ze strony http://www.adobe.com/cfusion/entitlement/index.cfm?e=flex3sdk. Aby wykorzystać pakiet SDK, pobierz go i rozpakuj archiwum. Umieść ścieżkę do katalogu bin pakietu SDK w zmiennej PATH systemu (tak jak w przypadku narzędzi Ant, Maven lub Java SDK). Pakiet SDK Fleksa udostępnia dwa kompilatory. Jeden jest przeznaczony dla komponentów, a drugi — dla aplikacji Fleksa. Jeśli chcesz kompilować komponenty, zastosuj kompilator compc. Do kompilowania aplikacji służy kompilator mxmlc. Przykładowe wywołanie kompilatora mxmlc nie wymaga komentarza: mxmlc main.mxml
Kompilatory z pakietu SDK Fleksa są napisane w Javie, dlatego można je wywołać także za pomocą polecenia java. Poniżej znajduje się wywołanie kompilatora przy użyciu Javy. Działa ono tak samo jak wcześniejsza instrukcja. Poniższe polecenie należy wprowadzić w katalogu bin pakietu SDK. Nie powinno zawierać przerw — należy je wpisać w całości w jednym wierszu. java -jar ../lib/mxmlc.jar +flexlib ../frameworks test1.mxml
Inny sposób tworzenia aplikacji polega na wykorzystaniu Mavena. Projekt FlexMojos (http:// flexmojos.sonatype.org/) to zestaw wtyczek służących do tworzenia projektów we Fleksie i określania zależności (podobne zadania Maven wykonuje w projektach Javy)1. Ponieważ środowisko uruchomieniowe i kompilatory są dostępne, także inne środowiska IDE obsługują Fleksa. Bardzo dobrze zintegrowane z Fleksem jest na przykład środowisko IDEA 9.0 firmy IntelliJ. Obsługuje ono pisanie kodu, kompilowanie, refaktoryzację, debugowanie i wszystkie podstawowe operacje, których wykonywania programiści oczekują od dobrego środowiska IDE. Choć IDEA nie udostępnia edytora graficznego ani licznych kreatorów ze środowiska Flex Builder, ma pewne zalety, których Flex Builder nie ma. Na przykład obsługuje formatowanie kodu, importowanie projektów FlexMojos, a także refaktoryzację fragmentów plików MXML. Flex Builder to zestaw wtyczek oparty na środowisku Eclipse. Flex Builder może być uruchamiany niezależnie lub w połączeniu z zainstalowanym środowiskiem Eclipse. Używane samodzielnie środowisko Flex Builder nie ma edytora XML-a, mechanizmów obsługi systemu Subversion ani obsługi Javy. Ponadto Flex Builder jest zwykle powiązany z jedną ze starszych wersji środowiska Eclipse. Oczywiście istnieje wiele narzędzi do pisania aplikacji w omawianych technologiach. Narzędzia ze środowiska Flex Builder będą wyglądały znajomo dla użytkowników produktów firmy Adobe. Użytkownicy środowiska IntelliJ nie muszą z niego rezygnować. Przyszli programiści Fleksa mogą też korzystać z pakietu SDK i różnych systemów budowania aplikacji opartych na tej technologii.
1
Projekt FlexMojos obecnie nie jest rozwijany — przyp. tłum.
363
ROZDZIAŁ 10. SPRING I FLEX
10.2. Poza piaskownicę Problem Programista chce zintegrować Fleksa z Javą, jednak nie wie, z którego rozwiązania skorzystać. Które technologie sprawdzają się najlepiej i w jakich sytuacjach? Które (jeśli w ogóle) są zalecane?
Rozwiązanie Platforma Flex jest oczywiście bardzo elastyczna2. Odpowiedź na postawione wcześniej pytania naturalnie brzmi: „To zależy”. We Fleksie można komunikować się ze zdalnymi usługami na kilka sposobów: za pomocą żądań HTTP, komunikatów AMF, serializowania danych w formacie JSON, a nawet specjalnej obsługi usług typu REST i danych w formatach XML oraz SOAP. Flex dobrze współdziała także ze stronami internetowymi zawierającymi aplikacje Fleksa. Używany jest do tego JavaScript. Komunikacja jest tu dwukierunkowa: Flex może manipulować stroną, a sama strona może sterować aplikacją Fleksa. Głównym ograniczeniem przy korzystaniu ze wszystkich tych technologii jest piaskownica (ang. sandbox) Flasha, która przypomina piaskownicę apletów. Stosuje się ją po to, aby zagwarantować bezpieczeństwo systemu, w którym działają aplikacje Fleksa. We Fleksie wyjście poza piaskownicę wymaga ustawienia uprawnień w pliku crossdomain.xml, uzyskania pośredniego dostępu do standardowo niedostępnych usług hosta (używana jest do tego technologia BlazeDS lub podobne narzędzie) i przestrzegania odpowiednich zasad związanych z wykonywaniem kodu z serwera źródłowego.
Jak to działa? Większość tego rozdziału jest poświęcona integrowaniu Springa z technologią BlazeDS. Dostępne są też jednak inne rozwiązania, o których warto wiedzieć. Czasem pozwalają one ułatwić sobie pracę.
FlashVars Zacznijmy od najprostszego zadania — komunikowania się z nadrzędną stroną. Aplikacje Fleksa często trzeba konfigurować w momencie uruchamiania. Przy włączaniu aplikacji flashowej można podać parametry FlashVars (podobnie aplikacje Javy można uruchamiać z argumentami). Przed przejściem do tych parametrów warto omówić instalowanie aplikacji Fleksa. Aby zainstalować taką aplikację, należy dodać do przeglądarki kod umożliwiający wyświetlanie materiałów we Flashu (przeglądarki nie mają wbudowanej tej funkcji). Potrzebną konfigurację można uzyskać na kilka sposobów. Konfiguracja znaczników embed i object bywa skomplikowana, ponieważ nie istnieje jeden zestaw ustawień zapewniający poprawną pracę aplikacji w licznych docelowych przeglądarkach. Flex Builder generuje domyślną konfigurację, jednak uzyskany w ten sposób kod jest podatny na błędy i trudno jest go modyfikować. W celu zachowania zwięzłości pomijamy tu ten kod. Zamiast niego warto wykorzystać bibliotekę SWF Object JavaScript (http://code.google.com/p/swfobject/wiki/documentation). Wystarczy ją pobrać i umieścić w aplikacji sieciowej. Ta biblioteka dynamicznie generuje odpowiednie znaczniki object i plugin oraz parametry FlashVars. Ponieważ biblioteka dynamicznie dodaje materiały do stron, w przeglądarce Internet Explorer 6 przez kilka lat powodowała wyświetlenie ostrzeżenia „Dynamic Content”. Obecnie stanowi to mniejszy problem, ponieważ nowsze wydania Internet Explorera 6 (a także nowe wersje tej przeglądarki) nie wyświetlają już tego ostrzeżenia. Oto prosty sposób zastosowania tej biblioteki: Witaj, świecie 2
Nazwa „Flex” pochodzi od ang. flexible, czyli właśnie „elastyczny” — przyp. tłum.
364
10.2. POZA PIASKOWNICĘ
Pierwszy wiersz wyróżniony pogrubieniem dołącza skrypt. Dalej wywoływana jest metoda swfobject. embedSWF. Pierwszym parametrem wywołania jest nazwa zasobu SWF (podawana względem bieżącej strony). Drugi parametr to identyfikator elementu HTML-a, który należy zastąpić, gdy swfobject próbuje wyświetlić aplikację Fleksa. Tu aplikacja ma pojawić się w miejscu występowania elementu div o identyfikatorze helloworld (ten element jest wyróżniony w dalszym kodzie pogrubieniem). Tworzone egzemplarze wtyczki i obiektu będą miały identyfikator helloworld. Wróćmy do tematu tej receptury. W początkowej części kodu zdefiniowana jest zmienna flashVars. Jest to tablica asocjacyjna z kluczami i wartościami. Te klucze i wartości są przekazywane jako parametry (w ostatnim parametrze metody swfobject.embedSWF) do aplikacji Fleksa. W tej aplikacji dostęp do przekazanych zmiennych można uzyskać za pomocą obiektu typu Application.
365
ROZDZIAŁ 10. SPRING I FLEX
Za pomocą tych zmiennych można skonfigurować stan aplikacji Flasha (i Fleksa). Jest to dobry sposób na ustawianie opcji konfiguracyjnych (czasem nie trzeba przekazywać z nadrzędnej aplikacji żadnych innych danych). Jest to jednak, niestety, jednostronny sposób komunikacji. Jeśli potrzebujesz dwukierunkowego kanału komunikacyjnego między zewnętrzną aplikacją (tu jest nią aplikacja sieciowa Springa) a aplikacją Fleksa, musisz poszukać innych technik.
Klasa ExternalInterface Ponieważ Flex działa w maszynie wirtualnej Flasha, może się wydawać, że jest ograniczony, jednak w rzeczywistości może swobodnie komunikować się z zewnętrznym środowiskiem. Do kontaktowania się z nadrzędną stroną HTML służy klasa flash.external.ExternalInterface. W tej klasie zdefiniowane są dwie metody statyczne: call() (umożliwia aplikacji Fleksa komunikowanie się z hostem) i addCallback() (pozwala hostowi komunikować się z Fleksem). Klasa ExternalInterface obsługuje komunikację dwukierunkową — między Fleksem a hostem i w drugą stronę. Jest to dobre rozwiązanie, jeśli nie trzeba wykonywać wielu operacji we Fleksie. Możliwe, że chcesz tylko powiadamiać nadrzędną stronę o zdarzeniach w trakcie rysowania wykresu lub zgłosić jedno ajaksowe wywołanie. Ponadto czasem aplikacja Fleksa jest wykorzystywana w celu obsługi multimediów lub generowania obrazu, jednak wyświetlanie elementów odbywa się na podstawie zdarzeń z nadrzędnego kontenera. To pośrednie rozwiązanie jest oczywiście powolniejsze niż bezpośrednie wykonywanie operacji na nadrzędnej stronie lub w aplikacji Fleksa, jednak w wielu sytuacjach okazuje się korzystne. Jeśli chcesz komunikować się ze środowiskiem hosta, zastosuj metodę call() klasy ExternalInterface. Jest to wywołanie blokujące. Załóżmy, że w nadrzędnej stronie HTML zdefiniowana jest funkcja updateStatus: Dodawanie
Oczywiście można też wywoływać dostępne w środowisku hosta metody, takie jak window.alert(String). Aby wywołać w aplikacji Fleksa funkcję updateStatus, można zastosować przedstawione poniżej podejście:
366
10.2. POZA PIASKOWNICĘ
'Zakończono ładowanie '+ 'aplikacji Fleksa.'); } ]]>
Jeśli potrzebujesz komunikacji w drugą stronę — ze środowiska hosta do aplikacji Fleksa — musisz zarejestrować funkcje, które mają być dostępne w tym środowisku. Służy do tego metoda addCallback klasy ExternalInterface. Przyjmijmy, że w aplikacji Fleksa dostępna jest metoda, która przyjmuje w obiekcie typu String ścieżkę do pliku .mp3 i odtwarza dany utwór. Zacznijmy od utworzenia kodu we Fleksie:
Pierwszym parametrem metody ExternalInterface.addCallback jest alias używany do wywoływania danej funkcji na stronie nadrzędnej. Na tej stronie wywoływanie potrzebnej funkcji powinno być proste, jednak tak nie jest — chyba że programista zna drobne różnice między przeglądarkami. Poniżej pokazano stosunkowo bezpieczny sposób wywoływania funkcji playTrack na stronie nadrzędnej. Szafa grająca
367
ROZDZIAŁ 10. SPRING I FLEX
To podejście działa dobrze, gdy trzeba udostępnić funkcje w jedną lub drugą stronę. Wyobraź sobie, że nadrzędna aplikacja jest zbudowana głównie za pomocą tradycyjnych platform (takich jak: Struts, Spring MVC, JSF itd.) i Ajaksa (z usługami Springa udostępnianymi przy użyciu technologii DWR). Jeśli jednak konkretną funkcję lepiej jest napisać we Fleksie, można wykorzystać opisane podejście do utworzenia kilku punktów kontaktu potrzebnych w aplikacji Fleksa do komunikowania się z nadrzędną aplikacją.
Protokoły HTTP i HTTPS Komunikowanie się z dostępnymi zasobami za pomocą protokołu HTTP lub HTTPS jest jednym z najprostszych, a jednocześnie dających najwięcej możliwości mechanizmów kontaktowania się z innymi systemami. Dzięki szybkiemu wzrostowi popularności usług typu REST protokół HTTP jest w wielu architekturach jeszcze bardziej wartościowy. Ponadto mechanizm obsługi protokołu HTTP daje dostęp do licznych usług zwracających dane w formatach JSON i XML. Na szczęście Flex zapewnia dobrą obsługę tego protokołu. Co więcej, istnieją dwie solidne techniki posługiwania się tym protokołem. Pierwsza jest oparta na klasie mx.rpc.http.HTTPService Fleksa. Z tej klasy można korzystać zarówno w ActionScripcie, jak i w plikach MXML. Ta klasa działa podobnie jak polecenia wget i curl. Należy podać żądany adres URL (lub zasób), a następnie określić odbiornik, który ma wykonać operacje na pobranych danych. Klasa HTTPService obsługuje metody GET i POST. Jeśli korzystasz z jednostki pośredniczącej, możesz też zastosować metody PUT i DELETE (zwykle używa się ich przy używaniu usług typu REST). Aby można było za pomocą protokołu HTTP zażądać zasobu, musi on znajdować się w tej samej domenie co dany plik .swf lub plik crossdomain.xml z domeny zasobu musi zezwalać na żądania zgłaszane z domeny aplikacji. W celu wykorzystania klasy mx.rpc.http.HTTPService utwórz obiekt tego typu. Przyjrzyj się przykładowi zastosowania takiego obiektu:
368
10.2. POZA PIASKOWNICĘ
Alert.show('Błąd: ' + fe.fault.content); }); service.send(); } ]]>
Tu tworzony jest obiekt typu HTTPService, a do konstruktora przekazywany jest podstawowy adres URL, używany przy późniejszym przetwarzaniu żądań. Można oczywiście nie ustawiać tego adresu i podawać we właściwości url pełne adresy URL. Tu stosowane są żądania z adresami względnymi, dlatego wartość właściwości url (test.txt) należy połączyć z podstawowym adresem http://127.0.0.1:8080. W efekcie pełny adres URL zasobu to http://127.0.0.1:8080/test.txt. Jeśli umieściłeś aplikację sieciową w innym kontekście niż kontekst główny (/), koniecznie dodaj do członu test.txt odpowiedni przedrostek. We właściwości method ustawiany jest typ żądania HTTP. Nie trzeba jednak określać żądania GET, ponieważ jest ono stosowane domyślnie. Aby otrzymywać powiadomienia o udanym obsłużeniu żądania HTTP, należy skonfigurować odbiornik zdarzeń (dla zdarzenia result). W odbiorniku dostęp do treści odpowiedzi można uzyskać za pomocą właściwości re obiektu typu ResultEvent. Jeśli przetwarzanie żądania zakończyło się niepowodzeniem, powiadamia o tym odbiornik zdarzeń fault. Tu informacje o błędzie są pobierane z właściwości fault obiektu typu FaultEvent. Ostatecznie żądanie jest wysyłane. Można też podać nagłówki w postaci tablicy asocjacyjnej przekazanej jako parametr metody send, na przykład: ... service.send({customerID: '23232432sssh543'});
Dostępne są też inne możliwości, na przykład obsługa protokołu HTTPS, plików cookie, bardziej zaawansowanych metod kodowania żądań i odpowiedzi itd. Jeśli wolisz stosować podejście deklaratywne, format MXML obsługuje też deklarowanie i konfigurowanie obiektów typu HTTPService. Aby uzyskać ten sam efekt co wcześniej, wykorzystaj następującą konfigurację:
Teraz dany obiekt typu HTTPService można wskazać za pomocą wartości właściwości id. Żądania można zgłaszać tak jak wcześniej, w kodzie w ActionScripcie. W odpowiedzi wywołane zostaną zadeklarowane w pliku MXML odbiorniki zdarzeń (result lub fault). ... service.send();
Drugi sposób zgłaszania żądań HTTP polega na zastosowaniu klasy flash.net.URLLoader. Przedstawiane techniki różnią się między sobą w niewielkim stopniu, jednak warto znać je obie. Aby wykorzystać klasę flash.net.URLLoader, utwórz obiekt tego typu, skonfiguruj go, a następnie wywołaj metodę load z parametrem typu flash.net.URLRequest. Przebiega to podobnie jak używanie klasy HTTPService. W celu uzyskania dostępu do tego samego zasobu co w przykładzie dotyczącym klasy HTTPService zastosuj poniższy kod:
369
ROZDZIAŁ 10. SPRING I FLEX
import mx.controls.Alert; import mx.events.FlexEvent; private function applicationCompleteHandler(evt:FlexEvent):void { var urlLoader:URLLoader = new URLLoader(); urlLoader.addEventListener(Event.COMPLETE, function(evt:Event):void { Alert.show('Odpowiedź:' + urlLoader.data); }); urlLoader.load( new URLRequest('http://127.0.0.1:8080/test.txt')); } ]]>
Warto zwrócić uwagę na kilka istotnych różnic. Po pierwsze, dostęp do wynikowych danych zapewnia sama zmienna urlLoader, a nie zdarzenie generowane w momencie nadejścia tych danych. Po drugie, zamiast konfigurować docelowy adres URL w zmiennej urlLoader, należy przekazać go jako parametr do metody urlLoader.load. W zależności od preferencji ten interfejs API może okazać się bardziej naturalny w użyciu. Działa na nieco niższym poziomie i jest bardziej zwięzły. Usługi związane z protokołem HTTP to ogólny mechanizm komunikowania się z wieloma zasobami, w tym z usługami typu REST. Możesz też preferować usługi typu REST (ogólne i niezależne od platformy) zamiast usług specyficznych dla technologii AMF i Flex. W Javie EE 6.0 i Springu 3.0 tworzenie usług typu REST jest bardzo proste. Możesz woleć komunikować się z nimi za pomocą tradycyjnych interfejsów API do obsługi wywołań zdalnych, na przykład przy użyciu formatu SOAP. Jeśli jednak chcesz komunikować się z usługami SOAP, pamiętaj, że ręczne pobieranie zasobów w formacie XML i ich przetwarzanie za pomocą obsługi XML-a dostępnej w ActionScripcie to żmudne zadanie. Zamiast tego warto zastosować narzędzie lepiej dostosowane do potrzeb.
Korzystanie z usług SOAP Czasem trzeba pobierać dane w formacie SOAP. W Springu udostępnianie usług SOAP jest bardzo łatwe. Jeśli aplikacja musi pobierać dane w tym formacie, można to łatwo zrobić za pomocą platformy Flex. Proces przebiega tu podobnie jak przy korzystaniu z klasy HTTPService. Należy utworzyć obiekt odpowiedniej klasy, skonfigurować go, a następnie przesłać żądanie. Używanie obiektu typu WebService w pliku MXML jest trochę bardziej uciążliwe niż stosowanie klasy HTTPService, ponieważ wymaga wymienienia operacji udostępnianych przez punkt końcowy usługi SOAP.
370
10.2. POZA PIASKOWNICĘ
{ var at:AsyncToken = echoService.echo.send('Witaj, świecie!'); at.addEventListener(ResultEvent.RESULT, function(re:ResultEvent) :void { Alert.show('Wynik: ' + re.result + ''); // 'Echo: Witaj, świecie!' }); at.addEventListener(FaultEvent.FAULT, function(fe:FaultEvent) :void { Alert.show(fe.fault.toString()); }); } ]]>
Używanie klasy WebService w JavaScripcie jest proste i nie odbiega od wcześniej omawianych technik.
Tu kod importuje klasę bazową (w pierwszym przykładzie wykorzystano podklasę wygodną do stosowania w plikach MXML). Następnie trzeba pamiętać o wywołaniu metody loadWSDL() po skonfigurowaniu obiektu klasy WebService. Usługi SOAP dobrze sprawdzają się przy integrowaniu starszych systemów, jednak mało osób stosuje je w nowych projektach (zwłaszcza związanych z Fleksem), ponieważ istnieją lepsze rozwiązania.
371
ROZDZIAŁ 10. SPRING I FLEX
Potrzebny jest szybki i kompresowany do postaci binarnej format, który dobrze współdziała z Fleksem i Flashem, dzięki czemu szeregowanie skomplikowanych typów nie będzie sprawiać trudności. Taki format powinien też być łatwy w obsłudze w kodzie Javy. Używanie SOAP-a z pewnością nie jest proste. Te właśnie cechy ma format AMF.
Zdalne wywołania Flasha za pomocą formatu AMF Ta technika daje największe możliwości. AMF to format, który klienty Flasha mogą wydajnie odczytywać. Ponieważ jest to format binarny, jego przesyłanie jest wydajne (zwłaszcza w porównaniu z rozwlekłymi formatami tekstowymi, takimi jak: SOAP, XML i JSON). AMF ma wszystko, co można sobie wymarzyć. Jest zwięzły i wydajny, dobrze współdziała z Flashem i jest łatwy w użyciu. Jest doskonały. A przynajmniej prawie idealny. Pierwszy problem dotyczy tego, że usługi AMF nie są powszechne, a nawet gdyby były, niekoniecznie byłyby używane w korporacjach. To zagadnienie omawiamy w dalszych recepturach. Na razie zobacz, jak pobierać dane z punktów końcowych usług AMF za pomocą MXML-a i ActionScriptu. Używanie usług AMF w plikach MXML wymaga tylko skonfigurowania usługi i przesłania żądania. Odbywa się to podobnie jak w opisanych wcześniej technikach.
W początkowej części kodu konfigurowany jest obiekt . Nie trzeba w nim dokładnie określać docelowego punktu końcowego. Zamiast tego należy podać adres brokera (tu jest nim BlazeDS) i ustawić w atrybucie destination docelowy zasób (jego abstrakcyjną nazwę). Docelowy zasób to jednostka, do której
372
10.2. POZA PIASKOWNICĘ
wysyłane są komunikaty. To, czy komunikat ostatecznie dociera do usługi, do kolejki JMS, czy w inne miejsce, jest określone po stronie serwera. Klient nie musi o tym wiedzieć. W dalszym przykładzie usługi są wywoływane asynchronicznie. Aby otrzymywać powiadomienia z wynikowymi danymi lub informacjami o błędzie, należy skonfigurować wywoływane zwrotnie funkcje. Są one argumentami obiektu typu AsyncResponder. To ten obiekt ostatecznie obsługuje wyniki wywołania metody. Tu używane są dwie wywoływane zwrotnie funkcje. Jedna otrzymuje powiadomienia o wyniku zwróconym z serwera, a druga odbiera komunikaty o błędach. Aby uzyskać ten sam efekt w ActionScripcie, wystarczy przenieść konfigurację obiektu z pliku MXML do pliku z kodem w ActionScripcie. Tak jak wcześniej, wymaga to usunięcia członu mxml z nazw pakietów i utworzenia obiektu w ActionScripcie.
AMF to bardzo wygodny protokół. Jeśli masz łatwą w użyciu warstwę obsługi połączeń, która współdziała z jednym z protokołów opisanych wcześniej w tej recepturze, możesz zastosować jeden z tych protokołów. We wszystkich pozostałych sytuacjach warto pomyśleć o wykorzystaniu protokołu AMF. Pozostaje jednak pytanie: „Jak udostępniać usługi AMF?”. Aby udostępnić punkt końcowy usługi AMF, możesz wykorzystać brokera, na przykład komercyjny produkt LifeCycle Data Services firmy Adobe, otwarte narzędzie GraniteDS lub omawianą w tym rozdziale otwartą technologię BlazeDS (jest ona częścią produktu LifeCycle Data Services). BlazeDS działa w domenie użytkownika oraz może być używana do udostępniania obiektów i usług Javy jako usług w formacie AMF. BlazeDS potrafi też udostępniać warstwę pośrednią do obsługi komunikatów (podobnie jak JMS) w taki sposób, że klienty napisane we Fleksie mogą pobierać komunikaty asynchronicznie. 373
ROZDZIAŁ 10. SPRING I FLEX
Na podobnej zasadzie ajaksowe klienty mogą używać narzędzia Comet do pobierania komunikatów nadawanych przez serwer. BlazeDS może też działać jak prosty pośrednik dla różnych usług z innych domen. Dostępne są serwery, które obsługują punkty końcowe AMF zgodne z większością technologii (między innymi z językami Python, PHP, Perl i Ruby). Jeśli używasz Javy i Springa, pomyśl o wykorzystaniu technologii BlazeDS. Jest to bardzo rozbudowany broker, jest powszechnie obsługiwany i (jak się przekonasz) można go bardzo łatwo skonfigurować. BlazeDS można pobrać i uruchomić niezależnie jako plik .war. Wymaga to jednak co najmniej niepotrzebnego instalowania narzędzi w architekturze, a w najgorszym przypadku — stawiania dodatkowego serwera. Dlatego większość osób instaluje tylko potrzebne elementy technologii BlazeDS bezpośrednio w aplikacjach sieciowych. W przeszłości proces ten był żmudny, ale rozwiązanie się sprawdzało. Nie współdziałało jednak dobrze ze Springiem i EJB, dlatego lepszym podejściem było zastosowanie innych brokerów (takich jak GraniteDS). Obecnie, dzięki integracji Springa z technologią BlazeDS, narzędzie to ma same zalety. Możliwe jest łatwe zainstalowanie technologii BlazeDS na potrzeby aplikacji oraz narzędzie to doskonale współdziała ze wszystkimi potrzebnymi elementami Springa: z komunikatami (zarówno z prostymi komunikatami JMS, jak i z danymi przesyłanymi magistralą usług projektu Spring Integration), a także z jego komponentami i usługami.
10.3. Dodawanie obsługi narzędzia Spring BlazeDS Integration do aplikacji Problem Załóżmy, że zapoznałeś się już ze wszystkimi możliwościami i zdecydowałeś się na zainstalowanie narzędzia Spring BlazeDS Integration w istniejącej aplikacji sieciowej. Proces instalacji zależy wtedy od docelowej aplikacji.
Rozwiązanie Jeśli instalowałeś już w aplikacji standardową infrastrukturę sieciową Springa (platformę Spring MVC), zainstalowanie mechanizmu obsługi narzędzia Spring BlazeDS Integration będzie dla Ciebie bardzo łatwe. Jeśli nie, zapoznaj się z opisem sposobu konfiguracji narzędzia Spring BlazeDS Integration, a także samej technologii BlazeDS.
Jak to działa? W trakcie instalowania narzędzia Spring BlazeDS Integration trzeba zadbać o dwa komponenty. Po pierwsze, należy zainstalować mechanizm obsługi integracji w Springu (a konkretnie — serwlet DispatcherServlet z platformy Spring MVC). Po drugie, trzeba skonfigurować samą technologię BlazeDS. To drugie zadanie jest bardzo proste. Opisujemy je w tym miejscu, jednak możesz też pobrać gotowy plik z poświęconej książce witryny i bez wprowadzania zmian dodać go do własnego kodu.
Instalowanie obsługi integracji w Springu Integracja Springa z technologią BlazeDS wymaga skonfigurowania tej samej infrastruktury co przy stosowaniu platform Spring Faces, Spring Web Flow, Spring MVC itd. Jest ona oparta na serwlecie DispatcherServlet. Nie trzeba korzystać z niestandardowego serwletu dla technologii BlazeDS ani dodatkowej konfiguracji w pliku web.xml. Konfiguracja znajduje się w pliku web.xml i wystarczy w niej określić, gdzie należy przesyłać żądania Fleksa. Zauważ, że poniższą konfigurację można też zastosować przy konfigurowaniu platform Spring MVC lub Spring Web Flow. W tym kodzie nie ma nic specyficznego dla technologii BlazeDS ani nawet dla integracji Springa z tym narzędziem.
374
10.3. DODAWANIE OBSŁUGI NARZĘDZIA SPRING BLAZEDS INTEGRATION DO APLIKACJI
Integrowanie Springa z Fleksem i pakietem BlazeDS spring-flex org.springframework.web.servlet.DispatcherServlet contextConfigLocation /WEB-INF/auction-flex-context.xml 1 spring-flex /mb/*
Jest to najprostsza możliwa konfiguracja, wystarczająca jednak do zainstalowania obsługi Springa w aplikacji sieciowej. W pliku kontekstu Springa należy ustawić brokera komunikatów Fleksa i inne elementy. Tu używany jest plik /WEB-INF/auction-flex-context.xml z konfiguracją takiego brokera. Poniżej znajdziesz zawartość tego pliku. Na razie nie udostępniamy żadnych usług ani punktów końcowych zwracających komunikaty. Te zagadnienia omawiamy w kilku dalszych recepturach.
W tym pliku kontekstu importowanych jest sporo przestrzeni nazw. Będą one potrzebne później, przy integrowaniu usług i kanałów przesyłania komunikatów. Na razie nie wszystkie z tych przestrzeni nazw są potrzebne — wystarczyłoby zaimportować przestrzenie context i flex. W tym kodzie element context służy do włączenia konfiguracji adnotacji i poinformowania Springa, w którym pakiecie ma szukać komponentów (ziaren), później automatycznie rejestrowanych. Te elementy są używane jak w standardowym Springu. Jedyny ciekawy aspektu w tej konfiguracji to broker komunikatów z technologii BlazeDS. Ten broker tworzy kanały (przypominają one porty nazwane), używane do wysyłania i pobierania komunikatów. Ustawienia kanału (częstotliwość sprawdzania informacji, limit czasu oczekiwania, konkretny adres URL, strumieniowanie itd.) należy skonfigurować w pliku z konfiguracją usług. Ten broker komunikatów domyślnie sprawdza plik /WEB-INF/flex/services-config.xml. Dlatego możesz usunąć deklarację tego pliku, ponieważ jest zbędna. W kodzie znalazła się ona tylko dlatego, że czasem programista chce wykorzystać inną lokalizację, przy czym katalog domyślny (WEB-INF/flex) jest uznawany niemal za standardowy. Przyjrzyj się teraz bardzo zaawansowanemu plikowi konfiguracyjnemu services-config.xml. W tym rozdziale ponownie wykorzystywany jest tylko mały fragment konfiguracji z tego pliku. Używanych jest bardzo wiele ustawień, z których część można w bardzo przydatny sposób powtórnie zastosować. Przeanalizowanie tego pliku pomoże Ci zrozumieć, dla jakich mechanizmów można dodać obsługę. false
376
10.3. DODAWANIE OBSŁUGI NARZĘDZIA SPRING BLAZEDS INTEGRATION DO APLIKACJI
class="flex.messaging.endpoints.AMFEndpoint"/> true 4 true 5 60000 1 200 [BlazeDS] false false false false Endpoint.* Service.* Configuration false
Nie prezentujemy tu wszystkich wierszy kodu. Jeśli zechcesz, możesz samodzielnie zapoznać się z poszczególnymi opcjami. Na potrzeby przykładów z tego rozdziału wystarczy poniższa konfiguracja, która zamyka się w oszałamiającej liczbie mniej niż dziesięciu wierszy kodu w XML-u!
377
ROZDZIAŁ 10. SPRING I FLEX
Atrybut url elementu endpoint określa, pod jakim adresem usługa ma być dostępna. Podany adres URL będzie używany dla wszystkich usług udostępnianych w kanale my-amf. Wpisana tu wartość jest potrzebna w aplikacji klienckiej Fleksa. Rozwijana w tym rozdziale prosta aplikacja do obsługi aukcji jest umieszczona w głównym kontekście sieciowym. W aplikacji klienckiej Fleksa należy więc zastosować adres URL w postaci http://127.0.0.1:8080/mb/amf. Aby kod był jak najłatwiejszy w instalacji, można w kliencie podawać adres URL usługi za pomocą parametrów. Jeśli jednak znasz nazwę domeny aplikacji, możesz dodać potrzebny wpis do pliku /etc/hosts (w systemach uniksowych) lub do pliku C:\WINDOWS\system32\drivers\etc\hosts (w systemie Windows). W tym wpisie należy powiązać adres 127.0.0.1 z docelową domeną. Dzięki temu wszystkie referencje do serwera (zarówno w wersji produkcyjnej, jak i rozwojowej) będą wiązane z odpowiednim hostem. W tym rozdziale bezpośrednio używamy nazwy localhost, jednak robimy to tylko dlatego, że piszemy książkę i nie mamy praw do odrębnej domeny, którą moglibyśmy tu wykorzystać. Interfejs przykładowej aplikacji jest bardzo prosty. Aplikacja używa dwóch synchronicznych usług i asynchronicznie pobiera jeden komunikat. Interfejs użytkownika wyświetla elementy w siatce. Dostępny jest też formularz przeznaczony do dodawania nowych produktów. Bezpośrednio po dodaniu elementu asynchronicznie pobrany komunikat powoduje zaktualizowanie widoku we wszystkich klientach, w których wyświetlona jest ta siatka. Po przygotowaniu brokera komunikatów pora przejść do tworzenia potrzebnych usług i — przede wszystkim —aplikacji klienckiej Fleksa, która będzie z nich korzystać.
10.4. Udostępnianie usług za pomocą technologii BlazeDS i Springa Problem Broker komunikatów jest już gotowy. Teraz należy udostępnić usługę Springa jako usługę AMF (w podobny sposób można wyeksportować usługę Springa do postaci usługi RMI). Warto uwzględnić przy tym zdobytą wcześniej wiedzę na temat korzystania z usług AMF.
Rozwiązanie Spring i BlazeDS posłużą tu do skonfigurowania prostej usługi. Dalej zobaczysz, jak wywoływać ją po stronie klienta, którym jest prosta aplikacja do obsługi aukcji (jej tworzeniem zajmiesz się w kolejnych recepturach). Omawiana usługa pobiera wszystkie dostępne produkty i zwraca opis, zdjęcie oraz cenę każdego z nich. W tej wersji używana jest prosta usługa POJO działająca w pamięci, jednak można też zbudować usługę dowolnego innego typu.
Jak to działa? Spring i technologia BlazeDS umożliwiają udostępnianie istniejących ziaren Springa jako punktów końcowych usług AMF. Należy zdefiniować usługę w standardowy sposób, a następnie zmodyfikować konfigurację samej usługi lub utworzyć odpowiednią referencję do niej. Przyjrzyj się najpierw interfejsowi przykładowej usługi.
378
10.4. UDOSTĘPNIANIE USŁUG ZA POMOCĄ TECHNOLOGII BLAZEDS I SPRINGA
package com.apress.springwebrecipes.flex.auction; import java.util.Set; import com.apress.springwebrecipes.flex.auction.model.Bid; import com.apress.springwebrecipes.flex.auction.model.Item; /** * Interfejs serwisu aukcyjnego * **/ public interface AuctionService { /** Tworzy nowy produkt i dodaje go W kodzie klienta można wywołać tę metodę, aby zademonstrować przekazywanie komunikatów */ Item postItem( String sellerEmail, String item, String description, double price, String imageUrl); /** * Zwraca widok ze wszystkimi produktami, które są * dostępne (w tym przykładzie zawsze są to wszystkie produkty) * Ta metoda służy do demonstrowania działania usługi */ Set- getItemsForAuction(); /** Ta metoda nie jest używana w przykładowym kliencie * Dodaliśmy ją, bo może być przydatna w przyszłości */ Bid bid(Item item, double price); /** Ta metoda nie jest używana w przykładowym kliencie * Dodaliśmy ją, bo może być przydatna w przyszłości */ void acceptBid(Item item, Bid bid); }
Teraz przyjrzyj się implementacji tego interfejsu. package com.apress.springwebrecipes.flex.auction; import import import import import import import import import import import
com.apress.springwebrecipes.flex.auction.model.Bid; com.apress.springwebrecipes.flex.auction.model.Item; org.apache.commons.collections.CollectionUtils; org.apache.commons.collections.Predicate; org.apache.commons.lang.StringUtils; org.apache.commons.lang.builder.ToStringBuilder; org.springframework.beans.factory.annotation.Autowired; org.springframework.jms.core.JmsTemplate; org.springframework.jms.core.MessageCreator; org.springframework.stereotype.Service; org.springframework.util.Assert;
import javax.annotation.PostConstruct; import javax.annotation.Resource;
379
ROZDZIAŁ 10. SPRING I FLEX
import import import import import import import
javax.jms.*; java.util.Date; java.util.HashSet; java.util.Set; java.util.concurrent.ConcurrentSkipListSet; java.util.concurrent.atomic.AtomicInteger; java.util.logging.Logger;
@Service("auctionService") public class AuctionServiceImpl implements AuctionService { static private Logger logger = Logger.getLogger(AuctionServiceImpl.class.getName()); private private private private
ConcurrentSkipListSet- items = new ConcurrentSkipListSet
- (); AtomicInteger uuidForBids = new AtomicInteger(0); AtomicInteger uuidForItems = new AtomicInteger(0); String images[] = "boat,car,carpet,motorbike".split(",");
@Resource(name = "itemPosted") private Topic itemPostedDestination; @Resource(name = "bidPosted") private Topic bidPostedTopic; @Resource(name = "bidAccepted") private Topic bidAcceptedTopic; @Autowired private JmsTemplate jmsTemplate; @PostConstruct public void setupFakeItems() { Assert.isTrue(jmsTemplate != null); String[] items = "boat,car,carpet,motorbike".split(","); String[] sellerEmails = "[email protected] ,[email protected] ,[email protected] , [email protected] ,[email protected] ,[email protected] ".split(","); for (String item : items) { String sellerEmail = sellerEmails[(int) Math.floor(Math.random() * sellerEmails.length)]; String description = String.format("Wspaniały %s", item); double basePrice = Math.random() * 100; String imageUrl = String.format("/images/%s.jpg", item); postItem(sellerEmail, item, description, basePrice, imageUrl); } logger.info(String.format("setupFakeItems(): liczba produktów = %s ", "" + this.items.size())); } private Message mapFromBid(javax.jms.Session session, Bid b) throws JMSException { MapMessage mm = session.createMapMessage(); mm.setLong("itemId", b.getItem().getId()); mm.setLong("bidId", b.getId()); mm.setLong("acceptedTimestamp", b.getAccepted() == null ? 0 : b.getAccepted().getTime());
380
10.4. UDOSTĘPNIANIE USŁUG ZA POMOCĄ TECHNOLOGII BLAZEDS I SPRINGA
mm.setDouble("amount", b.getAmount()); return mm; } private Message mapFromItem(javax.jms.Session session, Item item) throws JMSException { MapMessage mapMessage = session.createMapMessage(); mapMessage.setLong("itemId", item.getId()); mapMessage.setDouble("threshold", item.getThreshold()); mapMessage.setDouble("basePrice", item.getBasePrice()); mapMessage.setString("sellerEmail", item.getSellerEmail()); mapMessage.setString("description", item.getDescription()); return mapMessage; } @Override public synchronized void acceptBid(Item item, final Bid bid) { Date accepted = new Date(); item.setSold(accepted); bid.setAccepted(accepted); jmsTemplate.send(bidAcceptedTopic, new MessageCreator() { public Message createMessage(Session session) throws JMSException { return mapFromBid(session, bid); } }); } @Override public synchronized Bid bid(Item item, double price) { final Bid bid = new Bid(); bid.setAmount(price); bid.setItem(item); bid.setId(uuidForBids.getAndIncrement()); item.addBid(bid); jmsTemplate.send(bidPostedTopic, new MessageCreator() { @Override public Message createMessage(Session session) throws JMSException { return mapFromBid(session, bid); } }); if (item.getThreshold() <= bid.getAmount()) { acceptBid(item, bid); } return bid; } private String randomImage() { int indexOfImage = (int) (Math.random() * (this.images.length - 1)); return this.images[indexOfImage]; } @Override public Item postItem( String sellerEmail, String itemTitle,
381
ROZDZIAŁ 10. SPRING I FLEX
String description, double basePrice, String imageUrlParam) { String imageUrl = imageUrlParam; if (StringUtils.isEmpty(imageUrl)) { imageUrl = String.format("/images/%s.jpg", randomImage()); } final Item item = new Item(); item.setItem(itemTitle); item.setBasePrice(basePrice); item.setId(uuidForItems.getAndIncrement()); item.setImageUrl(imageUrl); item.setThreshold(10 * 1000); item.setDescription(description); item.setSellerEmail(sellerEmail); System.out.println("Dodawanie " + ToStringBuilder.reflectionToString(item)); items.add(item); jmsTemplate.send(itemPostedDestination, new MessageCreator() { @Override public Message createMessage(Session session) throws JMSException { return mapFromItem(session, item); } }); return item; } @Override public Set- getItemsForAuction() { Set
- uniqueItemsForAuction = new HashSet
- (); uniqueItemsForAuction.addAll(this.items); CollectionUtils.filter(uniqueItemsForAuction, new Predicate() { @Override public boolean evaluate(Object object) { Item itm = (Item) object; return itm.getSold() == null; } }); return uniqueItemsForAuction; } }
Wykonywanych jest tu wiele operacji, jednak są one stosunkowo proste. Aby uprościć kod, zrezygnowaliśmy z używania platformy Hibernate i innych magazynów danych. Zamiast tego w usłudze stosowana jest zmienna ConcurrentSkipListSet- (o „odkrywczej” nazwie items). Ponieważ nie występuje tu magazyn danych, trzeba utworzyć początkowe dane w metodzie setupFakeItems. Aplikacja ma powiadamiać użytkowników o dodanych produktach, więc wykorzystano tu klasę javax.jms.Topic z interfejsu JMS. Omówienie konfiguracji tych elementów znajdziesz dalej. Na razie wystarczy zapamiętać, że potrzebny jest prosty sposób na wysyłanie komunikatów JMS. To dlatego wstrzykiwany jest obiekt klasy org.springframework.jms.core.JmsTemplate Springa. W przykładowym kodzie metody acceptBid i bid nie są używane, jednak pozostawiono je, ponieważ mogą okazać się przydatne w przyszłości. W końcowej części kodu znajduje się najważniejsza część usługi: metody postBid i getItemsForAuction.
382
10.4. UDOSTĘPNIANIE USŁUG ZA POMOCĄ TECHNOLOGII BLAZEDS I SPRINGA
Metoda bid przyjmuje parametry potrzebne do opisania oferty i jej utworzenia. W bardziej zaawansowanym przykładzie można dodać sprawdzanie poprawności danych w obiekcie. Tu kod dodaje dane do kolekcji items i publikuje w obiekcie itemPosted klasy javax.jms.Topic informację o ich dodaniu. Metoda getItemsForAuction zwraca wszystkie niesprzedane jeszcze produkty. Należy je wyświetlić w siatce po stronie klienta. W tej aplikacji zawsze zwracane są wszystkie produkty z kolekcji, ponieważ funkcja składania ofert jest nieaktywna, tak więc nie można sprzedać żadnego towaru. Po utworzeniu kodu w Javie pora przejść do konfiguracji Springa. Należy skonfigurować dwie rzeczy: standardowe usługi oraz narzędzie Spring BlazeDS Integration. Dzięki dodaniu elementu context:component-scan w ostatniej recepturze duża część konfiguracji standardowych ziaren Springa jest już gotowa. Na razie pomijamy konfigurowanie interfejsu JMS (omówienie tej kwestii znajdziesz w następnej recepturze). Pozostaje więc skonfigurować samą usługę. Jeśli dodasz do klasy AuctionServiceImpl adnotację typu org.springframework.stereotype.Service("auction Service") Springa, nie będziesz musiał robić nic więcej, aby skonfigurować usługę Springa. W przeciwnym razie dodaj do pliku XML kontekstu Springa (plik /WEB-INF/auction-flex-context.xml) następujący kod:
W ostatnim kroku należy poinformować narzędzie Spring BlazeDS Integration o używanym ziarnie. Służy do tego poniższy kod w XML-u:
Dzięki temu można udostępnić istniejące ziarna jako punkty końcowe usług BlazeDS. Jeśli ziarno jest zdefiniowane w tej samej warstwie logicznej co usługi BlazeDS, na przykład jako fasada ogólnej usługi w aplikacji sieciowej, możesz zastosować następujący kod:
W tym podejściu nie trzeba ustawiać atrybutu ref w elemencie remoting-destination. Programista ma sporą kontrolę nad eksportowaniem ziarna. Za pomocą znacznika remoting-destination można wykluczyć lub dodać metody ziarna, które powinny być dostępne w zdalnych klientach. Służą do tego atrybuty exclude-methods i include-methods. Ponadto możesz skonfigurować nazwę, pod jaką eksportowana usługa ma być dostępna w BlazeDS. Domyślnie atrybut destination-id jest ustawiany na nazwę ziarna. Jeśli chcesz to zmienić, samodzielnie ustaw ten atrybut. Warto pamiętać, że w pliku XML z kontekstem Springa utworzonym dla brokera komunikatów znajduje się element flex:message-broker obejmujący element flex:message-service. Ten ostatni element ma atrybut default-channels. Jeśli ustawisz ten atrybut, możesz pominąć w elemencie atrybut channel (określa on kanał używany do komunikowania się z serwerem). Wtedy aplikacja użyje kanału domyślnego. Jeśli jednak chcesz zastosować inny kanał, możesz go ustawić w elemencie flex:remoting-destination.
Na zapleczu tworzony jest obiekt klasy org.springframework.flex.remoting.RemotingDestinationExporter. Jeśli chcesz zdebugować aplikację
i sprawdzić, jak działa, zwróć uwagę na tę właśnie klasę. Jeśli z jakichś przyczyn zechcesz samodzielnie skonfigurować rozwiązanie i zrezygnować z obsługi oferowanej przez przestrzeń nazw, także wykorzystaj ziarno tej klasy. Na podstawie przedstawionej wcześniej najprostszej definicji elementu remoting-destination ( ) można wywoływać usługę z poziomu klienta w taki sam sposób jak wcześniej. Ponieważ zobaczyłeś już pełną wersję przykładowego kodu, nie ma sensu powielać go w tym miejscu. Poniżej znajdziesz tylko najważniejsze fragmenty kodu w ActionScripcie.
383
ROZDZIAŁ 10. SPRING I FLEX
var auctionService:RemoteObject = new RemoteObject(); auctionService.endpoint = 'http://localhost:8080/mb/amf'; auctionService.showBusyCursor = true; auctionService.destination = 'auctionService'; var resultHandler:Function = function( resultEvent:ResultEvent, asyncToken:AsyncToken) :void { Alert.show('Wynik = ' + resultEvent.result); }; var faultHandler:Function = function( faultEvent:FaultEvent, asyncToken:AsyncToken) :void { Alert.show('Błąd = ' + faultEvent.fault); }; auctionService.getItemsForAuction().addResponder( new AsyncResponder(resultHandler, faultHandler));
10.5. Używanie obiektów działających po stronie serwera Problem W poprzedniej recepturze utworzyłeś usługę, która zwraca wynik dzięki wywołaniu operacji w usłudze Springa działającej po stronie serwera. Zwracana jest tu kolekcja Collection- . W kodzie w ActionScripcie nie ma informacji o typie danych, dlatego kod kliencki może próbować uzyskać dostęp do nieistniejących właściwości, przy czym nie spowoduje to zgłoszenia ostrzeżeń.
Rozwiązanie W środowisku uruchomieniowym Fleksa można określić, w jaki sposób mają być zachowywane informacje o typie obiektów serializowanych w formacie AMF. W tym celu należy wykorzystać w ActionScripcie po stronie klienta atrybut [RemoteClass] i określić w nim typ obiektu Javy.
Jak to działa? W kodzie Javy metoda getItemsForAuction zwraca kolekcję obiektów typu Item. W ActionScripcie odpowiada jej kolekcja dynamicznych obiektów typu Object. Słowo „dynamiczne” oznacza tu, że można swobodnie dodawać lub wskazywać pola i metody obiektu bez znajomości jego typu. ActionScript traktuje klasy tak jak JavaScript (ten ostatni język bez wątpienia znasz z przeglądarek). W przeglądarce to podejście nosi nazwę „właściwości expando” i jest przydatne, ponieważ Flash nie ma wbudowanego sposobu ustalania (i udostępniania — na przykład w mechanizmach autouzupełniania instrukcji w środowisku IDE) typu zwracanych obiektów. Domyślnie nie następuje żadne odwzorowanie typów. Dlatego aby w funkcji resultHandler przejść po elementach kolekcji, można zastosować następujący kod: import mx.collections.ArrayCollection; ... var acOfItems:ArrayCollection = resultEvent.result as ArrayCollection; for each(var item:* in acOfItems) Alert.show( 'item.id = '+ item.id + ', item.description='+ item.description);
384
10.5. UŻYWANIE OBIEKTÓW DZIAŁAJĄCYCH PO STRONIE SERWERA
Tu właściwość result obiektu resultEvent jest rzutowana na typ mx.collections.ArrayCollection. Właściwości ziarna JavaBean klasy Item (jest to encja stosowana po stronie serwera) są po stronie klienta używane jako właściwości obiektu typu Object. W małych aplikacjach jest to wystarczająco dobre rozwiązanie. Jednak w wielu sytuacjach lepiej byłoby zachować bezpieczeństwo ze względu na typ, takie, jakie jest zapewniane w środowisku Javy po stronie serwera. Wtedy po stronie klienta należy powielić obiekty używane w Javie po stronie serwera. Wymaga to utworzenia odwzorowania w obiektach. Aby uzyskać pożądany efekt, należy po stronie klienta utworzyć wersję encji używanej po stronie serwera. Oto utworzona w ActionScripcie deklaracja klasy Item z Javy: package com.apress.springwebrecipes.auction.model { [Bindable] [RemoteClass(alias="com.apress.springwebrecipes.flex.auction.model.Item")] public class Item { private var _sellerEmail:String; private var _basePrice:Number; private var _threshold:Number; private var _sold:Date; private var _item:String; private var _description:String; private var _imageUrl:String; private var _id:Number; public function get sellerEmail():String { return _sellerEmail; } public function set sellerEmail(value:String):void { _sellerEmail = value; } public function get basePrice():Number { return _basePrice; } public function set basePrice(value:Number):void { _basePrice = value; } public function get threshold():Number { return _threshold; } public function set threshold(value:Number):void { _threshold = value; } public function get sold():Date { return _sold; } public function set sold(value:Date):void { _sold = value; } public function get item():String {
385
ROZDZIAŁ 10. SPRING I FLEX
return _item; } public function set item(value:String):void { _item = value; } public function get description():String { return _description; } public function set description(value:String):void { _description = value; } public function get imageUrl():String { return _imageUrl; } public function set imageUrl(value:String):void { _imageUrl = value; } public function get id():Number { return _id; } public function set id(value:Number):void { _id = value; } } }
Co ważne, ta klasa jest opatrzona adnotacją RemoteClass. Ta adnotacja zwykle informuje środowisko uruchomieniowe Fleksa o tym, że należy zachować informacje o typie przy serializowaniu obiektów Flasha na format AMF i w drugą stronę. Tu ta adnotacja — w połączeniu z atrybutem alias — oznacza, że środowisko uruchomieniowe Fleksa ma dodać do danych AMF z serwera informacje o podanym typie. W atrybucie alias należy określić odpowiednią klasę Javy, której odpowiednikiem jest nowa klasa. Przedstawiony wcześniej fragment kodu można zapisać w następujący sposób: import mx.collections.ArrayCollection; import com.apress.springwebrecipes.auction.model.Item; ... var acOfItems:ArrayCollection = resultEvent.result as ArrayCollection; for each(var item:Item in acOfItems) Alert.show( 'item.id = '+ item.id + ', item.description='+ item.description);
Zmiany nie są duże, jednak teraz w środowisku IDE będzie działał mechanizm automatycznego uzupełniania instrukcji i możliwa będzie refaktoryzacja kodu. Ponadto można definiować metody w obiekcie w ActionScripcie (na przykład na potrzeby nietypowego aktualizowania stanu), dostępne jest także miejsce na kod służący do sprawdzania poprawności danych i wymuszania ograniczeń. Definiowanie w ActionScripcie klas dla każdej encji używanej po stronie serwera jest żmudne. Istnieją jednak pomocne rozwiązania. Wcześniej wspomnieliśmy o jednym z nich — brokerze encji GraniteDS. To narzędzie udostępnia wtyczkę dla środowiska Eclipse i zadanie Anta (to zadanie można też wykorzystać w Mavenie) oraz umożliwia skanowanie skompilowanych klas Javy i generowanie analogicznych klas
386
10.6. KORZYSTANIE Z USŁUG OPARTYCH NA KOMUNIKATACH W NARZĘDZIACH BLAZEDS I SPRING
w ActionScripcie. Dla każdej klasy Javy narzędzie to generuje dwie klasy ActionScriptu w plikach XBase.as i X.as (gdzie X to nazwa klasy Javy). Omawiane narzędzie tworzy w klasie XBase kod getterów i setterów oraz zoptymalizowany kod do obsługi serializowania. X jest klasą pochodną od XBase. Jeśli na przykład chcesz dodać kod sprawdzający, czy numery telefonów są zgodne z określonym wzorcem, możesz umieścić nową wersję metody w klasie X i po sprawdzeniu poprawności właściwości wywoływać wersję metody z klasy bazowej. Po ponownym uruchomieniu generatora klasa X nie jest modyfikowana (zmiany wprowadzane są tylko w klasie XBase). Dzięki temu dodane modyfikacje zostają zachowane. W innych projektach dostępne są podobne rozwiązania. Mamy dobre doświadczenia z korzystaniem z opisywanej tu wtyczki Granite Data Service Gas3. Zauważ, że kod generowany przez tę wtyczkę działa dobrze także w technologii BlazeDS. Aby dowiedzieć się czegoś więcej o opisanym generatorze, odwiedź stronę http://www.graniteds.org/confluence/display/DOC/2.+Gas3+Code+Generator.
10.6. Korzystanie z usług opartych na komunikatach w narzędziach BlazeDS i Spring Problem Jak zbudować aplikację, która potrafi obsługiwać komunikaty nadawane (w trybie push)? W świecie Ajaksa są to aplikacje typu Comet. Jak połączyć klienty Fleksa ze sobą w asynchroniczny sposób? Jak korzystać z usług warstwy pośredniej opartych na komunikatach (ang. Message-Oriented Middleware — MOM), na przykład usług JSM, do wysyłania i pobierania komunikatów? Ponadto jak opracować architekturę rozwiązań, które mają komunikować się z zaawansowanymi magistralami zdarzeń, takimi jak w projekcie Spring Integration?
Rozwiązanie Tu narzędzie Spring BlazeDS Integration posłuży do podłączenia klienta do mechanizmów obsługi komunikatów z pakietu BlazeDS. BlazeDS zapewnia obsługę usług JMS, a także oferuje standardowe mechanizmy obsługi komunikatów. Narzędzie Spring BlazeDS Integration upraszcza integrowanie obu tych rozwiązań i dodatkowo zapewnia trzecie, pozwalające aplikacjom klienckim wysyłać i pobierać komunikaty za pomocą kanałów projektu Spring Integration. Narzędzie Spring BlazeDS Integration współdziała z projektem Spring Integration i umożliwia powiązanie dowolnego punktu końcowego (serwera poczty elektronicznej, kanału z aktualizacjami wpisów z Twittera, serwera FTP, systemu plików lub dowolnej innej jednostki, dla której programista chce napisać adapter) z mechanizmami obsługi komunikatów technologii BlazeDS.
Jak to działa? Do tej pory aplikacja do przeprowadzania aukcji komunikowała się z serwerem, ale między tymi jednostkami nie toczyła się żadna konwersacja. Jeśli klient żądał czegoś od serwera, serwer odpowiadał. Co zrobić, jeśli serwer ma do przekazania dodatkowe informacje? W większości aplikacji sieciowych to zagadnienie jest pomijane, ponieważ firm nie stać na szukanie odpowiedzi na to pytanie. HTTP jest protokołem bezstanowym typu pull. Nie ma w nim możliwości nadawania danych w trybie push do klientów. Stanowi to problem, ponieważ w wielu aplikacjach nadawanie danych w trybie push byłoby wygodniejsze. Dotyczy to na przykład chatów, aplikacji z cenami akcji i innych programów, w których częstotliwość modyfikacji jest zmienna i niemożliwa do kontrolowania po stronie klienta (w praktyce dotyczy to większości aplikacji). Skoro tworzone są abstrakcyjne warstwy pozwalające radzić sobie z różnymi wadami protokołu HTTP, tak aby lepiej nadawał się do tworzenia aplikacji, dlaczego nie rozwiązać problemu nadawania danych w trybie push? Okazuje się, że programiści próbowali się z tym zmierzyć. Jednak nie jest to łatwe zadanie. W środowisku przeglądarek w celu uzyskania architektury typu push stosuje się technikę odpytywania (ang. polling), model Comet i piggybacking. Odpytywanie polega na zgłaszaniu w stałych odstępach czasu zapytań o znany zasób z serwera. W modelu Comet żądanie jest otwierane i utrzymywane w takim stanie. Gdy stan po stronie serwera się 387
ROZDZIAŁ 10. SPRING I FLEX
zmieni, nadawany jest bajt ping. Inna możliwość to piggybacking, czyli wysyłanie danych tylko wtedy, gdy do serwera wysyłane jest żądanie. Nie polega to na odpytywaniu, ponieważ żądanie nie ma na celu sprawdzenia, czy określony zasób udostępnił dane — stosuje się tu zwykłe żądania innych zasobów. Dane nadawane w trybie push (jeśli istnieją) są wtedy dołączane do odpowiedzi na to żądanie. Każde z tych podejść ma pewne wady i zalety. Odpytywanie może prowadzić do przeciążenia serwera, jeśli żądania są wymagające obliczeniowo lub czasowo albo przychodzą zbyt często. Model Comet też może prowadzić do przeciążenia serwera, ponieważ dla każdego klienta trzeba utrzymywać otwarty wątek. Piggybacking jest wolny od tego problemu, jednak korzystając z tego podejścia, nie da się zagwarantować aktualności danych. Co się stanie, jeśli klient nigdy nie wyśle żądania, do którego można byłoby dołączyć nadawane dane? Programiści starali się wbudować obsługę tych rozwiązań w serwery. Nowsze serwery mają wydajniejszą obsługę wątków. Dzięki temu wątki mogą działać w trybie pasywnym lub wymagają minimalnej ilości pamięci. Na przykład serwer Jetty obsługuje asynchroniczny i zgodny z techniką Comet model wątków oparty na obiektach typu Continuation. W serwerze Grizzly (Glassfish 3) dostępna jest klasa CometEngine. W języku Go firmy Google możliwe jest niemal bezkosztowe tworzenie wielu prostych wątków. Sam język HTML5 udostępnia standard WebSocket, który pozwala rozwiązać omawiane tu problemy. W trzeciej wersji specyfikacji serwletów (wchodzącej w skład Javy EE 6) możliwe jest tworzenie asynchronicznych serwletów. Jednak żadne z tych rozwiązań nie jest jeszcze w pełni gotowe, a programiści muszą pisać aplikacje. Dlatego i tak trzeba symulować działanie potrzebnych mechanizmów. Używamy tu słowa „symulować”, ponieważ dla programistów Javy ze środowiska korporacyjnego (a nawet w świecie zwykłych protokołów) opisywane tu techniki są sztuczkami. W świecie korporacji jeśli jedna usługa publikuje komunikat, dowolny subskrybent może zarejestrować odbiornik. Wtedy po pojawieniu się komunikatu subskrybenci natychmiast otrzymają powiadomienie. Oczywiście wiadomo, że na zapleczu wymaga to wydajnego odpytywania, jednak nie trzeba pracować na tym poziomie. Programowanie odbywa się na poziomie zdarzeń zgłaszanych w odpowiedzi na nadejście nowego komunikatu. Tak działa architektura sterowana zdarzeniami (ang. Event-Driven Architecture — EDA). Gdy programista Springa pisze sterowane komunikatami obiekty POJO lub programista EJB tworzy sterowane komunikatami ziarna, obiekty są niezależne od mechanizmów przesyłania komunikatów. Niestety programiści aplikacji sieciowych nie mogą korzystać z tak eleganckiego rozwiązania. Jedyny sposób na jego zasymulowanie to wykorzystanie usług opartych na komunikatach, które jednak tworzy się na poziomie platformy, a nie na poziomie klienta. Jeśli używasz projektu DWR (jest to popularny projekt ajaksowy), wiesz, że można tworzyć aplikacje, w których model Ajaksa jest „odwrócony”. W kodzie klienta dostępne są wtedy korzyści znane standardowo z platform. Także Flex udostępnia na poziomie platformy rozwiązanie, które umożliwia obsługę komunikatów w trybie push przy korzystaniu z brokera komunikatów (na przykład z technologii BlazeDS). Komunikaty są kierowane do technologii BlazeDS i zwracane z niej, a BlazeDS komunikuje się z klientem Fleksa. Zaletą tego rozwiązania jest to, że zarówno Flex, jak i BlazeDS śledzą stan klienta. Ponadto narzędzia te pomagają programiście nawet wtedy, gdy ten nie wie, że tego potrzebuje. Na przykład większość przeglądarek nie potrafi otworzyć więcej niż określoną liczbę połączeń HTTP. Jednak gdy na zapleczu klient Fleksa otwiera połączenie, aby oczekiwać na komunikaty nadawane w trybie push, stosuje jedną z trzech opisanych wcześniej technik (model Comet, piggybacking lub odpytywanie) albo ich kombinację. Dlatego jeśli klient Fleksa otwiera kanał do odbierania komunikatów za pomocą długotrwałego połączenia (model Comet), wykorzystuje połączenie HTTP przeglądarki, a tym samym zmniejsza o jeden liczbę dostępnych połączeń tego typu. Jeśli spróbujesz zgłosić wywołanie RPC, wykorzystane zostanie następne połączenie. To rozwiązanie działa, ponieważ większość przeglądarek obsługuje dwa jednoczesne połączenia. Jeśli jednak limit otwartych połączeń to dwa, a aplikacja utworzy dwa połączenia do odbierania nadawanych w trybie push komunikatów, nie będzie można otworzyć połączenia na potrzeby wywoływania usług. W takiej sytuacji można skonfigurować technologię BlazeDS i zmodyfikować udostępniane przez nią funkcje. W tym celu należy ustawić konfigurację w pliku servicesconfig.xml tego pakietu. Można w nim określić, że żądania kierowane do jednostki „a” mają być zgłaszane w modelu Comet, a mniej priorytetowe żądania do jednostki „b” mają być obsługiwane za pomocą odpytywania. Wtedy połączenia z drugą jednostką są nawiązywane okresowo, a klient może wywoływać usługi RPC. Przyjrzyjmy się teraz, jak we Fleksie pobierać komunikaty nadawane przez serwer (później przejdziemy do szczegółów potrzebnej na zapleczu konfiguracji). W aplikacji do obsługi akcji interfejs użytkownika ma być powiadamiany o opublikowaniu nowych komunikatów przez obiekt itemPosted typu Topic. W metodzie
388
10.6. KORZYSTANIE Z USŁUG OPARTYCH NA KOMUNIKATACH W NARZĘDZIACH BLAZEDS I SPRING
konfiguracyjnej klienta (wywoływanej w odpowiedzi na zgłoszenie zdarzenia applicationComplete) należy zainicjować konsumenta: import import import import import import import
mx.controls.Alert; mx.events.FlexEvent; mx.messaging.*; mx.messaging.channels.AMFChannel; mx.messaging.events.*; mx.rpc.*; mx.rpc.events.*;
... private var itemPostedDestinationConsumer:Consumer; ... public function onApplicationComplete(flexEvent:FlexEvent):void { var aChannelSet:ChannelSet = new ChannelSet(); aChannelSet.channels = [new AMFChannel('my-amf','http://localhost:8080/mb/amf')]; itemPostedDestinationConsumer = new Consumer() ; itemPostedDestinationConsumer.channelSet = aChannelSet; itemPostedDestinationConsumer.destination='itemPostedDestination' ; itemPostedDestinationConsumer.addEventListener( MessageEvent.MESSAGE, function(me:MessageEvent):void { // Reagowanie na pojawienie się nowego produktu (warto też odświeżyć interfejs użytkownika) layoutGridOfItems(); Alert.show('Pojawił się nowy produkt.'); }); itemPostedDestinationConsumer.subscribe(); }
Po pierwsze, ten kod może wydawać się rozwlekły w porównaniu z innymi przykładami z tego rozdziału. Spróbuj jednak napisać cały kod potrzebny do skonfigurowania obiektu typu MessageContainer i zasubskrybować komunikaty za pomocą samych usług JMS. Po drugie, można usprawnić nawet ten prosty kod, o czym przekonasz się w następnej recepturze. Kod działa w oczekiwany sposób. Należy utworzyć obiekt typu Consumer Fleksa i wskazać mu docelową jednostkę w BlazeDS, z której pochodzą pobierane komunikaty, oraz określić kanał używany przez klienta do komunikowania się. Docelową jednostką jest tu ustawiona w BlazeDS nazwa zasobu, z którego będą pobierane komunikaty. Po stronie BlazeDS tę jednostkę można powiązać z kolejką usługi JMS, kanałem projektu Spring Integration itd. Ta pośredniość jest przydatna, ponieważ w kodzie klienta nie trzeba wiedzieć, w jaki sposób generowane są komunikaty. Dalej rejestrowany jest odbiornik. Odbywa się to tak samo jak przy wykrywaniu na przykład kliknięć przycisku. Na końcu wywoływana jest metoda subscribe, co jest dla klienta informacją, że należy rozpocząć konsumowanie nadchodzących komunikatów. Trudno jest opracować wyraźnie prostsze rozwiązanie. Zobaczmy teraz, jak skonfigurować brokera komunikatów narzędzia Spring BlazeDS Integration po stronie serwera. Niezależnie od wybranego podejścia należy wykorzystać przestrzeń nazw narzędzia Spring BlazeDS Integration, zadeklarować jednostkę docelową Springa i przypisać do niej identyfikator itemPostedDestination (ponieważ taka nazwa występuje w subskrypcji po stronie klienta). Z kodem usługi zapoznałeś się już wcześniej, więc wiesz, że używana jest tu usługa JMS. Przyjrzyjmy się teraz, jak w Spring BlazeDS Integration komunikować się bezpośrednio z takimi usługami. Przykładowa aplikacja ma reagować na komunikaty w obiektach itemPosted typu javax.jms.Topic. W usługach JMS ten typ reprezentuje nazwany kanał z serwera JMS, z którego komunikaty są rozsyłane do wszystkich subskrybujących go klientów. Jest to stosowany w Javie EE odpowiednik dodania odbiornika akcji za pomocą Fleksa. Inaczej działa klasa javax.jms.Queue — przy jej stosowaniu każdy komunikat przesyłany do kolejki jest wczytywany przez jednego klienta, a następnie usuwany. W tym przykładzie używany jest broker ActiveMQ usług JMS, ponieważ dobrze współdziała on ze Springiem. Tak jak w każdym brokerze usług JMS, da się w nim skonfigurować fabryki połączeń, a dodatkowo można
389
ROZDZIAŁ 10. SPRING I FLEX
używać przestrzeni nazw Springa w celu skonfigurowania opcji po stronie serwera (na przykład jednostki docelowej typu javax.jms.Topic lub javax.jms.Queue) oraz dostępne są najlepsze w swojej klasie funkcje warstwy MOM, na przykład tworzenie klastrów i obsługa protokołu XMPP. Ponadto broker ten pozwala na szybkie rozpoczęcie pracy. Oto kroki potrzebne do jego zainstalowania: 1. Pobierz bezpłatny projekt Apache Active MQ (http://activemq.apache.org/). W tej książce używamy wersji 5.3, jednak nie powinieneś mieć trudności z korzystaniem z nowszych wersji (a nawet z wcześniejszych, zwłaszcza jeśli różnice w numerze są stosunkowo nieduże). 2. Wypakuj projekt. 3. Uruchom projekt. W tym celu wywołaj odpowiedni skrypt activemq (dostępne są dwie wersje: dla systemów uniksowych i dla systemu Windows) w katalogu bin wypakowanego projektu. Broker ActiveMQ jest tu używany zarówno w przykładzie wykorzystania usług JMS, jak i w przykładzie opartym na projekcie Spring Integration. Pora przejść do różnych sposobów generowania i konsumowania komunikatów we Fleksie.
Usługi JMS Dodanie obsługi usług JMS w technologii BlazeDS wymaga skonfigurowania fabryki połączeń JMS w Springu. Tu aplikacja ma korzystać z typu javax.jms.Topic, a nie z javax.jms.Queue, dlatego należy użyć dostępnej w bibliotece ActiveMQ obsługi przestrzeni nazw Springa. Poniżej pokazana jest standardowa konfiguracja, używana niezależnie od tego, do czego mają służyć usługi JMS. Dodaj poniższy kod do pliku WEB-INF/auction-flex-context.xml zdefiniowanego w poprzedniej recepturze. W tym pliku zaimportowano już przestrzeń nazw amq (dla biblioteki Active MQ).
Wewnętrzny element amq:connectionFactory definiuje obiekt typu rg.apache.activemq.spring.Active MQConnectionFactory. W ten sposób definiowana jest fabryka połączeń, ale aby usprawnić rozwiązanie, dodatkowo tworzona jest pula fabryk połączeń (org.springframework.jms.connection.CachingConnectionFactory). Na potrzeby korzystania w usługach z fabryki połączeń JMS należy utworzyć obiekt typu JmsTemplate i powiązać go ze skonfigurowanym wcześniej pośrednikiem fabryki połączeń.
W ostatnim kroku należy wykorzystać obsługę przestrzeni nazw ActiveMQ do utworzenia potrzebnych obiektów javax.jms.Topic (jeśli jeszcze nie istnieją).
Po dodaniu tych elementów nowa usługa ma wszystko, co jest potrzebne do komunikowania się z usługami JMS. Na razie konfiguracja nie obejmuje żadnych ustawień specyficznych dla Fleksa. Pora do nich przejść. Dodaj poniższy kod zaraz za wcześniejszą definicją brokera komunikatów.
390
10.6. KORZYSTANIE Z USŁUG OPARTYCH NA KOMUNIKATACH W NARZĘDZIACH BLAZEDS I SPRING
I to już wszystko! Atrybut id określa łańcuch znaków używany do łączenia się w kliencie Fleksa z jednostką docelową usług JMS. To jedyna rzecz potrzebna w dalszym kodzie. Atrybut jms-destination odpowiada identyfikatorowi używanemu do definiowania tematów za pomocą elementu amq:topic. Tak więc formularz do dodawania nowych elementów jest już gotowy. Ten formularz wywołuje metodę postItem usługi Springa, co z kolei powoduje przesłanie komunikatu (przy użyciu obiektu jmsTemplate) do docelowej jednostki itemPosted. W elemencie flex:jms-message-destination Fleksa skonfigurowano w taki sposób, aby odbierał tego rodzaju komunikaty. Ponieważ są one przesyłane do obiektu typu Topic, komunikaty są następnie rozsyłane, dzięki czemu zobaczą je wszyscy użytkownicy aplikacji. Aby wypróbować ten mechanizm, uruchom aplikację Fleksa w kilku przeglądarkach lub komputerach, a następnie zaloguj się do niej. Wykorzystaj różne przeglądarki, aby uniknąć używania tej samej sesji. W jednym z okien dodaj obiekt typu Item. Zauważ, że wszystkie pozostałe okna zostaną natychmiast zaktualizowane. Powinieneś zobaczyć wtedy komunikat z informacją: „Pojawił się nowy produkt”.
Projekt Spring Integration W poprzednim przykładzie usługi JMS posłużyły do utworzenia architektury typu publikuj – subskrybuj. Ponieważ elementy działały tam w zamkniętym cyklu, rozwiązanie funkcjonowało poprawnie. Jednak interfejs użytkownika aplikacji Fleksa często musi wykrywać także inne zdarzenia, generowane przez zewnętrzne jednostki. Te zdarzenia mogą być specyficzne dla firmy lub domyślnie nie obsługują komunikacji z usługami JMS. Lista aplikacji tego typu jest bardzo długa. Nie trzeba długo się zastanawiać, aby wymyślić ciekawe rozwiązania. Może aktualizować interfejs użytkownika za każdym razem, gdy nadejdzie nowy e-mail, nowa wiadomość z kanału RSS lub ktoś zaloguje się na serwerze Jabbera (XMPP), na przykład za pomocą aplikacji Google Talk? A może zaprojektować klienta Fleksa w taki sposób, żeby usprawniał proces redagowania witryny i wczytywał z systemu zarządzania treścią zmodyfikowane teksty, aby można je było zatwierdzić? Możliwe też, że tworzysz interfejs użytkownika w systemie kontrolnym i chcesz, by nowe wnioski kredytowe były po nadejściu przekazywane do analizy. Może chcesz wczytywać zawartość plików zapisanych na współużytkowanym napędzie? A może chcesz, aby aplikacja reagowała na zmiany statusu na Twitterze lub Facebooku? Jeszcze inny pomysł związany jest ze wstawianiem nowych wierszy w obserwowanym widoku bazy danych. Tego rodzaju zagadnienia z zakresu integracji wymagają warstwy pośredniej, tak aby kod mógł przetwarzać różne zdarzenia niezależnie od ich źródła. W takich sytuacjach zwykle wykorzystuje się magistralę ESB (ang. Enterprise Service Bus). Także tu zastosujemy pewną wersję takiej magistrali. Użyjemy projektu Spring Integration. Jest to doskonałe narzędzie, ponieważ nie działa jak zwykły serwer, ale jest zestawem zagnieżdżanych komponentów, które konfiguruje się za pomocą Springa. Dzięki temu można pobierać i generować komunikaty dla dowolnych punktów końcowych. Jeśli dla danych usług nie ma wbudowanej obsługi, można bardzo łatwo samodzielnie ją dodać. Tu zmodyfikujemy przykład oparty na usługach JMS. Zamiast wysyłać komunikaty do obiektów typu Topic usług JMS i korzystać z nich we Fleksie, będziemy używać dwóch sposobów dodawania produktów. Jeden (tak jak wcześniej) będzie oparty na usługach JMS, natomiast w drugim wykorzystamy współużytkowany system plików. Aplikacja będzie przetwarzać nowe pliki i przekształcać ich zawartość na nowe obiekty typu Item, dodawane w wyniku wywołania usługi. Ostatecznie dane wejściowe w obu sytuacjach trafią do usługi JMS, a projekt Spring Integration pobierze te komunikaty i przekaże je do docelowej lokalizacji w BlazeDS. Klient Fleksa tak jak wcześniej będzie wykorzystywał komunikaty z docelowej lokalizacji, dlatego nie trzeba go modyfikować. Aby wprowadzić potrzebne zmiany, należy wykorzystać projekt Spring Integration w kontekście aplikacji Springa. Projekt ten umożliwia utworzenie pewnego rodzaju procesu, przez który komunikat musi przejść. Komunikat musi trafić do każdego komponentu, aby proces został zakończony. Projekt Spring Integration pozwala skonfigurować sposób dodawania komunikatów do procesu i określić ich docelową lokalizację. Do odczytywania danych i zapisywania ich w zewnętrznych systemach służą adaptery. Za obsługę przekształcania komunikatów i inne operacje odpowiadają wyspecjalizowane komponenty. Każdy komponent pobiera i zwraca komunikat. Niektóre komponenty umożliwiają modyfikowanie pobranych komunikatów — wtedy komunikaty wejściowy i wyjściowy różnią się od siebie. W innych komponentach można rozdzielać zadania. Na przykład komponent pobiera jeden komunikat (obiekt typu File) i zwraca wiele komunikatów (po jednym na każdy wiersz). W projekcie Spring Integration, podobnie jak w platformach Spring Web Flow i Struts, dodawana jest warstwa pośrednia między komponentami. Jeśli istnieją dwa komponenty, „a” i „b”, dane 391
ROZDZIAŁ 10. SPRING I FLEX
wyjściowe z komponentu „a” można przekazać do komponentu „b”. Jeżeli w przyszłości okaże się, że przekazywane dane wymagają przetwarzania, można dodać komponent „c” między dwoma wcześniejszymi. W standardowych rozwiązaniach stanowiłoby to problem. Jednak w projekcie Spring Integration występuje poziom pośredni. Komponenty są połączone ze sobą kanałami, którymi są nazwane potoki. Potok przyjmuje dane wyjściowe z jednego komponentu i przekierowuje je jako dane wejściowe do innego. Aby zmienić uporządkowanie komponentów w potoku, wystarczy zmodyfikować wejścia i wyjścia. Podobnie można wyodrębnić strony w platformie Spring Web Flow i określić kolejność ich wyświetlania. W plikach należy umieścić dane w określonym formacie. Tu, w celu uproszczenia tekstu, przyjmijmy, że cała zawartość pliku jest zapisana w jednym wierszu, który można podzielić na kolumny na podstawie przecinków (,). Pierwsza kolumna zawiera adres e-mail sprzedawcy, druga kolumna — nazwę produktu, trzecia — opis, a czwarta — cenę. Następnie można wywołać usługę, aby dodała produkt i przesłała go do usługi JMS. Występują tu więc dwie jednostki przekazujące dane do obiektu typu Topic usług JMS (ostatecznie pobierany i przekazywany do docelowej lokalizacji w BlazeDS): mechanizm zintegrowany z systemem plików i usługa, która przesyła zdarzenie bezpośrednio do obiektu typu Topic usług JMS. Aby wprowadzić odpowiednie zmiany, należy skonfigurować kilka komponentów projektu Spring Integration. Potrzebny jest sposób wczytywania plików ze znanego katalogu. Posłuży do tego adapter przychodzących plików. Niezbędna jest też metoda przekształcenia obiektu typu java.io.File z systemu plików na obiekt typu String z zawartością pliku. Do tego posłuży mechanizm przekształcania plików na łańcuchy znaków. Oprócz tego trzeba przekształcać obiekty typu String na obiekty typu Item przekazywane do wywoływanej usługi. Wymaga to utworzenia następnego transformera. Potrzebny jest też komponent wywołujący metodę postItem usługi, która prześle obiekt typu Item do usługi JMS. Posłuży do tego komponent, który będzie przyjmował wszystkie komunikaty z obiektu typu Topic usługi JMS (niezależnie od ich źródła), a następnie przekaże je do docelowej lokalizacji BlazeDS. To proste. W ramach tego procesu wystarczy poinformować magistralę projektu Spring Integration, jak ma przekształcać proste dane w formacie CSV na obiekty typu Item i jak ma wywoływać usługę. Do wykonywania tych operacji utwórzmy dwie klasy Javy: FileToItemTransformer i ItemCreationServiceActivator. Pierwsza posłuży do przekształcania łańcucha znaków z danymi w formacie CSV, a druga — do wywoływania usługi. To rozwiązanie daje dużo możliwości. Przyjrzyj się pierwszej wersji kodu. Najpierw trzeba zadeklarować kanały łączące komponenty.
Po przygotowaniu kanałów pora przyjrzeć się łączonym przez nie komponentom.
Najpierw tworzony jest obiekt typu Resource Springa w celu zadeklarowania katalogu z nowymi plikami. Tu korzystamy z nowego języka wyrażeń Springa 3.0 i stosujemy standardową właściwość systemową user.home do wskazania katalogu głównego bieżącego użytkownika. Pliki znajdą się w podkatalogu flexCsvFiles w katalogu głównym. Następnie można utworzyć adapter file:inbound-channel-adapter, aby sprawdzać ten
392
10.6. KORZYSTANIE Z USŁUG OPARTYCH NA KOMUNIKATACH W NARZĘDZIACH BLAZEDS I SPRING
katalog i pobierać nowe pliki, gdy staną się dostępne. By zagwarantować, że po zainstalowaniu aplikacji katalog będzie dostępny, atrybut auto-create-directory jest ustawiany na wartość true. Żeby określić w adapterze używany katalog, ponownie stosowany jest język wyrażeń Springa. Przy jego użyciu należy uzyskać dostęp do łańcucha znaków reprezentującego zasób typu Resource, który to łańcuch otrzymano w wyniku dereferencji właściwości file tego zasobu. Następnie należy ponownie zastosować dereferencję, aby uzyskać ścieżkę absolutePath. Element jest skonfigurowany w taki sposób, by co 1000 milisekund (czyli co sekundę) sprawdzał, czy w katalogu pojawiły się nowe pliki. Jeśli tak się stało, należy utworzyć katalog i przesłać go kanałem inboundItemFiles. Zauważ, że można też ustawić zmienną globalną reprezentującą adapter kanału z danymi wejściowymi, jednak tu nie zastosowano tego podejścia.
Dwa następne komponenty w potoku to egzemplarze standardowego komponentu transformer z projektu Spring Integration. Pierwszy z nich, pokazany w poprzednim fragmencie kodu, to standardowy komponent ponownie używany też w innych miejscach kodu. Ten komponent przyjmuje obiekt typu java.io.File i zapisuje jego zawartość w obiekcie typu String. Zawartość tego ostatniego obiektu jest zwracana do kanału inboundItemFileStrings, skąd pobiera ją następny komponent (też typu transformer). Aby uniknąć zapętlenia, w komponencie file-to-string-transformer używana jest opcja delete-files. Powoduje ona usunięcie pliku z katalogu po wczytaniu i przesłaniu danych.
Następny komponent, przedstawiony w powyższym fragmencie kodu, to transformer klienta. Jest on potrzebny, ponieważ projekt Spring Integration „nie wie”, jak przekształcić dany obiekt typu String na obiekt typu Item. Dlatego musi pobrać dane wejściowe z kanału input-channel i przetworzyć je według podanej logiki. package com.apress.springwebrecipes.flex.auction.integrations; import import import import
com.apress.springwebrecipes.flex.auction.model.Item; org.apache.commons.lang.StringUtils; org.springframework.integration.annotation.Transformer; org.springframework.stereotype.Component;
import java.io.IOException; @Component public class FileToItemTransformer { @Transformer public Item transformFromFileStringToItem(String fileContent) throws IOException { if (StringUtils.isEmpty(fileContent)) throw new RuntimeException( "Plik jest pusty. Nie można utworzyć obiektu typu Item."); String[] parts = fileContent.split(","); if (parts.length != 4) throw new RuntimeException( "Błąd przetwarzania pliku. Nie można utworzyć obiektu typu Item."); String seller = parts[0],
393
ROZDZIAŁ 10. SPRING I FLEX
item = parts[1], description = parts[2], basePrice = parts[3]; Item itemObj = new Item(); itemObj.setDescription(description); itemObj.setItem(item); itemObj.setSellerEmail(seller); itemObj.setBasePrice(Double.parseDouble(basePrice)); return itemObj; } }
Projekt Spring Integration korzysta z kodu do przekształcania danych zapisanego w ziarnie podanym w atrybucie ref (podobnie jak wcześniej). W klasie tego ziarna wyszukiwane są metody opatrzone adnotacją @Transformer. To właśnie one zostają wywołane. Następnie Spring Integration przyjmuje wynik i przekazuje go do innego kanału, inboundItems. Ponieważ jest to nieskomplikowany przykład, kod jest celowo i sztucznie „optymistyczny” oraz uproszczony. Nie ma tu obsługi błędów ani sprawdzania poprawności.
Następny komponent potoku (przedstawiony w powyższym fragmencie kodu) wywołuje usługę Springa, AuctionService, aby ta wstawiła obiekt typu Item do repozytorium. Zauważ, że w tym komponencie nie skonfigurowano kanału output-channel. Jest tak, ponieważ wiadomo, że usługa bezpośrednio wywołuje usługę JMS i przesyła obiekt typu Item do obiektu itemPosted typu javax.jms.Topic. package com.apress.springwebrecipes.flex.auction.integrations; import import import import import
com.apress.springwebrecipes.flex.auction.AuctionService; com.apress.springwebrecipes.flex.auction.model.Item; org.springframework.beans.factory.annotation.Autowired; org.springframework.integration.annotation.ServiceActivator; org.springframework.stereotype.Component;
@Component public class ItemCreationServiceActivator { @Autowired private AuctionService auctionService; @ServiceActivator public void postItem(Item item) throws RuntimeException { auctionService.postItem( item.getSellerEmail(), item.getItem(), item.getDescription(), item.getBasePrice(), null); } }
Ten kod jest bardzo prosty. Przyjmuje komunikat wejściowy (obiekt typu Item) i przekazuje go do metody postItem obiektu typu AuctionService.
Następny krok wymaga pobrania tych komunikatów i przekazania ich do BlazeDS. Nie ma znaczenia, czy komunikaty JMS zostały dodane w aplikacji Fleksa, czy pobrano je z pliku. Należy przesłać je do warstwy pośredniej BlazeDS, która opublikuje nowy produkt we wszystkich zalogowanych klientach Fleksa.
394
10.6. KORZYSTANIE Z USŁUG OPARTYCH NA KOMUNIKATACH W NARZĘDZIACH BLAZEDS I SPRING
W tym fragmencie skonfigurowany jest adapter jms:message-driven-channel-adapter. Pobiera on komunikaty i przekazuje je do kanału inboundItemsPosted — podobnie jak adapter file:inbound-channel-adapter pobiera pliki i przekazuje je do kanału określonego w atrybucie channel. Ponieważ ten fragment jest specyficzny dla usług JMS, trzeba skonfigurować fabrykę połączeń, a także lokalizację docelową. Tu używane są referencje do zadeklarowanych wcześniej fabryki połączeń i tematów z biblioteki ActiveMQ. Standardowo każdy komunikat pobrany przez usługi JMS jest bezpośrednio przekazywany do BlazeDS. Jednak to podejście ma pewną wadę. Potok oparty na projekcie Spring Integration działa przy założeniu, że dostępny jest każdy komponent w tym potoku. Tu komponenty wykonują wszystkie operacje i przesyłają komunikaty do BlazeDS „z nadzieją”, że zdarzenie zostanie tam wykorzystane. Jeśli nikt nie jest zalogowany do klienta Fleksa, który pobiera komunikaty, danego komunikatu nie można wykorzystać, co prowadzi do wystąpienia wyjątku. Dlatego należy przesłać komunikat i zignorować informacje o błędach. Nie jest ważne, czy BlazeDS wykorzysta komunikat. Pamiętaj, że usługi JMS służą do wysyłania komunikatów ze zdarzeniami, a nie do przekazywania wymagających spójności komunikatów z danymi. Dlatego można zignorować błędy, ponieważ przy ładowaniu klienta Fleksa interfejs użytkownika i tak jest odświeżany na podstawie najnowszych danych. Z tego powodu należy dodać pośrednika, który przyjmuje komunikaty o błędach.
Tu używany jest komponent service-activator. Podobnie jak w przedstawionym wcześniej kodzie, przyjmuje on dane z kanału input-channel i przekazuje wykonanie skomplikowanych operacji ziarnu Springa określonemu w atrybucie ref. Także tu można zwrócić wyniki do kanału output-channel, jednak w pewnych sytuacjach (gdy nie istnieje żaden konsument danych) może to doprowadzić do wystąpienia wyjątku Exception. Dlatego stosujemy pewną sztuczkę i samodzielnie obsługujemy operację send. Komponent projektu Spring Integration można wstrzyknąć w taki sam sposób jak dowolne inne ziarno. Tu technika ta służy do wstrzyknięcia klasy kanału inboundItemsAudited. package com.apress.springwebrecipes.flex.auction.integrations; import import import import import
org.apache.commons.lang.builder.ToStringBuilder; org.springframework.integration.annotation.ServiceActivator; org.springframework.integration.core.Message; org.springframework.integration.core.MessageChannel; org.springframework.stereotype.Component;
import javax.annotation.Resource; import java.util.Map; @Component public class MessageAbsorber { @Resource(name = "inboundItemsAudited") private MessageChannel publishSubscribeChannel; @ServiceActivator public void handle(Message> msg) { boolean status = publishSubscribeChannel.send(msg); // Wartość zmiennej status nie ma znaczenia } }
395
ROZDZIAŁ 10. SPRING I FLEX
Tu wartość zmiennej status jest pomijana. Aplikacja nie zgłasza wyjątków ani żadnych innych problemów. Jeśli konsument istnieje, zmienna status ma wartość true. W przeciwnym razie też nie pojawiają się żadne kłopoty. Ponadto BlazeDS musi „wiedzieć”, że ma oczekiwać na komunikaty z kanału Spring Integration. Podobnie jak poinformowano BlazeDS o miejscu, z którego ma pobierać nowe komunikaty JMS, tak należy wskazać źródło nowych komunikatów z kanału Spring Integration.
Atrybuty channels i id powinny wyglądać znajomo. Jedyna nowość to element flex:integrationmessage-destination i atrybut message-channel. Oznaczają one, że BlazeDS ma pobierać komunikaty wysyłane z klasy MessageAbsorber.
BlazeDS Zobaczyłeś już, że narzędzie Spring BlazeDS Integration zapewnia rozbudowaną obsługę wiązania aplikacji z istniejącymi warstwami MOM i rozwiązaniami integracyjnymi. Jednak czasem nie istnieją żadne warstwy pośrednie do obsługi komunikatów ani rozwiązania integracyjne. W niektórych sytuacjach programista chce tylko powiadamiać o czymś inne klienty Fleksa. BlazeDS obsługuje także ten scenariusz. Aby zadeklarować lokalizację docelową, która tylko pobiera komunikaty od klientów Fleksa, wystarczy skonfigurować element .
Wysyłanie komunikatów z Fleksa Dodana lokalizacja docelowa może być używana tylko w klientach, które potrafią komunikować się z BlazeDS, na przykład w klientach Fleksa. W jaki sposób Flex wysyła komunikaty do takich lokalizacji (a także do innych przedstawionych miejsc)? Używany do tego interfejs API, podobnie jak interfejs służący do konsumowania komunikatów, jest bardzo prosty. Przyjrzyjmy się przykładowi przekazywania komunikatów do BlazeDS — aplikacji do obsługi chatu.
396
mx.events.FlexEvent; mx.messaging.ChannelSet; mx.messaging.Consumer; mx.messaging.Producer; mx.messaging.channels.AMFChannel; mx.messaging.events.MessageEvent; mx.messaging.messages.AsyncMessage;
10.6. KORZYSTANIE Z USŁUG OPARTYCH NA KOMUNIKATACH W NARZĘDZIACH BLAZEDS I SPRING
private var chatPublisher:Producer; private var chatConsumer:Consumer ; public function setup(fe:FlexEvent):void { var chatDestination:String = 'chatDestination'; var cs:ChannelSet = new ChannelSet(); cs.channels=[ new AMFChannel( 'my-amf','http://localhost:8080/mb/amf') ]; chatPublisher = new Producer(); chatPublisher.channelSet =cs; chatPublisher.destination = chatDestination; chatPublisher.connect(); chatConsumer = new Consumer(); chatConsumer.channelSet = cs; chatConsumer.destination = chatDestination; chatConsumer.addEventListener(MessageEvent.MESSAGE, function (msgEvent:MessageEvent):void { var msg:AsyncMessage = msgEvent.message as AsyncMessage; output.text += msg.body + "\n"; } ); chatConsumer.subscribe(); } private function sendChatMessage(me:MouseEvent):void { var msg:AsyncMessage = new AsyncMessage(); msg.body = input.text; chatPublisher.send(msg); input.text = ""; } ]]>
Ta aplikacja łączy obiekty typu Consumer i Producer z tą samą lokalizacją docelową BlazeDS — chatDestination. Definicja tej lokalizacji znajduje się w pliku konfiguracyjnym narzędzia Spring BlazeDS Integration i wygląda tak:
W tej aplikacji konfigurowane jest proste pole tekstowe, w którym można wpisać dane. Gdy tekst jest przesyłany, obiekt typu Producer wysyła komunikat do BlazeDS. Jeśli otworzysz aplikację w kilku przeglądarkach, zobaczysz, że po wpisaniu wiadomości w jednym oknie wszystkie pozostałe przeglądarki natychmiast aktualizują wyświetlane informacje.
397
ROZDZIAŁ 10. SPRING I FLEX
10.7. Wstrzykiwanie zależności w kliencie w ActionScripcie Problem Jak wstrzykiwać zależności w kliencie Fleksa? Spring pozwala tworzyć bardziej przejrzyste aplikacje za pomocą Javy i technologii .NET, ponieważ udostępnia zaawansowany sposób konfigurowania komponentów. Jak uzyskać ten sam efekt w kodzie w ActionScripcie we Flashu lub Fleksie? We wcześniejszych przykładach niezależnie od tego, czy konfigurowano obiekty typu Producer, Consumer lub RemoteObject w ActionScripcie albo MXML-u, kod był powielany. Jeśli chciałeś połączyć się z tymi samymi usługami w komponencie, musiałeś przekazywać referencje lub ponownie napisać kod do pobierania zasobu. Ponieważ w ActionScripcie typy są określane statycznie, a Flex to system oparty na komponentach, można bardzo łatwo tworzyć zaawansowane aplikacje z setkami komunikujących się między sobą komponentów. Dlatego bardzo ważne jest, aby zastosować najlepsze praktyki izolowania fragmentów kodu wymagających zmian od tych, które pozostają takie same. W Javie problem ten można rozwiązać za pomocą Springa.
Rozwiązanie Wykorzystamy tu projekt Spring ActionScript (jego wcześniejsza nazwa to Prana) do utworzenia nowej wersji prostego klienta chatu we Fleksie. Tu zastosowano zdalne usługi i zewnętrznie skonfigurowanych konsumentów. Elementy te można wielokrotnie wykorzystywać i pobierać w komponentach aplikacji w bardzo podobny sposób do tego, z jakiego korzystaliśmy przy stosowaniu Springa w Javie. Podobieństwa między tymi technikami są bardzo duże. W projekcie Spring ActionScript używana jest nawet adnotacja [Autowired]!
Jak to działa? Projekt Spring ActionScript to implementacja rozwiązań ze Springa napisana w języku ActionScript. Jest to projekt będący rozszerzeniem Springa. Strona główna poświęcona temu rozszerzeniu to http://www.springsource.org/ extensions/se-springactionscript-as (ponadto strona główna samego projektu to http://www.springactionscript.org). Projekt Spring ActionScript można wykorzystać w środowiskach Flash, Flex lub AIR. Tu koncentrujemy się na Fleksie. Projekt Spring ActionScript nie pomaga w wykorzystaniu w ActionScripcie usług Springa dla Javy, jednak ułatwia uporządkowanie kodu w ActionScripcie. Projekt ten działa podobnie jak sam Spring — należy skonfigurować obiekty (nie ziarna) w pliku XML i wczytać taki plik w klasie implementującej interfejs ApplicationContext. Istnieją dwie implementacje tego interfejsu: jedna dla środowiska Flex (org.spring extensions.actionscript.context.support.FlexXMLApplicationContext) i jedna dla środowiska Flash (org.springextensions.actionscript.context.support.XMLApplicationContext). Flex udostępnia dodatkowe udogodnienia, które umożliwiają automatyczne wstrzykiwanie obiektów do komponentów Fleksa. Ma to duże znaczenie, ponieważ komponenty Fleksa są zarządzane przez środowisko uruchomieniowe Fleksa, a nie przez kontener Springa. Dlatego mimo tego ostatniego ograniczenia możliwość opatrzenia zmiennych adnotacją [Autowired] jest bardzo cenna. Pora wykorzystać możliwości Fleksa i zmodyfikować przykładowy chat w taki sposób, aby aplikacja pobierała referencje do obiektów typu mx.messaging.Producer i mx.messaging.Consumer z kontekstu aplikacji Springa. Najpierw trzeba utworzyć kontekst aplikacji.
398
10.7. WSTRZYKIWANIE ZALEŻNOŚCI W KLIENCIE W ACTIONSCRIPCIE
Z tej aplikacji usunięto cały kod odpowiedzialny za jej inicjowanie. Skoncentrowaliśmy się tu na ładowaniu kontekstu aplikacji projektu Spring ActionScript. Istnieje wiele sposobów wczytywania kontekstu w XML-u. Można wykorzystać do tego zewnętrzny adres URL lub zagnieżdżony zasób. Najprostszy i najbardziej niezawodny sposób to prawdopodobnie zastosowanie zagnieżdżonego zasobu. Kompilator powinien wtedy umieścić zawartość zasobu w skompilowanym pliku SWF. Aby móc uzyskać dostęp do zasobu, należy dodać adnotację [Embed] i poinformować Fleksa, który zasób należy zagnieździć i jaki jest jego typ MIME. Flex wstrzyknie wtedy zawartość zasobu do zmiennej opatrzonej tą adnotacją. Następnie w odbiorniku zdarzeń applicationComplete zgłaszanych przez obiekt typu Application należy zainicjować obiekt typu FlexXMLApplicationContext. Wywołanie metody addEmbeddedConfig powoduje wczytanie kontekstu w XML-u na podstawie zawartości zagnieżdżonego zasobu (pliku XML). W ostatnim kroku dla kontekstu wywoływana jest metoda load.
399
ROZDZIAŁ 10. SPRING I FLEX
Czasem oprócz zasobu zagnieżdżonego używane są też inne zasoby. Ich wczytywanie odbywa się wtedy asynchronicznie. W takich scenariuszach należy dodać odbiornik informujący o załadowaniu odpowiedniego pliku XML. To dlatego operacja wczytywania jest bardzo ważna i dlatego tworzenie kontekstu nie odbywa się w konstruktorze klasy FlexXMLApplicationContext. Pora przejść do tworzenia pierwszego pliku XML z kontekstem aplikacji Springa, app-context4.xml. Obsługa konfiguracji aplikacji opartych na Fleksie i ActionScripcie jest bardzo rozbudowana, a z każdą nową wersją pojawiają się dodatkowe możliwości. W wersji 0.8.1 dostępna jest na przykład prosta obsługa przestrzeni nazw. Jednak tak naprawdę nie potrzebujesz korzystać ze skomplikowanych funkcji dostępnych w wersji dla Javy. Można ująć to inaczej — w Springu dla Javy opcje konfiguracyjne są używane zarówno do wymuszania zasady DRY (ang. Don’t Repeat Yourself, czyli nie powtarzaj się), jak i abstrakcyjnego przedstawiania skomplikowanych scenariuszy tworzenia obiektów (na przykład do deklaratywnego zarządzania transakcjami). Flex bywa żmudny, jednak częściej używa się go do unikania powtórzeń w kodzie. Oto najprostszy możliwy kontekst aplikacji:
Ten kod powinien być zrozumiały. Zamiast elementów beans używane są elementy objects. Ponadto stosowane są inne przestrzenie nazw. Dla programistów Springa nie powinno to być nic nowego. Dodajmy teraz kilka przydatnych funkcji do kontekstu aplikacji Springa. Jednym z pożądanych mechanizmów jest możliwość określania właściwości środowiska uruchomieniowego Fleksa udostępnianych przez klasę Application. Te właściwości informują aplikację o kontekście. Pozwala to ustalić, gdzie i jak szybko aplikacja działa, pod jakim adresem URL itd. Obsługiwane właściwości to na przykład: applicationurl.port, application.url, application.url.host i application.url.protocol. Aby określić te właściwości, należy dodać dostępny w projekcie Spring ActionScript odpowiednik postprocesora BeanPostProcessor. Dodaj między elementami objects poniższy kod:
Teraz, co nie jest zaskoczeniem, można używać wymienionych właściwości w pliku konfiguracyjnym. Pozwala na to składnia interpolowania zmiennych: ${application.url}. Następnym krokiem jest sprawienie, aby projekt Spring ActionScript automatycznie łączył zależności we wszystkich dodawanych do sceny komponentach. W tym celu dodaj poniższy kod:
Aby wykorzystać ten element, wystarczy w pokazany poniżej sposób zadeklarować zmienną komponentu dodawanego do sceny: import mx.rpc.remoting.RemoteObject; // ... Autowired(name = "auctionService")] public var auctionService:RemoteObject ;
Pamiętaj, że główny plik .mxml nie jest komponentem. Tworzenie komponentów nie jest bardzo skomplikowane, jednak omawianie tego procesu wykracza poza zakres tej książki. Komponent umożliwia utworzenie elementu o określonych funkcjach (i w razie potrzeby powiązanego z nim widoku) oraz korzystanie
400
10.7. WSTRZYKIWANIE ZALEŻNOŚCI W KLIENCIE W ACTIONSCRIPCIE
z niego w innych miejscach. Może to być na przykład przycisk używany w platformie. Wystarczy podać go w XML-u, a gotowy do zastosowania przycisk (którego wszystkie operacje są całkowicie ukryte) pojawi się w interfejsie użytkownika. Przedstawioną tu wstępną konfigurację warto przygotować przed rozpoczęciem pracy nad kodem w ActionScripcie, ponieważ pozwala zaoszczędzić dużo czasu, a jest bardzo prosta do napisania. Teraz przejdźmy do konfigurowania wstrzykiwanych obiektów. W aplikacji do obsługi chatu znajdują się klasy mx.messaging.Consumer i mx.messaging.Producer. Aby utworzyć obiekty tych typów, potrzebny jest poprawny obiekt typu mx.messaging.ChannelSet. Zacznijmy od skonfigurowania go. Dodaj do kontekstu aplikacji poniższy kod:
Ten przykład jest krótki, ale ilustruje wiele technik. Widać tu, jak można utworzyć tablicę w pliku XML projektu Spring ActionScript, jak skonfigurować konstruktor, a nawet jak skonfigurować obiekt. Tworzenie obiektów typu Producer i Consumer także jest proste. Wystarczą do tego po cztery wiersze kodu w XML-u.
Do tego miejsca wszystko powinno wyglądać bardzo naturalnie. W większości sytuacji można zastąpić element object elementem bean i uzyskać poprawny plik XML z kontekstem Springa dla Javy! Teraz można wykorzystać dodane obiekty. Wróćmy do aplikacji do obsługi chatu i pobierzmy referencje do obiektów typu Producer i Consumer z kontekstu aplikacji. Należy to zrobić po zakończeniu wczytywania aplikacji. Używanie ziaren w Springu nieco różni się od korzystania z projektu Spring ActionScript. Wynika to ze sposobu działania kompilatora Fleksa. Pierwszą oczywistą zmianą jest zastąpienie dawnego kodu odpowiedzialnego za inicjowanie kodem pobierającym potrzebne obiekty. Jest to możliwe dzięki utworzeniu obiektu typu FlexXMLApplicationContext w metodzie setup, którą skonfigurowano tak, aby reagowała na zdarzenia applicationComplete. chatConsumer = _applicationContext.getObject('consumer') as Consumer; chatConsumer.addEventListener(MessageEvent.MESSAGE, function (msgEvent:MessageEvent):void { var msg:AsyncMessage = msgEvent.message as AsyncMessage; output.text += msg.body + "\n"; }); chatConsumer.subscribe(); chatProducer = _applicationContext.getObject('producer') as Producer; chatProducer.connect();
To powinno być wszystko, co jest potrzebne do uzyskania programu analogicznego do wyjściowej aplikacji. Niestety trzeba zrobić coś jeszcze. Problem polega na tym, że kompilator Fleksa usuwa nieużywane
401
ROZDZIAŁ 10. SPRING I FLEX
klasy, aby zminimalizować wielkość wynikowego, skompilowanego pliku binarnego. Dotyczy to na przykład klas z podstawowej biblioteki platformy. Zwykle jest to dobre rozwiązanie. Jednak ponieważ aplikacja tworzy wiele obiektów za pomocą Springa, nie wiadomo, które klasy są używane. Ponieważ w kodzie nie ma na przykład deklaracji zmiennych typu AMFChannel, kompilator pomija tę klasę, choć obiekty tego typu są tworzone w Springu. Istnieje wiele rozwiązań tego problemu. Najprostsze polega na dodaniu referencji do każdej klasy, która jest potrzebna w skompilowanym kodzie. W tym celu tworzymy bloki anonimowe. Można je umieścić w różnych modułach lub na wyższym poziomie, na przykład w klasie głównej. Oto przykładowy blok tego rodzaju:
Dopiero teraz powstało rozwiązanie, które pozwala zastąpić wcześniejszy kod i uniknąć niepotrzebnego powtarzania tych samych fragmentów. Możesz — podobnie jak wcześniej — uruchomić klienta Fleksa w różnych przeglądarkach i upewnić się, że komunikat wygenerowany w jednej sesji jest natychmiast odzwierciedlany w innych oknach.
Podsumowanie W tym rozdziale poznałeś Flasha i architekturę technologii Flex. Dowiedziałeś się, jak wpasować w rozwiązanie trzy technologie: Flash, Flex i AIR. Zobaczyłeś różne mechanizmy pozwalające zmienić sposób komunikowania się aplikacji z innymi usługami oraz poznałeś ograniczenia tych technik. Przyjrzałeś się różnym sposobom współdziałania Fleksa z innymi technologiami, a także konfigurowaniu BlazeDS (jest to otwarty projekt warstwy pośredniej rozwijany przez firmę Adobe). BlazeDS pomaga przezwyciężyć wbudowane ograniczenia (lub cechy) platformy Flash. Zobaczyłeś też, jak wykorzystać projekt Spring BlazeDS Integration do uproszczenia tworzenia w aplikacji warstwy pośredniej opartej na technologii BlazeDS. Poznałeś usługi typu push i pull, które można stosować w aplikacjach typu klient-serwer, i dowiedziałeś się, jak korzystać z takich usług za pomocą BlazeDS i Fleksa. Wiesz już, jak udostępniać usługi Springa klientom Fleksa w synchroniczny i asynchroniczny sposób, korzystając z usług opartych na komunikatach, technologii JMS i projektu Spring Integration. Poznałeś też kontener Spring ActionScript, który pozwala uzyskać w aplikacjach w ActionScripcie precyzję i elegancję, jakie Spring oferuje programistom Javy.
402
ROZDZIAŁ 11
Grails
W trakcie tworzenia aplikacji sieciowej w Javie trzeba połączyć ze sobą zestaw klas Javy, utworzyć pliki konfiguracyjne i opracować określoną strukturę programu. Zadania te mają mało wspólnego z problemami rozwiązywanymi przez aplikację. Te elementy często nazywa się rusztowaniem (ang. scaffolding), ponieważ są tylko środkiem do celu, którym jest to, co aplikacja robi. Grails to platforma zaprojektowana w celu ograniczenia prac nad rusztowaniem w aplikacjach Javy. Jest oparta na języku Groovy (jest to język zgodny z wirtualną maszyną Javy) i na podstawie stosowanych konwencji automatyzuje wiele zadań, które trzeba wykonać w aplikacjach Javy. Na przykład w trakcie tworzenia kontrolerów aplikacji należy powiązać z nimi zestaw widoków (czyli stron JSP), a także przygotować plik konfiguracyjny. Jeśli generujesz kontroler za pomocą platformy Grails, automatyzuje ona na podstawie konwencji wiele kroków (na przykład tworzenie widoków i plików konfiguracyjnych). Później możesz zmodyfikować wygenerowany przez platformę kod pod kątem konkretnych scenariuszy, jednak i tak czas pracy jest wtedy znacznie krótszy, ponieważ nie trzeba pisać wszystkich elementów od podstaw (na przykład tworzyć plików XML z konfiguracją lub przygotowywać struktury katalogów projektu). Platforma Grails jest w pełni zintegrowana ze Springiem 3.0, dlatego możesz wykorzystać ją do szybkiego rozpoczęcia prac nad aplikacjami w Springu i przyspieszyć dzięki temu proces programowania.
11.1. Pobieranie i instalowanie platformy Grails Problem Programista chce rozpocząć tworzenie aplikacji za pomocą platformy Grails, ale nie wie, skąd ją pobrać i jak ją zainstalować.
Rozwiązanie Platformę Grails można pobrać ze strony http://www.grails.org/. Zwróć uwagę na to, aby pobrać wersję 1.2 lub nowszą, ponieważ tylko one obsługują Spring 3.0. Grails to samodzielna platforma, obejmująca różne skrypty, które pozwalają zautomatyzować proces tworzenia aplikacji w Javie. Dlatego wystarczy wypakować pobrany plik i wykonać kilka kroków w ramach instalacji, aby móc zacząć tworzyć aplikacje Javy.
ROZDZIAŁ 11. GRAILS
Jak to działa? Po wypakowaniu platformy Grails zdefiniuj w systemie operacyjnym dwie zmienne środowiskowe: GRAILS_HOME i PATH. Dzięki temu będziesz mógł wywoływać operacje tej platformy z dowolnego katalogu komputera. Jeśli używasz Linuksa, możesz zmodyfikować globalny plik bashrc z katalogu /etc/ lub wykorzystać plik .bashrc z katalogu głównego danego użytkownika. Zauważ, że w niektórych dystrybucjach Linuksa nazwa tego pliku może być nieco inna (na przykład bash.bashrc). W obu wymienionych plikach zmienne środowiskowe definiuje się za pomocą tej samej składni. W pierwszym definiowane są zmienne dla wszystkich użytkowników, natomiast w drugim — dla jednego z nich. Umieść w dowolnym z tych plików następujący kod: GRAILS_HOME=//grails export GRAILS_HOME export PATH=$PATH:$GRAILS_HOME/bin
Jeśli używasz komputera z systemem Windows, przejdź do panelu sterowania i kliknij ikonę System. W oknie, które się pojawi, kliknij opcję Zaawansowane ustawienia systemu. Następnie otwórz zakładkę Zaawansowane i kliknij przycisk Zmienne środowiskowe, aby otworzyć edytor zmiennych środowiskowych. W tym edytorze możesz dodawać i modyfikować zmienne środowiskowe dla jednego lub wszystkich użytkowników. W tym celu wykonaj następujące czynności: 1. Kliknij Nowa. 2. Utwórz zmienną środowiskową o nazwie GRAILS_HOME i wartości odpowiadającej katalogowi instalacyjnemu platformy Grails (na przykład //grails). 3. Zaznacz zmienną środowiskową PATH i kliknij Modyfikuj. 4. Dodaj wartość ;%GRAILS_HOME%\bin na końcu zmiennej środowiskowej PATH. Ostrzeżenie Koniecznie dodaj tę ostatnią wartość i nie modyfikuj zmiennej środowiskowej PATH w inny sposób, ponieważ mogłoby to uniemożliwić działanie niektórych aplikacji.
Po wykonaniu tych operacji na komputerze z systemem Windows lub Linux możesz zacząć tworzyć aplikacje za pomocą platformy Grails. Jeśli z poziomu dowolnego katalogu wywołasz polecenie grails help, pojawi się długa lista instrukcji platformy Grails.
11.2. Tworzenie aplikacji za pomocą platformy Grails Problem Programista chce utworzyć aplikację przy użyciu platformy Grails.
Rozwiązanie Aby utworzyć aplikację za pomocą platformy Grails, w katalogu, w którym ma znaleźć się ta aplikacja, wywołaj polecenie grails create-app . To spowoduje utworzenie katalogu z aplikacją platformy Grails o strukturze zgodnej z projektem tej platformy. Jeśli wywołanie podanej instrukcji zakończy się niepowodzeniem, zajrzyj do receptury 11.1. Jeżeli platforma Grails jest poprawnie zainstalowana, polecenie grails powinno być dostępne w dowolnej konsoli i w każdym terminalu.
404
11.2. TWORZENIE APLIKACJI ZA POMOCĄ PLATFORMY GRAILS
Jak to działa? Na przykład wpisanie polecenia grails create-app court tworzy aplikację platformy Grails w katalogu court. W tym katalogu pojawi się zestaw plików i katalogów wygenerowanych przez platformę Grails zgodnie z konwencjami. Początkowa struktura projektu w aplikacji platformy Grails wygląda tak: application.properties build.xml court.iml court.iws court-test.launch court.ipr court.launch court.tmproj ivy.xml ivysettings.xml grails-app/ lib/ scripts/ src/ test/ web-app/
Uwaga Oprócz tej struktury platforma Grails tworzy zestaw katalogów i plików roboczych aplikacji, których programista nie powinien sam modyfikować. Te elementy znajdują się w katalogu głównym użytkownika: .grails//.
Na ostatnim listingu widać, że Grails generuje zestaw plików i katalogów występujących w większości aplikacji Javy. Są to między innymi pliki narzędzi Apache Ant (build.xml) i Apache Ivy (ivy.xml), a także typowe katalogi, na przykład src (przeznaczony na pliki z kodem źródłowym) i web-app (zawierający typową strukturę aplikacji sieciowych Javy, czyli katalogi /WEB-INF/, /META-INF/, css, images i js). Tak więc Grails pozwala przyspieszyć pracę, ponieważ jedno polecenie wystarcza, aby utworzyć typowe elementy aplikacji Javy.
Struktura plików i katalogów w aplikacji platformy Grails Ponieważ niektóre pliki i katalogi są specyficzne dla platformy Grails, poniżej opisujemy przeznaczenie każdego z tych elementów. Plik application.properties służy do definiowania właściwości aplikacji, w tym wersji platformy Grails, wersji serwletu i nazwy aplikacji. Plik build.xml to skrypt narzędzia Apache Ant z zestawem zdefiniowanych zadań, które powodują utworzenie aplikacji platformy Grails. Plik court.iml to plik XML zawierający parametry konfiguracyjne aplikacji (określające na przykład lokalizacje katalogów i sposób obsługi plików JAR). Plik court.iws to plik XML zawierający parametry konfiguracyjne związane z instalacją aplikacji (na przykład port kontenera WWW i widoki projektu). Plik court-test.launch to plik XML z parametrami konfiguracyjnymi testów aplikacji. Plik court.ipr to plik XML z parametrami konfiguracyjnymi tablicy bibliotek aplikacji (czyli z plikami JAR). Plik court.launch to plik XML z uruchomieniowymi parametrami konfiguracyjnymi, na przykład z argumentami dla maszyny JVM. Plik court.tmproj to plik XML z parametrami konfiguracyjnymi w postaci tymczasowych właściwości roboczych aplikacji.
405
ROZDZIAŁ 11. GRAILS
Plik ivy.xml to plik konfiguracyjny narzędzia Apache Ivy używany do definiowania zależności aplikacji. Plik ivysettings.xml to plik konfiguracyjny narzędzia Apache Ivy używany do definiowania repozytoriów, z których pobierane są zależności. Katalog grails-app zawiera główne elementy aplikacji i obejmuje następujące podkatalogi: 1. katalog conf, zawierający pliki źródłowe z konfiguracją aplikacji; 2. katalog controllers, w którym znajdują się pliki kontrolerów aplikacji; 3. katalog domain, gdzie zapisane są pliki z klasami domenowymi aplikacji; 4. katalog i18n, obejmujący pliki związane z umiędzynarodawianiem aplikacji; 5. katalog services, gdzie znajdują się pliki z usługami aplikacji; 6. katalog taglib, który zawiera biblioteki znaczników aplikacji; 7. katalog utils, w którym zapisane są pliki narzędziowe aplikacji; 8. katalog views, obejmujący pliki widoków aplikacji. Katalog lib służy do przechowywania bibliotek (czyli plików JAR). Katalog scripts jest przeznaczony na skrypty. Katalog src zawiera pliki z kodem źródłowym aplikacji. Znajdują się w nim dwa podkatalogi, groovy i java, obejmujące kod źródłowy napisany w językach Groovy i Java. Katalog test obejmuje pliki z testami aplikacji. Znajdują się w nim dwa podkatalogi, integration i unit, gdzie przechowuje się testy integracyjne i jednostkowe. Katalog web-app służy do przechowywania struktury potrzebnej przy instalowaniu aplikacji. Znajdują się tu standardowe pliki WAR oraz struktura katalogów (/WEB-INF/, /META-INF/, css, images i js). Uwaga Platforma Grails sama w sobie nie zapewnia obsługi narzędzia Apache Maven. Jeśli jednak chcesz używać tego narzędzia, jest to możliwe. Potrzebne informacje znajdziesz na stronie http://www.grails.org/Maven+Integration.
Uruchamianie aplikacji Platforma Grails jest wstępnie skonfigurowana tak, aby uruchamiała aplikacje w kontenerze WWW Apache Tomcat. Zauważ, że pliki związane z tym kontenerem znajdują się w katalogu głównym użytkownika (.grails//) i nie są widoczne. Proces uruchamiania aplikacji platformy Grails (podobnie jak proces ich tworzenia) jest w dużym stopniu zautomatyzowany. Przejdź do katalogu głównego danej aplikacji platformy Grails i wywołaj polecenie grails run-app. W razie potrzeby aplikacja zostanie zbudowana, po czym nastąpi uruchomienie kontenera WWW Apache Tomcat i zainstalowanie aplikacji. Ponieważ Grails działa na podstawie konwencji, aplikacja jest instalowana w kontekście o nazwie pasującej do nazwy projektu. Na przykład aplikacja o nazwie court jest dostępna pod adresem URL http://localhost:8080/court/. Rysunek 11.1 przedstawia domyślną stronę główną aplikacji platformy Grails. Na razie w aplikacji nie wprowadzono żadnych zmian. Teraz dowiesz się, jak utworzyć pierwszy element aplikacji platformy Grails. Dzięki temu nauczysz się stosować procedury przyspieszające pracę.
Tworzenie pierwszego elementu aplikacji platformy Grails Skoro już wiesz, jak łatwe jest tworzenie aplikacji platformy Grails, pora dodać element takiej aplikacji. Tu będzie to kontroler. Zobaczysz dzięki temu, w jaki sposób Grails automatyzuje różne zadania w procesie tworzenia aplikacji Javy. Przejdź do katalogu głównego tworzonej aplikacji platformy Grails i wywołaj polecenie grails create-controller welcome. W efekcie wykonane zostaną następujące zadania: 1. Utworzenie kontrolera WelcomeController.groovy w katalogu grails-app/controllers aplikacji.
406
11.2. TWORZENIE APLIKACJI ZA POMOCĄ PLATFORMY GRAILS
Rysunek 11.1. Domyślna strona główna programu court (jest to aplikacja platformy Grails) 2. Utworzenie podkatalogu welcome w katalogu grails-app/views aplikacji. 3. Utworzenie klasy testów o nazwie WelcomeControllerTests.groovy w katalogu test/unit aplikacji. Zacznijmy od przeanalizowania zawartości kontrolera wygenerowanego przez platformę Grails. Oto kod tego kontrolera: class WelcomeController { def index = {} }
Jeśli nie znasz języka Groovy, składnia tego kodu może wydawać Ci się dziwna. Tworzona jest tu klasa WelcomeController z metodą index. Ta klasa ma to samo przeznaczenie co kontrolery platformy Spring MVC tworzone w rozdziale 8. Jest to klasa kontrolera z metodą obsługi index. Jednak w obecnym stanie kontroler
nie wykonuje żadnych operacji. Zmodyfikuj go więc w następujący sposób: class WelcomeController { Date now = new Date() def index = {[today:now]} }
Pierwszym dodatkiem jest obiekt typu Date przypisany do pola now klasy. Obiekt ten reprezentuje datę systemową. Ponieważ def index = {} to definicja metody obsługi, nowy fragment [today:now] oznacza zwracaną wartość. Jest nią zmienna today zawierająca pole now klasy. Wartość tej zmiennej jest przekazywana do widoku powiązanego z metodą obsługi. Po przygotowaniu kontrolera i metody obsługi zwracającej bieżącą datę można utworzyć powiązany widok. Jeśli przejdziesz do katalogu grails-app/views/welcome, zobaczysz, że nie zawiera on żadnych widoków. Jednak Grails próbuje znaleźć w tym katalogu widok dla kontrolera WelcomeController. Nazwa tego widoku powinna odpowiadać nazwie danej metody obsługi. Jest to jedna z wielu konwencji używanych w platformie Grails. Utwórz więc we wspomnianym katalogu stronę JSP o nazwie index.jsp i umieść w niej następujący kod: <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> Witaj
407
ROZDZIAŁ 11. GRAILS
Witaj w systemie rezerwowania kortów Dzisiejsza data:
Jak widać, jest to standardowa strona JSP, na której wykorzystano znacznik JSTL. Znacznik ten wyświetla zmienną o nazwie ${today}. Jest to jednocześnie nazwa zmiennej zwracanej przez metodę obsługi index z kontrolera. Warto zauważyć, że ponieważ do aplikacji dodano stronę JSP ze znacznikiem JSTL, Grails automatycznie dołącza do komponentu potrzebne zależności (pliki JAR). Nie trzeba ręcznie pobierać takich często używanych zależności ani zarządzać nimi. Następnie przejdź do katalogu głównego tworzonej aplikacji platformy Grails i wywołaj polecenie grails run-app. Spowoduje to: automatyczne zbudowanie aplikacji, skompilowanie klasy kontrolera, skopiowanie plików w odpowiednie miejsca, uruchomienie kontenera WWW Apache Tomcat i zainstalowanie aplikacji. Zgodnie z konwencjami stosowanymi w platformie Grails kontroler WelcomeController i jego metody obsługi oraz widoki są teraz dostępne w ścieżce kontekstu http://localhost:8080/court/welcome/. Ponieważ index to strona domyślna w tej ścieżce, to po uruchomieniu przeglądarki i wpisaniu adresu http://localhost:8080/ court/welcome/ (lub bezpośrednio http://localhost:8080/court/welcome/index) zobaczysz pokazaną wcześniej stronę JSP. Pojawi się na niej zwrócona przez kontroler bieżąca data. Zwróć uwagę na brak rozszerzenia widoku (czyli .jsp) w adresie URL. Grails domyślnie ukrywa informacje o stosowanych technologiach widoków. Przyczyny tego rozwiązania staną się jasne przy omawianiu bardziej zaawansowanych scenariuszy korzystania z tej platformy. Gdy będziesz powtarzał opisane tu proste kroki potrzebne do utworzenia kontrolera i widoku aplikacji, pamiętaj, że nie musisz tworzyć lub modyfikować żadnych plików konfiguracyjnych, ręcznie kopiować plików w inne lokalizacje ani konfigurować kontenera WWW na potrzeby uruchamiania aplikacji. W trakcie rozwijania aplikacji zautomatyzowanie operacji z zakresu tworzenia rusztowania, często potrzebnych w aplikacjach sieciowych Javy, pozwala znacznie przyspieszyć prace.
Eksportowanie aplikacji platformy Grails do pliku WAR Wszystkie opisane wcześniej zadania są wykonywane w ramach środowiska platformy Grails. Oznacza to, że posłużyła ona do przygotowania kontenera WWW i uruchomienia aplikacji. Jeśli jednak chcesz uruchamiać aplikację platformy Grails w środowisku produkcyjnym, będziesz musiał przygotować program w formacie, w którym będziesz mógł zainstalować go w zewnętrznym kontenerze WWW. W aplikacjach Javy tym formatem jest WAR. Przejdź do katalogu głównego rozwijanej aplikacji platformy Grails i wywołaj polecenie grails war. Spowoduje to wygenerowanie w katalogu głównym pliku WAR o nazwie -.war. Jest to niezależny plik WAR ze wszystkimi elementami potrzebnymi do uruchomienia aplikacji platformy Grails w dowolnym standardowym kontenerze WWW Javy. Dla aplikacji court platforma generuje plik court-0.1.war w katalogu głównym tej aplikacji. Numer wersji pochodzi z parametru app.version zdefiniowanego w pliku application.properties. Zgodnie z konwencjami instalowania aplikacji na serwerze Apache Tomcat plik WAR o nazwie court-0.1.war jest dostępny pod adresem URL http://localhost:8080/court-0.1/. Adresy URL zainstalowanych plików WAR są różne w zależności od używanego kontenera WWW Javy (można używać na przykład serwerów Jetty lub Oracle WebLogic).
11.3. Wtyczki platformy Grails Problem Programista chce mieć dostęp do mechanizmów platformy Java lub interfejsu API Javy w aplikacji platformy Grails, a jednocześnie móc korzystać z technik platformy Grails, aby uniknąć tworzenia rusztowania. 408
11.3. WTYCZKI PLATFORMY GRAILS
Problem nie polega na wykorzystaniu w aplikacji platformy Java lub interfejsu API Javy. To można osiągnąć dzięki umieszczeniu odpowiednich plików JAR w katalogu lib aplikacji. Chodzi tu o ścisłe zintegrowanie platformy Java lub interfejsu API Javy z platformą Grails. Aby uzyskać ten efekt, należy wykorzystać wtyczki platformy Grails. Ścisła integracja z platformą Grails polega na tym, że można korzystać ze skróconych instrukcji (na przykład grails ) do wykonywania konkretnych operacji platformy Java lub interfejsu API Javy. Możliwe jest też używanie mechanizmów Javy w klasach i plikach konfiguracyjnych aplikacji bez konieczności tworzenia rusztowania.
Rozwiązanie Grails ma kilka wbudowanych wtyczek, ale jeśli korzystasz tylko z podstawowych mechanizmów tej platformy, możesz o tym nie wiedzieć. Istnieje wiele wtyczek platformy Grails, dzięki którym korzystanie z konkretnej wersji platformy Java lub interfejsu API Javy jest równie wygodne jak używanie podstawowych funkcji platformy Grails. Oto popularne wtyczki tego rodzaju: Wtyczka App Engine zapewnia integrację pakietu App Engine SDK firmy Google z platformą Grails. Wtyczka Quartz zapewnia integrację z narzędziem Quartz Enterprise Job Scheduler. Pozwala to na szeregowanie zadań i wykonywanie ich w określonych odstępach czasu lub na podstawie wyrażeń narzędzia cron. Wtyczka Spring WS zapewnia integrację i obsługę usług sieciowych opartych na projekcie Spring Web Services. Wtyczka Clojure zapewnia integrację z językiem Clojure i umożliwia wykonywanie kodu w tym języku w komponentach platformy Grails. Aby poznać pełną listę wtyczek platformy Grails, wywołaj polecenie grails list-plugins. Ta instrukcja nawiązuje połączenie z repozytorium wtyczek platformy Grails, a następnie wyświetla wszystkie dostępne wtyczki. Ponadto można wykorzystać instrukcję grails plugin-info , aby uzyskać szczegółowe informacje na temat konkretnych wtyczek. Inna możliwość to wizyta na stronie poświęconej wtyczkom platformy Grails: http://grails.org/plugin/home. Aby zainstalować wtyczkę platformy Grails, należy w katalogu głównym aplikacji wywołać poniższe polecenie: grails install-plugin . W celu usunięcia wtyczki platformy Grails wywołaj w katalogu głównym aplikacji następującą instrukcję: grails uninstall-plugin .
Jak to działa? Wtyczka platformy Grails działa na podstawie zestawu konwencji, które umożliwiają ścisłą integrację platformy Grails z konkretną wersją platformy Java lub z interfejsem API Javy. Domyślnie w platformie Grails zainstalowane są wtyczki dla narzędzi Apache Tomcat i Hibernate. Obie te wtyczki znajdują się w katalogu plugins dystrybucji platformy Grails. W trakcie tworzenia pierwszej aplikacji platformy Grails wtyczki są kopiowane do katalogu roboczego .grails//plugins w katalogu głównym danego użytkownika. W tym samym katalogu generowane są dwa pliki: plugins-list-core.xml i plugins-list-default.xml. W pierwszym wymienione są wtyczki dostępne w każdej aplikacji platformy Grails (domyślnie są to wtyczki dla narzędzi Apache Tomcat i Hibernate). Drugi zawiera pełną listę wszystkich dostępnych wtyczek platformy Grails. Do każdej utworzonej później aplikacji platformy Grails dołączane są wtyczki wymienione w pliku plugins-list-core.xml. W tym celu każda wtyczka jest wypakowywana do katalogu roboczego aplikacji: .grails//projects//plugins/. Po wypakowaniu wtyczek aplikacja platformy Grails uzyskuje dostęp do funkcji zapewnianych przez wtyczki. Oprócz wtyczek domyślnych w poszczególnych aplikacjach można zainstalować dodatkowe wtyczki. Na przykład aby zainstalować wtyczkę Clojure, wywołaj w katalogu głównym aplikacji następujące polecenie: grails install-plugin clojure
409
ROZDZIAŁ 11. GRAILS
Ta instrukcja powoduje pobranie i skopiowanie wtyczki Clojure do katalogu roboczego platformy Grails .grails//plugins umieszczonego w katalogu głównym użytkownika. Następnie wtyczka jest kopiowana jako plik ZIP, a następnie wypakowywana w katalogu roboczym aplikacji: .grails//projects//plugins/. Jeśli chcesz usunąć wtyczkę z aplikacji platformy Grails, wywołaj następujące polecenie: grails uninstall-plugin clojure
Ta instrukcja powoduje usunięcie wypakowanej wtyczki z katalogu roboczego aplikacji: .grails// projects//plugins/. Jednak plik ZIP z wtyczką zostaje zachowany. Ponadto pobrana wtyczka nadal jest dostępna w katalogu roboczym platformy Grails: .grails//plugins. Proces instalowania i odinstalowywania wtyczek dotyczy konkretnych aplikacji. Aby wtyczka była automatycznie dodawana do każdej tworzonej aplikacji platformy Grails, trzeba ręcznie zmodyfikować plik plugins-list-core.xml. Wtedy wymienione w nim wtyczki będą dołączane (razem z domyślnymi wtyczkami dla narzędzi Apache Tomcat i Hibernate) do wszystkich utworzonych później aplikacji. Ostrzeżenie Zalecamy, aby oprócz wykonywania opisanych tu operacji nie modyfikować struktury katalogu roboczego aplikacji platformy Grails ani katalogów wtyczek. Wtyczki zawsze zmieniają strukturę katalogów aplikacji platformy Grails, by udostępnić potrzebne funkcje. Jeśli natrafisz na problemy z wtyczką platformy Grails, zapoznaj się z dokumentacją wtyczki lub za pośrednictwem listy mailingowej http://grails.org/Mailing%20lists zadaj pytanie twórcom platformy Grails.
11.4. Środowisko rozwojowe, produkcyjne i testowe w platformie Grails Problem Programista chce stosować różne parametry do tej samej aplikacji w zależności od środowiska uruchomieniowego (rozwojowe, produkcyjne lub testowe).
Rozwiązanie Twórcy platformy Grails przewidzieli, że aplikacja Javy może przechodzić przez różne stadia rozwoju, na których potrzebne są odmienne parametry. Te etapy w platformie Grails są określane jako „środowiska”. Są to na przykład środowiska: rozwojowe, produkcyjne i testowe. Najbardziej oczywista sytuacja dotyczy źródeł danych. Programiści zwykle używają innych systemów przechowywania danych w środowiskach rozwojowym, produkcyjnym i testowym. Ponieważ każdy z tych systemów wymaga innych parametrów do nawiązywania połączeń, można skonfigurować parametry dla poszczególnych środowisk i pozwolić platformie Grails łączyć się z poszczególnymi systemami w zależności od sposobu działania aplikacji. Platforma Grails obsługuje ten sam mechanizm nie tylko dla źródeł danych, ale też dla innych parametrów, które mogą zmieniać się w różnych środowiskach aplikacji. Dotyczy to na przykład adresów URL używanych przy tworzeniu pełnych odnośników do aplikacji. Parametry konfiguracyjne dla środowiska aplikacji platformy Grails podaje się w plikach z katalogu /grails-app/conf/ aplikacji.
Jak to działa? W zależności od wykonywanych operacji Grails automatycznie wykrywa najlepiej dostosowane środowisko: rozwojowe, produkcyjne lub testowe.
410
11.4. ŚRODOWISKO ROZWOJOWE, PRODUKCYJNE I TESTOWE W PLATFORMIE GRAILS
Na przykład wywołanie polecenia grails run-app oznacza, że programista wciąż rozwija aplikację na lokalnym komputerze. Dlatego używane jest środowisko rozwojowe. Po uruchomieniu tej instrukcji w danych wyjściowych pojawia się między innymi wiersz informujący o ustawieniu środowiska rozwojowego: Environment set to development
To oznacza, że przy budowaniu, konfigurowaniu i uruchamianiu aplikacji używane będą parametry dla środowiska rozwojowego. Inny przykład to polecenie grails war. Ponieważ eksportowanie aplikacji platformy Grails do niezależnego pliku WAR oznacza, że program będzie działał w zewnętrznym kontenerze WWW, Grails przyjmuje, że odpowiednie będzie środowisko produkcyjne. W danych wyjściowych znajduje się wtedy wiersz informujący o ustawieniu środowiska produkcyjnego: Environment set to production
To oznacza, że przy budowaniu, konfigurowaniu i eksportowaniu aplikacji używane będą parametry dla środowiska produkcyjnego. Jeżeli wywołasz instrukcję grails test-app, Grails przyjmie, że odpowiednie będzie środowisko testowe. To sprawia, że przy budowaniu, konfigurowaniu i uruchamianiu testów stosowane będą parametry dla środowiska testowego. W danych wyjściowych pojawia się wtedy wiersz informujący o ustawieniu tego środowiska: Environment set to test
W plikach konfiguracyjnych z katalogu /grails-app/conf/ aplikacji znajdziesz sekcje w następującej postaci: environments { production { grails.serverURL = "http://www.domain.com" } development { grails.serverURL = "http://localhost:8080/${appName}" } test { grails.serverURL = "http://localhost:8080/${appName}" } }
Jest to kod z pliku Config.groovy. Służy on do ustawiania różnych adresów URL na potrzeby tworzenia pełnych odnośników do aplikacji na podstawie używanego środowiska. Inny przykład to przedstawiona poniżej sekcja z pliku DataSource.groovy, gdzie zdefiniowane są źródła danych: environments { development { dataSource { dbCreate = "create-drop" // one of 'create', 'create-drop','update' url = "jdbc:hsqldb:mem:devDB" } } test { dataSource { dbCreate = "update" url = "jdbc:hsqldb:mem:testDb" } } production { dataSource { dbCreate = "update" url = "jdbc:hsqldb:file:prodDb;shutdown=true" } } }
411
ROZDZIAŁ 11. GRAILS
W tym kodzie na podstawie środowiska aplikacji ustawiane są różne parametry połączeń z pamięcią trwałą. Dzięki temu aplikacja może korzystać z różnych zbiorów danych, co jest korzystne, ponieważ w ramach tworzenia programu z pewnością nie chcesz modyfikować danych używanych w środowisku produkcyjnym. Nie są to jedyne parametry, które można konfigurować na podstawie środowiska aplikacji. W odpowiedniej sekcji environments { } możesz ustawiać dowolne parametry. W pokazanych tu przykładach występują parametry, które najczęściej zmienia się w poszczególnych środowiskach. Na podstawie używanego środowiska można też wykonywać określony kod programu (na przykład w klasie lub skrypcie). Służy do tego klasa grails.util.Environment. Poniżej pokazano, jak uzyskać taki efekt: import grails.util.Environment … switch(Environment.current) { case Environment.DEVELOPMENT: // Wykonywanie kodu rozwojowego break case Environment.PRODUCTION: // Wykonywanie kodu produkcyjnego break }
Ten fragment kodu pokazuje, że najpierw należy zaimportować klasę grails.util.Environment. Następnie na podstawie wartości Environment.current (określa ona środowisko, w którym działa aplikacja) instrukcja warunkowa switch ustala wykonywany kod. To rozwiązanie często stosuje się na przykład przy wysyłaniu poczty elektronicznej lub korzystaniu z geolokalizacji. Nie ma sensu wysyłać poczty lub sprawdzać lokalizacji użytkownika w środowisku rozwojowym, ponieważ położenie zespołu programistów nie ma wtedy znaczenia, a ponadto nie potrzebują oni powiadomień od aplikacji. Ponadto warto zauważyć, że można zmienić domyślne środowisko powiązane z poszczególnymi instrukcjami platformy Grails. Na przykład domyślnie polecenie grails run-app wykorzystuje parametry ustawione dla środowiska rozwojowego. Jeśli z jakichś powodów chcesz uruchamiać tę instrukcję z parametrami dla środowiska produkcyjnego, możesz to zrobić za pomocą polecenia grails prod run-app. Jeżeli zamierzasz zastosować parametry dla środowiska testowego, użyj instrukcji grails test run-app. Podobnie wygląda sytuacja dla poleceń takich jak grails test-app, dla których standardowo używane są parametry dla środowiska testowego. Aby wykorzystać parametry dla środowiska rozwojowego, wpisz instrukcję grails dev test-app. To samo dotyczy też innych poleceń — wystarczy wstawić po instrukcji grails słowo kluczowe prod, test lub dev.
11.5. Tworzenie klas domenowych aplikacji Problem Programista chce zdefiniować klasy domenowe aplikacji.
Rozwiązanie Klasy domenowe służą do opisywania podstawowych elementów i cech aplikacji. Jeśli aplikacja służy do dokonywania rezerwacji, prawdopodobnie będzie obejmować klasę domenową reprezentującą rezerwacje. Jeżeli rezerwacja jest powiązana z dokonującym jej człowiekiem, w aplikacji zapewne będzie używana klasa domenowa reprezentująca osoby. W aplikacjach sieciowych klasy domenowe są zwykle jednym z pierwszych definiowanych elementów, ponieważ reprezentują dane zachowywane w pamięci trwałej. Dlatego komunikują się z kontrolerami, a także zawierają dane wyświetlane w widokach. 412
11.5. TWORZENIE KLAS DOMENOWYCH APLIKACJI
W platformie Grails klasy domenowe umieszcza się w katalogu /grails-app/domain/. Tworzenie klas domenowych, podobnie jak większości innych komponentów w platformie Grails, odbywa się za pomocą jednej prostej instrukcji. Wygląda ona tak: grails create-domain-class
To polecenie tworzy w katalogu /grails-app/domain/ szkielet klasy domenowej w pliku o nazwie .groovy.
Jak to działa? Grails tworzy szkielet klasy domenowej, jednak każdą taką klasę trzeba zmodyfikować odpowiednio do przeznaczenia aplikacji. Utwórzmy system obsługujący rezerwacje, podobny do tego opracowanego w rozdziale 8. w ramach prób z platformą Spring MVC. Dodaj dwie klasy domenowe: Reservation i Player. W tym celu wywołaj następujące polecenia: grails create-domain-class Player grails create-domain-class Reservation
Gdy wywołasz te instrukcje, w katalogu /grails-app/domain/ aplikacji pojawią się pliki Player.groovy i Reservation.groovy. Ponadto dla każdej klasy domenowej generowane są pliki z testami jednostkowymi, zapisywane w katalogu test/unit. Omówienie testów znajdziesz w recepturze 11.10. Otwórz plik Player.groovy w celu zmodyfikowania jego zawartości. Pogrubioną czcionką wyróżnione są deklaracje, które trzeba dodać do klasy z tego pliku. class Player { static hasMany = [ reservations : Reservation ] String name String phone static constraints = { name(blank:false) phone(blank:false) } }
Pierwszy nowy wiersz, static hasMany = [ reservations : Reservation ], reprezentuje relację między klasami domenowymi. Ta instrukcja oznacza, że klasa domenowa Player ma pole reservations, z którym można powiązać wiele obiektów typu Reservation. Następne polecenia oznaczają, że klasa domenowa Player ma też dwa pola typu String: name i phone. Ostatni fragment, static constraints = { }, definiuje ograniczenia tej klasy domenowej. Tu deklaracja name(blank:false) oznacza, że pole name obiektu typu Player nie może być puste. Deklaracja phone(blank:false) informuje, że w obiekcie tego typu puste nie może być również pole phone. Po zmodyfikowaniu klasy domenowej Player otwórz plik Reservation.groovy. Fragmenty wyróżnione pogrubieniem to deklaracje, które trzeba dodać do klasy domenowej z tego pliku. class Reservation { static belongsTo = Player String courtName; Date date; Player player; String sportType; static constraints = { sportType(inList:["Tenis", "Futbol"] ) date(validator: { if (it.getAt(Calendar.DAY_OF_WEEK) == "SUNDAY" && ( it.getAt(Calendar.HOUR_OF_DAY) < 8 || it.getAt(Calendar.HOUR_OF_DAY) > 22)) { return ['invalid.holidayHour']
413
ROZDZIAŁ 11. GRAILS
} else if ( it.getAt(Calendar.HOUR_OF_DAY) < 9 || it.getAt(Calendar.HOUR_OF_DAY) > 21) { return ['invalid.weekdayHour'] } }) } }
Pierwsza instrukcja dodana do klasy domenowej Reservation, static belongsTo = Player, oznacza, że obiekt typu Reservation zawsze należy do obiektu typu Player. Dalsze polecenia informują, że klasa domenowa Reservation ma pole courtName typu String, pole date typu Date, pole player typu Player i pole sportType typu String. Ograniczenia w klasie domenowej Reservation są bardziej rozbudowane niż w klasie domenowej Player. Pierwsze ograniczenie, sportType(inList:["Tenis", "Futbol"] ), powoduje, że pole sportType w obiekcie typu Reservation może przyjmować tylko wartości Tenis lub Futbol. Drugie ograniczenie to niestandardowy walidator, który sprawdza, czy godzina w polu date w obiekcie typu Reservation jest dostosowana do przedziału zależnego od dnia tygodnia. Po przygotowaniu klas domenowych aplikacji możesz utworzyć odpowiadające im widoki i kontrolery aplikacji. Najpierw jednak warto pokrótce omówić klasy domenowe platformy Grails. Choć kod tej receptury pozwala zrozumieć podstawową składnię definiowania klas domenowych tej platformy, wykorzystano w nim tylko niewielką część dostępnych rozwiązań. Gdy relacje między klasami domenowymi są skomplikowane, w definicjach często trzeba stosować bardziej zaawansowane techniki. Wynika to z tego, że platforma Grails wykorzystuje klasy domenowe w różnych aspektach działania aplikacji. Na przykład jeśli obiekt domenowy jest aktualizowany lub usuwany z pamięci trwałej aplikacji, trzeba odpowiednio zarządzać relacjami między klasami domenowymi. W przeciwnym razie w aplikacji mogą pojawić się niespójne dane. Na przykład po usunięciu danej osoby trzeba też skasować powiązane z nią rezerwacje, aby uniknąć niespójnego stanu w tych ostatnich. Ponadto można stosować różne ograniczenia w celu wymuszenia struktury klasy domenowej. W niektórych sytuacjach ograniczenia stają się zbyt skomplikowane. Wtedy często sprawdza się je w kontrolerze aplikacji przed utworzeniem obiektu danej klasy domenowej. W tej recepturze pokazano proste ograniczenia, aby zilustrować budowę klas domenowych platformy Grails.
11.6. Generowanie kontrolerów CRUD i widoków na potrzeby klas domenowych aplikacji Problem Programista chce utworzyć kontrolery wykonujące operacje CRUD (ang. create, read, update i delete, czyli tworzenie, odczyt, aktualizowanie i usuwanie) oraz widoki na potrzeby klas domenowych aplikacji.
Rozwiązanie Klasy domenowe aplikacji same w sobie nie są specjalnie przydatne. Dane odpowiadające obiektom tych klas trzeba utworzyć, wyświetlić użytkownikom, a czasem także zapisać w trwałym magazynie w celu późniejszego wykorzystania. W aplikacjach sieciowych powiązanych z trwałym magazynem danych takie operacje na bazach danych często określa się mianem CRUD. W większości platform do tworzenia aplikacji sieciowych zbudowanie kontrolerów wykonujących operacje CRUD i widoków wymaga dużo pracy. Wynika to z tego, że kontrolery muszą tworzyć, wczytywać, aktualizować i usuwać obiekty domenowe w trwałych magazynach danych.
414
11.6. GENEROWANIE KONTROLERÓW CRUD I WIDOKÓW NA POTRZEBY KLAS DOMENOWYCH APLIKACJI
Ponadto trzeba utworzyć odpowiednie widoki (czyli strony JSP), aby użytkownicy mogli tworzyć, wczytywać, aktualizować i usuwać te obiekty. Jednak ponieważ platforma Grails działa na podstawie konwencji, mechanizm generowania dla klas domenowych kontrolerów wykonujących operacje CRUD i widoków jest łatwy w użyciu. Aby utworzyć takie kontrolery i widoki, wywołaj następującą instrukcję: grails generate-all
Jak to działa? Grails potrafi przeanalizować klasy domenowe aplikacji i wygenerować kontrolery oraz widoki potrzebne do tworzenia, wczytywania, aktualizowania i usuwania obiektów takich klas. Przypomnij sobie utworzoną wcześniej klasę domenową Player. Aby wygenerować dla niej kontroler wykonujący operacje CRUD i widoki, wystarczy w katalogu głównym aplikacji wywołać poniższe polecenie: grails generate-all Player
Podobną instrukcję można zastosować do klasy domenowej Reservation. Wykonaj poniższe polecenie, aby utworzyć kontroler wykonujący operacje CRUD i widoki dla tej klasy. grails generate-all Reservation
Co zostaje wygenerowane w wyniku wywołania tych instrukcji? Jeśli przyjrzałeś się danym wyjściowym, prawdopodobnie już to wiesz. Poniżej zamieszczamy przegląd całego procesu: 1. Kompilowanie klas aplikacji. 2. Generowanie 12 plików właściwości w katalogu grails-app/i18n na potrzeby obsługi umiędzynarodawiania aplikacji (pliki te mają nazwy w formacie messages_.properties). 3. Tworzenie kontrolera w pliku Controller.groovy w katalogu grails-app/controllers. Ten kontroler wykonuje operacje CRUD na relacyjnej bazie danych. 4. Tworzenie czterech widoków odpowiadających operacjom CRUD z klasy kontrolera. Nazwy plików tych widoków to: create.gsp, edit.gsp, list.gsp i show.gsp. Zauważ, że rozszerzenie .gsp oznacza Groovy Server Pages. Jest to odpowiednik technologii JavaServer Pages, przy czym do deklarowania instrukcji wykorzystuje się tu język Groovy zamiast Javy. Wygenerowane widoki są umieszczane w katalogu grails-app/views/ aplikacji. Po wykonaniu tych operacji możesz uruchomić aplikację platformy Grails (za pomocą polecenia grails run-app) i wypróbować ją z perspektywy użytkownika. To nie żaden żart — po wywołaniu podanych wcześniej prostych instrukcji aplikacja jest już gotowa do użycia. Na tym właśnie polega często wspominane działanie na podstawie konwencji w platformie Grails. Dzięki temu proces tworzenia rusztowania zostaje uproszczony do wywoływania jednowyrazowych instrukcji. Po uruchomieniu aplikacji możesz wykonywać operacje CRUD na obiektach klasy domenowej Player pod następującymi adresami URL: tworzenie: http://localhost:8080/court/player/create, wczytywanie: http://localhost:8080/court/player/list (dla wszystkich graczy) lub http://localhost:8080/court/player/show/, aktualizowanie: http://localhost:8080/court/player/edit/, usuwanie: http://localhost:8080/court/player/delete/. Przechodzenie między tymi widokami jest wygodniejsze niż wprowadzenie podanych adresów URL. Dalej znajdziesz kilka ilustrujących to zrzutów. Ważną rzeczą związaną z tymi adresami jest ich format. Zwróć uwagę na następujący wzorzec: //