понедельник, 7 апреля 2008 г.

Использование Spring Framework для Web. Полное руководство.

К сожалению, по Spring до сих пор никто не написал нормального вменяемого руководства, объясняющего все шаги. Перерыв кучу англоязычной документации, как официальное руководство, так и неофициальные статьи и форумы, мне всё-таки удалось вменяемо понять эту дивную технологию, чем спешу поделиться с вами, а в первую очередь – записать дабы не забыть в последствии самому.

Мы будем использовать следующие технологии: Hibernate, Spring Framework, JSP (для вывода на фейс). И структура нашего проекта при этом будет следующей:

Парочка объяснений: DAO самописное, поскольку все DAO-генераторы суют в классы кучу процедур, при этом абсолютно забывая даже всякие вещи первой необходимости.

Плюс логика всего етого подхода в том чтоб с ORM работали только DAO-классы, а вся бизнес-логика была вынесена в Application Service, где 1-2 класса будут всем собственно рулить.

Spring controllers отвечают за вывод данных неподсредственно в веб. Ну а JSP вместо любимых FreeMarker и прочих я в последнее время использую по довольно банальной причине: когда ЖОПА и надо срочный хотфикс прямо на живом сервере, JSP позволяет находу обратиться к ORM а то и к самой базе, без всяких выкрутасов, которые будут делать в последствии разработчики, когда ситуация «жопа» уже не будет подгонять.

Шаг 1. Создаём базу данных

Создаём обычным образом, желательно конечно чтоб база поддерживала вторичные ключи, потому никаких MyISAM не советую. Наша база будет выглядеть для примера чрезвычайно просто: одна единственная табличка User с полями Id (примари ключ) и Login(уникальное). Вот её ERD для тех кто совсем не имеет пространственного мышления:
Пекедж вокруг базы нарисован т.к. я использую DB Visual Architect (DBVA), чего и вам советую.

Шаг 2. Создаём маппинг классов.

Я решил предпочитать на будущее EJB-style, если вы тоже решите – не забывайте кроме hibernate подключать еще hibernate-annotations, hibernate-commons-annotations и ejb3-persistance. Класс у нас один, тем более DBVA его сам генерит (generate->code from ERD->POJO), потому исключительно покажу его листинг без всяких объяснений:

net.altertech.vtest.data.User

package net.altertech.vtest.data;
import java.io.Serializable;
import javax.persistence.*;

@Entity
@Table(name="User")
@org.hibernate.annotations.Proxy(lazy=false)

public class User implements Serializable {

public User() {

}


@Column(name="Id", nullable=false)
@Id
@GeneratedValue(generator="VAC100101115DE0F3CF801BCF")
@org.hibernate.annotations.GenericGenerator(name="VAC100101115DE0F3CF801BCF", strategy="native")
private int id;

@Column(name="Login", nullable=false, length=40, unique = true)
private String login;

private void setId(int value) {
this.id = value;
}

public int getId() {
return id;
}

public void setLogin(String value) {
this.login = value;
}

public String getLogin() {
return login;
}

public String toString() {
return String.valueOf(getId());
}

}

Парочка примечаний:

  1. Не забываем ставить key generator: native, дабы возложить почётную миссию генерации ключей на БД и не париться.
  2. Некоторые генераторы кода, в частности DBVA, забывают мелочи. Обязательно перепроверьте маппинг объекта. Например часто забывается атрибут unique, в результате когда exception будет получена не от ORM-движка а уже от драйвера БД, всё надолго и печально рухнет так как ORM-движок будет долго пытаться вставить Insert в батч-транзакцию и откатывать её раз за разом, вместе с новыми изменениями.

Шаг 3. Создаём DAO для работы с нашими юзерами.

Пойдём по кошерному пути и напишем для начала интерфейс

net.altertech.vtest.UserDao

package net.altertech.vtest;
import net.altertech.vtest.data.User;
import java.util.List;

public interface UserDao {
List getUsers();
User getUser(Integer id);
void save(User user);
void delete(User user);
}

А затем уже создадим саму имплементацию класса

net.altertech.vtest.UserDaoImpl

package net.altertech.vtest; package net.altertech.vtest;

import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import java.util.List;
import net.altertech.vtest.data.User;

public class UserDaoImpl extends HibernateDaoSupport implements UserDao {

public List getUsers() {
return getHibernateTemplate().loadAll(User.class);
}

public User getUser(Integer id) {
return (User) getHibernateTemplate().load(User.class, id);
}

public void save(User user) {
getHibernateTemplate().save(user);
}

public void delete(User user) {
getHibernateTemplate().delete(user);
}

}

На что следует обратить внимание: наш класс наследует HibernateDaoSupport. А это значит, что в классе изначально будет доступна текущая HibernateSessionFactory и функции типа getSession() и getHibernateTemplate(). Подробней смотрите описание класса в документации по Spring Framework.

Шаг 4. Создаём Application Service для бизнес-логики

Мудрить особо не будем, да и объёмы не позволяют, потому создадим буквально пару функций. Интерфейс прилагается

net.altertech.vtest.AppService

package net.altertech.vtest;

import org.springframework.transaction.annotation.Transactional;
import java.util.List;

public @Transactional interface AppService {
void setUserDao(UserDao userDao);
UserDao getUserDao();
List getUserList();
void createUser(String parameter);
void deleteUser(Integer id);
}

