@Spring

Adnotacje. Wprowadzone w Javie 5, miały usprawnić i ułatwić programowanie. Wszyscy o nich wiemy, wszyscy z nich korzystamy. Korzystamy, prawda? Niestety… Często jedyna adnotacją, którą można znaleźć w źródłach wielu aplikacji jest @SupressWarnings :)
W Springu adnotacje pojawiły się jakiś czas temu. Nie używało się ich za często bo i niewiele robiły. Najczęściej stosowane były @Transactional oraz @Required. Dopiero wyjście Springa 2.5 znacząco zmieniło sytuacje. W końcu można było praktycznie zrezygnować z XML’a na rzecz metadanych. Kiedy pisałem ten artykuł moim celem stało sie skonfigurowanie aplikacji przy absolutnie minimalnej liczbie plików konfiguracyjnych i wydaje mi się, ze zadanie osiągnałem :)
Pokazuję…
Grunt to podstawa
Pierwszą rzeczą jest oczywiście stworzenie szkieletu projektu. Możemy to zrobić poleceniem maven’owym:
mvn archetype:create -DarchetypeArtifactId=maven-archetype-webapp -DgroupId=pl.jcommerce.example -DartifactId=Example
Edytujemy plik pom.xml, dodając do niego kilka zależności i konfigurujemy jetty’ego w celu łatwiejszego testowania aplikacji. Końcowy plik pom.xml powinien wyglądać tak
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>pl.jcommerce.example</groupId> <artifactId>spring-annotations</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>Spring configuration using annotations</name> <url>http://j2ee.pl</url> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.3</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring</artifactId> <version>2.5.1</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>2.5.1</version> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency> <!-- Hibernate --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate</artifactId> <version>3.2.1.ga</version> <exclusions> <exclusion> <artifactId>jta</artifactId> <groupId>javax.transaction</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-annotations</artifactId> <version>3.2.1.ga</version> </dependency> <dependency> <groupId>hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>1.8.0.7</version> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.2.2</version> <exclusions> <exclusion> <groupId>xml-apis</groupId> <artifactId>xml-apis</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <finalName>annotationConfiguration</finalName> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> <resource> <directory>src/main/webapp/WEB-INF</directory> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <encoding>utf8</encoding> <source>1.6</source> <target>1.6</target> </configuration> </plugin> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>maven-jetty-plugin</artifactId> <configuration> <connectors> <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector"> <port>8080</port> <maxIdleTime>60000</maxIdleTime> </connector> </connectors> <scanIntervalSeconds>5</scanIntervalSeconds> <scanTargets> <scanTarget>src/main/webapp/WEB-INF/dispatcher-servlet.xml</scanTarget> </scanTargets> <systemProperties> <systemProperty> <name>org.apache.commons.logging.Log</name> <value>org.apache.commons.logging.impl.SimpleLog</value> </systemProperty> </systemProperties> </configuration> </plugin> </plugins> </build> </project>
Następnie tworzymy projekt eclipse’owy poleceniem:
mvn eclipse:eclipse
startujemy jetty’ego poleceniem:
mvn jetty:run
i możemy zaczynać prace :).
Jezeli ktos lubi pracowac tylko w eclipsie i ma uczulenie na konsole, mozna troche ulepszyć projekt eclipse’owy. Dzieki najnowszemi mavenowi i dodaniu -Dwtpversion=2.0 do mvn eclipse:eclipse możemy stworzyć projekt WTP w praktycznie najnowszej wersji. W przypadku tej aplikacji nie jest to bardzo potrzebne, ale przydaje sie np. przy tworzeniu aplikacji Seamowych.
XML dla początkujących
Podstawowa konfigurację naszej aplikacji webowej rozpoczynamy standardowo, od pliku web.xml.
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
</web-app>
Następnie tworzymy dispatcher-servlet.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <context:component-scan base-package="pl.jcommerce.example" /> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" /> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" /> </beans>
Kluczowe są dwie linijki:
<context:component-scan base-package="pl.jcommerce.example"/>
oraz
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
Pierwsza określa pakiet od którego spring ma zacząć szukać swoich komponentów, które potem będzie automatycznie wstrzykiwał. Druga tworzy adapter który wiąże zapytania HTTP z odpowiednimi kontrolerami oznaczonymi adnotacją @RequestMapping.
Kolejnym plikiem jest applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"> <context:annotation-config /> <tx:annotation-driven transaction-manager="transactionManager" /> <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="annotatedSessionFactory" /> </bean> <bean id="hibernateInterceptor" class="org.springframework.orm.hibernate3.HibernateInterceptor"> <property name="sessionFactory" ref="annotatedSessionFactory" /> </bean> <bean id="annotatedSessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="annotatedClasses"> <list> <value>pl.jcommerce.example.model.Example</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop> <prop key="hibernate.cache.use_second_level_cache">false</prop> <prop key="hibernate.cache.use_query_cache">false</prop> <prop key="hibernate.hbm2ddl.auto">create-drop</prop> <prop key="hibernate.show_sql">true</prop> </props> </property> </bean> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> <property name="url" value="jdbc:hsqldb:." /> </bean> </beans>
Tutaj istotną linijką jest:
<context:annotation-config />
która automatycznie rejestruje standardowe elementy Spring’a i konfiguruje je do współpracy z adnotacjami.
Model, DAO i serwisy
Ponieważ ma to być tylko przykład aplikacji nasza klasa modelowa będzie banalnie prosta:
package pl.jcommerce.example.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Example {
private int id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Id
@GeneratedValue
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
DAO dla odmiany jest niezwykle skomplikowane i wygląda następująco:
package pl.jcommerce.example.dao;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.stereotype.Repository;
import pl.jcommerce.example.model.Example;
@Repository
public class ExampleDao extends AbstractDao {
private final static Log logger = LogFactory.getLog(ExampleDao.class);
public Example getById(int id){
final Example example = (Example) getHibernateTemplate().get(Example.class, id);
if(example != null){
return example;
} else {
logger.error("entity with id " + id + " not found");
throw new DataRetrievalFailureException("entity with id " + id + " not found");
}
}
@SuppressWarnings("unchecked")
public List<Example> getAll(){
return getHibernateTemplate().loadAll(Example.class);
}
public void persist(Example example){
getHibernateTemplate().persist(example);
}
}
Tak może wyglądać kontroler obsługujący formularz:
package pl.jcommerce.example.controllers;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import pl.jcommerce.example.model.Example;
import pl.jcommerce.example.model.ExampleDto;
import pl.jcommerce.example.service.ExampleService;
import pl.jcommerce.example.validators.ExampleValidator;
@Controller
@RequestMapping("/addExample.html")
public class AddExampleController {
private final static Log logger = LogFactory.getLog(AddExampleController.class);
private ExampleService exampleService;
private ExampleValidator exampleValidator;
@Autowired
public void setExampleService(ExampleService exampleService){
this.exampleService = exampleService;
}
@Autowired
public void setExampleValidator(ExampleValidator exampleValidator){
this.exampleValidator = exampleValidator;
}
@RequestMapping(method = RequestMethod.POST)
public String handleSubmit(@ModelAttribute("exampleDto") ExampleDto exampleDto, BindingResult result, ModelMap modelMap){
logger.debug("add example form submitted");
exampleValidator.validate(exampleDto, result);
if(result.hasErrors()){
return "addExample";
} else {
final Example example = exampleService.addExample(exampleDto.getName());
return "redirect:viewExample.html?id=" + example.getId();
}
}
@RequestMapping(method = RequestMethod.GET)
public String showForm(Model model){
ExampleDto exampleDto = new ExampleDto();
model.addAttribute("exampleDto", exampleDto);
logger.debug("add example page requested");
return "addExample";
}
}
, a tak wyświetlający dane:
package pl.jcommerce.example.controllers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import pl.jcommerce.example.service.ExampleService;
@Controller
public class ViewExampleController {
private ExampleService exampleService;
@Autowired
public void setExampleService(ExampleService exampleService){
this.exampleService = exampleService;
}
@RequestMapping("/viewExample.html")
public String viewExample(@RequestParam(value="id", required = true) int id, ModelMap model){
model.addAttribute("example", exampleService.getExample(id));
return "viewExample";
}
}
… i objaśniam
As serwisowy
Adnotacja @Repository, służy głównie do oznaczenia roli klasy w aplikacji (jak sama nazwa wskazuje, jest to repozytorium, inaczej zwane DAO).
Podobnie ma się sprawa z @Service, którą można znależć w klasie ExampleService.java. Oprócz tego używam także @Autowired, które automatycznie wstrzykuje zależności.
Power is nothing without control(ler)
W końcu dobrnęliśmy do najciekawszego punktu programu, czyli konfiguracji kontrolera. @Controller dodaje klasę do ścieżki automatycznego wykrywania (classpath scanning), natomiast ważniejszy jest @RequestMapping.
Adnotację @RequestMapping można umieścić nad metodą, lub nad klasą, jako argument („value”) podając URI. W tym drugim wypadku, trzeba jeszcze doprecyzować które klasy zajmą się poszczególnymi typami zapytania HTTP (GET, POST, HEAD, etc). Dodatkowo można jeszcze określić parametry przy których dana metoda będzie wywoływana (np. „action=save”).
Metody które używają @RequestMapping mogą przyjmować wiele rodzajów argumentów: począwszy od ModelMap (które jest kolejną generacją ModelAndView), poprzez parametry zapytania (oznaczone adnotacją @RequestParam), a skończywszy na statusie sesji (SessionStatus).
Metoda ta może również zwracać kilka różnych typów, min.: String, który określa plik jsp, użyty do renderowania strony; standardowy ModelAndView lub ModelMap (tylko w wypadku, gdy widok jest zdefiniowany, np. przy pomocy RequestToViewNameTranslator);wygenerowany wcześniej obiekt typu View lub po prostu void jeżeli metoda sama zajmuje się zapytaniem (np. generując zawartość pliku JPG).
Wnioski, wnioseczki
Krótkie obcowanie z takim sposobem konfiguracji utwierdziło mnie w przekonaniu, że adnotacje są OK :) Konfiguracja jest prosta, łatwa i przyjemna. Praktycznie wyeliminowano dłubanie w XML’u, co dla mnie jest ogromną zaletą. Są niestety pewne wady… W celu uzyskania niektórych elementów Springa (np. kreatorów), trzeba sie nakombinować z @RequestMapping lub powrócić na chwilę do plików konfiguracyjnych.
Jeżeli miałbym możliwość wybrania sposobu konfiguracji dla następnego większego projektu byłby to niestety XML. Przestawienie się na nowy typ konfiguracji i stosunkowo niewielka (jeszcze) ilość dokumentacji w Internecie mogą sprawiać problemy. Jednak wydaje mi się, że jeszcze kilka(-naście) miesięcy i XML zniknie na dobre, zastąpiany dużą ilością małpek. Czego sobie i wam życze ;)








