EJB и RMI на примерах

В этой статье будут рассмотрены практические методы и примеры использования технологий RMI и EJB. Помимо этого, много внимания будет уделено проблемам совместного использования EJB и RMI и проблеме оптимизации транзакций. Все приемы работы будут снабжены простыми примерами и диаграммами, наглядно иллюстрирующими принципы работы отдельных частей этих технологий. Автор предполагает, что вы уже знакомы с языком программирования Java и имеете некоторый опыт работы с ним. Какое-либо представление о технологиях EJB и RMI для освоения изложенного материала не обязательно.

Предыстория RMI


Технология Remote Method Invocation (RMI) впервые была внедрена еще в JDK версии 1.1. Именно эта технология подняла сетевое программирование на Java на более высокую ступень. Несмотря на то, что технология RMI достаточно проста в использовании — это удивительно мощная технология, которая позволяет рядовому Java-разработчику войти в полностью новую область деятельности, в мир распределенных объектных вычислений. Основной целью дизайнеров этой технологии было позволить программистам при разработке распределенных Java-приложений использовать такой же синтаксис и семантику, которые используются для нераспределенных приложений. Для того чтобы это осуществить, им пришлось тщательно исследовать, каким образом Java-классы и Java-объекты функционируют на отдельной JVM (Java Virtual Machine), и в соответствии с этим придумать новую модель работы классов и объектов, которая бы уже применялась в среде распределенных объектных вычислений (т.е. на нескольких JVM). В следующем разделе этой статьи мы рассмотрим архитектуру RMI. Она определяет, как объекты себя ведут, в каком месте и из-за чего могут возникнуть исключения (exceptions), каким образом происходит управление памятью, а также как параметры передаются в удаленные методы и как они оттуда возвращаются.

Архитектура RMI

Архитектура технологии RMI основывается на одном важном правиле: определение поведения и реализация этого поведения должны быть физически разделены. Это значит, что RMI позволяет хранить отдельно описание поведения и код его реализации на нескольких отдельно работающих JVM. Такая концепция идеально подходит для нужд распределенных систем, где клиенту нужно знать лишь определение используемого сервиса, а сервер непосредственно обеспечивает сам сервис (его реализацию). Изначально в качестве определения удаленного сервиса в RMI используют интерфейсы Java (interfaces). Соответственно реализация сервиса кодируется в классах. Таким образом, ключом к пониманию RMI является понимание того, что интерфейсы определяют поведение, а классы — реализацию. Ниже представлена диаграмма, которая наглядно демонстрирует эту концепцию разделения.

Важно помнить, что интерфейсы не содержат никакого исполняемого кода. RMI поддерживает два класса, которые реализуют эти интерфейсы. Первый из этих двух классов — непосредственно реализация поведения, которая работает на стороне сервера. Второй класс играет роль прокси (proxy) для удаленного сервиса и работает на стороне клиента. Когда клиентская программа делает вызов метода прокси-объекта, RMI посылает запрос удаленной JVM, которая пересылает этот запрос коду реализации. Любые возвращаемые значения вызываемого метода отсылаются обратно прокси-объекту, а потом уже программе клиента. Реализация RMI по существу состоит из трех абстрактных слоев.

Первый — слой Stub и Skeleton классов, реализация которых скрыта от глаз разработчика. Этот слой занимается перехватом вызовов методов заданного интерфейса, которые делает клиент, и переадресацией их удаленному RMI-сервису. Следующий слой — Remote Reference Layer. Этот слой "знает", как нужно интерпретировать и управлять ссылками от клиента к объектам удаленного сервиса. И последний слой — транспортный. Этот слой работает с TCP/IP-соединениями между машинами, находящимися в сети. Он предоставляет базовые возможности установки соединений, а также некоторые стратегии обхода firewall-систем (брандмауэров). Благодаря использованию такой архитектуры мы имеем возможность заменять любой из этих трех слоев новым слоем, с иной логикой поведения. Например, транспортный слой можно заменить слоем с поддержкой протоколов UDP/IP. Причем при этом ни один из верхних слоев затронут не будет.

