Spring JavaConfig, czyli dlaczego Google Guice jest kwaśny

Zacznę od tego, że Szanownemu Czytelnikowi należy się słowo wyjaśnień jak i przeprosin. W moim pierwotnym zamierzeniu artykuł ten traktować miał nie o Spring JavaConfig lecz o Google Guice czyli o ADIF od Google’a. ADIF (Another Dependency Injection Framework), czyli w rozwinięciu kolejny nie wiele wnoszący szkielet programistyczny zbudowany w oparciu o ideę wstrzykiwania zależności. Należy sobie jednak postawić pytanie, czy aby napewno Google Guice można zaliczyć do tego małoszacownego grona.
Nie mając tej pewności na własnej skórze postanowiłem się przekonać, co Google Guice jest mi w stanie zaoferować. Zbliżające się wielkimi krokami drugie pełne wydanie Google Soku wydawało się być dobrze dobranym ku temu momentem. Swoją edukację rozpocząłem od zapoznania się z wprowadzającą w temat leciwą już prezentacją przygotowaną przez Boba Lee. Następnie rozpoznałem dokumentację dostępną na oficjalnej stronie projektu, by za chwilę wziąć się już za własnoręczne eksperymentowanie z biblioteką.
Google Guice
Muszę przyznać, że Google Guice zrobił na mnie nie małe wrażenie! Poniżej postaram się wymienić kilka ulepszeń, które zwróciły moją uwagę w sposób szczególny oraz pokrótce opisać każde z nich.
Definicja zależności w plikach Java
Niesie to ze sobą szereg implikacji. Jedną z nich jest z pewnością brak konieczności żmudnego pisania wielolinijkowych deklaracji w plikach XML. Jeżeli uczestniczyłeś w większym projekcie Springowym, to z pewnością spotkałeś się z sytuacją kiedy ilość plików XML składających się na kontekst aplikacji oraz ich wielkość stanowiła znaczącą przeszkodę w procesie tworzenia oprogramowania.
Kolejnym problemem jaki rodziła konfiguracja zawarta w XMLu jest w znaczącym stopniu ograniczona możliwość weryfikacji jej poprawności na etapie kompilacji. Większość literówek, czy też innych drobnych błędów (np. brak zamykającego znacznika) wymagała bardzo wprawnego oka lub jak też częściej bywało uruchomienia aplikacji. Znalezienie prostej literówki w nazwie klasy po kilkunastu godzinach pracy może urastać do problemu większej rangi.
Ułatwiona edycja oraz refaktoring
Konfiguracja zależności podlega w Google Guice takim samym zasadom jak reszta kodu aplikacji. A co za tym idzie IDE takie jak Eclipse w sposób naturalny wspierają programistę w procesie edycji jak i refaktoringu. Refaktoring aplikacji napisanej w Springu wymagał od użytkownika instalowania dodatkowych pluginów do środowiska developerskiego w celu zapewnienia spójności kodu w postaci plików Java z konfiguracją znajdującą się w plikach XML. I o ile nie mogę powiedzieć złego słowa na temat pluginu Spring IDE w kwestii funkcjonalnej, to w kwestii wydajnościowej takie zastrzeżenia się już pojawiają.
Naturalny sposób definicji zależności
Framework Google Juice pozostaje pod mocnym wpływem Language-oriented programming. Konstrukcje składniowe związane z Sokiem są przemyślane oraz zaprojektowane w ten sposób by ich zapis był bardzo zbliżony do języka naturalnego.
/* Bind TransactionLog to DatabaseTransactionLog */ bind(TransactionLog.class).to(DatabaseTransactionLog.class);
Wydajność
Wiele zadań zostało przeniesionych z etapu wykonywania aplikacji do etapu kompilacji. Nie bez znaczenia jest także brak konieczności parsowania pliku konfiguracyjnego XML w czasie startu aplikacji. Zatem kontener Guice’a startuje naprawdę szybko chociaż mówię to jedynie po testach wykonanych organoleptycznie (czyt: na oko).
Elastyczność
Zapisywanie zależności w postaci kodu Javowego jest często o wiele bardziej zwięzłe, ale także bardziej elastyczne. Przykładem tutaj może być wyciąganie wartości stałej z klasy, co w Springu jeszcze do niedawna opierało się na dosyć dziwacznej konstrukcji.
Kropla dziegciu w szklance soku
Z pewnością Guice ma jeszcze wiele zalet o których tutaj nie napisałem, ale dla mnie jest bardziej istotne, że ma też swoje znaczące wady. O jednej z nich boleśnie przekonałem się próbując wymyślić przykład z życia wzięty na podstawie którego mógłbym zaprezentować rozwiązanie w pełnej krasie.
Poziom integracji Guice’a z innymi rozwiązaniami, zwłaszcza frameworkami sieciowymi, jest na dzień dzisiejszy w mojej ocenie bardzo słaby. Chęć zbudowania aplikacji opartej o Sok wiąże się z koniecznością ścisłego doboru rozwiązań, które nie koniecznie z naszego punktu widzenia są najlepsze lecz tych, które z Guice integrują się w sposób dobry lub w ogóle się integrują.
Stąd też bierze się moje przekonanie, że Sok od Google’a jest co najmniej lekko kwaśnawy. Może się jednak zdarzyć, że zalety Google Guice będą dla Ciebie na tyle przekonujące, że nie będziesz chciał z nich rezygnować pracując na codzień z Spring Framework? Mam dla Ciebie dobrą wiadomość – już nie musisz! Twórcy Springa przygotowali dla Ciebie prezent w postaci Spring JavaConfig.
Spring JavaConfig
Słowem wprowadzenia
JavaConfig jest odpowiedzią chłopców (i dziewczyn) ze Springa na zdobywające popularność rozwiązanie Google’a. Trzeba przyznać, że odpowiedzią raczej dosyć trafną. Nie będę opisywał dokładnie jej zalet jako, że są one z dokładnością do e bardzo podobne jak w przypadku Google Guice. Miast tego skupię się na przygotowanym przeze siebie przykładzie, który pozwoli mi na zaprezentowanie sposobu użycia JavaConfig w standardowej aplikacji webowej zbudowanej w oparciu o Spring Web MVC.
Należy tutaj wspomnieć, że JavaConfig jest projektem dynamicznie rozwijającym się, lecz projektem, który nie doczekał się jeszcze swojej pierwszej wersji finalnej. Na dzień publikacji artykułu dostępny jest czwarty milestone wersji 1.0.0 wydany 6. listopada 2008 i z tej wersji będę tutaj korzystał.
Przykład
Aby zaprezentować podstawowe możliwości JavaConfig stworzyłem prostą aplikację sieciową opartą o Spring Web MVC. Składa się ona z jednego formularza WWW, który pozwala na wprowadzenie imion oraz nazwisk członków Wielce Tajnej Organizacji Podziemnej. Formularz ten prezentuje także kompletną listę osób należących już do WTOPy. Co często bywa podstawową charakterystyką organizacji takiej jak ta, znalezienie się na liście jej członków jest nieodwołalne. Zatem w tym wypadku brakującą funkcjonalność usuwania osób z listy potraktujmy jako feature. ;)
Model
Model będzie się składał z dwóch klas Member oraz SecurityKey, przy czym druga z nich była mi potrzebna jedynie do zaprezentowania funkcjonalności lookup method, ale o tym później.
@Entity
public class Member {
@Id
@GeneratedValue
private long id;
private String firstName;
private String lastName;
@Embedded
private SecurityKey securityKey;
//(...)
}
@Embeddable
public class SecurityKey {
private static Random random = new Random();
private int securityKey;
public SecurityKey() {
securityKey = random.nextInt(10000);
}
public int getSecurityKey() {
return securityKey;
}
}
Serwis i DAO
W aplikacji posiadamy jeden serwis oraz jedną implementację DAO opartą o JPA. Zbiór tych dwóch klas umożliwi nam wykonanie czynności pobrania listy użytkowników z bazy danych oraz dodania do niej nowych użytkowników. Aplikacja opiera się o relacyjną bazę danych HSQLDB przechowywaną w całości w pamięci operacyjnej. Jest to rozwiązanie wystarczające dla zaprezentowania koncepcji lecz należy pamiętać, że dane wprowadzone do bazy danych są tracone wraz z zakończeniem pracy przez serwer aplikacji. Warto zauważyć, że to podejście zdejmuje z nas konieczność startowania niezależnej od serwera aplikacji instancji serwera bazy danych takiego jak np. PostgreSQL.
public abstract class MemberServiceImpl implements
MemberService {
// automatycznie wstrzykiwane DAO
@Autowired
private MemberDao memberDao;
// transakcyjna metoda pobierania listy członków
@Transactional(readOnly = true)
@Override
public List<Member> getAllMembers() {
return memberDao.getAllMembers();
}
// transakcyjna metoda dodawania nowego członka
@Transactional
@Override
public Member addMember(Member member) {
// dla każdego nowego członka tworzony jest nowy klucz bezpieczeństwa
member.setSecurityKey(createSecurityKey());
return memberDao.addMember(member);
}
// metoda zwracająca nową instancję SecurityKey za każdym razem kiedy jest
// wywoływana
protected abstract SecurityKey createSecurityKey();
}
// automatycznie rozpoznawany przez kontener Springa obiekt DAO
@Repository
public class JpaMemberDao implements MemberDao {
// Spring automatycznie dokona wstrzymnięcia EntityManagera
@PersistenceContext
private EntityManager entityManager;
@Override
public Member addMember(Member member) {
entityManager.persist(member);
return member;
}
@Override
public List<Member> getAllMembers() {
return entityManager.createQuery(
"SELECT OBJECT(m) FROM Member m").getResultList();
}
}
Kontroler(y)
Do obsługi żądań wystarczy jeden kontroler, którego kod przedstawiam poniżej
// kontroler zostanie automatycznie rozpoznany przez kontener Springa
@Controller
// i będzie odpowiedzialny za przechwytywanie żądań pod wziązanych z
// poniższym adresem
@RequestMapping("/members.asp")
public class MembersController {
// automatycznie wstzykiwanie serwisu
@Autowired
private MemberService memberService;
// obsługa żądania GET
@RequestMapping(method = RequestMethod.GET)
public String showMembers(ModelMap model) {
model.addAttribute("members", memberService
.getAllMembers());
model.addAttribute("member", new Member());
return "members";
}
// obsługa żądania POST
@RequestMapping(method = RequestMethod.POST)
public String addMember(@ModelAttribute Member member,
ModelMap model) {
memberService.addMember(member);
return "redirect:/members.asp";
}
}
Konfiguracja kontenera Springowego
W przeciwieństwie do standardowego podejścia do konfiguracji kontenera Springowego nie znajdziemy tutaj ani jednego pliku XML. Całość konfiguracji dla tej nie do zaprzeczenia trywialnej aplikacji składa się jedynie z klas Javowych. Zwiększenie skomplikowania aplikacji w oczywisty sposób będzie wymagało od nas także zwiększenia ilości kodu konfiguracyjnego lecz w dalszym ciągu będziemy mogli pozostać wolni od plików XML. W ramach ciekawostki mogę tutaj zdradzić, że istnieje możliwość mieszania ze sobą konfiguracji w postaci XML oraz klas Java, czego ja natomiast tutaj robił nie będę.
// główna konfiguracja kontenera Springowego
@Configuration
// importowanie konfiguracji znajdującej się w klasie PersistenceConfig
@Import(PersistenceConfig.class)
// automatyczna obsługa transakcji opatrzonych adnotacją @Transactional
@AnnotationDrivenTx
// wyszukiwanie komponentów znajdujących się na Classpath opatrzonych takimi
// stereotypami jak @Controller, @Repository, @Service itp.
@ComponentScan("pl.j2ee.example.javaconfig")
public abstract class WebAppConfig extends
ConfigurationSupport {
// tworzenie komponentu o cyklu życia PROTOTYPE
// (nowa instancja dla każdego wywołania metody)
@Bean(scope = DefaultScopes.PROTOTYPE)
public SecurityKey securityKey() {
return new SecurityKey();
}
// tworzenie komponentu o cyklu życia SINGLETON
@Bean
public MemberServiceImpl memberService() {
// wstrzyknięcie typu lookup method umożliwiające dostęp MemberServiceImpl
// do tworzenia nowych instancji SecurityKey na żądanie
return new MemberServiceImpl() {
@Override
protected SecurityKey createSecurityKey() {
// odwołanie się do zdefiniowanego wyżej Beana
return securityKey();
}
};
}
}
@Configuration
// importowanie wartości z pliku Properties
@PropertiesValueSource(locations = "classpath:hibernate.properties")
public abstract class PersistenceConfig extends
ConfigurationSupport {
// wartośc z pliku Properties wyżej zaimportowanego
@ExternalValue("hibernate.connection.url")
public abstract String connectionUrl();
// wartośc z pliku Properties wyżej zaimportowanego
@ExternalValue("hibernate.connection.username")
public abstract String connectionUsername();
// wartośc z pliku Properties wyżej zaimportowanego
@ExternalValue("hibernate.connection.password")
public abstract String connectionPassword();
// źródło danych oparte na powyższych wartościach
@Bean
public DriverManagerDataSource dataSource() {
final DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl(connectionUrl());
dataSource.setUsername(connectionUsername());
dataSource.setPassword(connectionPassword());
return dataSource;
}
// EntityManagerFactory oparty o Hibernate
@Bean
public EntityManagerFactory entityManagerFactory() {
final LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean
.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
entityManagerFactoryBean
.setPersistenceXmlLocation("classpath:/persistence.xml");
// zwracanie instacji Beana przy wykorzystaniu obiektu FactoryBean
return getObject(entityManagerFactoryBean,
EntityManagerFactory.class);
}
// utworzenie instacji Beana i wstrzyknięcie do niego odpowiednich zależności
// należy zwrócic uwagę, że metoda ta jest abstrakcyjna
@AutoBean
public abstract JpaTransactionManager transactionManager();
}
@Configuration
@ComponentScan("pl.j2ee.example.javaconfig")
// konfiguracja Dispatchera Web MVC
public class DispatcherConfig extends ConfigurationSupport {
@Bean
public HandlerMapping handlerMapping() {
// używamy adnotacji do określania mapowań URL do kontrolerów
return new DefaultAnnotationHandlerMapping();
}
@Bean
public HandlerAdapter handlerAdapter() {
// używamy adnotacji do wiązania parametrów żądania z parametrami wywołania
// metody kontrolera
return new AnnotationMethodHandlerAdapter();
}
@Bean
public ViewResolver viewResolver() {
final InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
// używamy prostego JSTLowego widoku
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
Powyżej zostały zaprezentowane trzy klasy stanowiące kompletną konfigurację Springa. Jak łatwo się można domyślić klasy WebAppConfig oraz PersistenceConfig stanowią odpowiednik applicationContext.xml, a DispatcherConfig jest odpowiednikiem XMLowej konfiguracji Dispatchera wchodzącego w skład Web MVC.
Łatwo można zauważyć, że w powyższych plikach nie są skonfigurowane komponenty adnotowane przez @Service, @Repository oraz @Controller. Dzięki adnotacji @ComponentScan są one automatycznie odnajdywane na classpath i rejestrowane. Stanowi to znaczne udogodnienie zmniejszające rozmiar klas konfiguracyjnych do całkowitego minimum.
Warto także zwrócić uwagę na adnotację @AnnotationDrivenTx, która jest odpowiednikiem taga XMLowego <tx:annotation-driven />. Bardzo wygodne jest także importowanie wartości z plików Properties, które równoważy funkcjonalność klasy PropertyPlaceholderConfigurer.
Element <lookup-method /> został zastąpiony przez konieczność implementacji anonimowej klasy, co moim zdaniem jest o wiele bardziej intuicyjne niż wcześniejsze podejście do problemu. Chęć sprawdzenia jak sprawuje się nowe rozwiązanie spowodowała, że uciekłem się do implementacji klasy SecurityKey. Chciałem zobaczyć, czy rzeczywiście wszystko odbędzie się tak jak powinno, to znaczy, że przy każdym wywołaniu metody MemberServiceImpl.createSecurityKey() zostanie zwrócona nowa instancja SecurityKey. Ku mojemu całkowitemu zaskoczeniu ;) zadziałało.
Z oczywistych względów wiele ciekawych elementów JavaConfig nie udało mi się zaprezentować na tym prostym przykładzie lecz z pełną świadomością pozostawię ich zgłębienie bardziej zainteresowanym czytelnikom.
Konfiguracja kontenera aplikacji
Dla uzyskania kompletności rozwiązania wymagane jest także zawarcie odpowiednich elementów w pliku web.xml, który w całości prezentuję poniżej.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- używamy JavaConfig -->
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.config.java.context.JavaConfigWebApplicationContext</param-value>
</context-param>
<!-- główna klasa konfiguracji w stylu JavaConfig -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>pl.j2ee.example.javaconfig.configuration.WebAppConfig</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- konfiguracja Dispatchera wraz ze wskazaniem na styl konfiguracji (JavaConfig) -->
<!-- oraz wskazanie na klasę konfiguracyjną -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.config.java.context.JavaConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>pl.j2ee.example.javaconfig.configuration.DispatcherConfig</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.asp</url-pattern>
</servlet-mapping>
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
Uruchomienie przykładowej aplikacji
Należy pobrać pliki źródłowe przykładu i po ich rozpakowaniu uruchomić komendę wiersza poleceń mvn jetty:run. Od tego momentu aplikacjia powinna być dostępna pod adresem http://localhost:8080/example-java-config/members.asp
Problemy
W trakcie moich eksperymentów z Spring JavaConfig natrafiłem na jeden problem, który mam nadzieje zostanie rozwiązany w finalnej wersji. Dla komponentów rejestrowanych automatycznie przy użyciu @ComponentScan w moim przypadku nie działało tworzenie transakcyjnego proxy. Co za tym idzie musiałem zdefiniować MemberServiceImpl w klasie WebAppConfig. Nie wykluczam także, że błąd leżał gdzieś po mojej stronie, ale z dużą dozą prawdopodobieństwa jest to jednak bug.
Konkluzja
Autorzy Springa musieli przejść długą drogę by wyeliminować konieczność używania plików XML w aplikacjach opartych na ich frameworku. Wydaje się, że dotarli właśnie do celu. Jako nieukrywany fan Google Guice’a nie mogę nie być prawdziwie zachwycony, że rozwiązania przez niego wprowadzone są także dostępne w moim ulubionym frameworku. Jestem gorącym zwolennikiem JavaConfig i będę starał się przekonać moich współpracowników do jego wykorzystywania. Mam nadzieję, że już niedługo pojawi się wersja finalna, co da mi do tego mandat.








Recent Comments