Pull to refresh

ZK+Spring 3+Hibernate: две головы хорошо, а три лучше, быстрее, сильнее

Reading time 11 min
Views 9.6K
Всем доброго времени суток. Вот решил поделиться тем, как можно объединить двух монстров Spring Framework и Hibernate с мощным j2ee фреймворком ZK. Для начала почему же именно ZK, а не GWT или вообще native jsp? Потому что (чисто субъективно) у ZK самая безболезненная интеграция с этими фреймворками, да и вообще проще я пока не встречал, что и вам попытаюсь доказать.
Мой пример будет прост, так как моя цель показать как эти все фреймворки заставить заработать, причем с наименьшей головной болью. В качестве базы данных возьмем Oracle. И напишем простое веб-приложение, которое будет отображать имена пользователей. Также, дабы показать мощь и простоту ZK, добавим немного функциональности, к примеру, удаление пользователей из системы и редактирование его имени.

Первое, что мы должны сделать — создать ZK project (опция появляется, только после установки плагина в Eclipse), или просто dynamic web project, разница в сгенерированных web.xml файле, в файлах index.zul, timeout.zul, zk.xml и в библиотеках фреймворка, которые автоматом будут добавлены.
Так как наше приложение будет строиться по MVC шаблону, то сначала опишем model. Создадим в папке src маппинг-таблицу под названием Person, которая должна быть у нас в схеме Oracle:
package com.sample.data;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "PERSON")
public class Person implements Serializable {

	private static final long serialVersionUID = 4624748294368212545L;
	@Id
	private long id;
	@Column(name = "Name")
	private String personName;

	public Person() {
	}

	public Person(long id, String personName) {
		super();
		this.id = id;
		this.personName = personName;
	}

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getPersonName() {
		return personName;
	}

	public void setPersonName(String personName) {
		this.personName = personName;
	}

}

Следующим шагом определимся в том, что будем делать с данным классом: отображать коллекцию и редактировать имя пользователя. Опишем интерфейс для доступа к нашему классу:
package com.sample.service;
import java.util.List;
import com.sample.data.Person;
public interface IPerson
{
	List<Person> findAllPerson();
	boolean delete(Person pers);
	boolean saveOrUpdate(Person person);
}

и конечно же реализацию:

package com.sample.service;

import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.sample.data.Person;

@Repository
@Transactional(readOnly = true)
public class PersonImpl implements IPerson
{

	private static SessionFactory	sessionFactory;

	//получаем наш datasource 
	@Autowired
	public void setSessionFactory(SessionFactory sessionFactory)
	{
		PersonImpl.sessionFactory = sessionFactory;
	}

	@SuppressWarnings("unchecked")
	@Override
	@Transactional(readOnly = true, propagation = Propagation.REQUIRED)
	public List<Person> findAllPerson()
	{
		Session session = sessionFactory.getCurrentSession();
		return (List<Person>) session.createQuery("from Person").list();
	}

	@Override
	@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
	public boolean delete(Person pers)
	{
		try
		{
			Session session = sessionFactory.getCurrentSession();
			session.delete(pers);
			return true;
		} catch (Exception e)
		{
			return false;
		}

	}

	@Override
	@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
	public boolean saveOrUpdate(Person person)
	{
		try
		{
			Session session = sessionFactory.getCurrentSession();
			session.saveOrUpdate(person);
			return true;
		} catch (Exception e)
		{
			return false;
		}
	}
}

Как видно из кода класса PersonImpl, работу с транзакциями, коннект к базе мы возложили на плечи Spring, а все CRUD действия будет выполнять hibernate.
Придерживаясь идеологии MVC опишем контроллер. В ZK это делается несколькими способами, я опишу самый простой — наследованием от класса Window:

package ui.component;

import java.util.List;
import javax.servlet.ServletContext;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.Messagebox;
import org.zkoss.zul.Textbox;
import org.zkoss.zul.Toolbarbutton;
import org.zkoss.zul.Window;
import com.sample.data.Person;
import com.sample.service.IPerson;

public class PersonInfo extends Window
{

	private static final long	serialVersionUID	= -6186588639173052172L;
	// компонен для отоброжения наших persons в виде списка
	private Listbox				personListBox;
	private Toolbarbutton		tbbDelete;
	private List<Person>		personList;
	private IPerson			dao;
	private Textbox			tbPersonName;
	private Toolbarbutton		tbbSave;

	// переопределим метод, который выполняется при первой загрузки страницы
	public void onCreate()
	{
		// получаем наш контекст
	        ApplicationContext ctx = WebApplicationContextUtils
                   .getRequiredWebApplicationContext((ServletContext) getDesktop().getWebApp().getNativeContext());
		// получаем бин
		dao = (IPerson) ctx.getBean("personImpl");
		personList = dao.findAllPerson();
		tbPersonName = (Textbox) this.getFellow("tbPersonName");
		tbbSave = (Toolbarbutton) this.getFellow("tbbSave");
		if (personList.size() > 0)
		{
			// получаем компонент из нашей веб-страницы
			personListBox = (Listbox) this.getFellow("lbPerson");
			tbbDelete = (Toolbarbutton) this.getFellow("tbbDelete");
			// заполняем его данными
			populateListBox();
		}
	}

