В этой статье я буду обсуждать пример приложения «ZkToDo2», которое демонстрирует шаблоны MVC для настольных компьютеров с использованием инфраструктуры ZK RIA AJAX, Spring и JPA. Конфигурация Spring и JPA, использованная в этой статье, адаптирована из надстройки персистентности платформы Loom. Я благодарю Игнасио Колому и его сотрудников в Loom за хорошо документированный и обсужденный код JPA.
ZKявляется зрелой платформой с открытым исходным кодом, чисто Java Rich Internet Application (RAI / AJAX), размещенной на SourceForge. Уникальная особенность ZK заключается в том, что вы используете XML (ZUL / XHTML) для описания рабочего стола Java, который транслируется в DHTML с поддержкой AJAX для браузера. Все события пользовательского интерфейса, запускаемые в DOM веб-браузера, отправляются через AJAX на сервер, где выполняются обработчики событий Java. В обработчиках событий AJAX любые изменения, которые вы вносите в рабочий стол Java, отправляются обратно в браузер, где обновляется DOM. Поскольку логика пользовательского интерфейса программируется как обработчики событий Java, разработчик свободен от отвлекающих факторов кросс-браузерного программирования JavaScript на DHTML. Вместо этого платформа ZK предоставляет кросс-браузерный DHTML и JavaScript.Разработчик свободен от необходимости писать код для преодоления разрыва между браузерами и серверами и может сосредоточиться на том, что является уникальным для их приложения, а не на том, чтобы писать большой объем кода «базовой платы». Чистый Java-подход также означает, что вы можете отлаживать код пользовательского интерфейса в вашей любимой IDE.
Получение и сборка образца кода
Как правило, настройка среды для тестирования примера кода J2EE может быть всесторонней и трудоемкой. Пример кода ZkToDo2, размещенного на SourceForge в рамках проекта ZKForge, предназначен для самостоятельной установки, чтобы упростить отладку и изучение работающего кода. Все, что вам нужно, это система сборки Maven (мощный преемник Ant) или плагин Maven для выбранной вами IDE. Maven загрузит все зависимые файлы jar, скомпилирует код, протестирует код и запустит встроенный контейнер сервлетов со встроенной базой данных с помощью одной команды. Шаги для извлечения, сборки, запуска и отладки приложения с использованием Eclipse с подключаемым модулем m2eclipse Maven находятся здесь . В качестве альтернативы, если вы хотите собрать и запустить приложение в командной строке, используя только стандартный Mavenдистрибутив и подключил удаленный отладчик, затем следуйте инструкциям здесь .
Привязки объекта домена и базы данных
Вот выдержка из класса домена и его отображение в базе данных:
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "REMINDER")
public class Reminder {
@Id @GeneratedValue
@Column(name = "EVENT_ID")
private Long id;
@Column(name = "NAME")
private String name;
@Column(name = "PRIORITY")
private int priority;
@Column(name = "DATE")
private Date date;
...
Аннотации привязки базы данных, используемые в классе, взяты из пакета javax.persistence. Это означает, что они будут работать с любой реализацией JPA, такой как EclipseLink, Hibernate, OpenJPA или TopLink, и это лишь некоторые из них. Состояние каждого объекта домена будет сохранено в базе данных классом BasicDao, подробно описанным ниже. Сначала мы рассмотрим пользовательский интерфейс и его класс контроллера, который отображает и обновляет объекты напоминания, хранящиеся в базе данных.
Интерфейс пользователя
Пользовательский интерфейс ZkToDo2 определен в одном файле zktodo_b.zul . Вот снимок экрана полностью веб-страницы с поддержкой AJAX, определенной в этом файле:
Весь пользовательский интерфейс представляет собой следующую разметку XML:
<?xml version="1.0" encoding="UTF-8"?>
<zk xmlns="http://www.zkoss.org/2005/zul">
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
<?variable-resolver class="org.zkoss.zkplus.spring.DelegatingVariableResolver"?>
<window title="To do list" width="640px" border="normal" apply="${toDoControllerV2}">
<listbox id="list" multiple="true" rows="4" model="@{toDoModel.reminders}" selectedItem="@{toDoModel.selectedReminder}">
<listhead>
<listheader label="Item" />
<listheader label="Priority" width="80px" />
<listheader label="Opened" width="90px" />
</listhead>
<listitem self="@{each=reminder}">
<listcell label="@{reminder.name}"/>
<listcell label="@{reminder.priority}"/>
<listcell label="@{reminder.date, converter='org.zkforge.zktodo2.DateFormatConverter'}"/>
</listitem>
</listbox>
<vbox>
<hbox>
Item:<textbox id="name" cols="40" constraint="no empty" value="@{toDoModel.selectedReminder.name}"/>
Priority:<intbox id="priority" cols="1" constraint="no empty" value="@{toDoModel.selectedReminder.priority}"/>
Date:<datebox id="date" cols="14" constraint="no empty" value="@{toDoModel.selectedReminder.date}"/>
</hbox>
<hbox>
<button id="add" label="Add" width="36px" height="24px"/>
<button id="update" label="Update" width="46px" height="24px"/>
<button id="delete" label="Delete" width="46px" height="24px"/>
</hbox>
</vbox>
</window>
</zk>
Диалект XML — это ZUML, язык разметки интерфейса пользователя ZK. ZUML может быть смесью XHTML, ZUL (диалект ZK XUL) и произвольного XML. Платформа ZK поддерживает более 170 готовых XUL- или HTML-совместимых Ajax-компонентов и многочисленные сторонние виджеты: JFreeChart, JasperReports, Google Maps, FCKeditor, Timeline, Timeplot, ExtJS, Dojo и многие другие. На приведенном выше снимке экрана показан внешний вид ZK по умолчанию, но вы можете определить собственный стиль оформления на основе CSS или HTML. Вы даже можете просто использовать свои корпоративные веб-страницы XHTML и просто добавить несколько текстовых полей XUL с поддержкой Java, даты и интбокс, которые отображаются как простые старые элементы HTML-формы. Демоверсия ZKпоказывает многие из предлагаемых компонентов. Вы даже можете редактировать ZUML на вкладках «Просмотр исходного кода», а затем нажать «Попробуй меня!» кнопка под источником для отображения ваших изменений.
В разметке используется специальный декларативный синтаксис привязки данных («@ {binding}»), чтобы связать пользовательский интерфейс с нашим кодом Java. Внутри приложения аннотации JPA обеспечивают декларативную привязку между нашими POJO и таблицами базы данных. Функция привязки данных ZK предоставляет аналогичную возможность для декларативного связывания наших POJO с компонентами пользовательского интерфейса. Эта функция будет обсуждаться позже в этой статье.
Уровень приложений
Многоуровневая структура компонента контроллера пользовательского интерфейса, компонента модели приложения, компонента службы и компонента доступа к данным определяется в файле конфигурации Spring следующим образом:
<bean id="basicDao" class="org.zkforge.zktodo2.BasicDao" />
<bean id="reminderService" class="org.zkforge.zktodo2.ReminderService"
p:basicDao-ref="basicDao"
/>
<bean id="toDoModel" class="org.zkforge.zktodo2.ZkToDoModelImpl"
p:reminderService-ref="reminderService" scope="session"/>
<bean id="toDoControllerV2" class="org.zkforge.zktodo2.ZkToDoControllerV2"
p:toDoModel-ref="toDoModel" scope="prototype"
/>
Контроллер снабжен модельным компонентом. Модельный компонент имеет служебный компонент. Служебному компоненту предоставляется компонент доступа к данным. Служебный компонент и компонент доступа к данным имеют область действия по умолчанию, поэтому они будут одноэлементными объектами. Модельный бин имеет сессионный охват. Это заставит Spring сохранить один и только один экземпляр компонента, связанный с сеансом HTTP пользователя. Бин контроллера имеет прототипную область. Это приводит к созданию нового компонента для каждой отображаемой страницы. Установка областей таким образом позволяет компоненту bean-компонента и компоненту-контроллеру сохранять состояние. Состояние управляющих компонентов связано с рабочим столом Java, определенным на странице. Состояние bean-компонентов модели связано с веб-сеансом пользователя и может использоваться несколькими контроллерами экрана в большом приложении.
Пример приложения ZkToDo2 очень мал, только с одним компонентом в каждом слое и соотношением 1: 1 между каждым уровнем. В более крупном приложении будет несколько компонентов в каждом слое по ширине сложного приложения. Затем наслоение позволяет использовать конфигурации «один ко многим», «многие ко многим» и «многие ко многим», чтобы обеспечить максимальное повторное использование кода и четкое разделение ответственности между классами и уровнями приложения.
Класс контроллера
Приведенный выше файл экрана ZUML определяет инертную страницу без поведения приложения. Поведение должно быть обеспечено нашим классом контроллера. Тег окна имеет атрибут «apply», установленный в «$ {toDoControllerV2}». Это требует, чтобы инфраструктура ZK предварительно обработала объект Java Window Java Desktop с объектом, на который ссылается переменная toDoControllerV2. Преобразователь переменных Spring был включен вверху страницы. Внедрение распознавателя переменных Spring обеспечивает полную интеграцию со средой Spring. Все, что нам нужно сделать, это определить bean-компоненты в конфигурации Spring, и мы можем ссылаться на bean-объекты по имени в нашем файле экрана ZUML. Bean-компонент toDoControllerV1 определяется этой записью в файле spring-config.xml :
<bean id="toDoControllerV2" class="org.zkforge.zktodo2.ZkToDoControllerV2"
p:toDoModel-ref="toDoModel" scope="prototype"/>
Класс bean-компонента toDoControllerV2 — ZKToDoControllerV2 . Область применения боба — «прототип». Это означает, что Spring будет создавать новый компонент каждый раз, когда ZK запрашивает компонент с таким именем. Это происходит каждый раз, когда загружается страница ZUML. Конфигурация контроллера указывает, что он требует bean-компонента с именем toDoModel. На bean-компонент «toDoModel» также ссылаются в файле ZUML. Вот выдержка из класса контроллера:
public class ZkToDoControllerV2 extends GenericForwardComposer {
protected Textbox name;
protected Intbox priority;
protected Datebox date;
protected Listbox list;
protected ListModelList listModelList;
public void onClick$add(org.zkoss.zk.ui.event.Event e) {
// ...
}
public void onClick$update(org.zkoss.zk.ui.event.Event e) {
// ...
}
public void onClick$delete(org.zkoss.zk.ui.event.Event e) {
// ...
}
}
ZkToDoControllerV2 подклассы GenericForwardComposer. GenericForwardComposer — это необязательный класс поддержки ZK, явно разработанный для поддержки шаблона MVC. Код в классе GenericForwardComposer автоматически соединит наш объект контроллера ZkToDoControllerV2 с компонентами пользовательского интерфейса, определенными в файле ZUML. Автоматическое подключение выполняется через ZK, сопоставляя имена и типы в нашем классе Java с элементами XML и идентификаторами, определенными в нашем файле ZUL.
Если мы сравним перечисленные выше списки Java и ZUML, мы найдем совпадения для «name», «priority», «data» и «list». Кроме того, типы в коде Java соответствуют компонентам, определенным в файле ZUL. ZK будет вставлять ссылки на названные компоненты экрана в наш класс контроллера. Листинг Java также определяет три обработчика событий. Подписи обработчиков событий аналогичны стандартному обработчику событий ZK «onClick». Имена методов начинаются с «onClick» и заканчиваются идентификатором кнопки, определенной в файле ZUL. ZK свяжет три метода контроллера как обработчики событий на соответствующих компонентах кнопки. Без какого-либо JavaScript-программирования AJAX, когда пользователь нажимает на кнопки нашего пользовательского интерфейса, наш чистый Java-код будет работать на сервере.
Уровни обслуживания и данных
Вот выдержка из класса ReminderService:
public class ReminderService {
@Transactional(readOnly=true)
public List findAll(){
List events = this.basicDao.findAll(Reminder.class);
return(List) events;
}
@Transactional(readOnly=false,propagation = Propagation.REQUIRED)
public void persistEvent(Reminder reminder){
this.basicDao.persist(reminder);
}
@Transactional(readOnly=false,propagation = Propagation.REQUIRED)
public void deleteEvent(Reminder reminder) throws EntityNotFoundException {
this.basicDao.remove(Reminder.class, reminder.getId());
}
@Transactional(readOnly=false,propagation = Propagation.REQUIRED)
public void mergeEvent(Reminder reminder) throws EntityNotFoundException {
this.basicDao.merge(reminder);
}
}
Напоминание о сервисе имеет аннотации транзакций Spring для определения транзакционной семантики бизнес-логики. Пример приложения очень прост и не определяет много операций с базой данных в каждом методе сервиса. В более крупной системе, которая должна выполнять набор операций с данными атомарным способом, использование транзакций будет иметь большее значение для обеспечения целостности данных. Аннотации транзакций активируются одной строкой в файле spring-config.xml :
<tx:annotation-driven />
Этот тег аннотации tx не называет конкретного менеджера транзакций для использования. Таким образом, в соответствии с соглашением Spring будет использовать компонент с именемactionManager для обработки аннотаций транзакции компонента службы. Мы можем изменить диспетчер транзакций и источник данных между выполнением тестов JUnit в IDE и развертыванием на сервере приложений. Таким образом, эти bean-компоненты разделены на файл с именем dataSourceContext.xml .
Последней частью конфигурации Spring является JPA EntityManagerFactory:
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
p:persistence-xml-location="classpath:META-INF/persistence.xml"
p:data-source-ref="dataSource"
>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
p:showSql="true"
p:generateDdl="true">
</bean>
</property>
<property name="jpaProperties">
<value>
hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy
hibernate.dialect=
hibernate.hbm2ddl.auto=
</value>
</property>
</bean>
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
Конфигурация определяет Hibernate JPA EntityManagerFactory, который использует наш компонент dataSource, определенный в dataSourceContext.xml. Использование PersistenceAnnotationBeanPostProcessor обеспечивает связь между нашим менеджером сущностей JPA и нашим компонентом BasicDao.
Класс BasicDao был взят из исходного кода платформы Loom вместе с конфигурацией Spring + JPA, описанной выше. Поддержка разбиения на страницы класса была удалена, поскольку она не требовалась для этого примера приложения. Рекомендуется, чтобы читатели изучили исходный класс Loom и окружающие пакеты, которые обеспечивают более богатое поведение, чем было необходимо для реализации zktodo2.
Модельные классы
The model class encapsulates the state and behaviour of the business layers of the system. The model class is stateful in nature and can be reused amongst multiple screen and multiple controllers within a larger application. The model class utilizes the service layer of the application to sync its state with the database.
An explicit model interface for the sample application is here with the implementation here. The interface is shown below:
package org.zkforge.zktodo2;
import java.util.List;
public interface ZkToDoModel {
public abstract void deleteEvent(Reminder reminder)
throws EntityNotFoundException;
public abstract List findAll();
public abstract void mergeEvent(Reminder reminder)
throws EntityNotFoundException;
public abstract void persistEvent(Reminder reminder);
//used by selectedItem="@{controller.selectedReminder}" and others
public abstract Reminder getSelectedReminder();
//used by selectedItem="@{controller.selectedReminder}" and others
public abstract void setSelectedReminder(Reminder reminder);
//used by model="@{controller.reminders}"
public abstract List getReminders();
}
Notice that the Model class holds the «selected reminder» which is the reminder that the current user is editing.
ZK Databindings
Databindings are declarative bindings. Within the application the JPA Annotations provide a declarative binding between our POJOs and the database tables. The ZK databindings feature provides declarative bindings between POJOs and UI Components. An extract highlighting the databindings within the ZUML screen file is shown here:
...
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
<?variable-resolver class="org.zkoss.zkplus.spring.DelegatingVariableResolver"?>
...
<listbox id="list" multiple="true" rows="4" model="@{toDoModel.reminders}" selectedItem="@{toDoModel.selectedReminder}">
<listhead>
<listheader label="Item" />
<listheader label="Priority" width="80px" />
<listheader label="Opened" width="90px" />
</listhead>
<listitem self="@{each=reminder}">
<listcell label="@{reminder.name}"/>
<listcell label="@{reminder.priority}"/>
<listcell label="@{reminder.date, converter='org.zkforge.zktodo2.DateFormatConverter'}"/>
</listitem>
</listbox>
...
Item:<textbox id="name" cols="40" constraint="no empty" value="@{toDoModel.selectedReminder.name}"/>
Priority:<intbox id="priority" cols="1" constraint="no empty" value="@{toDoModel.selectedReminder.priority}"/>
Date:<datebox id="date" cols="14" constraint="no empty" value="@{toDoModel.selectedReminder.date}"/>
In the listing above the page has an «init» instruction referencing the AnnotateDataBinderInit class. Preprocessing the page with AnnotateDataBinderInit causes the zul properties containing «@{binding}» declarative bindings to be bound onto the application POJOs. Collectively the databindings show how to render the initial state of the screen and how to write UI input from the edit area into the selected POJO.
The databindings for the Listbox initially reads from «@{toDoModel.reminders}» and writes to «@{toDoModel.selectedReminder}» when an item is selected from the list by the user. The variables are resolved to our Spring bean via the Spring variable resolver. ZK reflects on the class of the model bean and maps the bindings onto the methods «toDoModel.getReminders()» and «toDoModel.setSelectedReminder(…)» methods.
Within the Listbox there are bindings which define how to render each Reminder object into a set of Listcells within a Listitem. The self=»@{each=reminder}» binding sets up a loop variable «reminder» that is set for each reminder within the list loaded from the database. For each reminder object in the list a new Listitem is instantiated. The bindings such as «@{reminder.name}» applied to the Listcells map the getters of the reminder object onto the setters on the Listcell objects.
Below the Listbox the selected reminder is rendered into the edit area using bindings such as «@{toDoModel.selectedReminder.name}». ZK resolves this to the «getName()» method of the selected reminder object.
Putting It All Together
The effect of the databindings is that when you load the page ZK will call the model bean method to load the reminders from the database and will then render them into the list. When the user clicks on a reminder within the list its data appears within the edit area. No bespoke code is required to do this as ZK does it automatically as directed by the databindings. If you edit the name of the reminder in the edit area, then tab away, the name of the reminder shown within the list changes. This is because both the listcell and edit area is bound to the POJO so ZK keeps all three in synch. ZK knows that the POJO name has changed so it will automatically update the ListCell at the same time as it updates the POJO. Once again no bespoke code has been written to achieve this effect.
It is also important to know that the Java POJO on the server has been updated. There is no «web page post» when writing user interfaces with ZK. The ZK framework has fired AJAX events asynchronously to the server and has called the setters on our Java objects without having to write a single line of Javascript or Java. ZK gives us the illusion that our Java desktop and application Java objects are being rendered directly into the browser window without any Servlet or DHTML programming. In reality ZK is implemented as a set of pure Servlets and a JavaScript library. The ZK developers have done all of the Servlet and DHTML programming to give us a pure Java Desktop programming environment that is «painted» into a browser window without any browser plug-ins.
When you click the Update button ZK will run the Java method of the controller class that it has automatically bound to the event handler of the button. The POJO is already on the server and has already been updated by ZK via AJAX when the user has input data into the edit area. There is no need to write any boiler-plate code to copy the strings out of a web-request and call setters on our POJO. As the POJO is already fully up to date we simply have to add code into the Java event handler of the controller to flush the modified POJO to the database. Here is the controller event handler method that is called when we click the Update button:
public void onClick$update(Event e) {
Reminder selectedReminder = this.toDoModel.getSelectedReminder();
if( selectedReminder != null ){
ListModelList listModelList = (ListModelList)this.list.getModel();
try {
this.toDoModel.mergeEvent(selectedReminder);
} catch (EntityNotFoundException exception){
int index = list.getSelectedIndex();
listModelList.remove(index);
alert("Reminder "+selectedReminder.getName()+" has been deleted by another user.");
if( listModelList.size() > 0 ){
selectedReminder = (Reminder)listModelList.get(0);
list.setSelectedIndex(0);
name.setValue(selectedReminder.getName());
date.setValue(selectedReminder.getDate());
priority.setValue(selectedReminder.getPriority());
} else {
selectedReminder = null;
}
}
List reminders = toDoModel.findAll();
listModelList.clear();
listModelList.addAll(reminders);
}
}
The majority of the code of that method is dealing with the exceptional case of discovering that the reminder has been deleted from the database by another user. The actual bespoke code to take the user input and call our back-end Java business logic to persist the input to the database can be simplified down to a single line of code:
this.toDoModel.mergeEvent(this.toDoModel.getSelectedReminder());
As all of the DHTML and Servlet programming has been handled by the ZK framework and our databindings mark-up we are free to focus on what is unique to our application. The source code of the project contains no bespoke Servlet or DHTML code only pure Java business logic. The Update logic above is within our prototype scoped controller bean which Spring instantiated and injected with a model bean. ZK injected references to the Java desktop Components defined within the ZUML page. ZK also bound the Java event handlers of the Components within the Java desktop to the event handlers defined within our controller bean. The model bean was given session scope so it is bound to the HTTPSession. Spring injected into the model bean a service bean. The service bean was given transactional annotations such that Spring will roll-back any database changes if an exception is thrown within the service call. Spring injected the data access bean into the service bean and configured the data access bean with a JPA entity manager.
Summary
In this article we introduced the ZkToDo2 sample application. The application makes use of the ZK support class GenericForwardComposer which provides explicit support for the Model-View-Controller pattern. The user interface defined in the ZUL file was wired to the controller class of the application automatically by matching elements and IDs within the ZUL file to member variable names and types within the controller class.
Integration with the Spring framework was demonstrated using the ZK support class DelegatingVariableResolver. Spring was used to create application layering through configuration using its support for the Inversion-Of-Control pattern. Logical layers for the UI controller bean, an application model bean, a singleton service bean and a singleton data access bean were configured via Spring. These layers provide a clear separation of concerns between the classes of the application. Spring support for transaction management was used to configure the database transaction boundaries of the service bean. Spring support for JPA was used to connect an EntityManager to the data access bean that was injected into the service bean.
The ZkToDo2 sample application uses an explicit Model class and ZK databindings. The introduction of an explicit model class provides for better encapsulation of the application business logic to facilitate code re-use between controllers. The introduction of ZK databindings removed much of the boiler-plate code required to initialize the UI and update application POJOs with user input.