Praktyczna Java. Aplikacje webowe ze Springiem bez XMLa

By 9 lipca 2016Praktyczna Java

W naszym kursie Javy część konfiguracji jest w postaci plików XML – jest to nadal sposób, z którym można się często spotkać w różnych systemach. Tworząc nową aplikacje są jednak wygodniejsze podejścia, o których będzie ten wpis.

Konfiguracja XML początkowo była standardem w przypadku Springa – pomimo alternatywnych możliwości, które istniały już dawno temu, nie pozwalały one na to samo, na co pozwalały stare dobre pliki XML. Na szczęście sytuacja zmieniła się z czasem i obecnie możliwa jest pełna konfiguracja z użyciem adnotacji.

W tej kwestii zdania są nadal podzielone – zdecydowana większość preferuje jednak konfigurację za pomocą adnotacji. Dzięki temu możemy wykorzystać pełne możliwości naszego środowiska IDE w zakresie analizy składni, wyszukiwania użycia, odniesień do klas itp. Dodatkowo nie wymaga znajomości tagów specyficznych dla Springa – całość konfiguracji odbywa się za pomocą języka Java, co jest też ułatwieniem dla osób dopiero zaczynających karierę w branży IT.

Pozbywamy się pliku application-context.xml

W naszych lekcjach korzystaliśmy z dość minimalistycznego pliku application-context.xml – służył on nam właściwie wyłacznie do inicjowania Spring’a i konfiguracji skanowania pakietów. Pokażemy, jak zamieniać poszczególne jego elementy na konfigurację z użyciem języka Java

Uwaga! W tej opcji nadal niezbędny jest plik web.xml, który służy do uruchamiania aplikacji webowej – o tym, jak i jego możemy się pozbyć, czytaj dalej.

Zmiany w pliku web.xml

Pierwszym krokiem jest utworzenie klasy z adnotacją @Configuration oraz zmiana w pliku web.xml. Póki co stworzymy puste klasy:

@Configuration
public class SpringRootConfiguration {

}
@Configuration
public class SpringMvcConfiguration {

}

a następnie zamienimy obecny plik web.xml (z lekcji 9 naszego kursu Javy):

<web-app id="WebApp_ID" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <display-name>Baza danych kotow</display-name>

    <servlet>
        <servlet-name>mvc-dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/applicationContext.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>mvc-dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/rootApplicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

na następujący:

<web-app id="WebApp_ID" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <display-name>Baza danych kotow</display-name>

    <servlet>
        <servlet-name>mvc-dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>pl.kobietydokodu.spring.SpringMvcConfiguration</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>mvc-dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>pl.kobietydokodu.spring.SpringRootConfiguration</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

Zwróć uwagę, że dodaliśmy parametr contextClass – dzięki temu mogliśmy zamienić domyślny XmlWebApplicationContext na AnnotationConfigWebApplicationContext (czyli z takiego, który bazuje na konfiguracji XML-owej na nowszy, bazujący na adnotacjach). Możesz w tym momencie spróbować uruchomić aplikację – najprawdopodobniej nie zadziała tak jak byś chciała, ponieważ nie przenieśliśmy jeszcze konfiguracji z plików XML do naszych klas, czym się teraz zajmiemy.

Konfiguracja skanowania pakietów

Skanowanie pakietów możemy uzyskać za pomocą adnotacji @ComponentScan. Konfiguracje XML:

<context:component-scan base-package="pl.kobietydokodu.pakiet,pl.kobietydokodu.inny" />

możemy zamienić na następującą:

@ComponentScan(basePackages = {"pl.kobietydokodu.pakiet","pl.kobietydokodu.inny"})
@Configuration
public class SpringConfig {

}

Uwaga: jeśli nie podamy żadnych pakietów (użyjemy po prostu @ComponentScan bez żadnych parametrów), skanowanie rozpocznie się od bieżącego pakiet (tzn tego, w którym znajduje się klasa z tą adnotacją) – przeskanowane zostaną także wszystkie pakiety ‚podrzędne’ do obecnego.

Deklarowanie beanów

Deklarowanie beanów w XML-u wyglądało następujaco:

<bean id="someService" class="pl.kobietydokodu.SomeService" init-method="init">
    <property name="someDependency" ref="someDependencyImpl" />
</bean>

