Ваш первый Enterprise JavaBean
Первый EJB, который мы будем реализовывать, является системой продажи билетов. Я выбрал этот тип объекта, поскольку он не содержит бизнес логики и имеет немного постоянной информации, так что это не будет отвлекать нас от реальной практики реализации EJB.
Так как фильмы содержат информацию (id и title), которая должна быть постоянно, мы будет реализовывать их как entity bean, используя Container Managed Persistence. Этот EJB будет называться, как это ни удивительно, Movie.
Пожалуйста, обратите внимание, что этот пример фундаментален, так как, хотя мы подходим к реализации специфического типа EJB (entity bean), большая часть информации, которую вы увидите, применима и к session bean, и к entity bean во всех их вариациях.
Для реализации компонента Movie, вам понадобиться определить два интерфейса, домашний интерфейс и компонентный интерфейс, а также один класс реализации. В зависимости от того, представление какого рода должен проявлять компонент, домашний и компонентный интерфейс могут быть реализованы как удаленный или локальный. В этом случае мы определим интерфейс Movie как удаленный.
Вам также понадобиться предоставить XML файл, называемый дескриптором развертки, который вы используете, чтобы сообщить Контейнеру о некоторых характеристиках вашего EJB, включая постоянство, транзакционные атрибуты и атрибуты безопасности. Наконец, так как реализуемый нами компонент является entity bean'ом, мы может пожелать снабдить его классом, являющимся первичным ключом компонента (или вы можете просто использовать некий предопределенный класс, что мы и сделаем).
Я буду объяснять каждый из пяти элементов, перечисленных выше (домашний и компонентный интерфейсы, класс реализации, дескриптор развертки и первичный ключ) в следующих разделах. Я также дам вам немного информации о внутренностях Контейнера и развертке Bean'а.
Домашний интерфейс
Начнем с того, что предоставим нашему компоненту Movie удаленное представление. Это означает, что когда клиент создает экземпляр EJB, реальный экземпляр создается в пространстве памяти процесса на сервере, а клиент получает ссылку на этот экземпляр.
Так как экземпляр EJB становится удаленным, клиентский код не может создавать экземпляр EJB используя оператор new Java, поскольку new всегда выделяет экземпляр в локальном адресном пространстве.
Теоретически, мы можем использовать оператор new для создания экземпляра компонента, который предоставляет локальное представление, но спецификация EJB скрывает различия удаленного/локального создания с помощью объекта фабрики. Фабрика предоставляет метод для создания экземпляра вашего компонента, а ее (фабрики) реализация, сгенерированная Контейнером, предоставляет логику для создания экземпляра объекта EJB в адресном пространстве Контейнера.
Таким образом, проблема просто сдвигается: как нам получить ссылку на объект фабрики? Ответ содержится в JNDI. Как вы помните, JNDI является нашим универсальным поисковым механизмом в EJB, а все EJB Контейнеры запускают внутреннюю Службу JNDI, чтобы клиенты могли получить доступ по сети.
Когда EJB становится доступным для Контейнера, ему также дается JNDI имя либо Поставщиком Компонент (Component Provider), либо Развертывателем (Deployer). Клиентское приложение использует это JNDI имя для нахождения фабрики объекта для этого EJB (в нашем случае это Movie), а затем использует фабрику для создания объектов Movie.
Фабрика предоставляет домашний интерфейс компонента, который вы вольны определить (не полностью произвольным образом, он должен соответствовать спецификации EJB). Тот факт, что вы сами определяете интерфейс, позволяет вам указать, какие аргументы передаются при вызове методов. Например, вы можете решить, какая информация требуется для создания экземпляра Movie. Реализация этого интерфейса будет автоматически синтезирована Контейнером, который получит данные об интерфейсе посредством Рефлексии (Reflection).
Если вы определяете домашний интерфейс для entity bean, есть дополнительные требования, которые необходимо выполнить для создания экземпляра. Вы должны найти существующий экземпляр. Это имеет смысл, если вы решили, что entity bean, по определению, постоянен в некотором хранилище данных; так что вы должны быть способны получить ссылку на один или несколько компонентов, созданных кем-либо некоторое время назад, вот что вы должны ожидать.
В домашнем интерфейсе методы, которые вы используете для создания новых EJB, называются методами создания, а методы, которые вы используете для получение экземпляров entity bean, называются методами поиска.
Ниже определен MovieHome, домашний интерфейс для Movie entity bean.
//: c18:example02:src:ejb-tier:javatheater:ejb:MovieHome.java
package
javatheater.ejb;
import
javax.ejb.*;
import
java.util.Collection;
import
java.rmi.RemoteException;
public interface
MovieHome
extends
EJBHome
{
public
Movie create
(
Integer id, String title
)
throws
RemoteException,
CreateException;
public
Movie findByPrimaryKey
(
Integer id
)
throws
RemoteException,
FinderException;
}
// /:~
Вы должны обратить внимание, что этот интерфейс наследуется от javax.ejb.EJBHome, который, в свою очередь, наследуется от java.rmi.Remote. Это сделано потому, что мы решили обеспечить наш компонент Movie удаленным представлением, и смысл здесь в том, что все методы нашего домашнего интерфейса будут соответствовать спецификации Java RMI. Практически, все методы будут иметь java.rmi.RemoteException в спецификации исключений.
Спецификация EJB описывает все синтаксические и семантические требования для методов создания и поиска. Если вы хотите узнать все детали, пожалуйста, обратитесь к спецификации EJB, которую вы можете загрузить на веб сайте Sun'а. Однако, в этой главе, в связи с ограниченностью места, я просто опишу смысл сигнатуры метода.
Первый метод интерфейса является методом создания, который клиент может использовать для создания нового экземпляра EJB Movie. Он возвращает ссылку на Movie. Movie - это компонентный интерфейс нашего компонента Movie, и я буду описывать его в следующем разделе. Первый аргумент для метода create( ) это то, что мы используем в качестве первичного ключа нашего компонента. В этом случае, мы решаем использовать Integer для уникальной идентификации экземпляра Movie. Первичный ключ будет описан более детально в одном из следующих разделов. В качестве второго аргумента мы передаем информацию, которая, в данном случае, представляет "состояние" Movie, это название фильма.
Наконец, исключения метода указывают, что метод create( ) может выбросить RemoteException и CreateException. RemoteException требуется RMI, и не должно удивлять вас. CreateException - это исключение уровня приложения, являющегося частью EJB API, и оно генерируется в том случае, если создание экземпляра провалилось внутри Контейнера. Особые причины, почему создание может закончиться неудачей, заключаются в том, если вы попробуете создать экземпляр компонента с одинаковым первичным ключом, что и у другого существующего entity bean того же типа. Этот особый случай представляется DuplicateKeyException, подклассом CreateException.
Второй метод интерфейса является методом поиска, который клиент использует для получения одного существующего экземпляра entity bean. Он называется findByPrimaryKey( ) и, как это не удивительно, он принимает Integer в качестве аргумента (Iteger является типом первичного ключа) и возвращает ссылку на Movie. Этот метод также выбрасывает исключение FinderException в том случае, если что-то прошло не так внутри Контейнера. Если возникнет специфическая проблема, когда не будет найдено значение первичного ключа, Контейнер сгенерирует ObjectNotFoundException, которое является производным классом от FinderException.
Все остальные поисковые методы, которые возвращают Java коллекцию, а не одиночную ссылку, не будут генерировать ObjectNotFoundException, а просто возвратят null. Необходимо добавить, что метод findByPrimaryKey( ) является обязательным для домашнего интерфейса entity bean.
Вы можете иметь столько поисковых методов, сколько вы хотите, для поиска постоянного экземпляра по любому критерию, который только можете придумать. Однако, все поисковые методы должны иметь имя, начинающееся с find&, должны возвращать коллекцию и должны выбрасывать FinderException. Только один поисковый метод возвращает единственный экземпляр - это findByPrimaryKey( ), так как только первичный ключ гарантирует уникальность результата - более подробно о поисковых методах я расскажу в следующей главе.
Также в домашнем интерфейсе может быть несколько методов создания и даже EJB бизнес методы (что-то похожее на статические методы Java, которые не оперируют над конкретным экземпляром). Мы поговорим об этих дополнительных методах позднее; в настоящее время давайте рассмотрим следующий интерфейс.
Компонентный интерфейс
Компонентный интерфейс содержит бизнес методы, которые клиент вызывает для определенного экземпляра EJB, например getTitle( ) для объекта Movie. По этой причине компонентный интерфейс произвольным образом определяется Bean Provader'ом, но он все равно должен удовлетворять спецификации EJB. Единственное требование такое: этот интерфейс наследуется от javax.ejb.EJBObject и все его методы выбрасывают RemoteException.
Ниже приведено определение компонентного интерфейса для entity bean Movie.
//: c18:example02:src:ejb-tier:javatheater:ejb:Movie.java
package
javatheater.ejb;
import
javax.ejb.*;
import
java.rmi.RemoteException;
/**
*
* Фильм с идентификатором(id) и названием (title).
*
*
*
* Заметьте, что в интерфейсте нет метода setId(),
*
* чтобы клиент не смог произвольно сменить
*
* первичный ключ фильма.
*
*/
public interface
Movie
extends
EJBObject
{
public
Integer getId
()
throws
RemoteException;
public
String getTitle
()
throws
RemoteException;
public
void
setTitle
(
String title
)
throws
RemoteException;
}
// /:~
Вы видите, что интерфейс просто предоставляет геттеры и сеттеры для атрибутов фильма (здесь исключительно бизнес логика для entity bean). Однако, здесь отсутствует сеттер для идентификатора (id) фильма. Причина этого состоит в том, что вам не нужно давать клиенту возможность изменять первичный ключ существующего постоянного объекта, так как при этом легко сломать ссылочную целостность вашей системы, если этот экземпляр участвует в каких-либо связях.
Первичный Ключ
На концептуальном уровне, первичный ключ в EJB - это то же самое, что и первичный ключ в реляционной базе данных. Это то, что вы используете для уникальной идентификации информационной группы: записи в базе данных или экземпляра entity bean в Контейнере EJB.
Однако, в EJB эта концепция намеренно более абстрактная, чем в реляционной базе данных, а причина этого в том, что EJB архитектура предназначена для работы со многими различными оконечными системами, включая, но не ограничиваясь, реляционную базу данных. Например, ваше оконечное постоянное хранилище может быть объектно-ориентированной базой данных, в которой нет первичного ключа, как в реляционной базе данных, а вместо этого используются объекты-идентификаторы.
Так что в EJB в качестве первичного ключа вы можете использовать объекты любого вида. Другими словами, вы можете использовать только классы, унаследованные от Object, но не примитивные типы. Вот почему мы использовали Integer вместо int в качестве первичного ключа Movie.
Первичный ключ EJB может содержать одно единственное значение или множество значений (получим составной первичный ключ). Он может также быть классом, определение которого отложено до завершения момента развертывания. Более подробно, включая требования к реализации, вы можете посмотреть в рекомендованной литературе в конце этой главы. В этой главе в качестве первичных ключей я просто использую простые стандартные классы.
Класс реализации
Теперь, когда мы определили наш домашний и компонентный интерфейсы, мы можем приступить к реализации компонента Movie. Вы можете ожидать, что мы определим класс, реализующий оба этих интерфейса, но, что достаточно интересно, вы не правы. Вместо этого вам необходимо определить класс, который реализует один из интерфейсов EntityBean, SessionBean или MessageDrivenBean, в зависимости от того, какого рода компонент вы реализуете.
Тогда кто будет реализовывать домашний и компонентный интерфейс, который вы определили? Конечно же, Контейнер. Приведенная внизу диаграмма должна помочь вам понять, как и почему Контейнер динамически синтезирует реализацию классов для вашего домашнего и компонентного интерфейса. На приведенной ниже диаграмме мы предполагаем, что клиентское приложение работает с использованием TCP/IP соединения, хотя я пренебрегаю этим на диаграмме.
Если вы внимательно взглянете на приведенную выше диаграмму, вы увидите, что вы реально не реализуете ни одного из определенных вами интерфейсов. Они все реализуются сгенерированными Контейнером классами: компонентами RMI, объектом Home и объектом EJB.
Есть очень хорошая причина для отказа Bean Provider'а, от реализации этих интерфейсов. Если вы изучите иерархию интерфейсов, вы обнаружите, что ваши интерфейсы содержат несколько методов, наследованных от базового интерфейса, которые не предназначены для ручной реализации, поскольку они являются частью инфраструктуры RMI и Movie.
Контейнер предоставляет реализацию домашнего интерфейса (объект Home), и реализацию компонентного интерфейса (объект EJB), так что все вызовы клиента будут перехвачены. Обычно Контейнер будет делегировать вызовы методов экземпляру Movie, но класс реализации Movie не реализует интерфейсы MovieHome и Movie.
Какой же интерфейс нашего экземпляра EJB предназначен для того, чтобы быть внешним, так чтобы EJB Объект мог делегировать ему клиентские вызовы? Например, метод Movie getTitle( ), определенный в интерфейсе Movie, который не реализован в экземпляре EJB Movie. Но как интерфейс определит семантический контакт между EJB объектом Movie и экземпляром Movie?
Вы должны запомнить, что реализация EJB Объекта синтезируется Контейнером. Эта реализация моделируется по определению компонентного интерфейса, который вы предоставили, и который Контейнер обнаруживает благодаря рефлексии.
Вы обязательно должны следовать конвенции имен, определенной в спецификации EJB, которая описывает, какие методы EJB объекта должны быть найдены в вашем экземпляре EJB (в нашем случае это Movie). Эта конвенция имен описывает семантические правила, которым вы должны следовать, чтобы Контейнер мог взаимодействовать с вашим экземпляром EJB. В этой главе я следую этим правилам для компонентного мета-интерфейса, не приводя их здесь. Это не Java интерфейс в строгом смысле этого слова, но это все еще набор методов, которые, на практике, определяют способ контакта между Контейнером (сгенерированным EJB объектом) и программистом (определенным EJB экземпляром).
Есть две категории методов в компонентном мета-интерфейсе: методы уведомления Контейнера и бизнес методы.
Методы уведомления Контейнера являются теми методами в классе реализации компонента, которые Контейнер вызывает в особых случаях. Их легко обнаружить, поскольку их названия все начинаются с префикса ejb<&> - например, ejbCreate( ). Исключениями из этого правила являются методы уведомления, связанные с Контекстом: setEntityContext( ), unsetEntityContext( ), setSessionContext( ) и setMessageDrivedContext( ). Я объясню каждый из методов уведомления Контейнера позднее, когда мы посмотрим исходный код класса реализации.
Бизнес методы суть просто методы, которые вы определили в интерфейсе для своего компонента. Все эти методы должны быть в вашем классе реализации EJB с абсолютно одинаковой сигнатурой, за исключением RemoteException, которое вы не должны включить в спецификацию исключений для методов.
Однако есть одна важная архитектурная вещь спецификации EJB 2.x, которая влияет на реальные бизнес-методы, которые вы реализуете. Начиная с EJB 2.0, архитектура предоставляет абстрактную модель программирования для CMP entity bean. Это значит, что реализация постоянных полей entity bean не подпадает под ответственность BenaProvider'а, а вместо этого делегируется Контейнеру. Мы ссылаемся на сгенерированные Контейнером постоянные поля, как на виртуальные поля.
Давайте возьмем, например, компонент Movie. Вы можете сказать, обнаружив геттеры и сеттеры компонентного интерфейса, что в Movie есть два свойства: id и title. Вы можете ожидать, что: a) вам нужно реализовать геттеры и сеттеры; b) что вам нужно объявить Integer и String в классе реализации для хранения значений id и title соответственно, и: c) состояние этих переменных будет синхронизироваться Контейнером с соответствующим состоянием в постоянном хранилище.
Вот что вы должны были бы ожидать в том случае, если бы вы кодировали для EJB 1.x. Однако в EJB 2.0 введена простая концепция, которая состоит в том, что Контейнер может делать работу гораздо лучше, чем разработчик, в месте реализации таких постоянных свойств. Причина такого предположения состоит в том, что Контейнер может иметь начальные знания о специфике хранения, которых нет у разработчика, так что он может лучше оптимизировать (например, если вы используете контейнер WebSphere с базой данных DB2, оба поставляются IBM).
Таким образом, в случае представления методов постоянных свойств entity bean, вы не предоставляете реализацию этих методов, поскольку Контейнер будет генерировать их за вас.
Если в вашем коде вы декларируете класс реализации вашего entity bean, как абстрактный, и он будет использоваться, как базовый класс для сгенерированного Контейнером класса (да, еще одного!), то он реализует постоянную логику. Вы просто должны в реализации вашего класса декларировать методы, представляющие постоянные свойства, как абстрактные.
Давайте посмотрим на MovieBean, класс реализации нашего Movie CMP entity bean.
//: c18:example02:src:ejb-tier:javatheater:ejb:implementation:MovieBean.java
package
javatheater.ejb.implementation;
import
javax.ejb.CreateException;
import
javax.ejb.EntityBean;
import
javax.ejb.EntityContext;
public abstract class
MovieBean
implements
EntityBean
{
// Методы уведомления Контейнера
public
Integer ejbCreate
(
Integer id, String title
)
throws
CreateException
{
if
(
id ==
null
)
throw new
CreateException
(
"Primary key cannot be null"
)
;
if
(
id.intValue
()
==
0
)
throw new
CreateException
(
"Primary key cannot be zero"
)
;
setId
(
id
)
;
setTitle
(
title
)
;
return null
;
}
public
void
ejbPostCreate
(
Integer id, String title
) {
}
public
void
ejbLoad
() {
}
public
void
ejbStore
() {
}
public
void
ejbRemove
() {
}
public
void
ejbActivate
() {
}
public
void
ejbPassivate
() {
}
public
void
setEntityContext
(
EntityContext ctx
) {
}
public
void
unsetEntityContext
() {
}
// Бизнес методы
public abstract
void
setId
(
Integer id
)
;
public abstract
String getTitle
()
;
public abstract
void
setTitle
(
String title
)
;
public abstract
Integer getId
()
;
}
// /:~
Обратите внимание, что класс является абстрактным, и что он реализует интерфейс EntityBean, но не реализует ни домашний, ни компонентный интерфейсы.
Относительно бизнес методов, последние четыре метода класса, больше нечего добавить. Они представляют постоянные свойства нашего компонента Movie CMP, так что вы просто декларируете их, как абстрактные, а контейнер предоставить их реализацию.
Методы уведомления Контейнера нуждаются в более подробном объяснении. Помните, что beanProvider определяет эти методы, но никогда не вызывает их. Все эти методы вызываются только Контейнером при разных условиях.
Вот краткое описание всех уведомляющих методов entity bean'а:
- ejbCreate( ): вызывается при создании экземпляра и назначении его клиенту, обычно при ответе на клиентский вызов метода создания в домашнем интерфейсе. Метод ejbCreate отвечает за проверку входных аргументов и инициализацию полей экземпляра посредством сеттеров. Он принимает те же аргументы, что и соответствующий метод создания в домашнем интерфейсе, но возвращаемым значением является тип первичного ключа компонента, а не компонентный интерфейс. Возвращаемое значение первичного ключа будет null, если entity bean использует CMP, если используется BMP, то будет возвращено реальное значение. Этот метод вызывается прежде, чем новый экземпляр будет сохранен в постоянном хранилище.
- ejbPostCreate( ): должен существовать соответствующий метод ejbPostCreate(&) для каждого метода ejbCreate( ) в классе. Возвращаемый тип всегда void. Этот метод вызывается после того, как новый экземпляр будет запомнен в постоянном хранилище, он используется для реализации кода, который предполагает, что entity bean'у было присвоено допустимое значение первичного ключа в постоянном хранилище - обычно для установления взаимоотношений с другими компонентами.
- ejbRemove( ): вызывается в ответ на вызов клиентом метода remove( ) домашнего или компонентного интерфейса. Выполняется до того, как entity bean будет удален из постоянного хранилища.
- ejbLoad( ): вызывается сразу после того, как состояние EJB объекта будет восстановлено из постоянного хранилища.
- ejbStore( ): вызывается прежде, чем состояние EJB объекта будет скопировано в постоянное хранилище.
- ejbActivate( ): вызывается сразу после того, как состояние EBJ объекта будет восстановлено из некоторого вторичного хранилища, обычно после того, как экземпляр был запрошен из пула.
- ejbPassivate( ): вызывается перед тем, как состояние EJB объекта будет записано в некоторое вторичное хранилище, обычно перед тем, как экземпляр будет возвращен в пул.
- ejbSetEntityContext( ): вызывается Контейнером, чтобы передать EJB объекту его Контекст. Объект Контекста предоставляет специфическую информацию времени выполнения для этого EJB объекта, например роль в безопасности, под которой будет вызываться метод. EJB объект предназначен для хранения ссылки Контекста в своем поле для последующего использования.
- unsetEntityContext( ): вызывается, чтобы проинформировать EJB объект, что переданный перед этим Контекст больше не действителен.
Повторюсь, я хочу, чтобы вы обратились к спецификации EJB или к одному из источников, перечисленных в конце этой главы, за более детальной информацией относительно методов уведомления и интерфейсе Контекста. В этом месте, той информации, которую я предоставил, должно быть достаточно для понимания того, что делает класс MovieBean.
Вы видите, что мы реализовали только один метод - ejbCreate( ), где мы проверяем id и title, и выбрасываем исключение, если они содержат недопустимые значения. После того, как аргументы будут проверены, мы используем сеттеры для инициализации значений id и title, постоянных полей нашего компонента. Все остальные методы уведомления Контейнера не нужны для выполнения каких-либо специальных действий нашего простого компонента Movie, так что мы просто предоставили методы с пустым телом.
Таким образом, выглядит обертка из Java кода, но это еще не исходный код. Нам все еще нужно снабдить наш компонент Movie дополнительными элементами.
Дескриптор Развертки
Дескриптор развертки является XML файлом, который содержит структурированную информацию относительно EJB. Эта информация включает, но не ограничивается: тип EJB; Java интерфейсы и классы, которые представляют домашний и компонентный интерфейсы; класс реализации и первичный ключ; логическое имя EJB, используемое в этом специфическом дескрипторе развертки; и постоянные поля entity bean.
Дескриптор развертки может также включать сборочную информацию относительно EJB, например логические имена, используемые в реализации компонента, которые необходимы для отображения на физические ресурсы.
Этот файл, который должен называться ejb-jar.xml, создается Bean Provider'ом, и может быть отредактирован Bean Developer'ом и/или Application Assembler'ом. Другими словами, дескриптор развертки содержит информацию конфигурирования компонента, которая может быть изменена для адаптации компонента к специфической операционной среде.
Ниже приведен дескриптор развертки для компонента Movie (я пропустил элементы xml и DOCTYPE):
<ejb-jar> <enterprise-beans> <entity> <ejb-name>Movie</ejb-name> <home>javatheater.ejb.MovieHome</home> <remote>javatheater.ejb.Movie</remote> <ejb-class> javatheater.ejb.implementation.MovieBean </ejb-class> <persistence-type>Container</persistence-type> <prim-key-class>java.lang.Integer</prim-key-class> <reentrant>False</reentrant> <cmp-version>2.x</cmp-version> <abstract-schema-name>Movie</abstract-schema-name> <cmp-field><field-name>id</field-name>-</cmp-field> <cmp-field><field-name>title</field-name>-</cmp-field> <primkey-field>id</primkey-field> </entity> </enterprise-beans> </ejb-jar>
Структура дескриптора должна быть самообъясняющейся, так что я не буду вникать во все детали. Однако, следующие XML элементы требуют несколько слов:
- <entity>: определяет entity bean.
- <ejb-name>: Логическое имя, под которым на компонент можно будет ссылаться из других частей текущего дескриптора развертки. Не нужно путать его с JNDI именем (обсуждается далее).
- <home>: Полностью квалифицированное имя Java интерфейса, используемого в качетсве удаленного домашнего интерфейса компонента.
- <remote>: Полностью квалифицированное имя Java интерфейса, используемого в качетсве удаленного компонентного интерфейса компонента.
- <ejb-class>: Полностью квалифицированное имя Java класса, используемого в качестве класса реализации компонента.
- <persistence-type>: Контейнер для CMP, Bean для BMP.
- <prim-key-class>: Полностью квалифицированное имя Java класса, используемое в качестве первичного ключа entity bean.
- <primkey-field>: Имя поля в entity bean, которое представляет его первичный ключ.
- <cmp-field>: Имя поля, постоянство которого контролируется Контейнером. Entity bean может иметь несколько <cmp-field> элементов.
- <abstract-schema-name>: Имя, используемое для ссылки на этот entity bean в запросных выражениях в EJB Query Language (EJB-QL), который рассмотрим позднее.
Дескриптор развертывания может быть, и обычно бывает, богаче и сложнее, чем то, что вы видите в примере. Поэтому дескриптор создания на сегодняшний день редко создается вручную. Разработчик вместо этого полагается на инструмент разработки, который оказывает помощь в определении интерфейсов компонента, классах и дескрипторе развертки. Большинство из этих коммерческих инструментов предоставляют однотипную RAD среду, в которой "мастер" (wizard) предоставляет вам структурную информацию для генерации исходного кода. Как вы увидите позднее в этой главе, мы будем использовать полностью отличающийся и достаточно мощный инструмент для автоматической генерации кода.
Packaging
Теперь у вас есть все исходные файлы, которые составляют реализацию нашего компонента Movie, мы должны быть собраны в пакет для распространения.
Во-первых, вы должны скомпилировать код, так как мы должны распространять class-файлы, а не исходные файлы. Во-вторых, вы должны собрать в пакет все требуемые файлы в jar-файл, называемый ejb-jar для вашего компонента, который вы создаете с помощью утилиты jar, которая поставляется вместе с J2SE SDK.
Вы знаете, что jar-файлы могут содержать внутренние директории. Внутренняя структура файла ejb-jar должна удовлетворять следующей спецификации: все файлы Java классов начинаются с верхнего уровня, и распределяются по директориям в соответствии с именами пакетов; а дескриптор развертывания должен находиться в поддиректории с названием META-INF на верхнем уровне (все буквы должны быть в верхнем регистре):
Приведенная ниже диаграмма показывает внутреннюю организацию javatheater.jar для компонента ejb-jar Movie.
javatheater.jar
javatheater/
ejb/
MovieHome.class
Movie.class
implementation/
MovieBean.class
META-INF/
ejb-jar.xml
Этот файл является распространяемым модулем вашего компонента: он содержит интерфейсы, используемые клиентским кодом, классы реализации и конфигурационную информацию.
Ejb-jar предназначен для EJB Контейнера, запущенного на сервере, но обычно он не доступен для клиентского приложения. По этой причине вы обычно создаете другой jar-файл. Он называется client.jar и содержит только class-файлы, требующиеся клиентскому приложению для доступа к вашему удаленному компоненту.
В нашем примере есть только два class-файла, которые необходимы клиенту - это определение интерфейсов MovieHome и Movie. Хотя есть другой архитектурный кусок, который понадобиться удаленному клиенту - это RMI прокси. Следующий раздел о развертывании осветит этот кусок.
Развертывание
Развертывание является актом создания EJB компонента, поддерживаемого определенной реализацией EJB Контейнера, которая использует специфичные для приложения внутренние и внешние ресурсы. Развертывание - это неотъемлемая часть работы специфичная для Контейнера и приложения, так как оно вовлекает подстройку EJB Контейнера и компонента (например, ассоциацию общего компонента с предопределенной, установленной базой данных).
Даже для развертывания нашего простого компонента Movie вам понадобится позаботиться, как минимум, о: a) присвоении JNDI имени вашему компоненту, чтобы клиент мог найти его; b) ассоциации Movie entity bean с физической базой данных, и c) необходимо обеспечить удаленного клиента RMI прокси для интерфейсов MovieHome и Movie.
Концептуально, развертывание одинаково для всех платформ, но практика развертывания может значительно отличаться в зависимости от используемого Контейнера. Все коммерческие EJB контейнеры поставляются с некоторой графической утилитой, которую вы используете для сборки вашего ejb-jar и client.jar, чтобы передать ejb-jar вашему контейнеру, для подстройки вашего EJB и ассоциации его с системными ресурсами, такими как база данных, очередь сообщений и так далее.
Пожалуйста, обратите внимание что, так как мы используем JBoss в качестве EJB Контейнера, этот раздел развертывания специфичен для JBoss, но вся информация, которую вы увидите, является не зависящей от Контейнера.
JBoss упрощает процесс развертывания: все, что вам нужно сделать, это разместить ваш ejb-jar в специальном поддиректории установленного пакета JBoss, что будет описано позднее. JBoss определит наличие новых файлов (даже когда Контейнер запущен), просмотрит их и проверит. Если все пройдет без ошибок, новый компонент будет развернут и клиент будет способен использовать его.
Однако, нет GUI инструмента, поставляемого с JBoss, который поможет вам в создании ejb-jar'а, развертывании и конфигурировании вашего enterprise bean. Вы предоставите всю необходимую информацию в дескрипторе развертывания и/или в некоторых специфичных для JBoss XML-файлах.
Давайте теперь взглянем на то, что вам нужно сделать в JBoss для правильной установки EJB имени в JNDI, базы данных и RMI прокси.
JNDI имя: по умолчанию, JBoss создаст JNDI имя для вашего EJB, используя то же имя, что и ejb-name в дескрипторе развертывания (в нашем случае это "Movie"). Однако, в реальном приложении вам может понадобиться указать ваше собственное JNDI имя для Movie bean, возможно, для предотвращения конфликта с другими компонентами. Чтобы сделать это, вы должны предоставить XML файл с именем jboss.xml, который должен быть в том же директории, что и дескриптор развертывания, и использовать его для связывания JNDI с желаемым именем ejb-name компонента. Ниже приведен пример файла jboss.xml (обратите внимание, что JNDI имя отличается от ejb-name).
<?xml version="1.0" encoding="UTF-8"?> <jboss> <enterprise-beans> <entity> <ejb-name>Movie</ejb-name> <jndi-name>javatheater/Movie</jndi-name> </entity> </enterprise-beans> </jboss>
База данных: JBoss поставляется по умолчанию с базой данных, которая стартует при запуске Контейнера. В отсутствии другой информации развертывания, все CMP entity bean будут сделаны постоянными в этой базе данных. JBoss также будет создавать таблицы в базе данных, именуемые так же, как и ваши entity bean, в которых каждый экземпляр entity bean является записью в соответствующей таблице. По умолчанию JBoss использует базу данных HypersonicSQL, которая поставляется в одном пакете с JBoss. Вы можете управлять данными в поставляемой по умолчанию базе данных с помощью Hypersonic Database Manager (более детально смотрите в инструкции по установке). Однако если вы используете JBoss в качестве реального продукта, возможно, вы захотите использовать другую базу данных, и/или предоставить свой собственный механизм отображения entity/таблица (пожалуйста, обратитесь к инструкции для JBoss за более детальной информацией). Также, пожалуйста, обратите внимание, что другие EJB контейнеры требуют, чтобы вы создали схему базы данных заблаговременно, и отобразили ваши entity bean'ы и поля на таблицы и колонки.
RMI прокси: Большинство Контейнеров EJB генерируют класс реализации для RMI прокси при развертывании entity bean с помощью графического инструмента Контейнера. Class-файлы для RMI прокси будут включены в клиентский jar-файл, который вы сделаете доступным для клиентского приложения. JBoss не требует этого шага, так как он синтезирует классы RMI прокси во время выполнения и автоматически посылает class-файлы клиенту по сети. Так что, хотя при использовании JBoss есть RMI прокси, нет необходимости делать какие-либо class-файлы доступными для клиента.
Полная сборка всего
Как вы можете себе представить, процесс сбора всех частей вместе, чтобы закончить правильную развертку компонента, очень утомителен и подвержен ошибкам. Определенно, вы не хотите вручную компилировать все требуемые файлы, создавать ejb-jar и клиентский jar, и производить развертку.
Как я и упоминал, все коммерческие EJB Контейнеры поставляются с некоторыми инструментами для помощи вам в этом процессе. JBoss не сопровождается такими инструментами, но мы все равно можем автоматизировать процесс сборки и развертывания с помощью Ant. Все пример в исходном коде для этой главы поставляются со скриптом для Ant, который вы можете использовать для компиляции, развертывания, создания jar'ов и тому подобное - просто запустите Ant скрипт без аргументов, чтобы получить список возможных целей.
Еще одна вещь, которую делает JBoss, когда вы отдаете ему ejb-jar - это проверка этого файла. То есть, JBoss проверяет содержимое XML файлов и class-файлы в ejb-jar, удостоверяясь, что они соответствуют синтаксическим и семантическим правилам EJB спецификации. Но проверщик JBoss является инструментом, который вы можете также запустить отдельно, и это именно то, что делает наш Ant скрипт в процессе построения и развертывания компоненты javatheater.
И, наконец, отсутствие GUI инструмента для развертывания компонент только помогает, поскольку вы получаете большее управление при программном построении и развертывании компонент.
Клиентское EJB приложение
Код в директории c18/example03/rmiclients содержит пример простейшего удаленного клиентского Java приложения, которое использует наш компонент Movie для создания, просмотра и удаления записей о фильмах в системе на стороне сервера.
Как и для любого другого удаленного RMI приложения, есть четыре вещи, которые вам необходимо сделать, чтобы запустить приложение: 1) убедитесь, что клиентское приложение может найти пакеты, требуемые для EJB API и для вашего Контейнера; 2) поместите клиентский jar, содержащий определения домашнего и компонентного интерфейса в каталог, который виден клиентскому приложению в переменной CLASSPATH; 3) сконфигурируйте параметр JNDI так, чтобы клиентское приложение могло соединиться к службе JNDI, поставляемой EJB Контейнером; и 4) используйте JNDI API для того, чтобы найти EJB дом.
Клиентские пакеты для EJB и JBoss есть в пакете поставки JBoss, а клиентский jar для компонента Movie может быть легко создан с помощью Ant скрипта.
Мы выполним настройку параметров JNDI с помощью внешнего файла jndi.properties, который должен также располагаться в директории, зарегистрированном в CLASSPATH, что я уже объяснял в приведенном выше разделе, посвященном JNDI.
Нашим поставщиком службы JNDI будет JBoss (вернее, одна из его служб), так что нам необходимо предоставить следующую специфическую конфигурационную информацию JNDI в файле jndi.properties:
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.provider.url=localhost:
1099
java.naming.factory.url.pkgs=org.jboss.naming
Пожалуйста, обратите внимание, что если ваше клиентское приложение и ваш JBoss работают на разных машинах, вам необходимо заменить localhost в приведенном выше файле на IP адрес машины, на которой запущен JBoss.
Ниже приведен код для нашего клиентского приложения MovieClient:
//: c18:example03:src:rmiclients:javatheater:client:MovieClient.java
package
javatheater.client;
import
javax.naming.*;
import
javatheater.ejb.*;
public class
MovieClient
{
public static
void
main
(
String
[]
args
)
throws
Exception
{
javax.naming.Context initial =
new
javax.naming.InitialContext
()
;
Object objRef = initial.lookup
(
"javatheater/Movie"
)
;
MovieHome movieHome =
(
MovieHome
)
javax.rmi.PortableRemoteObject
.narrow
(
objRef, MovieHome.
class
)
;
// Генерируем значение первичного ключа
int
pkValue =
(
int
)
System.currentTimeMillis
()
% Integer.MAX_VALUE;
// Создаем новый Movie entity
Movie movie = movieHome.create
(
new
Integer
(
pkValue
)
,
"A Bug's Life"
)
;
// В качестве проверки, находим только что созданный entity
movie = movieHome.findByPrimaryKey
(
new
Integer
(
pkValue
))
;
// Доступ к параметрам компонента
System.out.println
(
movie.getId
())
;
System.out.println
(
movie.getTitle
())
;
// Удаляем компонент
movie.remove
()
;
}
}
// /:~
Если вы помните раздел относящийся к JNDI, первая пара инструкций будет вам понятна: мы создаем InitialContext, который, в соответствии нашим установкам JNDI, будет указывать на корень имен в JBoss; затем, мы используем метод lookup( ) для нахождения интерфейса Movie, используя компонентное имя JNDI, которое мы указали в файле jboss.xml.
Использование PortableRemoteObject должно быть объяснено. Мы пытаемся здесь выполнить типобезопасное удаленное приведение к базовому типу. Вы помните, что метод lookup( ) возвращает ссылку на общий Object; а так как мы знаем, что мы ищем интерфейс MovieHome, нам необходимо привести его к соответствующему типу времени выполнения. Также мы хотим быть информированы, если мы попытаемся привести к неправильному типу. Но, если вы подумаете над этим, то поймете, что проверка должна быть выполнена над объектом на сервере, а не в локальном процессе. Локально у нас есть только RMI прокси, а не реальный объект. Метод narrow( ) реализует такую форму типобезопасного удаленного приведения типа. Он совершает передачу по сети и проверяет, что тип времени выполнения объекта, переданного в качестве первого аргумента, совместим с типом, представленным вторым аргументом. Если типы не совместимы, вы получите исключение. Если удаленное приведение типов закончится успешно, вам просто нужно выполнить приведение типа для локальной ссылки к нужному вам типу, вот какой смысл для такого приведения типа.
Пожалуйста, обратите внимание, что для определенных целей может быть допустимо использование простого приведения типов Java, но использование метода narrow( ) гарантирует, что удаленное приведение будет работать на всех платформах и в любом окружении.
Как только мы получим ссылку на домашний интерфейс Movie, мы можем выполнить создание, поиск и удаление экземпляра Movie. Так как Movie является entity bean'ом, нам необходимо передавать новый первичный ключ при каждом создании нового экземпляра. Для снижения риска использования существующего первичного ключа, что может вызвать ошибку вашего приложения при получении DuplicateException, я использую простое вычисление, основываясь на System.currentTimeMillis( ) для генерации приемлемо уникального значения. Пожалуйста, обратите внимание, что не следует использовать эту технику в коде вашего продукта, так как она не гарантирует уникальности полученного значения (проблема генерации уникальных первичных ключей не является тривиальной и не может быть освещена здесь).
В оставшейся части примера, когда Movie уже создан, мы используем первичный ключ для получения экземпляра из постоянного хранилища. Конечно, метод create( ) возвращает ссылку на новый экземпляр, но я хотел показать использование поискового метода. В конце мы печатаем параметры и удаляем экземпляр из постоянного хранилища.
Чтобы запустить этот пример, вы должны использовать один из скриптов запуска клиента, который вы можете найти в директории исходных кодов. Пожалуйста, просмотрите сначала содержимое скрипта и проверьте все пакеты, которые требует клиентское приложение для запуска.
Как только вы получите работающий пример, у вас может возникнуть желания попытаться закомментировать строку, которая удаляет экземпляр, запустить пример еще раз или два, оставить JBoss запущенным и, используя Hypersonic Database Manager проверить содержимое базы данных JBoss по умолчанию. Просто запустите Database Manager как описано в руководстве по установке в этой главе, затем выберите "HSQL Database Engine Server" в качестве типа, и используйте порт 1476 в URL'е.
В каталоге c18/example04/rmiclients вы найдете различные типы клиентов: JUnit тестовый случай для нашего компонента Movie (он определен в пакете javatheater.test). Тестовый случай для запуска EJB с помощью Java приложения (пускатель JUnit теста или вашего собственного кода), так что все, что я объяснял относительно удаленного клиентского приложения применимо для запуска любых тестовых случаев.
Есть еще важная информация относительно JUnit. Если вы используете один или два графических пускателя, полученных вместе в JUnit, вы должны выключить опцию "перезагрузка классов при каждом запуске (reload classes on every run)" (есть такой элемент выбора в главном окне). Если вы оставите эту опцию включенной, JUnit загрузит ваши классы, используя свой собственный загрузчик классов. К сожалению, этот загрузчик классов не предназначен для работы через сеть, что обязательно необходимо для загрузки динамического RMI прокси, который JBoss посылает клиенту. Если вы не выключите эту опцию, ваш тестовый инструмент JUnit не будет способен присоединится к удаленному enterprise bean и не сработает.
Для запуска тестов с помощью JUnit я рекомендую использовать один из скриптов тестового запуска из директория исходных кодов, а когда вы проверите его содержимое, что увидите, как использовать JUnit с удаленным EJB.
Упрощение разработки EJB
Первый пример EJB был предназначен для того, чтобы показать вам все и каждую линию кода, необходимые для реализации EJB. В процессе, вы также увидели, что вам необходимо, как минимум, четыре исходных файла, содержание которых должно быть синхронизировано. Даже в проектах среднего размера такое расположение тесно связанных файлов может легко стать неуправляемым или, как минимум, нанести ущерб производительности разработчика.
Ясно, что мы нуждаемся в инструменте, снижающем количество работы, требуемое для совмещения всех этих кусков вместе. Такой инструмент существует и может быть получен как "мастер", который интегрирован во многие коммерческие среды разработки EJB.
Тем не менее существует другая возможность, которую мы будем использовать в этой главе. Это XDoclet - инструмент с открытым исходным кодом, который вы можете загрузить с веб-сайта SourceForge.net. XDoclet на самом деле является связкой двух разных инструментов: WebDoclet, о котором я не буду рассказывать здесь, и EJBDoclet.
Оба эти инструмента дают преимущества при использовании Doclet API, это то же самое, на чем базируется JavaDoc. JavaDoc читает ваш исходный код и использует исходники Java, комментарии Java и специальные тэги для генерации HTML документации ваших классов. EJBDoclet читает исходные файлы с определением вашего класса реализации компонента, и, вместо генерации HTML файлов, он генерирует исходные файлы Java с определением интерфейсов компонента, и XML файлы для дескриптора развертки и конфигурационной информации, специфичной для Контейнера. EJBDoclet может также генерировать другие классы поддержки, но я не буду рассказывать об этом здесь.
EJBdoclet достаточно мощный инструмент, который может значительно упростить процесс разработки, не только потому, что он генерирует исходный код для вашей прользы, но также потому, что он может быть привлечен программно с помощью Ant скриптов, демонов сборки или других утилит Java, таким образом, он может стать частью полностью автоматизированного процесса сборки.
Однако это отнимает некоторое время и терпение, чтобы заставить EJBDoclet работать правильно в первый раз, обычно потому, что в этом инструменте очень много настроек. Я не буду объяснять, как работает EJBDoclet, я просто покажу кое-что из того, что он может сделать для вас. Большинство из Ant скриптов этой главы используют EJBDoclet, как часть процесса сборки, и если вы решите использовать EJBDoclet в ваших собственных проектах, вы можете найти его очень сложным поначалу. Но будьте терпеливы и потратьте несколько дней на эксперименты с ним: польза, которую вы получите от EJBDoclet, возвратит их вам с избытком.
Основной механизм EJBDoclet'а очень прост: вы предоставляете класс реализации компонента, насыщенный специальными тегами в ваших JavaDoc комментариях, а EJBDoclet сгенерирует все остальные исходные файлы, которые дополнят ваш EJB в соответствии с вашими инструкциями.
EJBDoclet также сгенерирует реализацию по умолчанию для методов оповещения Контейнера, которые вы должны, в противном случае, кодировать руками в вашем классе реализации компонента. Однако, так как EJBDoclet не может и не должен изменять ваш исходный код, дополнительный код реализации, сгенерированный EJBDoclet'ом, будет унаследован от предоставленного вами класса. Класс реализации, сгенерированный EJBDoclet'ом, который унаследует весь ваш код, станет реальным классом, который будет использоваться контейнером.
Ваш класс реализации должен быть объявлен абстрактным, и если вы предоставите реализацию любого метода уведомления Контейнера, сгенерированный EJBDoclet'ом класс будет передавать вызов в вашу реализацию. Пожалуйста, обратите внимание, что когда вы используете EJBDoclet, ваши классы реализации будут абстрактными независимо от типа компонента, реализуемого вами, в то время, как в нашем первом примере класс MovieBean был абстрактным потому, что для CMP entity bean в EJB 2.x определена абстрактная модель программирования.
Ниже приведена новая версия реализации класса компонента Movie, для которой будет использоваться EJBDoclet. Этот код расположен в директории c18/example05/src/ejb-tier. Пожалуйста, обратите внимание, что нет исходного кода для интерфейсов компонента и для дескриптора развертки в этом примере, так как они будут сгенерированы EJBDoclet'ом.
//: c18:example05:src:ejb-tier:javatheater:ejb:implementation:MovieBean.java
package
javatheater.ejb.implementation;
import
javax.ejb.*;
import
java.util.Collection;
/**
* Это реализация
<code>
Movie
</code>
entity bean.
*
@see
javatheater.ejb.MovieHome
* @ejb:bean
* name="Movie"
* jndi-name="javatheater/Movie"
* type="CMP"
* primkey-field="id"
* reentrant="False"
* @ejb:pk class="java.lang.Integer"
* @ejb:home remote-class="javatheater.ejb.MovieHome"
* @ejb:interface remote-class="javatheater.ejb.Movie"
* @ejb:finder signature=
* "java.util.Collection findByTitle(java.lang.String title)"
*/
public abstract class
MovieBean
implements
EntityBean
{
/**
* Проверяет первичный ключ и инициализирует
* поля компонента.
* @ejb:create-method
*/
public
Integer ejbCreate
(
Integer id, String title
)
throws
CreateException
{
if
(
id ==
null
)
throw new
CreateException
(
"Primary key cannot be null"
)
;
if
(
id.intValue
()
==
0
)
throw new
CreateException
(
"Primary key cannot be zero"
)
;
setId
(
id
)
;
setTitle
(
title
)
;
return null
;
}
public
void
ejbPostCreate
(
Integer id, String title
) {
}
/**
* Возвращает идентификатор Movie, который также является первичным ключом
* Movie.
* @ejb:pk-field
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public
Integer getId
()
;
/**
* Этот метод не является частью удаленного интерфейса.
*/
abstract public
void
setId
(
Integer id
)
;
/**
* Возвращает заголовок Фильма.
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public
String getTitle
()
;
/**
* Устанавливает заголовок Фильма.
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public
void
setTitle
(
String title
)
;
}
// /:~
Посмотрите на приведенный выше код, вы должны заметить, что есть три значительных преимущества в использовании EJBDoclet'а:
- Приведенный выше код является только исходным файлом, который вам нужен для реализации вашего компонента. Здесь нет файлов определения интерфейса, нет дескриптора развертки, нет XML файлов, специфичных для контейнера.
- Приведенный выше класс содержит только код, который имеет значение для вас - здесь нет "заплаток", типа методов уведомления Контейнера.
- Вы не определяете метод findByPrimaryKey(…). EJBDoclet всегда генерирует этот метод за вас, так как он требуется спецификацией EJB, но другие поисковые методы вы должны определить явно (более подробно об этом я расскажу позднее).
В этом Java коде нет ничего нового. Наиболее интересная часть содержится в комментариях, которые насыщают ваш код мета-информацией, которую использует EJBDoclet для генерации кода.
Все тэги, начинающиеся с ejb, являются комментариями EJBDoclet'а. Например, тэг ejb:bean вы используете, чтобы предоставить информацию о всем компоненте целиком, это такая информация, как тип, EJB имя, JNDI имя, поле первичного ключа и многое другое. Другие теги, такие как ejb:interface-metod, вы используете для предоставления информации, которая применяется к определенной части компонента. В данном случае это метод, который вы хотите показать в компонентном интерфейсе компонента. Более полное описание того, что означает каждый тэг можно найти в руководстве по EJBDoclet'у.
Однако, сами по себе теги не предоставляют всей информации, требующейся для генерации кода исходных файлов. Например, тэги не указывают в каком директории должны генерироваться файлы, или как они должны быть вызваны. Эта дополнительная информация передается в EJBDoclet в то время, когда он запускает задачу Ant. Ниже приведен отрывок из скрипта Ant из директории этого примера:
<ejbdoclet sourcepath="${ejb.source.dir};${gen.source.dir}" destdir="${gen.source.dir}" excludedtags="@version, @author" ejbspec="2.0"> <fileset dir="${ejb.source.dir}"> <include name="**/*Bean.java" /> </fileset> <classpath> <path refid="server.classpath" /> <path refid="xdoclet.classpath" /> </classpath> <deploymentdescriptor destdir="${gen.source.dir}/META-INF" /> <homeinterface /> <localhomeinterface /> <remoteinterface /> <localinterface /> <entitycmp /> <session /> <jboss destdir="${gen.source.dir}/META-INF" /> </ejbdoclet>
Есть два интересных свойства EJBDoclet, которые показаны в этой задаче Ant: первое из них состоит в том, что вы можете решить, какой версии спецификации EJB будут удовлетворять сгенерированные файлы. Другими словами, значение параметра ejbspec может быть либо "1.1", либо "2.0", а EJBDoclet сгенерирует XML дескрипторы и соответствующие Java файлы. Второе свойство состоит в том, что вы можете указать какие элементы вы хотите сгенерировать и как. В приведенной выше задаче элементы, начиная от <deploymentdescriptor> до <jboss> определяют, какие исходные файлы назначено генерировать EJBDoclet'у, и где они должны быть (если вы не хотите использовать расположение по умолчанию). Определенно, мы инструктируем EJBDoclet сгенерировать за нас: дескриптор развертки, домашний и компонентный интерфейсы для локального и удаленного представления компонента (если это также запрашивается ejb: тэгами в исходных файлах), класс реализации entity bean, класс реализации session bean и все XML файлы, необходимые для JBoss (пожалуйста, обратите внимание, что EJBDoclet также поддерживает генерацию специфичных для Контейнера файлов для некоторых различных EJB Контейнеров).
Теперь вы имеете представление о том, что EJBDoclet может делать за нас, и мы будем использовать его во всех наших следующих примерах в этой главе. Прежде чем продолжить, я хотел бы, чтобы вы собрали example05 и проверили содержимое директория sub, куда будут переданы все сгенерированные исходные файлы, чтобы лучше понимать процесс сборки и работу EJBDoclet'а.
Реализация session bean
Мы используем session bean для реализации бизнес-логики как дискретный, развертываемый элемент. Например, session bean может реализовать логику, требуемую для проверки истинности кредитной карты и, если вы разрабатываете приложение для электронной коммерции, вы можете просто включить этот компонент в ваше приложение, а не реализовывать проверку подлинности кредитной карты с чистого листа.
Session bean также часто используется для реализации распределенных façades (фасадов) (хотя не только этот шаблон удовлетворяет модели session bean). Façades является EJB, который доступен удаленному приложению и который прячет от клиента многие или все другие EJB, используемые приложением, как показано на приведенной ниже диаграмме.
Преимущества façade'а заключаются в упрощении клиентского представления удаленного приложения, так как он прячет большую часть внутренних сложностей. Он также может быть применен для снижения сетевого трафика, если он обеспечивает доступ большей части клиентских методов, которые передают массу структурированной информации в одном едином сетевом вызове, вместо того, чтобы заставить клиента взаимодействовать с группой различных объектов.
В приведенном ниже примере мы будем реализовывать session bean без состояний, который мы назовем ShowManager, и который будет предоставлять методы для редактирования программы показа - пока добавление и удаление фильмов. ShowManager также будет взаимодействовать с façade'ом, чтобы спрятать генерацию первичного ключа от клиента (помните, при каждом создании нового фильма мы должны предоставить уникальное значение первичного ключа).
Генерация уникального первичного ключа это сложная задача, которую я не буду полностью освещать здесь, но она исчерпывающе раскрыта во многих источниках, которые перечислены в конце этой главы. Для следующих примеров я хочу представить более надежное решение, чем то, что вы видели в предыдущем клиентском коде. Мы создадим новый CMP entity bean, называемый AutoCounter, который мы используем для генерации уникального значения первичного ключа. Принцип его работы очень прост: экземпляр AutoCounter'а содержит только одно поле типа int и имеет метод getNext( ), который увеличивает текущее значение поля и возвращает результат. Так как AutoCounter является entity bean, значение счетчика автоматически постоянно хранится в базе данных. Мы будем использовать тип String в качестве первичного ключа компонента, который также будет уникальным именем счетчика.
Ниже приведена реализация CMP bean'а AutoCounter, как это определено в каталоге c18/example06/src/ejb-tier:
//: c18:example06:src:ejb-tier:javatheater:ejb:implementation:AutoCounterBean.java
package
javatheater.ejb.implementation;
import
javax.ejb.*;
/**
* @ejb:bean name="AutoCounter" jndi-name="javatheater/AutoCounter" type="CMP"
* primkey-field="name" reentrant="False"
* @ejb:pk class="java.lang.String"
* @ejb:home remote-class="javatheater.ejb.AutoCounterHome"
* @ejb:interface remote-class="javatheater.ejb.AutoCounter"
* @ejb:finder signature="java.util.Collection findAll()"
*/
public abstract class
AutoCounterBean
implements
EntityBean
{
/**
* Проверяет первичный ключ и инициализирует поля bean.
*
* @ejb:create-method
*/
public
String ejbCreate
(
String name
)
throws
CreateException
{
if
(
name ==
null
)
throw new
CreateException
(
"Counter name can not be null"
)
;
if
(
name.length
()
==
0
)
throw new
CreateException
(
"Counter name can not be an empty string"
)
;
setName
(
name
)
;
setValue
(
0
)
;
return null
;
}
public
void
ejbPostCreate
(
String name
) {
}
/**
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public
String getName
()
;
/**
* @ejb:persistent-field
*/
abstract public
void
setName
(
String name
)
;
/**
* @ejb:persistent-field
*/
abstract public
int
getValue
()
;
/**
* @ejb:persistent-field
*/
abstract public
void
setValue
(
int
value
)
;
/**
* Возвращает следующее значение последовательности.
*
* @ejb:interface-method
*/
public
int
getNext
() {
int
value = getValue
()
;
value +=
1
;
setValue
(
value
)
;
return
value;
}
}
// /:~
Каждый entity bean в приложении JavaTheater будет использовать различные экземпляры AutoCounter, так что каждый из них будет использовать свой собственный набор значений первичного ключа. В настоящее время у нас есть только Movie entity bean, но позже мы реализуем Show entity bean. Так как каждый счетчик идентифицируется строкой (своим первичным ключом), а различные entity bean'ы будут использовать различные счетчики, мы должны предоставить для счетчиков Фильмов (Movie) и Показов (Show) свои собственные имена.
Я использую эту простую проблему для того, чтобы ввести еще одну особенность EJB 2.0: домашние методы EJB. Домашний метод реализуется в домашнем интерфейсе компонента, и поэтому он не вызывается ни в одном определенном экземпляре - нечто сродни статическим методам Java, но для распределенной среды. Домашние методы могут реализовывать произвольный код, не только код для создания, поиска и удаления EJB экземпляра, как вы это видели для фабрики.
В следующем примере, в директории c18/example07/src/ejb-tier, я добавил домашний метод getCounterName( ) в Movie entity bean:
/**
* Это реализация
<code>
Movie
</code>
entity bean.
*
*
@see
javatheater.ejb.MovieHome
* @ejb:bean name="Movie" jndi-name="javatheater/Movie" type="CMP"
* primkey-field="id" reentrant="False"
* @ejb:pk class="java.lang.Integer"
* @ejb:home remote-class="javatheater.ejb.MovieHome"
* @ejb:interface remote-class="javatheater.ejb.Movie"
* @ejb:env-entry name="CounterName" value="MoviePKCounter"
* type="java.lang.String"
*/
public abstract class
MovieBean
implements
EntityBean
{
// … Пропущенные методы
/**
* Домашний метод, возвращающий имя счетчика (тип String) для генерации
* уникального значения первичного ключа фильма. Реальное имя определяется,
* как переменная окружения компонента Movie.
*
* @ejb:home-method
*/
public
String ejbHomeGetCounterName
() {
try
{
Context initial =
new
InitialContext
()
;
return
(
String
)
initial.lookup
(
"java:comp/env/CounterName"
)
;
}
catch
(
NamingException e
) {
throw new
EJBException
(
e
)
;
}
}
// Пропущенные методы…
}
Метод носит название ejbHomeGetCounterName(). Префикс ejbHome требуется по правилу именования мета-интерфейсов EJB. Однако, в домашнем интерфейсе этот метод будет показан, как getCounterName( ). Реализация метода использует кое-что такое, чего вы до сих пор не видели: переменную окружения EJB. Концептуально, переменная окружения EJB не отличается от переменной окружения, которую вы определяете в оболочке операционной системы, исключение состоит в том, что она зависит от специфики EJB. Переменная окружения состоит просто из пары имя/значение, с двумя главными отличиями: 1) Значение может быть любым объектом, и 2) переменная определяется в специальном JNDI контексте, который называется контекст именования окружения (ENC), который всегда отображается во всех Контейнерах на java:comp/env. Значение определяется в дескрипторе развертывания и может быть получено с помощью поиска JNDI. Используя EJBDoclet, вы можете определить переменную окружения, используя тэг ejb:env-entry, как показано выше, а реальная переменная будет сгенерирована в дескрипторе развертывания. Преимущество от использования переменных окружения состоит в том, что их значения могут быть изменены Deployer'ом, что придаст большую гибкость в конфигурировании компонента.
Реализация session bean ShowManager (смотри ниже) должна сделать вещи понятнее:
//: c18:example07:src:ejb-tier:javatheater:ejb:implementation:ShowManager.java
package
javatheater.ejb.implementation;
import
javatheater.ejb.*;
import
javax.ejb.*;
import
javax.naming.*;
import
java.rmi.RemoteException;
/**
* @ejb:bean type = "Stateless" name = "ShowManager" jndi-name =
* "javatheater/ShowManager" reentrant = "false"
* @ejb:home remote-class="javatheater.ejb.ShowManagerHome"
* @ejb:interface remote-class="javatheater.ejb.ShowManager"
*/
public abstract class
ShowManagerBean
implements
SessionBean
{
AutoCounterHome autoCounterHome =
null
;
MovieHome movieHome =
null
;
String movieCounterName =
null
;
/**
* @ejb:create-method
*/
public
void
ejbCreate
()
throws
CreateException
{
try
{
Context initial =
new
InitialContext
()
;
// Получаем домашний интерфейс EJB, который мы будем использовать
// позже.
autoCounterHome =
(
AutoCounterHome
)
initial
.lookup
(
"javatheater/AutoCounter"
)
;
movieHome =
(
MovieHome
)
initial.lookup
(
"javatheater/Movie"
)
;
movieCounterName = movieHome.getCounterName
()
;
}
catch
(
NamingException e
) {
throw new
EJBException
(
e
)
;
}
catch
(
RemoteException e
) {
throw new
EJBException
(
"Error accessing the Movie home interface"
,
e
)
;
}
}
/**
* Создаем новый фильм и возвращаем новое значение первичного ключа.
*
* @ejb:interface-method
*/
public
Integer createMovie
(
String movieName
)
throws
TheaterException
{
try
{
Integer pk =
new
Integer
(
getCounter
(
movieCounterName
)
.getNext
())
;
movieHome.create
(
pk, movieName
)
;
return
pk;
}
catch
(
CreateException e
) {
throw new
TheaterException
(
e
)
;
}
catch
(
RemoteException e
) {
throw new
TheaterException
(
e
)
;
}
}
/**
* Удаляем фильм, передавая его первичный ключ.
*
* @ejb:interface-method
*/
public
void
deleteMovie
(
Integer moviePk
)
throws
TheaterException
{
try
{
Movie movie = movieHome.findByPrimaryKey
(
moviePk
)
;
movie.remove
()
;
}
catch
(
FinderException e
) {
throw new
TheaterException
(
e
)
;
}
catch
(
RemoveException e
) {
throw new
TheaterException
(
e
)
;
}
catch
(
RemoteException e
) {
throw new
TheaterException
(
e
)
;
}
}
/**
* Вспомогательный метод, чтобы получить генератор первичного ключа для
* компонента Movie.
*/
AutoCounter getCounter
(
String counterName
)
throws
RemoteException,
CreateException
{
AutoCounter counter =
null
;
try
{
counter = autoCounterHome.findByPrimaryKey
(
counterName
)
;
}
catch
(
FinderException e
) {
}
if
(
counter ==
null
)
counter = autoCounterHome.create
(
counterName
)
;
return
counter;
}
}
// /:~
Session bean без состояний ShowManager начинается с определения метода создания без аргументов (спецификация запрещает аргументы для метода создания session bean без состояний), в котором он находит с помощью JNDI домашний интерфейс компонента, который будет использоваться в других методах. Он также вызывает домашний метод getCounterName( ) для домашнего интерфейса Movie, чтобы получить имя счетчика, используемого для генерации уникального первичного ключа для фильмов.
Компонент также предоставляет метод createMovie( ), который может использовать клиентское приложение для создания нового объекта Movie в системе. Вы можете видеть, как этот метод прячет от клиента сложность генерации первичного ключа: в качестве информации, которую клиент должен предоставить, выступает только название фильма. Метод createMovie( ) использует AutoCounter, чтобы получить новый уникальный первичный ключ для Фильма, создает объект Movie и возвращает первичный ключ клиенту.
Если создание фильма закончится неудачей, клиенту будет послано TheaterException. Это исключение бизнес-уровня, которое мы определили, чтобы указать приложению на проблему. Его реализация достаточно проста, как вы можете видеть ниже. Пожалуйста, обратите внимание, что этот код просто демонстрирует вам, как легко определить и использовать ваше собственное исключение в спецификации методов enterprise компонент. В более реалистичной ситуации вы можете захотеть выполнить некое корректирующее действие или предоставить больше информации клиенту, используя различные исключения.
//: c18:example07:src:ejb-tier:javatheater:ejb:TheaterException.java
package
javatheater.ejb;
public class
TheaterException
extends
Exception
{
public
TheaterException
() {
}
public
TheaterException
(
String message
) {
super
(
message
)
;
}
public
TheaterException
(
String message, Throwable cause
) {
super
(
message, cause
)
;
}
public
TheaterException
(
Throwable cause
) {
super
(
cause
)
;
}
}
// /:~
Session bean ShowManager также определяет метод deleteMovie( ), который клиенты используют для удаления фильма из программы. Его реализация проста, но польза заключена в том, что компонент теперь работает как façade, пряча сложность и предоставляя клиенту только те методы и классы, которые специфичны для бизнес области JavaTheater.
Дескриптор развертывания session bean ShowManager генерируется XDoclet'ом. Ниже вы можете увидеть часть, описывающую ShowManager session bean:
<session> <description><![CDATA[No Description.]]></description> <ejb-name>ShowManager</ejb-name> <home>javatheater.ejb.ShowManagerHome</home> <remote>javatheater.ejb.ShowManager</remote> <ejb-class> javatheater.ejb.implementation.ShowManagerSession </ejb-class> <session-type>Stateless</session-type> <transaction-type>Container</transaction-type> </session>
Если вы сравните это описание с определением entity bean, вы увидите, что они очень похожи, и что определение session bean проще. Есть только два новых элемента - это <session-type>, который в данном случае устанавливает, что компонент ShowManager является session bean'ом без состояний, и <transaction-type>, который мы используем для указания того, что мы хотим, чтобы Контейнер позаботился о разграничении транзакций. Альтернативное значение, указание значения <Bean>, означает, что мы программно управляем жизненным циклом транзакции в коде реализации ShowManager (в этой главе мы не будем рассматривать программное разграничение транзакций).
Все классы, интерфейсы и XML дескрипторы для компонент будут собираться группой в наш ejb-jar (javatheater.jar) с помощью Ant скрипта, а все классы, необходимые клиентскому приложению, будут собираться в клиентский jar. Я подразумеваю, что вы выполните полную сборку этого примера с помощью Ant скрипта и проверите содержимое директория gen и двух сгенерированных jar файлов в директории dist.
Локальные интерфейсы EJB
Таким образом, мы реализовали entity bean и session bean, который предоставляет клиенту удаленное представление. Это означает, что все вызовы методов этих компонентов будут происходить посредством RMI, через прокси и сериализацию аргументов метода, возвращаемых значений и всех исключений. Этот процесс очень затратный с точки зрения производительности (вызов RMI метода может быть на несколько порядков медленнее, чем обычный вызов метода внутри той же самой JVM), но вы получаете выгоду в виде полной прозрачности вашего компонента - клиентский код не знает в каком процессе расположен экземпляр компонента и не беспокоится об этом.
Важно запомнить, что когда клиентский код получает доступ к enterprise компоненту посредством удаленного представления компонента, вызовы всегда совершаются через RMI, даже если клиентский код и экземпляр компонента расположены в одной и той же JVM (на самом деле, контейнер обычно оптимизирует входящие в JVM RMI вызовы, используя некий специальный прокси, но какой-то прокси все равно существует и вызов все равно происходит медленнее, чем это могло быть, если бы клиентский код был способен получить прямой доступ к компоненту).
EJB 1.1 enterprise bean может предоставлять только удаленное представление. Однако, в попытках улучшить производительность, производители Контейнеров начали вводить в свои продукты некоторые "магические" флаги, которые могли бы использовать администраторы для отключения RMI для enterprise bean. Смысл этого, как я думаю, заключается в том, что клиентский код располагается в той же самой JVM, что и компоненты.
В конце концов, спецификация EJB 2.0 учла эту подсказку, поданную индустрией, и ввела концепцию локального представления enterprise bean. Когда enterprise bean предоставляет локальное представление, то RMI при этом не участвует вообще, а домашний и компонентный интерфейс компонента могут быть доступны из клиентского кода в той же самой JVM. Это более эффективно, но предотвращает использование компонентов в сильно распределенном приложении.
Компонент может поддерживать и локальное, и удаленное представления, в таком случае разработчик должен реализовывать домашний и компонентный интерфейсы в их локальной и удаленной версии. Так как локальный и удаленный интерфейсы объявляются разными тегами в дескрипторе развертывания, они легко могут быть идентифицированы Контейнером и пользователем компонента. Только session и entity bean могут иметь удаленное представление, в то время как message-driven bean'ы могут располагать только локальным представлением (другими словами, они никогда не могут быть доступны удаленно).
Хотя идея локального и удаленного представления сначала звучит хорошо, в обществе происходили некоторые дебаты относительно чистоты такого дизайна. Наибольшие нарекания вызывала неоднозначность при передаче аргументов. Когда интерфейс является удаленным (доступ через RMI) все аргументы при вызове метода передаются по значению: значения аргумента копируются в пространство процесса удаленного экземпляра, в то время как оригиналы остаются на клиентской стороне. С другой стороны, когда интерфейс локальный, все аргументы передаются по ссылке (за исключением примитивных типов данных и ссылок), а затем реализация метода оперирует с оригиналами. Проблема локального и удаленного представления EJB состоит в том, что нет ничего в интерфейсе Java, что могло бы проинформировать программиста, какой вид передачи аргументов используется. Ключом может быть только то, что интерфейсы были получены посредством JNDI, но как только вы получили ссылку на интерфейс, нет способа узнать происходит ли передача по значению или по ссылке. Как вы можете представить, в больших приложениях, в который разные части системы разрабатываются различными людьми, а ссылки на интерфейсы передаются по кругу постоянно, нет никакой информации времени компиляции или времени выполнения относительно модели передачи аргументов, что может привести к ошибкам.
Так как язык программирования и спецификация не помогают предотвратить эту проблему, вы должны прийти к своей собственной стратегии защиты. Это может быть, конечно, соглашение об именовании ваших интерфейсов компонент, например MovieLocal против MovieRemote; другой подход может заключаться в решении, что entity bean предназначен для предоставления только локального представления (что соответствует шаблону дизайна Façade).
Теперь пришло время для реализации нашего первого локального представления EJB. Вместо создания новой марки EJB, мы изменим реализацию Movie и AutoCounter entity bean'ов так, чтобы они предоставляли только локальное представление и не имели удаленного представления. Вот код для компонента Movie:
//: example08/src/ejb-tier/javatheater/ejb/implementation/MovieBean.java
package
javatheater.ejb.implementation;
import
javax.ejb.*;
import
javax.naming.Context;
import
javax.naming.InitialContext;
import
javax.naming.NamingException;
/**
* Это реализация
<code>
Movie
</code>
entity компонента.
*
*
@see
javatheater.ejb.MovieHome
* @ejb:bean name="Movie" local-jndi-name="javatheater/Movie" view-type="local"
* type="CMP" primkey-field="id" reentrant="False"
* @ejb:home local-class="javatheater.ejb.MovieHome"
* @ejb:interface local-class="javatheater.ejb.Movie"
* @ejb:pk class="java.lang.Integer"
* @ejb:env-entry name="CounterName" value="MoviePKCounter"
* type="java.lang.String"
*/
public abstract class
MovieBean
implements
EntityBean
{
/**
* @ejb:create-method
*/
public
Integer ejbCreate
(
Integer id, String title
)
throws
CreateException
{
if
(
id ==
null
)
throw new
CreateException
(
"Primary key cannot be null"
)
;
if
(
id.intValue
()
==
0
)
throw new
CreateException
(
"Primary key cannot be zero"
)
;
setId
(
id
)
;
setTitle
(
title
)
;
return null
;
}
/**
* Не делаем ничего
*/
public
void
ejbPostCreate
(
Integer id, String title
) {
}
/**
* @ejb:home-method
*/
public
String ejbHomeGetCounterName
() {
try
{
Context initial =
new
InitialContext
()
;
return
(
String
)
initial.lookup
(
"java:comp/env/CounterName"
)
;
}
catch
(
NamingException e
) {
throw new
EJBException
(
e
)
;
}
}
/**
* @ejb:pk-field
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public
Integer getId
()
;
/**
* Этот метод не будет частью удаленного интерфейса.
*/
abstract public
void
setId
(
Integer id
)
;
/**
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public
String getTitle
()
;
/**
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public
void
setTitle
(
String title
)
;
}
// /:~
В приведенном выше коде видны только незначительные отличия от предыдущей версии с удаленным представлением, но смысл отличается значительно. Давайте разберем этот код: первое, что вы должны заметить - это новые параметры для EJBDoclet'а в параметрах ejb:bean, ejb:home и ejb:interfase. В тэге ejb:bean мы использовали параметр local-jndi-name для указания JNDI имени локального домашнего интерфейса компонента Movie, а так же мы использовали view-type="local" для указания, что этот компонент, фактически, предоставляет только локальное представление. В тэгах ejb:home и ejb:interface мы использовали новый параметр local-class для указания имени домашнего и компонентного интерфейсов, который EJBDoclet сгенерирует за нас.
В самом Java коде нет отличий, но это происходит потому, что EJBDoclet берет на себя заботу о генерации кода, который подходит для локального представления. Я предполагаю, что прежде чем продолжить читать, вы запустите Ant скрипт для сборки компонента, и если сборка закончится успешно, вы взглянете на сгенерированный EJBDoclet'ом код в директории example08/gen. Начните с рассмотрения MovieHome.java, исходного файла для домашнего интерфейса Movie: вы должны заметить, что интерфейс больше не наследуется от javax.ejb.EJBHome, который является RMI интерфейсом, а от javax.ejb.EJBLocalHome, который не является RMI интерфейсом. Так что ваш домашний интерфейс больше не доступен удаленным клиентам и, соответственно, его методы больше не выбрасывают RemoteException. То же самое верно и для локального компонентного интерфейса Movie, определенного в Movie.java и расположенного в том же директории; отличия состоят в том, что интерфейс более не наследуется от javax.ejb.EJBObject, а вместо этого от javax.ejb.EJBLocalObject.
Теперь, когда удаленные клиенты не могут получить доступ к экземпляру компонента Movie, необходимо внести изменения в клиентский jar и в клиентское приложение. Ant скрипт был изменен и не поместил домашний и компонентный интерфейсы для Movie и AutoCounter в клиентский jar, так как они теперь бесполезны для клиентского приложения и могут, очевидно, ввести в заблуждение клиентского программиста. Клиентское приложение и тестовый случай будут также изменены, и каждая ссылка на Movie и AutoCounter будут удалены. Вы можете пожелать взглянуть на код в директории rmiclients этого примера, чтобы осведомится о деталях.
Однако, в данном случае важно не то, как поменялся код, а последствия изменений. Их можно разбить на две главные категории: производительность и дополнительное управление контейнера. Говоря о производительности, должно быть понятно, что теперь вызов методов локального интерфейса будет происходить гораздо быстрее, чем при вызове того же метода в удаленном интерфейсе, поскольку в этом случае не задействован RMI. Плюс к этому, так как компонент предоставляет только локальное представление, которое может быть доступно только для других локальных компонентов в этом же процессе, удаленные клиенты должны получать не прямой доступ к локальным компонентам, возможно, посредством использования удаленного session bean, работающего по принципу façade, который в последствии взаимодействует с локальными entity bean'ами. Session bean реализует бизнес логику и принимает вызовы методов, представляющие "макро-функции" вашего приложения; при реализации своей логики session bean может взаимодействовать с несколькими локальными компонентами. В этом случае, количество вызовов методов, которые реально происходят по сети, сильно сокращается.
Другой важный смысл использования локальных интерфейсов для компонента состоит в том, что контейнер может безопасно предполагать, что никакой другой процесс не способен воздействовать на этот экземпляр компонента, и поэтому, может делать с ним больше, чем он мог бы сделать с удаленным экземпляром. Наиболее примечательным в этой особенности EJB, применимой только для локального представления, является взаимосвязь между компонентами, одно из главных улучшений в EJB 2.0. Мы поговорим о взаимосвязях в следующем разделе, но сначала нам необходимо поговорить об языке запросов (EJB Query Language) EJB (EJB-QL), а перед этим вспомнить о группе аксессоров и объектах значения.
← | Enterprise JavaBeans | Группа аксессоров и объекты значения | → |