	private void populateListBox()
	{
		for (Person pers : personList)
		{
			personListBox.appendChild(new Listitem(pers.getPersonName(), pers));
		}
	}

        //удаление пользователя из системы
	@SuppressWarnings("unchecked")
	public void onDeletePerson()
	{
		for (Listitem li : (List<Listitem>) personListBox.getItems())
		{
			if (li.isSelected())
			{
				if (dao.delete((Person) li.getValue()))
				{
                                        //удаляем также из коллекции
					personList.remove((Person) li.getValue());
					personListBox.removeChild(li);
				}
			}
		}
	}
        
         // если пользователь выбран, то активируем кнопку удаления пользователя
	public void onSelectLB()
	{
		if (personListBox.getSelectedCount() > 0)
		{
			tbbDelete.setDisabled(false);
			tbbSave.setDisabled(true);
			tbPersonName.setValue(((Person) personListBox.getSelectedItem().getValue()).getPersonName());
		} else
		{
			tbbDelete.setDisabled(true);
		}
	}

         // сохраняем изменения
	public void onSave() throws InterruptedException
	{
		if (tbPersonName.getValue() != null && tbPersonName.getValue().trim().length() > 0)
		{
			Person pers = (Person) personListBox.getSelectedItem().getValue();
			pers.setPersonName(tbPersonName.getValue().trim());
			if (!dao.saveOrUpdate(pers))
			{
				Messagebox.show("Не удалось сохранить изменения в базу данных");
			} else
			{
				personListBox.getSelectedItem().setLabel(pers.getPersonName());
				personListBox.getSelectedItem().setValue(pers);
			}
		} else
		{
			Messagebox.show("Поле \"Имя\" не должно быть пустым");
		}
	}
}

Теперь перейдем к файлам конфигурации. Начнем с контекста соединения к базе данных, а именно с настройки Connection Pool. Создадим в папке META-INF файл context.xml с таким текстом:
<?xml version="1.0" encoding="UTF-8"?>
<!-- JDBC -->
<Context path="/db1" docBase="db1" debug="5" reloadable="true"
	crossContext="true">
	<Resource name="jdbc/taskdb" username="test" password="test"
		url="jdbc:oracle:thin:@localhost:1521:test"
		auth="Container" defaultAutoCommit="false" driverClassName="oracle.jdbc.driver.OracleDriver"
		maxActive="2" maxIdle="10" maxWait="-1" timeBetweenEvictionRunsMillis="60000"
		testOnBorrow="true" testWhileIdle="true" queryTimeout="20000"
		validationQueryTimeout="2" initialSize="1" removeAbandoned="true"
		removeAbandonedTimeout="60" logAbandoned="true" type="javax.sql.DataSource" />
</Context>

так как при маппинге к таблице Person мы использовали аннотации, то создадим файл hibernate.cfg.xml в папке WEB-INF, в котором укажем наш класс:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
                                         "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
	<session-factory>
		<mapping class="com.sample.data.Person" />
	</session-factory>
</hibernate-configuration>

Создадим spring конфигурационный файл, в котором опишем пусть к контексту соединения с Oracle при помощи jndi, bean sessionFactory, конфигурацию hibernate и пакет, в который должен смотреть spring, для применения аннотаций. Создадим файл spring-config.xml в папке WEB-INF:
<?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:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/context
		http://www.springframework.org/schema/context/spring-context-3.0.xsd
		http://www.springframework.org/schema/tx
		http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
	<context:annotation-config />
	<context:component-scan base-package="com.sample" />
	<tx:annotation-driven transaction-manager="txManager" />
	
	<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
		<property name="jndiName">
			<value>java:comp/env/jdbc/taskdb</value>
		</property>
	</bean>
	
	<bean id="txManager"
		class="org.springframework.orm.hibernate3.HibernateTransactionManager">
		<property name="dataSource" ref="dataSource" />
		<property name="sessionFactory" ref="sessionFactory" />
	</bean>
	<bean id="sessionFactory"
		class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
		<property name="dataSource">
			<ref bean="dataSource" />
		</property>
		<property name="hibernateProperties">
			<props>
				<prop key="hibernate.dialect">org.hibernate.dialect.OracleDialect</prop>
				<prop key="hibernate.show_sql">false</prop>
			</props>
		</property>
		<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
		<property name="configLocation" value="/WEB-INF/hibernate.cfg.xml" />
	</bean>
</beans>