На что тут обратить внимание: на то что интерфейс @Transactional. Документация объясняет нам что при таком подходе все изменения в базе автоматом проходят через транзакцию и при ошибке на каком-то из уровней происходит полный rollback. Однако на практике не пробовал, ошибки предпочитаю обрабатывать сам потому данное определение опционально.

Реализация интерфейса:

net.altertech.vtest.AppServiceImpl

package net.altertech.vtest;
import net.altertech.vtest.data.User;
import java.util.List;

public class AppServiceImpl implements AppService {
public UserDao getUserDao() {
return userDao;
}

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

private UserDao userDao;

public List getUserList() {
return userDao.getUsers();
}

public void createUser(String login) {
User u = new User();
u.setLogin(login);
userDao.save(u);
}

public void deleteUser(Integer id) {
User u = userDao.getUser(id);
userDao.delete(u);
}

}

Обратите внимание что методы для чтения начинаются с get – таким образом мы можем работать с этим классом как с bean и выводить данные из этих методов непосредственно на странице. Setter'ы делать врядли будет необходимость а вот «get»ter'ы для таких методов пригодятся. Класс так же имеет переменную UserDao – он получит ее автоматически при запуске Spring-приложения, точно так же как UserDao получит автоматически SessionFactory для работы с ORM.

Шаг 4. Разработка простого контроллера

Я не буду описывать контроллеры для добавления и удаления пользователей, впринципе контроллеры – самая лёгкая и понятная часть Spring Framework. Потому приведу листинг только контроллера для вывода списка юзеров.

net.altertech.vtest.listUserController

package net.altertech.vtest;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

public class listUserController implements Controller {

public void setApplication(AppService application) {
this.application = application;
}

private AppService application;

public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
Map<String, Object> model = new HashMap<String, Object>();
model.put("application",application);
return new ModelAndView("/listuser.jsp",model);
}

}

Обратите внимание на переменную application, она будет так же получена автоматически при запуске.

Шаг 5. Вывод в веб

Еще проще чем контроллер

listuser.jsp

<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib uri="http://java.sun.com/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jstl/fmt" prefix="fmt" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
<c:forEach items="${application.userList}" var="user">
<c:out value="${user.login}" /><br />
</c:forEach>
</body>
</html>

Шаг 6. Маппинг ресурсов.

Наиболее сложный этап для новичков, спешу обрадовать что этот этап – последний. Как вы помните, UserDao должен получить работающие ORM-ресурсы, AppService – работающие Dao а контроллеры – работающий AppService. Не говоря уже что необходимо обеспечить интерфейс к базе для ORM и объяснить какие именно объекты мы хотим построить. Ну и для веб не забыть объяснить какие урлы мапятся на какой контроллер.

В результате должно получиться нечто типа




а spring-маппинг этого безобразия выглядит так (далее читать комментарии в самом 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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<!— промапим веб-ресурсы -->
<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/listuser.html">listUser</prop>
</props>
</property>
</bean>

<!— промапим контроллеры на классы, не забудем что каждый контроллер получает по application -->
<bean id="listUser" class="net.altertech.vtest.listUserController">
<property name="application" ref="appService" />
</bean>

<!—вынесем некоторые параметры во внешний файл конфигурации для удобства -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>WEB-INF/vtest.properties</value>
</list>
</property>
<property name="ignoreUnresolvablePlaceholders" value="true"/>
</bean>

<!—подключим базу. я использовал апачевский basicDataSource так как он используется почти во всех примерах. Какой datasource использовать в продакше – решать вам -->

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${database}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</bean>

<!—подключаем ORM (hibernate). Hibernate должен получить datasource и список наших POJO-объектов. Так как я описывал объекты в EJB-стиле, используется AnnotationSessionFactoryBean -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="annotatedPackages">
<list>
<value>net.altertech.vtest.data</value>
</list>
</property>
<property name="annotatedClasses">
<list>
<value>net.altertech.vtest.data.User</value>
<value>net.altertech.vtest.data.Video</value>
</list>
</property>

<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.cache.provider_class">org.hibernate.cache.NoCacheProvider</prop>
</props>
</property>
</bean>


<!—опишем userDao. Обратите внимание на autowire -->
<bean id="userDao" class="net.altertech.vtest.UserDaoImpl" autowire="byType" />

<!—подключаем прокси транзакций. кошерно -->
<bean id="baseTransactionProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
abstract="true">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributeSource">
<bean class="org.springframework.transaction.annotation.AnnotationTransactionAttributeSource"/>
</property>
</bean>

<!—подключаем главный класс бизнес-логики. Так же имеем autowire -->
<bean id="appService" parent="baseTransactionProxy">
<property name="target">
<bean class="net.altertech.vtest.AppServiceImpl" autowire="byType" />
</property>
</bean>

<!— подключаем менеджер транзакций. Объясняем что он должен получить SessionFactory от ORM -->
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
</beans>

Вот впринципе и всё. Приведу напоследок еще web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app 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"
version="2.5">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<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>


Если вы всё сделали верно, всё должно красиво и быстро заработать. Кстате не пробовал на практике, как там с печально знаменитой thread-local сессией у Hibernate. Но вроде как на глаз, никаких дополнительных фильтров не требуется.

Для создания примера использовались (лицензиённые) DBVA 4.1 и IDEA 7.0.1, за что им отдельное спасибо. Ни один бобёр не пострадал.

1 коммент.:

анализ веб разработки комментирует...
Этот комментарий был удален администратором блога.