Остается открытым еще один важный вопрос: как клиент будет определять местонахождение RMI-сервиса (сервера)? Для этого обычно используют специальные сервисы имен и директорий (например, JNDI). Мы можем воспользоваться любым из них включая и Java Naming and Directory Interface (JNDI). Но RMI включает в себя свой собственный довольно простой подобный сервис, который называется RMI Registry (rmiregistry). RMI Registry запускается на каждой машине, которая используется как хранилище объектов удаленного сервиса (т.е. на сервере) и принимает запросы на порт по умолчанию 1099. На стороне сервера программа, чтобы создать удаленный сервис, сначала создает локальный объект, который является реализацией этого сервиса, после чего экспортирует этот объект в RMI. После того, как объект экспортирован, RMI создает сервис, который ждет соединений с клиентами и запросов от них. После экспортирования сервер регистрирует этот объект в RMI Registry под определенным общедоступным именем. На стороне клиента программа обращается к RMI Registry с помощью static-класса Naming. Программа использует метод lookup() этого класса, чтобы получить строку URL, которая определяет имя сервера и имя желаемого сервиса. Этот метод возвращает удаленную ссылку на объект сервиса. URL обычно имеет следующий вид:

rmi://<HOST_NAME>[:<PORT>]/<SERVICE_NAME>

Здесь HOST_NAME — адрес сервера, PORT — необязательный параметр, указывающий номер порта (по умолчанию 1099), а SERVICE_NAME — имя желаемого сервиса.

Пример использования RMI

В качестве простого примера распределенного приложения очень часто используют простой арифметический калькулятор. Это стало чуть ли не традицией, поэтому не будем ее нарушать. Наша цель — проводить все фактические вычисления на стороне сервера, а результат получать на стороне клиента. Все это будет представлять собой RMI-систему. Рабочая RMI-система состоит из следующих частей:
. Определение интерфейсов к удаленным сервисам.
. Реализация удаленных сервисов.
. Файлы Stub и Skeleton.
. Сервер, содержащий удаленные сервисы.
. Сервис RMI Naming, позволяющий клиентам находить удаленные сервисы.
. Поставщик файлов классов (HTTP- или FTP-сервер).
. Клиентская программа, которая нуждается в удаленных сервисах.
Итак, начнем с определения интерфейсов. В первую очередь опишем интерфейс нашего калькулятора, в котором определим все возможности, предлагаемые удаленным сервисом:

public interface Calculator
extends java.rmi.Remote {
public long add(long a, long b)
throws java.rmi.RemoteException;

public long sub(long a, long b)
throws java.rmi.RemoteException;

public long mul(long a, long b)
throws java.rmi.RemoteException;

public long div(long a, long b)
throws java.rmi.RemoteException;
}

Заметьте, что каждый из перечисленных методов может выбрасывать исключение класса RemoteException или производных. Каждый из этих методов выполняет соответствующую арифметическую операцию (сложение, вычитание, умножение и деление) над двумя числами a и b. Сохраняем этот файл под именем Calculator.java и компилируем с помощью команды "javac Calculator.java". Далее реализуем этот интерфейс для нашего сервера. Класс реализации назовем CalculatorImpl.java:

public class CalculatorImpl
extends java.rmi.server.UnicastRemoteObject
implements Calculator {

// Реализация должна иметь конструктор, чтобы определить
// возможность выброса исключения RemoteException
public CalculatorImpl()
throws java.rmi.RemoteException {
super();
}

public long add(long a, long b)
throws java.rmi.RemoteException {
return a + b;
}

public long sub(long a, long b)
throws java.rmi.RemoteException {
return a — b;
}

public long mul(long a, long b)
throws java.rmi.RemoteException {
return a * b;
}

public long div(long a, long b)
throws java.rmi.RemoteException {
return a / b;
}
}