W przypadku konfiguracji w Javie musimy samodzielnie stworzyć dany obiekt – problemem z tym podejściem mogą być pola, które nie mają setterów (i nie są możliwe do ustawienia przez konstruktor), ani adnotacji @Autowired (lub analogicznej). Dlatego ta zmiana konfiguracji może wymagać delikatnych zmian w klasach (lub użycia refleksji, co jest niezalecane). W podstawowej wersji powyższa konfiguracja wyglądałaby następująco:

@Configuration
public class SpringConfig {
    @Autowired
    @Qualifier("someDependencyImpl")
    private SomeDependency someDependency;

    @Bean(name="someService", initMethod="init")
    public SomeService someService {
        SomeService service = new SomeService();
        service.setSomeDependency(someDependency);
        return service;
    }

}

Jak widzisz pierwsza ważna zmiana to to, że zależności (do których w XMLu odnosiliśmy się poprzez atrybut ‚ref’) teraz musimy ‚pobrać’ (poprzez utworzenie pola i dodanie adnotacji @Autowired) a następnie ręcznie wstrzyknąć je do klasy (o ile nie ma ona adnotacji @Autowired nad swoimi polami). Bezpośrednim odpowiednikiem atrybutu ref jest adnotacja @Qualifier – pozwala podać id (lub nazwę) beana, który chcemy w tym miejscu ‚pobrać’.

Różnica pomiędzy id oraz name występuje tylko w XML i jest czysto teoretyczna – z uwagi na składnie XML, element może mieć wyłącznie jedno id, nie wszystkie znaki są dozwolone. Bean w Springu może mieć wiele nazw, które są funkcjonalnie tym samym co id – są po prostu mniej restrykcyjne. Konfiguracja za pomocą adnotacji nie rozróżnia pomiędzy id oraz name (możliwe jest ustawienie wyłącznie name – jednego lub wielu).

Konfiguracja MVC

W konfiguracji XML za ‚włączenie’ adnotacji i obsługi MVC odpowiadał fragment:

<mvc:annotation-driven />

Analogiczny efekt możemy uzyskać dodając do naszej konfiguracji adnotację @EnableWebMvc:

@Configuration
@EnableWebMvc
public class SpringConfig {

}

Jeśli potrzebujemy dodatkowej konfiguracji modułu MVC (np. jeśli chcesz dodać obsługę statycznych plików), klasa konfiguracji może dziedziczyć po WebMvcConfigurerAdapter oraz nadpisać stosowne metody (przykład dla obsługi statycznych plików):

@Configuration
@EnableWebMvc
public class SpringConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
            .addResourceHandler("/css/**/*")
            .addResourceLocations("/WEB-INF/static/css/");
    }
}

Oczywiście możesz nadpisać inne metody, aby zmodyfikować inne aspekty SpringMVC (np. dodać interceptory zapytań czy zmienić sposób mapowania adresów URL lub dodać własne Validatory).

Konfiguracja (i pliki .properties)

Konfiguracja XML wymagała jedynie jednej linijki:

<context:property-placeholder location=”classpath:configuration.properties” />

W przypadku konfiguracji w Javie wymagane są dwa kroki – musimy dodać adnotację @PropertySource oraz stworzyć beana typu PropertySourcesPlaceholderConfigurer. Sama adnotacja jedynie pobiera wartości z pliku .property oraz pozwala się do nich dostać poprzez interfejs Environment. Bean pozwala na używanie tych wartości np. w połączeniu z adnotacją @Value, stąd w większości przypadków jest ona potrzebna.