May 18th, 2008 at 22:17
Ja jestem zdania, że jakkolwiek przejadłem się XMLem i długo czekałem na pełne wsparcie adnotacji w Javie, to coraz częściej dostrzegam zaletę stosowania XML jako mechanizmu konfiguracji bez infekowania klas, które każdorazowo musiałbym rekompilować przy każdej zmianie. Sam XML jest do kitu, ale XML połączony z domyślną i ustandaryzowaną konfiguracją jest wspaniałym narzędziem. Właśnie za to uwielbiam Korporacyjną Javę 5, która położyła duży nacisk na konfigurację domyślną stałą dla każdego serwera aplikacyjnego, ale możliwą do nadpisania przez XMLa. Słowo-klucz to właśnie nadpisanie. Wcześniej, XMLem robiło się wszystko, od początku do końca, koniecznie z najmniejszymi szczegółami. Tym razem XML sprowadzono do niezbędnej, wyjątkowej konfiguracji. Raczej stroniłbym od zalecania stosowania wyłącznie adnotacji. Zresztą jak czytam i Tobie takie podejście nie wydaje się być na chwilę obecną dojrzałe.
Można prosić o wyjaśnienie:
Dzieki najnowszemi mavenowi i dodaniu -Dwtpversion=2.0 do mvn eclipse:eclipse możemy stworzyć projekt WTP w praktycznie najnowszej wersji.
? Jakie elementy projektu webowego miałbym w Eclipse przy takim jego utworzeniu?
Czekam z niecierpliwością na kolejne wpisy!
Jacek
May 19th, 2008 at 09:03
Z tego co pamietam to pojawily sie jakies nowe validatory, wsparcie dla JSF i kilka innych ficzurków :) Czekalem na WTP 2.0 z powodu Seam’a, gdyz WTP 1.5 sprawial problemy w kilku miejscach i nie dalo sie wykorzystac wszystkich mozliwosci eclipse’a przy korzystaniu z projektu tworzonego w mavenie.
July 11th, 2008 at 13:00
Chciałem tylko zaznaczyć, że Repository to nie to samo co DAO….
August 12th, 2008 at 14:17
Hmm… A jakie sa roznice?