Mariusz Prowaźnik

o programowaniu w Javie, Scali i Clojure.


Aplikacja webowa w Springu bez konfiguracji w xml

W tym poście pokażę jak stworzyć aplikację webową w Springu, która poza plikiem pom.xml, nie zawiera, żadnej konfiguracji w xml'u. Aplikacją tą będzie prosta lista rzeczy do zrobienia:
Do zbudowania projektu będzie potrzebny Apache Maven 3. Przyda się jakieś IDE, wszystko jedno jakie.

Wygenerowanie projektu

Wygeneruj projekt z linii poleceń:
mvn archetype:generate
Zaimportuj projekt do swojego IDE. Dodaj zależności w pliku pom.xml, oraz zmień packaging z jar na war. Zaktualizuj projekt.
    <packaging>war</packaging>
       <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <scope>compile</scope>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <scope>compile</scope>
            <groupId>cglib</groupId>
            <artifactId>cglib-nodep</artifactId>
            <version>2.2.2</version>
        </dependency>

Konfiguracja kontekstu Spring'a

Zamiast tradycyjnych plików *-context.xml z konfiguracją beanów Springowych można stworzyć klasę pełniącą tę samą funkcję. Trzeba ją oznaczyć adnotacją @Configuration.
package pl.mpro;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("pl.mpro")
public class SpringContextConfiguration {
}
Nie zadeklarowałem żadnych bean'ów, ale określiłem jaka paczka ma być skanowana w poszukiwaniu klas oznaczonych adnotacjami @Service @Component @Repository, poprzez @ComponentScan("pl.mpro").

Logika aplikacji

Oto prosta klasa realizująca funkcjonalność dodawania nowych "todo", oraz pobierania istniejących.
package pl.mpro;

import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class TodoService {
    private ArrayList<String> todos = new ArrayList<String>();

    public List<String> getAll() {
        return new ArrayList<String>(todos);
    }

    public void addItem(String item) {
        todos.add(item);
    }
}

Kontroler

Kontroler będzie obsługiwać żądania HTTP, wywoływać odpowiednie metody wstrzykniętej klasy TodoService i decydować, który widok będzie wyrenderowany. W tym przypadku kontroler posiada dwa mapowania dla \index.s, oddzielne dla metody GET i POST - ta pierwsza będzie zwracać stronę z listą "todo", druga dodawać nowy.
package pl.mpro;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
public class TodoController {
    private TodoService todoService;

    @Autowired
    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    @RequestMapping(value = "index.s",method = RequestMethod.GET)
    public ModelAndView printAll() {
        List<String> all = todoService.getAll();
        ModelAndView mav = new ModelAndView("/WEB-INF/views/list.jsp");
        mav.addObject("todoList", all);
        return mav;
    }

    @RequestMapping(value = "index.s",method = RequestMethod.POST)
    public ModelAndView addItem(@RequestParam String item) {
        if (item != null) todoService.addItem(item);
        return new ModelAndView("redirect:index.s");
    }
}
Dlaczego metoda addItem nie zwraca ModelAndView ze ścieżką do strony jsp? Po to aby uchronić przed ponownym dodaniem elementu po odświeżeniu strony. Zrealizowane jest to w ten sposób, że po obsłużeniu POST /index.s zostanie zwrócona odpowiedź z kodem 302 i nagłówkiem Location z url, na który nastąpi przekierowanie. Gdy potem w przeglądarce odświeżymy stronę, powtórzone będzie ostatnie żądanie, czyli GET.

Strona jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
    <title>Lista rzeczy do zrobienia</title>
    <meta charset="utf-8">
</head>
<body>
    <form method="POST" accept-charset="utf-8">
        <input type="text" name="item" />
        <input type="submit" value="Dodaj"/>
    </form>
    <c:if test="${not empty todoList}">
         <ul>
             <c:forEach items="${todoList}" var="todo">
                 <li>${todo}</li>
             </c:forEach>
          </ul>
    </c:if>
</body>
</html>

Inicjalizacja kontenera serwletów

Tradycyjnie, informacje o servletach, filtrach itp znajdowały się w pliku web.xml. Obecnie nie jest on niezbędny i można zarejestrować servlet programatycznie. W tej aplikacji potrzebujemy DispatcherServlet. Będzie on przyjmował żądania http i rozdzielał je do odpowiednich metod kontrolerów. Mapowania określone zostały przez adnotacje @RequestMapping w klasie kontrolera.
package pl.mpro;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

public class TodoWebInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        registerDispatcherServlet(servletContext);
    }

    private void registerDispatcherServlet(ServletContext servletContext) {
        WebApplicationContext springContext = createContext(SpringContextConfiguration.class);
        DispatcherServlet dispatcherServlet = new DispatcherServlet(springContext);
        ServletRegistration.Dynamic dispatcher = servletContext.
                                      addServlet("dispatcherServlet", dispatcherServlet);
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("*.s");
    }

    private WebApplicationContext createContext(Class<?>... contextClasses) {
        AnnotationConfigWebApplicationContext springContext = 
                                            new AnnotationConfigWebApplicationContext();
        springContext.register(contextClasses);
        return springContext;
    }
}

Uruchomienie aplikacji

Do pom.xml trzeba dodać jeszcze konfigurację dwóch wtyczek. Maven nie zbuduje projektu webowego bez web.xml, jeśli nie dodamy
<failOnMissingWebXml>false</failOnMissingWebXml>
Wtyczka jetty-maven-plugin umożliwi wygodne uruchomienie aplikacji przy użycia silnika servletów Jetty.
    <build>
        <!-- ... -->
        <plugins>
           <!-- ... -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>8.1.5.v20120716</version>
            </plugin>
        </plugins>
    </build>
Teraz wystarczy:
mvn clean install jetty:run
i można przetestować aplikację:
http://localhost:8080/index.s