@Configuration
@PropertySource("classpath:configuration.properties")
public class SpringConfig {
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

Jeśli potrzebujemy zarejestrować większą ilość plików .property, wystarczy powielić adnotację podając ścieżkę do każdego pliku osobno.

Importowanie innych konfiguracji

XMLe pozwalały nam dołączać inne pliki konfiguracji za pomocą tagu <import … /> . W przypadku konfiguracji za pomocą adnotacji są trzy sposoby na dołączenie innych konfiguracji:

Skanowanie pakietów

Jeśli konfiguracja, którą chcemy dołączyć, znajduje się w jednym z pakietów skanowanych przez Springa (patrz wyżej, jak skonfigurować skanowanie), to nie musimy robić nic więcej – Spring sam ‚odkryje’ te klasy i odpowiednio je skonfiguruje.

Dołączanie klas konfiguracji

Jeśli chcemy dołączyć klasę, która nie jest ‚wykrywana’ przez Springa podczas skanowania pakietów, możemy użyć adnotacji @Import:

@Configuration
@Import(OtherConfig.class)
public class SpringConfig {
}

Dołączanie konfiguracji XML

Czasem z różnych powodów konieczne jest dołączenie konfiguracji w postaci pliku XML, w tym wypadku możemy posłużyć się adnotacją @ImportResource:

@Configuration
@ImportResource("classpath:/spring-config/legacy.xml")
public class SpringConfig {
}

Sprzątanie

Po wszystkim warto uporządkować pliki projektu – ponieważ pliki XML z konfiguracją nie są nam już potrzebne, możemy je w tym momencie całkowicie usunąć.

Pozbywamy się pliku web.xml

W tym momencie w naszej aplikacji pozbyliśmy się konfiguracji Springa w XMLu – został jeszcze plik web.xml. Od Servlet API 3.0 możliwe jest pozbycie się i tego pliku oraz konfiguracja aplikacji czysto Javowa. Ważne jest jednak, aby upewnić się, że nasz serwer aplikacji wspiera Servlet API w odpowiedniej wersji, np. dla Tomcata wsparcie jest oferowane od wersji 7.0. Dla innych serwerów aplikacji bez problemu znajdziesz podobne zestawienie – przed wprowadzaniem poniższych zmian, upewnij się, że Twoja konfiguracja będzie działała po zmianach.

Zamieniamy plik web.xml

Jako bazę do zmian wykorzystamy zmodyfikowany plik web.xml przytoczony w sekcji powyżej. Uruchamia on dwa konteksty Springa, w tym jeden ‚główny’ i jeden powiązany z Servletem.

Aby stworzyć konfigurację odpowiadającą web.xml, musisz utworzyć nową klasę, która implementuje interfejs WebApplicationInitializer oraz przesłonić metodę onStartup:

public class SpringWebsiteInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext container) {

    }
}

Taka klasa oczywiście nic jeszcze nie robi, ale możesz zweryfikować (np. debuggerem lub wypisując jakiś tekst do logu), że faktycznie jest ona uruchamiana. Kolejnym krokiem jest dodanie konfiguracji DispatcherServlet:

public class SpringWebsiteInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext container) {
        AnnotationConfigWebApplicationContext dispatcherContext = new AnnotationConfigWebApplicationContext();
        dispatcherContext.register(SpringWebConfig.class);

        ServletRegistration.Dynamic dispatcher = container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
    }
}

Uwaga: możliwe jest także skorzystanie z konfiguracji XML – konieczna jest wtedy zamiana AnnotationConfigWebApplicationContext na XmlWebApplicationContext (przykład użycia znajdziesz w dokumentacji interfejsu WebApplicationInitializer).

Ta konfiguracja odpowiada stosownym parametrom w pliku web.xml – zwróć uwagę, że w tym wypadku nie było konieczne osobne określanie klasy, aby umożliwić konfiguracje z użyciem adnotacji – dzięki temu, że operujemy na obiektach, wystarczy stworzyć obiekt odpowiedniego typu (w tym wypadku AnnotationConfigWebApplicationContext).

Zwróć także uwagę na wywołanie dispatcherContext.register(…) – ta metoda po prostu dodaje do kontekstu nowego beana. Jeśli chcesz, możesz dodać pozostałe pliki konfiguracji w tym miejscu (choć zalecamy ‚łączenie’ ich już na poziomie samych klas konfiguracji jak zostało to opisane wyżej, a nie tworząc kontekst).

Ostatnim krokiem jest dodanie także ‚głównego’ kontekstu, po czym nasza klasa będzie wyglądała następująco:

public class SpringWebsiteInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext container) {
        //główny kontekst
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(SpringRootConfig.class);
        container.addListener(new ContextLoaderListener(rootContext));

        //kontekst Servletu
        AnnotationConfigWebApplicationContext dispatcherContext = new AnnotationConfigWebApplicationContext();
        dispatcherContext.register(SpringWebConfig.class);
        ServletRegistration.Dynamic dispatcher = container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
    }
}

Jeszcze prościej