Каждый из предложенных в реализации методов простым образом реализует арифметические функции. Как видно из примера, этот класс также наследует (extends) класс UnicastRemoteObject, который напрямую завлекает его в RMI-систему. Это не обязательное требование. Вместо того, чтобы наследовать этот класс, для введения класса в RMI-систему можно воспользоваться методом exportObject(). Если класс наследует
UnicastRemoteObject, то в этом классе обязательно нужно определить конструктор, в котором объявить, что он может выбрасывать исключение RemoteException. Когда конструктор вызывает метод super(), он выполняет код конструктора UnicastRemoteObject, который обеспечивает связь с RMI и инициализацию удаленных объектов. Следующим шагом мы создадим файлы Stub и Skeleton. Это делается с помощью специального RMI-компилятора, которому в качестве параметра передается файл класса реализации, например, вот так:

rmic CalculatorImpl

И, наконец, следует написать серверную и клиентскую части для заданного приложения. В нашем случае это калькулятор. Итак, серверная часть кода:
import java.rmi.Naming;

public class CalculatorServer {

public CalculatorServer() {
try {
Calculator c = new CalculatorImpl();
Naming.rebind("rmi://localhost:1099/CalculatorService", c);
} catch (Exception e) {
System.out.println(e.printStackTrace());
}
}

public static void main(String args[]) {
new CalculatorServer();
}
}

Как видим, этот класс просто создает экземпляр интерфейса Calculator на основе его реализации CalculatorImpl и связывает с ним имя в именном сервисе с помощью метода Naming.rebind(). Код клиента будет выглядеть вот так:

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.net.MalformedURLException;
import java.rmi.NotBoundException;

public class CalculatorClient {

public static void main(String[] args) {
try {
Calculator c = (Calculator) Naming.lookup("rmi://localhost/CalculatorService");
System.out.println( c.sub(8, 2) );
System.out.println( c.add(4, 5) );
System.out.println( c.mul(6, 7) );
System.out.println( c.div(8, 2) );
}
catch (Exception e) {
System.out.println(e.printStackTrace());
}
}
}

Теперь, скомпилировав все предложенные классы и создав файлы Stub и Skeleton, мы можем приступать к запуску нашей RMI-системы. Для этого нам потребуется три консоли. В первой мы запускаем RMI Registry с помощью команды "rmiregistry". После этого уже в другой консоли запускаем наш сервер командой "java CalculatorServer". Сервер запустится, загрузит реализацию в память и будет ждать соединений от клиентов. И, наконец, запускаем клиентское приложение командой "java CalculatorClient". Если все прошло правильно, то вы должны увидеть следующий вывод на клиентской консоли:

6
9
42
4

Все. Мы создали и опробовали в действии законченную RMI-систему.

Предыстория EJB

Технология JavaBeans привнесла в мир Java-программирования идею компонентного программного обеспечения, т.е. программного обеспечения, в основе построения которого лежат компоненты. Компоненты — это самодостаточные модули ПО, которые можно использовать многократно. Плюс к этому компоненты JavaBeans можно визуально встраивать в Java-программы с помощью специальных инструментов визуальной разработки приложений. Таким образом компоненты JavaBeans позволяют Java-разработчикам "компонентизировать" свои Java-классы.

12 апреля 1997 г. компания Sun Microsystems заявила о своей инициативе разработать Java-платформу для корпоративных решений (enterprise). С помощью Java Community Process (JCP, www.jcp.org) Sun поддерживала разработку набора стандартных Java-расширений, известных как Enterprise Java API. Сердце Enterprise Java API — это Enterprise JavaBeans API, который определяет серверную компонентную модель и независимый от производителя интерфейс программирования для серверов Java-приложений. Черновики первой версии спецификации EJB были опубликованы в декабре 1997 года, а окончательный релиз (версия 1.0) был в марте 1998. Авторы спецификации преследовали много целей, среди которых, естественно, было стремление сделать разработку EJB-компонент проще разработки приложений, обеспечить переносимость серверной бизнес-логики, независимость от конкретного производителя сервера приложений и пр. Кроме этого, технология EJB изначально предполагала две фундаментальные модели, которые используются для построения корпоративных приложений. В первом случае клиент начинает сессию с объектом, который ведет себя, как приложение, выполняющее единицу работы от имени клиента с возможностью выполнения множественных транзакций с базами данных. Это первая модель. Во втором случае клиент получает доступ к объекту, который представляет собой некую сущность (entity) и хранится в базе данных. Это вторая модель. Таким образом, согласно этим моделям мы имеем два понятия (компонента): session bean (для первой модели) и entity bean (для второй модели).