Теперь пришел черед web.xml. В нем, кроме настроек ZK, надо будет указать листенер для spring, и путь файлов конфигурации spring и hibernate:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	id="WebApp_ID" version="2.5">
	<display-name>zkSpringHibernate</display-name>
	<listener>
		<description>
  	Used to cleanup when a session is destroyed</description>
		<display-name>ZK Session cleaner</display-name>
		<listener-class>org.zkoss.zk.ui.http.HttpSessionListener</listener-class>
	</listener>
	<servlet>
		<description>
  	The ZK loader for ZUML pages</description>
		<servlet-name>zkLoader</servlet-name>
		<servlet-class>org.zkoss.zk.ui.http.DHtmlLayoutServlet</servlet-class>
		<init-param>
			<param-name>update-uri</param-name>
			<param-value>/zkau</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet>
		<description>
  	The asynchronous update engine for ZK</description>
		<servlet-name>auEngine</servlet-name>
		<servlet-class>org.zkoss.zk.au.http.DHtmlUpdateServlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>zkLoader</servlet-name>
		<url-pattern>*.zul</url-pattern>
	</servlet-mapping>
	<servlet-mapping>
		<servlet-name>zkLoader</servlet-name>
		<url-pattern>*.zhtml</url-pattern>
	</servlet-mapping>
	<servlet-mapping>
		<servlet-name>auEngine</servlet-name>
		<url-pattern>/zkau/*</url-pattern>
	</servlet-mapping>
	<session-config>
		<session-timeout>60</session-timeout>
	</session-config>
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring-config.xml</param-value>
	</context-param>
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	<filter>
		<filter-name>hibernateFilter</filter-name>
		<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>hibernateFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<welcome-file-list>
		<welcome-file>index.zul</welcome-file>
	</welcome-file-list>
</web-app>

Следующее звено — это view, опять-таки исходя из MVC терминологии. Опишем наше веб-приложение с zul разметкой (файл index.zul, путь -WebContent ):

<?page title="ZK integration"?>
<window title="view person" border="normal" width="100%"
	id="wndViewPerson" use="ui.component.PersonInfo">
	<listbox id="lbPerson" hflex="1" vflex="1" checkmark="true"
		emptyMessage="data not found" onSelect="wndViewPerson.onSelectLB()">
		<listhead>
			<listheader label="Имя" />
		</listhead>
	</listbox>
	<hlayout>
		<label value="Имя" />
		<textbox width="150px" id="tbPersonName"
			onChanging="tbbSave.setDisabled(false)" />
		<toolbarbutton label="save" onClick="wndViewPerson.onSave()"
			id="tbbSave" disabled="true" />
		<toolbarbutton label="delete" disabled="true" id="tbbDelete"
			onClick="wndViewPerson.onDeletePerson()" />
	</hlayout>
</window>

Одно маленькое уточнение, в конфигурационном файле zk.xml, который должен находиться в папке WEB-INF, укажем класс, ответственный за сериализацию:
<?xml version="1.0" encoding="UTF-8"?>
<zk>
	<system-config>
		<ui-factory-class>
			org.zkoss.zk.ui.http.SerializableUiFactory
		</ui-factory-class>
	</system-config>
</zk>

Вот теперь все, обратите внимание как легко, удобно и главное без промежуточных звеньев и костылей, можно работать с refcursor-ом, и вообще с базой данных.
А это минимальный список библиотек, необходимых для запуска приложения:
  • antlr-2.7.7.jar
  • aopalliance-1.0.jar
  • bsh.jar
  • commons-collections-3.1.jar
  • commons-fileupload.jar
  • commons-io.jar
  • commons-logging-1.1.1.jar
  • dom4j-1.6.1.jar
  • hibernate3.jar
  • hibernate-jpa-2.0-api-1.0.0.Final.jar
  • javassist-3.12.0.GA.jar
  • js.jar
  • jta-1.1.jar
  • ojdbc6.jar
  • org.springframework.aop-3.1.0.M1.jar
  • org.springframework.asm-3.1.0.M1.jar
  • org.springframework.aspects-3.1.0.M1.jar
  • org.springframework.beans-3.1.0.M1.jar
  • org.springframework.context-3.1.0.M1.jar
  • org.springframework.core-3.1.0.M1.jar
  • org.springframework.expression-3.1.0.M1.jar
  • org.springframework.instrument.tomcat-3.1.0.M1.jar
  • org.springframework.instrument-3.1.0.M1.jar
  • org.springframework.jdbc-3.1.0.M1.jar
  • org.springframework.jms-3.1.0.M1.jar
  • org.springframework.orm-3.1.0.M1.jar
  • org.springframework.oxm-3.1.0.M1.jar
  • org.springframework.transaction-3.1.0.M1.jar
  • org.springframework.web.servlet-3.1.0.M1.jar
  • org.springframework.web-3.1.0.M1.jar
  • slf4j-api-1.6.0.jar
  • zcommon.jar
  • zcommons-el.jar
  • zhtml.jar
  • zk.jar
  • zkplus.jar
  • zkspring-core.jar
  • zul.jar
  • zweb.jar

P.S. надеюсь у всех заведется, ну а если нет, то пишите, вышлю проект. И еще, если эта статья вам показалась интересной, и хотелось бы еще узнать либо про сам фреймворк ZK, либо про интеграции например со Spring Security, то дайте знать.
Tags:
Hubs:
+5
Comments 14
Comments Comments 14

Articles