Powyższy opis obrazuje jak można skonfigurować Springa ‚ręcznie’ – sam Spring zapewnia kilka klas, które jeszcze bardziej upraszczają konfigurację. Jeśli mamy domyślną konfigurację, z dwoma kontekstami i jednym Servletem, są one lepszym rozwiązaniem. W przeciwnym razie najprawdopodobniej będziesz musiała skorzystać z opcji powyżej.

Uproszczona konfiguracja sprowadza się do dziedziczenia po klasie AbstractAnnotationConfigDispatcherServletInitializer (uwaga: klasa ta dostępna jest dopiero od Springa 3.2 w odróżnieniu do opisanych wcześniej rozwiązań!) oraz implementacji kilku metod (zwracających jedynie proste parametry konfiguracyjne):

public class SpringInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] {SpringRootConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] {SpringMvcConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

Jak sama widzisz, ta konfiguracja jest jeszcze prostsza i dużo czytelniejsza niż web.xml :)

Ważne – sprzątanie!

Uwaga! Opisana powyżej konfiguracja oraz plik web.xml nie wykluczają się wzajemnie! Tzn. jeśli nie skasujesz pliku web.xml po dokonaniu powyższych zmian, Twoja aplikacja będzie uruchamiała dwie aplikacje – w zależności od ustawień może to spowodować błąd, ale może też nie być widoczne od razu i prowadzić do ‚dziwnych’ błędów w aplikacji. Dlatego pamiętaj, aby po dokonaniu zmian usunąć plik web.xml

Spring Boot

Alternatywną opcją do powyższych jest skorzystanie z biblioteki Spring Boot – biblioteka ta nieco upraszcza tworzenie aplikacji w Springu i jeśli nie jest problemem dołączenie nowych zależności do projektu, warto ją rozważyć. Różnica w przypadku Spring Boot polega na tym, że nie potrzebujemy implementować WebApplicationInitializer’a – wystarczy adnotacja @SpringBootApplication oraz prosta metoda main – dzięki temu możemy uruchamiać aplikację także poprzez zwykłe ‚Run as -> Java Application’.

Poza brakiem pliku web.xml oraz powiązanej z tym konfiguracji, tworzenie czy rozwijanie aplikacji z użyciem SpringBoot niczym nie różni się od zwykłej aplikacji w SpringMVC – sposób przenoszenia konfiguracji z XML do Javy czy korzystania z funkcji Springa jest dokładnie taki sam (SpringBoot oferuje głównie ułatwienie w kwestii konfiguracji i uruchamiania, a nie modyfikuje samego bazowego frameworka – ma więc wpływ głównie na development, a nie działanie aplikacji).

Ale zacznijmy od początku – czyli jak dodać Spring Boot do projektu.

Dodawanie zależności

W tym miejscu zakładamy, że projekt jest budowany z użyciem Mavena. Spring Boot wspiera także Gradle, stosowną konfigurację znajdziesz oczywiście w dokumentacji.

Najprostszym sposobem konfiguracji jest dodać sekcję <parent> do naszego POM’a (o ile jeszcze takiej nie ma – w tym wypadku zerknij do linka do dokumentacji poniżej, gdzie jest opisane jak skonfigurować projekt w takiej sytuacji):

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.6.RELEASE</version>
</parent>

A następnie do sekcji <dependencies> dodać następującą zależność:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Taka konfiguracja wystarczy, aby korzystać ze Spring Boota. Jeśli w zależnościach masz także SpringFramework i moduły – możesz je usunąć, ponieważ są one pobierane w odpowiednich wersjach przez powyższą zależność.

Więcej informacji o tym, jak skonfigurować projekt z Mavenem (w tym jak skonfigurować projekt nie używając ‚parent poma’) znajdziesz w dokumentacji, w sekcji o Mavenie.

Konfiguracja aplikacji

Podstawowa konfiguracja sprowadza się do stworzenia klasy z jedną adnotacją – @SpringBootApplication :

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

Adnotacja ta powoduje, że uruchamiana jest automatyczna konfiguracja (m.in. aktywowana jest konfiguracja MVC, jeśli dodaliśmy do zależności moduł -web) a także wyszukiwane są wszystkie klasy z adnotacjami Springa w danym pakiecie (i podrzędnych – zachowanie jest identyczne jak w przypadku adnotacji @ComponentScan bez parametrów opisanej wyżej).

Taka klasa zastępuje nam konfigurację kontenera (web.xml) oraz podstawową konfigurację modułów – w tym momencie aplikacja jest gotowa do uruchomienia (zakładając oczywiście, że wszystkie niezbędne do jej działania elementy zostaną wykryte podczas skanowania pakietów).

Więcej szczegółów i opcji znajdziesz w dokumentacji SpringBoot.

Pozostałe ustawienia i funkcje

Ponieważ Spring Boot oferuje znacznie więcej opcji i ciekawych możliwości, w przyszłości poświęcimy mu osobny wpis z bardziej szczegółowym opisem – z pewnością jest to biblioteka warta uwagi, nawet jeśli nie planujesz korzystać ze wszystkich oferowanych przez nią możliwości.

Podsumowanie

W tym wpisie opisaliśmy, jak można zamienić aplikację z konfiguracją XML na konfigurację w 100% z użyciem Javy. Wszystkie opisane techniki są dostępne od Springa 3.1 – jeśli Twoja aplikacja korzysta z nowszej wersji (a zdecydowanie powinna ;) ) to możesz zastosować opisane sposoby. W kolejnej części opiszemy także konfigurację SpringSecurity – przeniesienie konfiguracji do Javy w tym wypadku jest nieco mniej oczywiste.

Czy warto?

Kiedyś najprawdopodobniej zmierzysz się z problemem aplikacji, która zawiera tony konfiguracji XML – zamiana jej całej wymagałaby znaczącego wysiłku i dużej ilości czasu. Pytanie ‚czy warto’ w tym wypadku należy szacować indywidualnie – jeśli potrzebujesz dodać tylko jedną funkcję czy coś poprawić, porywanie się na zmianę całej konfiguracji jest prawdopodobnie zbyt ryzykowne. Konfiguracja XML i adnotacje mogą jednak współdziałać bez problemu! Dlatego dodając nową konfigurację, skorzystaj z adnotacji. Jeśli jednak w aplikacji planowane są duże zmiany, warto poświęcić czas na przepisanie (i weryfikację!) konfiguracji – nie tylko ułatwi to przyszłe zmiany, ale pozwoli zespołowi lepiej poznać sposób, w jaki działa system.

Podsumowując – w długim terminie warto, ale czasem ryzyko i koszt są zbyt duże.

  • 9
  •  
  •  
  • 1
  •  
  • Ola

    Fajny wpis :) Już nie mogę się doczekać lekcji z konfiguracją SpringSecurity oraz więcej informacji o Spring Boot :)

    • Nie wiemy jeszcze dokładnie kiedy, ale na pewno się pojawią!

      • Ola

        Super, nie mogę się doczekać :)

  • CodingMonkey

    Jakie adnotacje? Adnotacje to @Component itp, ktore też występowały w xmlowej konfiguracji kontekstu. To, co opisuje się to JAVACONFIG, w którym używa się adnotacji.

    • JavaConfig to podejście do konfiguracji, korzystające m.in. z adnotacji i włączone do Spring Core od wersji 3, nie występuje obecnie jako osobny moduł.
      O ile faktycznie adnotacje @Component i pochodne nie były częścią JavaConfig, nie bardzo widzę w tym problem – opisujemy jak za pomocą adnotacji i Javy, z pominięciem XMLi, skonfigurować aplikację – trochę nie ma znaczenia w ramach jakiego modułu w historii Spring’a używane elementy się pojawiły. Jednocześnie adnotacje nigdy nie występowały w XMLowej konfiguracji kontekstu (aczkolwiek faktycznie mogły z nią współistnieć od wersji 2.5, kiedy to zostały wprowadzone)
      Dlatego obawiam się, że nie bardzo rozumiem problem, o którym wspominasz :)

      • CodingMonkey

        Nie dziwię się, że nie rozumiesz.

  • Piotr Nowicki

    Jak użyjesz interfejsu Environment do propertiesów to nie musisz tworzyć beana PropertySourcesPlaceholderConfigurer ;)

    • Cześć, dzięki za pomysł! Masz może jakiś link opisujący szerzej takie zastosowanie? Do tej pory widziałem tylko rozwiązania, które nie działały z @Value globalnie niestety :(