Архитектура EJB

Архитектура EJB такова, что для работы с сервером приложений и его компонентами можно использовать любой клиент. Это возможно благодаря отсутствию четко фиксированного используемого протокола. Это означает, что сервер может поддерживать одновременно несколько протоколов связи с клиентом — например, RMI, IIOP (CORBA) и DCOM. Это также означает, что клиент не обязательно должен быть написан на Java. EJB-сервер — это приложение, предоставляющее набор сервисов для поддержки установки EJB. Эти сервисы включают в себя функции управления распределенными транзакциями, управления распределенными объектами и распределенными вызовами этих объектов, а также системные сервисы низкого уровня. Вкратце EJB-сервер управляет ресурсами, необходимыми для поддержки EJB-компонентов.

Пример использования EJB под RMI

Очень важно помнить, что существует ряд различных сценариев для построения EJB-приложений и компонент. Процесс разработки существенно различается при написании, например, компонента session bean, entity bean, цельного приложения или же целой системы, которая включает сразу несколько таких типов. Рассмотрим простой сценарий разработки компонента session bean, который касается обновления специального счета (account). Разработка EJB-компонента сама по себе несложна. Сначала вы описываете бизнес-логику вашего компонента или приложения на его основе с помощью интегрированной среды разработки (IDE), например, Eclipse или NetBeans. После компиляции компоненты упаковываются в EJB JAR файл. Он представляет собой обычный JAR-архив, внутри которого при этом содержится сериализованный (serialized) экземпляр класса DeploymentDescriptor. В нем помещаются настройки безопасности и различные описания. Далее этот компонент (session bean) нужно развернуть (deploy) в EJB-сервере с помощью специальных инструментов, поставляемых вместе с этим сервером. После этого deployer (например, администратор баз данных) будет производить настройку различных специфических атрибутов этого компонента — например, режим транзакций или уровень безопасности. Как только компонент был установлен на сервере, клиенты могут начинать вызывать удаленные методы получаемых объектов. В качестве примера разберем случай из области электронной коммерции — корзину покупателя (shopping cart). "Корзина" — это абстрактный контейнер, в который покупатель (посетитель электронного магазина) складывает товары, которые он собирается приобрести через Web. Рассмотрим пример представления этой корзины в качестве EJB-компонента. Сначала мы должны создать удаленный интерфейс для нашего компонента:

public interface ShoppingCart
extends javax.ejb.EJBObject {
boolean addItem(int itemNumber) throws java.rmi.RemoteException;
boolean purchase() throws java.rmi.RemoteException;
}

Этот интерфейс определяет два метода: addItem() и purchase(). Первый используется для добавления товара, а второй завершает транзакцию (производит покупку вещей, находящихся в корзине). После этого нужно написать класс нашего компонента:

public class ShoppingCartEJB implements SessionBean {
public boolean addItem(int itemNumber) {
// Процесс добавления товара в корзину.
// Может содержать код JDBC.
}
public boolean purchase () {
// Код для осуществления покупки
}
public ejbCreate(String accountName, String account) {
// Код инициализации объекта
}
}

Заметьте: этот класс не реализует описанный ранее удаленный интерфейс нашего компонента. Это сделает позже класс EJBObject. Также следует отметить, что компоненты типа session bean не поддерживают режим automatic persistence. Поэтому прямой доступ к базе данных должен производиться в его методах. Например, в методе purchase() JDBC-вызовы могут использоваться для обновления информации о покупке в базе данных. Класс EJBObject, который создается EJB-контейнером сервера во время установки компонента реализует его удаленный интерфейс. Этот класс действует как прокси, пропуская через себя вызовы методов, передавая их компоненту, развернутому на севрере. Что касается клиента, то он в первую очередь должен узнать местонахождение объекта EJBHome для нужного компонента с помощью службы JNDI. В нашем примере это делается следующим образом:

public interface CartHome extends javax.ejb.EJBHome {
Cart create(String customerName, String account) throws RemoteException;
}

Интерфейс CartHome содержит метод create(), который будет вызываться в каждый момент, когда клиент запрашивает новый экземпляр нашего компонента. Этот метод уже реализован в классе EJBObject, и при его вызове будет вызываться метод ejbCreate() класса нашего компонента. Теперь рассмотрим пример того, как может выглядеть код на стороне клиента для использования session bean для покупательской корзины. В качестве клиента может выступать сервлет, тонкий клиент (браузер), приложение, написанное на Java, или C++-приложение с помощью технологии CORBA. Последний вариант нам не подходит, потому что мы рассматриваем работу EJB под RMI, а не IIOP. Объект EJBHome для класса ShoppingCart можно получить с помощью следующего куска кода:

Context initialContext = new InitialContext();
CartHome cartHome = (CartHome) initialContext.lookup("applications/mall/shopping-carts");

В этом примере InitialContext() используется для получения корня (root) всей иерархии имен JNDI. Метод lookup() используется для получения объекта CartHome. В нашем случае applications/mall/shopping-carts — это JNDI-путь к искомому классу CartHome. Таким образом, теперь мы имеем ссылку cartHome на объект EJBHome для получения доступа к ShoppingCartEJB. Однако не нужно забывать, что именное пространство JNDI может быть сконфигурировано таким образом, чтобы включать EJB-контейнеры, расположенные на множестве машин в сети. Фактическое расположение EJB-контейнера не может быть определено клиентом. Следующий код демонстрирует, как клиент использует объект EJBHome для вызова методов полученного компонента:
ShoppingCartEJB cart = cartHome.create("Alex", "4167");
cart.addItem(162);
cart.addItem(375);
cart.purchase();

Здесь мы методом create() создаем новый объект компонента session bean. Переменная cart теперь содержит ссылку на удаленный объект EJB и позволяет вызывать его методы: addItem() и purchase().

"Чистый" RMI или EJB?

Очень часто встает вопрос, стоит ли вообще использовать EJB, если можно легко обойтись средствами RMI? Это зависит от многих факторов. Конечно, можно использовать чистый RMI и при этом работать с распределенными объектами, частично реализуя архитектуру EJB. Кроме того, в этом случае приложение будет работать намного быстрее, поскольку здесь вы описываете практически все самостоятельно. Framework EJB — это достаточно общий framework, который можно использовать в большинстве случаев, где нужны распределенные компоненты. И при таком общем подходе его код должен выполнять очень много проверок и иметь достаточное множество логических слоев, чтобы уметь решать практически все возможные типы проблем. Все это дает значительный проигрыш в производительности. Поэтому, если высший приоритет приложения отдан производительности, то, несомненно, решение в пользу "чистого" RMI-кода будет самым правильным. Кроме этого, вы можете вообще отойти даже от рамок RMI framework и работать напрямую с Sockets API.

Учитывая все вышесказанное, можно сделать простой вывод: если сервер приложений предоставляет некоторые необходимые вам функции, и сделать это с помощью EJB framework гораздо проще и быстрее, чем разрабатывать все самому с нуля (например, реализовывать pooling соединений с базой данный), лучше остановить выбор на EJB. В большинстве случаев именно на EJB многие проекты оказываются более прибыльными и успешными.

Резюме

Технологии Remote Method Invocation и Enterprise JavaBeans представляют новое направление разработки, установки и управления распределенными бизнес-приложениями. Кроме того, компонентная архитектура Enterprise JavaBeans является гигантским шагом к упрощению процесса разработки и управления корпоративными приложениями.

Алексей Литвинюк, litvinuke@tut.by


Компьютерная газета. Статья была опубликована в номере 31 за 2005 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета