Thinking in Java, 2nd edition, Revision 11

©2000 by Bruce Eckel

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]

15:Распределенные вычисления

Исторически, программирование для нескольких машин было трудоемким, сложным и полным ошибок.

Программист должен знать множество деталей о сети, а иногда, также об аппаратном обеспечении. Вам нужно было разбираться с различными “слоями” сетевых протоколов, и, кроме того, существовало множество функций в сетевых библиотеках для соединения, распаковки, упаковки блоков информации; передачи этих блоков туда и обратно; и подтверждения связи. Это была тяжелая задача.

Однако, основная идея распределенного программирования не так сложна, и легко резюмируется в библиотеках Java. Вы хотите:

Для каждого пункта будет дано небольшое введение в этой главе. Обратите внимание, что каждая тема довольно объемная и сама по себе может занимать отдельную книгу, так что эта глава предназначена для ознакомления с этими разделами. Это, конечно, не сделает Вас экспертами по данным вопросам (хотя, Вы можете довольно далеко пройти с помощью информации представленной здесь по сетевому программированию, сервлетам и JSPs).

Сетевое программирование

Одним из больших достижений Java является безболезненное общение в сети. Проектировщики сетевой библиотеки Java сделали ее похожей на чтение и запись файлов, за исключением того, что этот “файл” находится на удаленной машине и удаленная машина может в точности определить, что нужно сделать с информацией, которую Вы посылаете либо требуете. Насколько возможно, внутренние детали сетевого общения абстрагированы и обрабатываются внутри JVM и локальной машины, на которой установлена среда Java. Модель программирования такая же, как Вы используете в файле; в действительности, Вы просто окутываете сетевое соединение (“сокет”) потоковым объектом, и Вы действуете используя те же вызовы методов, что и с другими потоковыми объектами. К тому же, встроенная в Java множественность процессов очень удобна при общении в сети: установка нескольких соединений сразу.

Этот раздел описывает поддержку сети в Java, используя простые примеры.

Идентификация машины

Конечно, чтобы убедиться, что соединение установлено с конкретной машиной в сети, должен быть способ уникальной идентификации машины в сети. Раньше, при работе в сети было достаточно предоставить уникальные имена для машин внутри локальной сети. Но, Java работает в Internet, который требует уникальной идентификации машины с любой другой во всей сети по всему миру. Это достигается с помощью IP (Internet Protocol) адресации, которая может иметь две формы:

  1. Обычная DNS (Domain Name System) форма. Мое доменное имя - bruceeckel.com, и если у меня есть компьютер с именем Opus в моем домене, его доменное имя может быть Opus.bruceeckel.com. Это в точности тип имени, который Вы используете при отсылке почты, и очень часто включается в WWW адрес.
  2. С другой стороны, Вы можете использовать четырехточечную” форма, в которой четыре номера разделены точками, например 123.255.28.120.

В обоих случаях, IP адрес представляется как 32 битное значение[72] (каждое число из 4-х не может превышать 255), и Вы можете получить специальный объект Java для представления этого номера из формы, представленной выше с помощью метода static InetAddress.getByName( ) в пакете java.net. Результат это объект типа InetAddress, который Вы можете использовать для создания “сокета”, как Вы позднее увидите.

Простой пример использования InetAddress.getByName( ), показывает что происходит, когда у Вас есть провайдер интернет по коммутируемым соединениям (ISP). Каждый раз, когда Вы дозваниваетесь, Вам присваивается временный IP. Но, пока Вы соединены, Ваш IP адрес ничем не отличается от любого другого IP адреса в интернет. Если кто-то подключится к Вашей машине, используя Ваш IP адрес, он сможет подключиться также к Web или FTP серверу, который запущен на Вашей машине. Конечно, сначала необходимо узнать Ваш IP адрес, а т.к. при каждом соединении присваивается новый адрес, как Вы сможете его узнать?

Следующая программа использует InetAddress.getByName( ) для определения Вашего IP адреса. Чтобы использовать его, Вы должны знать имя своего компьютера. В Windows 95/98, зайдите в “Settings”, “Control Panel”, “Network”, а затем выберите страничку “Identification”. “Computer name” это имя, которое необходимо задать в командной строке.

//: c15:WhoAmI.java
// Определяет Ваш сетевой адрес
// когда Вы подключены к Internet.
import java.net.*;

public class WhoAmI {
  public static void main(String[] args) 
      throws Exception {
    if(args.length != 1) {
      System.err.println(
        "Usage: WhoAmI MachineName");
      System.exit(1);
    }
    InetAddress a = 
      InetAddress.getByName(args[0]);
    System.out.println(a);
  }
} ///:~

В этом случае, машина называется “peppy”. Итак, когда я соединяюсь с моим провайдером, я запускаю программу:

java WhoAmI peppy

Я получаю в ответ сообщение подобное этому (конечно адрес каждый раз новый):

peppy/199.190.87.75

Если я сообщу этот адрес моему другу, и у меня будет Web сервер, запушенный на компьютере, они могут соединиться с ним, зайдя на URL http://199.190.87.75 (только пока я остаюсь в этом сеансе связи). Это иногда может быть удобным способом предоставления информации кому-то другому, либо тестирования конфигурации Web сайта перед тем как опубликовать его на “реальном” сервере.

Сервера и клиенты

Основная задача сети - предоставление двум машинам возможности соединиться и общаться друг с другом. Как только две машины нашли друг друга, они могут отличное иметь двухстороннее общение. Но как они находят друг друга? Это как потеряться в парке развлечений: одна машина должна оставаться на месте и слушать, пока другая машина не скажет, “Эй! Где ты?”

Машина, которая “остается на одном месте” называется сервером, а тот, который ищет, называется клиентом. Это различие важно только когда клиент пытается подключиться к серверу. Как только они соединятся, происходит процесс двухстороннего общения и не важно, что один является сервером, а другой - клиентом.

Итак, работа сервера - слушать соединение, и это выполняется с помощью специального серверного объекта, который Вы создаете. Работа клиента - попытаться создать соединение с сервером, что выполняется с помощью специального клиентского объекта. Как только соединение установлено, Вы увидите, что у клиента и сервера соединение магически превращается в потоковый объект ввода/вывода, и с этого момента Вы можете рассматривать соединение как файл, который Вы можете читать, и в который Вы можете записывать. Т.о., после установления соединения Вы будете использовать уже знакомые Вам команды ввода/вывода из Главы 11. Это одно из отличных расширений сетевой библиотеки Java.

Тестирование программ без наличия сети

По многим причинам, Вам может не быть доступна клиентская машина, серверная машина, и вообще сеть для тестирования Вашей программы. Вы можете выполнить упражнения в классной комнате, либо, Вы можете написать программу, которая недостаточно устойчива для работы в сети. Создатели интернет протокола были осведомлены о таких проблемах, и они создали специальный адрес, называемый localhost, “локальная петля”, который является IP адресом для тестирования без наличия сети. Обычный способ получения этого адреса в Java это:

InetAddress addr = InetAddress.getByName(null);

Если Вы ставите параметр null в метод getByName( ), то, по умолчанию используется localhost. InetAddress это то, что Вы используете для ссылки на конкретную машину, и Вы должны предоставлять это, перед тем как продолжить дальнейшие действия. Вы не можете манипулировать содержанием InetAddress (но Вы можете распечатать его, как Вы увидите в следующем примере). Единственный способ создать InetAddress - это использовать один из перегруженных статических методов getByName( ) (который Вы обычно используете), getAllByName( ), либо getLocalHost( ).

Вы можете создать адрес локальной петли, установкой строкового параметра localhost:

InetAddress.getByName("localhost");

(присваивание “localhost” конфигурируется в таблице “hosts” на Вашей машине), либо с помощью четырехточечной формы для именования зарезервированного IP адреса для петли:

InetAddress.getByName("127.0.0.1");

Все три формы производят одинаковые результаты.

Порт: уникальное место
внутри машины

IP адреса недостаточно для индикации уникального сервера, т.к. много серверов может существовать на одной машине. Каждая машина в IP также содержит порты, и когда Вы устанавливаете клиента или сервера Вы должны выбрать порт, по которому сервер и клиент договорились соединиться; если Вы встречаете кого-то, IP адрес это окрестность и порт это бар.

Порт это не физическое расположение в машине, а программная абстракция (большей частью для бухгалтерского назначения). Клиентская программа знает, как соединиться с машиной по IP адресу, но как соединиться с нужной службой (потенциально одной из многих на этой машине)? Вот где номера портов являются вторым уровнем адресации. Идея в том, что Вы запрашиваете конкретный порт, этим запрашивая службу, ассоциированную с этим портом. Время дня это простой пример службы. Обычно, каждая служба ассоциируется с уникальным номером порта на заданной серверной машине. Необходимо знать заранее, на каком порту запущена необходимая служба.

Системные службы резервируют номера портов с 1 по 1024, так что Вы не должны использовать ни один из портов, который Вы знаете, что он используется. Первый выбор порта для примеров в этой книге это порт номер 8080 (в память почтенного и древнего 8-битного чипа Intel 8080 на моем первом компьютере, с операционной системой CP/M).

Сокеты

Сокет это программная абстракция, используемая для представления “терминалов” соединений между двумя машинами. Для данного соединения, существует сокет на каждой машине, и Вы можете представить гипотетический “кабель” соединяющий две машины, каждый конец которого вставлен в сокет. Конечно, какое аппаратное обеспечение и кабель между ними неизвестно. Основной смысл абстракции в том, что нам не нужно знать больше, чем необходимо.

В Java, Вы создаете сокет для установления соединения с другой машиной, затем Вы получаете InputStream и OutputStream (либо с помощью соответствующих преобразователей, Reader и Writer) из сокета, который соответствующим образом представляет соединение, как потоковый объект ввода вывода. Есть два класса сокетов, основанных на потоках: ServerSocket - используется сервером, чтобы “слушать” входящие соединения и Socket - используется клиентом для инициирования соединения. Как только клиент создает соединение по сокету, ServerSocket возвращает (с помощью метода accept( ) ) соответствующий объект Socket по которому будет происходить связь на стороне сервера. Начиная с этого момента, у Вас появляется соединение Socket к Socket, и Вы считаете эти соединения одинаковыми, потому что они действительно одинаковые. В результате, Вы используете методы getInputStream( ) и getOutputStream( ) для создания соответствующих объектов InputStream и OutputStream из каждого Socket. Они должны быть обернуты внутри буферов и форматирующих классов, как и любой другой потоковый объект, описанный в Главе 11.

ServerSocket может показаться еще одним примером запутанной схемы имен в библиотеках Java. Вы можете подумать, что ServerSocket лучше назвать “ServerConnector” либо как-нибудь иначе без слова “Socket” в нем. Вы также можете подумать, что ServerSocket и Socket должны быть оба унаследованы от одного из базовых классов. В самом деле, оба класса содержат несколько методов совместно, но этого недостаточно, чтобы дать им общий базовых класс. Взамен, работа ServerSocket ожидать, пока не подсоединится другая машина, и затем возвратить подлинный Socket. Вот почему ServerSocket кажется немного неправильно названным, т.к. его работа в действительности не быт сокетом, а просто создавать объект Socket, когда кто-то другой к нему подключается.

Однако ServerSocket создает физический “сервер” либо слушающий сокет на серверной машине. Этот сокет слушает входящие соединения и затем возвращает “установленный” сокет (с определенными локальными и удаленными конечными точками) посредством метода accept( ). В замешательство приводит то, что оба этих сокета (слушающий и установленный) ассоциированы с тем же самым серверным сокетом. Слушающий сокет может допустить только запросы на новое соединение но не пакеты данных. Итак, пока ServerSocket не имеет смысла программного, зато имеет смысл “физический”.

Когда Вы создаете ServerSocket, Вы задаете для него только номер порта. Вам не нужно задавать IP адрес, т.к. он уже существует на машине. Однако когда Вы создаете Socket, Вы должны задать и IP адрес и номер порта машины, с которой Вы хотите соединиться. (Тем не менее, Socket который возвращается методом ServerSocket.accept( ) уже содержит всю эту информацию.)

Простой пример сервера и клиента

Этот пример показывает простую работу сервера и клиента используя сокеты. Все, что делает сервер - это просто ожидание соединения, затем использует Socket полученный из того соединения для создания InputStream и OutputStream. Они конвертируются в Reader и Writer, затем в BufferedReader и PrintWriter. После этого, все что он получает из BufferedReader он отправляет на PrintWriter пока не получит строку “END,” после чего, он закрывает соединение.

Клиент устанавливает соединение с сервером, затем создает OutputStream и выполняет те же операции, что и сервер. Строки текста посылаются через результирующий PrintWriter. Клиент также создает InputStream (снова, с соответствующими конверсиями и облачениями) чтобы слушать, что говорит сервер (а в нашем случае он возвращает слова назад).

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

Вот сервер:

//: c15:JabberServer.java
// Очень простой сервер, который только
// отображает то, что посылает клиент.
import java.io.*;
import java.net.*;

public class JabberServer {  
  // Выбираем номер порта за пределами 1-1024:
  public static final int PORT = 8080;
  public static void main(String[] args) 
      throws IOException {
    ServerSocket s = new ServerSocket(PORT);
    System.out.println("Started: " + s);
    try {
      // Блокируем пока не произойдет соединение:
      Socket socket = s.accept();
      try {
        System.out.println(
          "Connection accepted: "+ socket);
        BufferedReader in = 
          new BufferedReader(
            new InputStreamReader(
              socket.getInputStream()));
        // Вывод автоматически обновляется 
        // классом PrintWriter:
        PrintWriter out = 
          new PrintWriter(
            new BufferedWriter(
              new OutputStreamWriter(
                socket.getOutputStream())),true);
        while (true) {  
          String str = in.readLine();
          if (str.equals("END")) break;
          System.out.println("Echoing: " + str);
          out.println(str);
        }
      // всегда закрываем оба сокета...
      } finally {
        System.out.println("closing...");
        socket.close();
      }
    } finally {
      s.close();
    }
  } 
} ///:~

Вы видите, что объекту ServerSocket нужен только номер порта, не IP адрес (т.к. он запущен на этой машине!). Когда Вы вызываете метод accept( ), метод блокирует выполнение программы, пока какой-нибудь клиент не попробует соединиться. То есть, он ожидает соединение, но другие процессы могут выполняться (см. Главу 14). Когда соединение сделано, accept( ) возвращает объект Socket представляющий это соединение.

Ответственность за очищение сокетов is crafted carefully here. Если конструктор ServerSocket завершается неуспешно, программа просто завершается (обратите внивание, что мы должны считать что конструктор ServerSocket не оставляет открытых сетевых сокетов если он завершается неудачно). В этом случает, main( ) выбрасывает исключение IOException и блок try не обязателен. Если конструктор ServerSocket завершается успешно, то остальные вызовы методов должны быть окружены блоками try-finally, чтобы убедиться, что независимо от того как блок завершит работу, ServerSocket будет корректно закрыт.

Та же логика используется для Socket возвращаемого методом accept( ). Если вызов accept( ) неуспешный, то мы должны считать что Socket не существует и не держит никаких ресурсов, так что он не нуждается в очистке. Но, если вызов успешный, следующи объявления должны быть окружены блоками try-finally так что в случае неуспешного вызова Socket будет очищен. Заботиться здесь об этом обязательно, т.к. сокеты используют важные ресурсы располагающиеся не в памяти, так что Вы должны тщательно очищать их (поскольку в Java нет деструктора, чтобы сделать это за Вас).

И ServerSocket и Socket созданные методом accept( ) печатаются в System.out. Это значит, что их методы toString( ) вызываются автоматически. Вот что получается:

ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]
Socket[addr=127.0.0.1,PORT=1077,localport=8080]

Скоро Вы увидите как how они объединяются вместе с тем что делает клиент.

Следующая часть программы выглядит как программа для для открытия файлов для чтения и записи за исключением того, что InputStream и OutputStream создаются из объекта Socket. И объект InputStream и объект OutputStream конвертируются в объекты Reader и Writer используя классы “конвертеры” InputStreamReader и OutputStreamWriter, соответственно. Вы можете также использовать напрямую классы из Java 1.0 InputStream и OutputStream, но с выводом есть явное преимущество при использовании Writer. Это реализуется с помощью PrintWriter, в котором перегруженный конструктор берет второй аргумент, а boolean флаг который индицирует когда какой автоматически сбрасывает вывод в конце каждого вывода println( ) (но не print( )) выражения. Каждый раз, когды Вы направляете данные в out, его буфер должен сбрасываться так информация передается о сети. Сброс важен для этого конкретного примера, т.к. клиент и сервер ждут строку данных друг от друга, перед тем, как что-то сделать. Если сброса буферов не происходит, информация не будет отправлена по сети, пока буфер полон, что вызовем много проблем в этом примере.

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

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

Бесконечный цикл while читает строки из BufferedReader in и записывает информацию вSystem.out and to the PrintWriter out. Запомните, что in и out могут быть любыми потоками, они просто соединены в сети.

Когда клиент отсылает строку, состоящую из “END,” программа прерывает выполнение цикла и закрывает Socket.

Вот клиент:

//: c15:JabberClient.java
// Очень простой клиент, который просто отсылает строки серверу
// и читает строки, которые посылает сервер
import java.net.*;
import java.io.*;

public class JabberClient {
  public static void main(String[] args) 
      throws IOException {
    // Установка параметра в null в getByName()
    // возвращает специальный IP address - "Локальную петлю",
    // для тестирования на одной машине без наличия сети
    InetAddress addr = 
      InetAddress.getByName(null);
    // Альтернативно Вы можете использовать 
    // адрес или имя:
    // InetAddress addr = 
    //    InetAddress.getByName("127.0.0.1");
    // InetAddress addr = 
    //    InetAddress.getByName("localhost");
    System.out.println("addr = " + addr);
    Socket socket = 
      new Socket(addr, JabberServer.PORT);
    // Окружаем все блоками try-finally to make
    // чтобы убедиться что сокет закрывается:
    try {
      System.out.println("socket = " + socket);
      BufferedReader in =
        new BufferedReader(
          new InputStreamReader(
            socket.getInputStream()));
      // Вывод автоматически сбрасывается
      // с помощью PrintWriter:
      PrintWriter out =
        new PrintWriter(
          new BufferedWriter(
            new OutputStreamWriter(
              socket.getOutputStream())),true);
      for(int i = 0; i < 10; i ++) {
        out.println("howdy " + i);
        String str = in.readLine();
        System.out.println(str);
      }
      out.println("END");
    } finally {
      System.out.println("closing...");
      socket.close();
    }
  }
} ///:~

В методе main( ) Вы видите все три пути для возврата IP адреса локальной петли: используя null, localhost, либо явно зарезервированный адрес 127.0.0.1. Конечно, если Вы хотите соединиться с машиной в сети Вы подставляете IP адрес этой машины. Когда InetAddress addr печатается (с помощью автоматического вызова метода toString( )) получается следующий результат:

localhost/127.0.0.1

Подстановкой параметра null в getByName( ), она по умолчанию использует localhost, и это создает специальный адрес 127.0.0.1.

Обратите внимание, что Socket названный socket создается и с типом InetAddress и с номером порта. Чтобы понимать, что это значит, кгда Вы печаете один из этих объектов Socket, помните, что соединение с Интернет определяется уникально этими четырьмя элементами данных: clientHost, clientPortNumber, serverHost, и serverPortNumber. Когда сервер запускается, он берет присвоенный ему порт (8080) на localhost (127.0.0.1). Когда клиент приходит, распределяется следующий доступный порт на той же машине, в нашем случае - 1077, который, так случилось, оказался расположен на той же самой машине (127.0.0.1), что и сервер. Теперь, необходимо данные перемещать между клиентом и сервером, каждая сторона должнва знать, куда их посылать. Поэтому, во время процесса соединения с “известным” сервером, клиент посылает “обратный адрес”, так что сервер знает, куда отсылать его данные. Вот, что Вы видите в примере серверной части:

Socket[addr=127.0.0.1,port=1077,localport=8080]

Это значит, что сервер тоьлко что принял соединение с адреса 127.0.0.1 и порта 1077 когда слушал свой локальный порт (8080). На стороне клиента:

Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]

это значит, что клиент создал соединение с адресом 127.0.0.1 и портом 8080, используя локальный порт 1077.

Вы увидите, что каждый раз, когда Вы запускаете нового клиента, номер локального порта увеличивается. Отсчет начинается с 1025 (предыдущие являются зарезервированными) и продолжается до того момента, пока Вы не перезагрузите машину, после чего он снова начинается с 1025. (На UNIX машинах, как только достигается максимальное число сокетов, нумерация начинается с самого меньшего доступного номера.)

Как только объект Socket создан, процесс превода его в BufferedReader и PrintWriter тот же самый, что и в серверной части (снова, в обоих случаях Вы начинаете с Socket). Здесь, клиент инициирует соединение отсылкой строки “howdy” следующе за номером. Обратите внимание, что буфер должен быть снова сброшен (что происходит автоматически по второму аргументу в конструкторе PrintWriter). Если буфер не будет сброшен, все общение зависнет, т.к. строка “howdy” никогда не будет отослана (буфер не будет достаточно полным, чтобы выполнить отсылку автоматически). Каждая строка, отсылаемая сервером обратно записывается в System.out для проверки, что все работает правильно. Для прекращения общения, отсылается условный знак - строка “END”. Если клиент прервывает соединение, то сервер выбрасывает исключение.

Вы видите, что такая же забота здесь тоже присутствует, чтобы убедиться, что ресурсы представленные Socket корректно освобождаются, с помощью блока try-finally.

Сокеты создают “подписанное” (dedicated) соединение, которое сохраняется, пока не произойдет явный разрыв соединения. (Подписанное соединение может еще быть разорвано неявно, если одна сторона , либо промежуточное соединение, разрушается.) Это значит, что обе стороны заблокированы в общении и соединение постоянно открыто. Кажется, что это просто логический подход к передаче данных, однако это дает дополнительную нагрузку на сеть. Позже, в этой главе Вы увидите другой метод передачи данных по сети, в котором соединения являются временными.

Обслуживание нескольких клиентов

JabberServer работает, но он может обслуживать только одного клиента одновременно. В типичном сервере, Вы захотите иметь возможность общаться со несколькими клиентами одновременно. Решение - это много поточность, а в языках, которые напрямую не поддерживают многопоточность это означает все виды сложностей. В Главе 14 Вы увидели, что многопоточность в Java настолько просто, насколько это возможно, в то время, как многопоточность вообще является сложной темой. Т.к. поддерка нитей в Java является прямой и открытой, то создание сервера, поддерживающего множество клиентов оказывается относительно простой задачей.

Основная схема это создание единичного объекта ServerSocket в серверной части и вызвать метод accept( ) для ожидания нового соединения. Когда accept( ) возвращает управления, Вы берете возвращенный Socket и используете его для создания новой нити(потока), чьей работой является обслуживание этого клиента. Затем Вы вызываете метод accept( ) снова, для ожидания нового клиента.

В следующем коде серверной части Вы увидите, что это выглядит также как и в примере JabberServer.java за исключением того, что все операции по обслуживанию конкретного клиента перемещены внутрь класса нити (потока):

//: c15:MultiJabberServer.java
// Сервер, использующий многопоточность 
// для обслуживания любого числа клиентов.
import java.io.*;
import java.net.*;

class ServeOneJabber extends Thread {
  private Socket socket;
  private BufferedReader in;
  private PrintWriter out;
  public ServeOneJabber(Socket s) 
      throws IOException {
    socket = s;
    in = 
      new BufferedReader(
        new InputStreamReader(
          socket.getInputStream()));
    // Включение автосброса буферов:
    out = 
      new PrintWriter(
        new BufferedWriter(
          new OutputStreamWriter(
            socket.getOutputStream())), true);
    // Если какой либо, указанный выше класс выбросит исключение 
    // вызывающая процедура ответственна за закрытие сокета
    // В противном случае нить(поток) закроет его.

    start(); // Вызывает run()
  }
  public void run() {
    try {
      while (true) {  
        String str = in.readLine();
        if (str.equals("END")) break;
        System.out.println("Echoing: " + str);
        out.println(str);
      }
      System.out.println("closing...");
    } catch(IOException e) {
      System.err.println("IO Exception");
    } finally {
      try {
        socket.close();
      } catch(IOException e) {
        System.err.println("Socket not closed");
      }
    }
  }
}

public class MultiJabberServer {  
  static final int PORT = 8080;
  public static void main(String[] args)
      throws IOException {
    ServerSocket s = new ServerSocket(PORT);
    System.out.println("Server Started");
    try {
      while(true) {
        // Останавливает выполнение, до нового соединения:
        Socket socket = s.accept();
        try {
          new ServeOneJabber(socket);
        } catch(IOException e) {
          // Если неудача - закрываем сокет,
          // в противном случае нить закроет его:
          socket.close();
        }
      }
    } finally {
      s.close();
    }
  } 
} ///:~

Нить ServeOneJabber берет объект Socket, который создается методом accept( ) в main( ) каждый раз, когда новый клиент создает соединение. Затем, как раньше, он создает объект BufferedReader и объект PrintWriter с авто-сбросом используя Socket. Наконец, он вызывает специальный метод объекта Thread - start( ), который выполняет инициализации в нити и, затем вызывает метод run( ). Здесь выполняются те же действия, что и в предыдущем примере: чтение данных из сокета, а затем возврат этих данных обратно, пока не придет специальный сигнал - строка “END”.

Ответственность за очистку сокета должна быть снова тщательно обработана. В этом случае, сокет создается за пределами ServeOneJabber так что ответственность может быть разделена. Если конструктор ServeOneJabber завершится неудачно, он просто вызовет исключение вызывающему методу, который затем очистит нить. Но если конструктор завершится успешно, то объект ServeOneJabber возьмет ответственность за очистку нить на себя, в его методе run( ).

Посмотрите, насколько протая реализация у MultiJabberServer. Как раньше, ServerSocket создается и вызывается метод accept( ) для ожидания нового соединения. Но в этот момент, возвращаемое значение accept( ) (объекты Socket) передается в конструктор ServeOneJabber, который создает новую нить для обраболтки этого соединения. Когда соединение закрывается, нить завершает свою работу.

Если создание ServerSocket прерывается, снова выбрасывается в main( ). Но если создание успешное, внешний блок try-finally гарантирует его очистку. Внутренний блок try-catch защищает только от ошибок в конструкторе ServeOneJabber; если конструктор выполняется без ошибок, то нить ServeOneJabber закроет связанный с ней сокет.

Для проверки того, что сервер поддерживает несколько клиентов, следующая программа создает множество клиентов (используя нити) которые подключаются к одному и тому же серверу. Максимальное число нитей определяется переменной final int MAX_THREADS.

//: c15:MultiJabberClient.java
// Клиент для проверки MultiJabberServer
// посредством запуска множества клиентов.
import java.net.*;
import java.io.*;

class JabberClientThread extends Thread {
  private Socket socket;
  private BufferedReader in;
  private PrintWriter out;
  private static int counter = 0;
  private int id = counter++;
  private static int threadcount = 0;
  public static int threadCount() { 
    return threadcount; 
  }
  public JabberClientThread(InetAddress addr) {
    System.out.println("Making client " + id);
    threadcount++;
    try {
      socket = 
        new Socket(addr, MultiJabberServer.PORT);
    } catch(IOException e) {
      System.err.println("Socket failed");
      // Если сокет не создался, 
      // ничего не надо чистить.
    }
    try {    
      in = 
        new BufferedReader(
          new InputStreamReader(
            socket.getInputStream()));
      // Включение авто-очистки буфера:
      out = 
        new PrintWriter(
          new BufferedWriter(
            new OutputStreamWriter(
              socket.getOutputStream())), true);
      start();
    } catch(IOException e) {
      // Сокет должен быть закрыт при появлении любой ошибки 
      try {
        socket.close();
      } catch(IOException e2) {
        System.err.println("Socket not closed");
      }
    }
    // Иначе сокет будет закрыт 
    // методом run() у нити.
  }
  public void run() {
    try {
      for(int i = 0; i < 25; i++) {
        out.println("Client " + id + ": " + i);
        String str = in.readLine();
        System.out.println(str);
      }
      out.println("END");
    } catch(IOException e) {
      System.err.println("IO Exception");
    } finally {
      // Всегда закрывает его:
      try {
        socket.close();
      } catch(IOException e) {
        System.err.println("Socket not closed");
      }
      threadcount--; // Завершение нити
    }
  }
}

public class MultiJabberClient {
  static final int MAX_THREADS = 40;
  public static void main(String[] args) 
      throws IOException, InterruptedException {
    InetAddress addr = 
      InetAddress.getByName(null);
    while(true) {
      if(JabberClientThread.threadCount() 
         < MAX_THREADS)
        new JabberClientThread(addr);
      Thread.currentThread().sleep(100);
    }
  }
} ///:~

Конструктор JabberClientThread берет InetAddress и использует его для открытия Socket. Вы возпожно уже начинаете видешь шаблон: Socket используется всегда для создания некоторых типов Reader и/или Writer (или InputStream и/или OutputStream) объект, что является единственным способом, в котором Socket может быть использован. (Вы можете, конечно, написать один-два класса для автоматизации этого процесса, вместо того , чтобы заново все это набирать, если для Вас это сложно.) Итак, start( ) выполняет инициализации нити и вызывает метод run( ). Здесь, сообщения отсылаются серверу и информация с сервера печатается на экране. Однако, нить имеет ограниченное время жизни и в один прекрасный момент завершается. Обратите внимание, что сокет очищается если конструктор завершается неуспешно, после того как сокет создается, но перед тем, как конструктор завершится. В противном случае ответственность за вызов метода close( ) для сокета ложится на метод run( ).

Переменная threadcount хранит число - сколько объектов JabberClientThread в данный момент существует. Она увеличивается в конструкторе и уменьшается при выходе из метода run( ) (и это значит, что нить завершается). В методе MultiJabberClient.main( ), Вы видите, что число нитей проверяется, и если нитей слишком много - новые не создаются. Затем метод засыпает. При этом, некоторые нити будут завершаться и новые смогут быть созданы. Вы можете поэкспериментировать с MAX_THREADS, чтобы посмотреть когда у Вашей системы появятся проблемы с обслуживанием большого количества соединений.

Дейтаграммы

Примеры, которые Вы увидели используют протоколTransmission Control Protocol (TCP, также известный каксокеты основанные на потоках), который создан для исключительной надежности и гарантирует, что данные будут доставлены туда, куда необходимо. Он позволяет организовать повторную передачу потерянных данных, он предоставляет возможность отсылки отдельных частей через разные маршрутизаторы, в случает если один из них выйдет из строя, и байты будут приняты именно в том порядке, в ктором они были посланы. Весь этот контроль и надежность имеет цену: в TCP высокие накладные расходы.

Существует второй протокол, называемый User Datagram Protocol (UDP), который не гарантирует, что пакеты будут доставлены и не гарантирует доставки в том порядке, в котором они были посланы. Он называется “ненадежным протоколом” (TCP это “надежный протокол”), и это звучит не очень хорошо, однако он намного быстрее и потому может быть полезным. Существуют некоторые приложения, такие как аудио сигналы, в которых потеря нескольких пакетов не очень не имеет большого значения, но скорость очень важна. Либо представьте сервер предоставляющий информацию о времени, где действительно не имеет значени, если одно из сообщений потеряется. Также, некоторые приложения могут посылать UDP сообщения на сервер, а затем, при отсутствии отклика в течение некоторого времени, считать, что сообщение было потеряно.

На самом деле, Вы будете делать большинство сетевых приложений с протоколом TCP, и только некоторые будут испольовать UDP. Существует более полное описание UDP, включающее примеры, в первой редакции книги (доступных на CD ROM вместе с книгой, либо свободно загружаемы с сайта www.BruceEckel.com).

Использование ссылок URL внутри апплета

Апплеты могут отобразить любую ссылку URL через Web браузер, внутри которого запускается апплет. Вы можете выполнить это с помощью следующей строки:

getAppletContext().showDocument(u);

в которой u - это объект URL. Вот простой пример, который перенаправляет Вас на другую Web страничку. Хотя, Вы перенаправляетесь на HTML страничку, Вы также можете перенаправить на программу CGI.

//: c15:ShowHTML.java
// <applet code=ShowHTML width=100 height=50>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.io.*;
import com.bruceeckel.swing.*;

public class ShowHTML extends JApplet {
  JButton send = new JButton("Go");
  JLabel l = new JLabel();
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    send.addActionListener(new Al());
    cp.add(send);
    cp.add(l);
  }
  class Al implements ActionListener {
    public void actionPerformed(ActionEvent ae) {
      try {
        // Это может быть программа CGI вместо
        // HTML странички.
        URL u = new URL(getDocumentBase(), 
          "FetcherFrame.html");
        // Отображается вывод URL используя
        // Web браузер, как обычную страничку:
        getAppletContext().showDocument(u);
      } catch(Exception e) {
        l.setText(e.toString());
      }
    }
  }
  public static void main(String[] args) {
    Console.run(new ShowHTML(), 100, 50);
  }
} ///:~

Красота класса URL в том, насколько сильно Вас он защищает от тонкостей реализации стороны сервера. Вы можете присоединиться к Web серверу практически ничего не зная, что происходит у него внутри.

Чтение файла с сервера

Вариация приведенной выше программы читает файл, расположенный на сервере. В этом случае, файл определяется клиентом:

//: c15:Fetcher.java
// <applet code=Fetcher width=500 height=300>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.io.*;
import com.bruceeckel.swing.*;

public class Fetcher extends JApplet {
  JButton fetchIt= new JButton("Fetch the Data");
  JTextField f = 
    new JTextField("Fetcher.java", 20);
  JTextArea t = new JTextArea(10,40);
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    fetchIt.addActionListener(new FetchL());
    cp.add(new JScrollPane(t));
    cp.add(f); cp.add(fetchIt);
  }
  public class FetchL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      try {
        URL url = new URL(getDocumentBase(),
          f.getText());
        t.setText(url + "\n");
        InputStream is = url.openStream();
        BufferedReader in = new BufferedReader(
          new InputStreamReader(is));
        String line;
        while ((line = in.readLine()) != null)
          t.append(line + "\n");
      } catch(Exception ex) {
        t.append(ex.toString());
      }
    }
  }
  public static void main(String[] args) {
    Console.run(new Fetcher(), 500, 300);
  }
} ///:~

Создание объекта URL сходно с пердыдущим примером —getDocumentBase( ) - стартовая позиция как и раньше, но в это т раз имя файла читается из поля JTextField. Как только объект URL создан, его строковая версия отображается в JTextArea так что мы видим, как она выглядит. Затем создается InputStream из URL, который в этом случае легко создает поток символов в файле. После конвертирования в Reader и буферизации, читается каждая строка и добавляется в JTextArea. Обратите внимание, что JTextArea было помещено внутрь JScrollPane так что прокрутка происходит автоматически.

Дальнейшее сетевое программирование

На самом деле существует еще множество вещей, которые могли бы аходиться в этом разделе. Сетевая поддержка в Java также широкую поддержку URLs, включая управление протоколами для различных типов содержания. Все это может быть найдено на интернет сайте. Вы можете найти полное и подробное описание сетевого программирования на Java в книге Java Network Programming by Elliotte Rusty Harold (O’Reilly, 1997).

Java Database Connectivity (JDBC)

Приблизительно было подсчитано, что половина всего программного обеспечения использует клиент/серверные операции. Многообещающей возможностью Java была способность строить платформонезависимые клиент/серверные прилажения для работы с базами данных. Это стало возможным благодаря Java DataBase Connectivity (JDBC).

Одна из основных проблемм при работе с базами данных - это война особенностей между компаниями, разрабатывающими базы данных. Есть “стандартный” язык базы данных, Structured Query Language (SQL-92), но вы обычно должны знать с базой данных какого производителя вы работаете, несмотря на стандарт. JDBC предназначена для независимости от платформы, так что вам нет необходимости заботится о том, какую базу данных вы используете при программировании. Однако все еще возможно делать зависимые от производителя вызовы из JDBC, так что вы не ограничены тем, что вы должны делать.

В одном месте программистам может понадобиться использовать SQL имена типов в SQL выражении TABLE CREATE, когда они создают новую таблицу данных и определяют SQL тип для каждой колонки. К сожалению существуют значительные различия между SQL типами, поддерживаемыми различными продуктами баз данных. Различные базы данных, поддерживающие SQL типы с одинаковой семантикой и структурой, могут иметь различные имена типов. Большинство наиболее известных баз данных поддерживают типы данных SQL для больших бинарных значений: в Oracle этот тип называется LONG RAW, Sybase называет его IMAGE, Informix называет его BYTE, а DB2 называет го LONG VARCHAR FOR BIT DATA. Поэтому, если переносимость между базами данных является вашей целью, вы должны попробовать обойтись только основными идентификаторами SQL типов.

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

JDBC, как и многие API в Java, предназначен для упрощения. Вызовы методов, которые вы делаете, соответствует логическим операциям, которые вы думаете выполнить для сбора данных из базы данных: подключиться к базе данных, создать выражение и выполнить запрос, затем посмотреть результирующую выборку.

Для получения платформонезависимости, JDBC предоставляет менеджер драйверов (driver manager) который динамически использует все объекты драйверов, которые необходимы для опроса вашей базы данных. Так что если у вас есть базы данных от трех производителей, к которым вы хотите подсоединиться, вам нужно три различных объекта драйверов. Объекты драйверов регистрируют себя с помощью менеджера драйверов вл время загрузки, а вы можете принудительно выполнить загрузку, используя Class.forName( ).

Для открытия базы данных вы должны создать “URL базы данных”, котрый указывает:

  1. Что вы используете JDBC с помощью “jdbc.”
  2. “Подлежащий протокол”: имя драйвера или имя механизма соединения с базой данных. Так как назначение JDBC было вдохнавлено ODBC, первый доступный подлежащий протокол - это “jdbc-odbc мост”, обозначаемый “odbc”.
  3. Идентификатор базы данных. Он варьируется в зависимости от используемого драйвера базы данных, но обычно предоставляет логическое имя, которое отображается програмным обеспечением администрирования базы данных на физический директорий, в котором расположены таблицы базы данных. Для вас иденификатор базы данных имеет различные значения, вы должны зарегистрировать имя, используя ваше програмное обеспечение администирования базы данных. (Процесс регистрации различен для разных платформ.)

Вся эта информация комбинируется в одну строку: “URL базы даных”. Например, для подключения черед подлежащий протокол ODBC к базе данных с идентификатором “people”, URL базы данных может быть:

String dbUrl = "jdbc:odbc:people";

Если вы подключаетесь по сети, URL базы данных будет содержать информацию для подключения, идентифицирующую удаленную машину и может быть немного пугающим. Вот пример работы с базой данных CloudScape, которую вызывает удаленных клиент, использующий RMI:

jdbc:rmi://192.168.170.27:1099/jdbc:cloudscape:db

Этот URL базы данных на самом деле содержит два jdbc вызова в одном. Первая часть “jdbc:rmi://192.168.170.27:1099/” использует RMI для создания соединения с удаленной машиной баз данных, следящей за портом 1099 по IP адресу 192.168.170.27. Вторая часть URL, “jdbc:cloudscape:db” передает более привычные установки, используя подлежащий протокол и имя базы данных, но это произойдет только после того, как первая секция установит соединение с удаленной машиной через RMI.

Когда вы готовы присоединиться к базе данных, вызовите статический (static) метод DriverManager.getConnection( ) и передайте ему URL базы данных и пароль для входа в базу данных. Обратно вы получите объект Connection, который затем вы можете использовать для опроса и манипуляций с базой данных.

Следующий пример открывает контактную информацию базы данных и ищет имя человека, переданное из командной строки. Он выбирает только имена людей, имеющий электронные адреса, затем печатает те из них, имя которых совпадает с заданным:

//: c15:jdbc:Lookup.java
// Поиск электронных адресов в
// локальной базе данных с помощью JDBC.
import java.sql.*;

public class Lookup {
  public static void main(String[] args) 
  throws SQLException, ClassNotFoundException {
    String dbUrl = "jdbc:odbc:people";
    String user = "";
    String password = "";
    // Загружаем драйвер (регистрируем себя)
    Class.forName(
      "sun.jdbc.odbc.JdbcOdbcDriver");
    Connection c = DriverManager.getConnection(
      dbUrl, user, password);
    Statement s = c.createStatement();
    // SQL код:
    ResultSet r = 
      s.executeQuery(
        "SELECT FIRST, LAST, EMAIL " +
        "FROM people.csv people " +
        "WHERE " +
        "(LAST='" + args[0] + "') " +
        " AND (EMAIL Is Not Null) " +
        "ORDER BY FIRST");
    while(r.next()) {
      // Регистр не имеет значения:
      System.out.println(
        r.getString("Last") + ", " 
        + r.getString("fIRST")
        + ": " + r.getString("EMAIL") );
    }
    s.close(); // Закрываем ResultSet
  }
} ///:~

Вы можете увидеть создание URL базы данных, как это описано выше. В этом примере нет защитного пароля для базы данных, поэтому имя пользователя и пароль представлены пустыми строками.

Как только соединение установлено с помощью DriverManager.getConnection( ), вы можете использовать полученный объект Connection для создания объекта Statement, используя метод createStatement( ). С помощью Statement вы можете вызвать executeQuery( ), передав в него строку, содержащую SQL выражение стандарта SQL-92. (Скоро вы увидите как вы можете генерировать это выражение автоматически, так что вам не нужно много знать об SQL.)

Метод executeQuery( ) возвращает объект ResultSet, который является итератором: метод next( ) перемещает итератор на следующую запись в выражении или возвращает false, если достигнут конец результирующего множества. Вы всегда получите назад объект ResultSet от executeQuery( ), даже если результатом запроса является пустое множество (если так, исключение не возникает). Обратите внимание, чтовы должны вызвать next( ) прежде, чем попробовать прочесть любую запись. Если результирующее множество - пустое, этот первый вызов next( ) вернет false. Для каждой записи результирующего множества вы можете выбрать поля, используя (наряду с другими подходами) имя поля, как строку. Также обратите внимание, что регистр в имени поля игнорируется — это не так с базой SQL данных. Вы определяете тип, который получите, вызвав getInt( ), getString( ), getFloat( ) и т.д. В этом месте вы получаете данные из вашей базы данных в родном формате Java и можете делать с ними все, что хотите, используя обычный Java код.

Получение примера для работы

При использовании JDBC понимание кода относительно проще. Самое сложное - это заставить его работать на вашей конкретной системе. Причина сложности в том, что при этом от вас требуется, чтобы вы понимали как правильно загрузить ваш JDBC и как установить базу данных, используя ваше программное обеспечение администрирования базы данных.

Конечно этот процесс может радикально отличаться на разных машинах, но тот алгоритм, который я применил для 32-bit Windows, может дать вам ключ к решению и поможет разобраться в вашей собственной ситуации.

Шаг 1: Поиск JDBC Драйвера

Программа, приведенная выше, содержит инструкцию:

Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

Это означает структуру директориев, которая вводит в заблуждение. С данной конкретной установкой JDK 1.1 небыло файла, называемого JdbcOdbcDriver.class, так что если вы, взглянув на этот пример, пошли бы искать его, вы были бы расстроены. Другой опудликованный пример использует псевдо имя, такое как “myDriver.ClassName”, которое помгает еще меньше. Фактически, приведенное выше выражение загрузки jdbc-odbc драйвера (только того, который реально поставляется с JDK) возникает только в некоторых местах онлайн документации (обычно на страницах, помеченных “JDBC-ODBC Bridge Driver”). если преведенная выше инструкция не работает, это значит что имя могло измениться вместе со сменой версии Java, так что вы должны снова углубиться в документацию.

Если инструкция загрузки неверна, вы получите исключение в этом месте. Чтобы проверить, что ваша инструкция загрузки работает правильно, закоментируйте код после инструкции вплоть до выражения catch. Если программа не выбрасывает исключений, это означает, что драйвер загружен правильно.

Шаг 2: Конфигурирование базы данных

Опять таки, это специфично для 32-bit Windows. Вам может понадобиться выполнить определенное исследование, чтобы определить что нужно для вашей конкретной платформы.

Во-первых, откройте контрольную панель. вы можете найти две иконки с надписью “ODBC”. Вы должны использовать ту, на под которой написано “32bit ODBC”, так как другая иконка предназначена для обратной совместимости с программным обеспечением 16-bit ODBC и не дас результатов для JDBC. Когда вы откроете иконку “32bit ODBC”, вы увидите диаог с несколькими закладками, включая “User DSN”, “System DSN”, “File DSN” и т. д. в которых “DSN” означает “Data Source Name”. Это не имеет значения для JDBC-ODBC моста, важна только установка вашей базы данных в “System DSN”, но вы также можете протестировать вашу конфигурацию и сделать опрос, найдя то, что вам необходимо для установки вашей базы данных в “File DSN”. Это можно делать с помощью инструмента Microsoft Query (который поставляется вместе с Microsoft Office), чтобы найти базу данных. Имейте в виду, что инструмент опроса есть и у других произвдителей.

Наиболее интересной базой данных является та, которую вы уже использовали. Стандарт ODBC поддерживает нескоько различных форматов файлов, включая такую почтенную рабочую лошадку, как DBase. Однако, он также включает простой формат “разделения запятой ASCII”, который может записывать фактически каждый инструмент рабты с данными. В моем случае я просто взял базу данных “people”, которую поддерживал до этого долгие годы, используя различные инсрументы управления и экспортировал ее в ASCII файл с разделением запятыми (обычно такие файлы имеют расширение .csv). В разделе “System DSN” я выбрал “Add”, выбрал текстовый дравер для обработки моего ASCII файла, а затем снял пометку с “использовать текущий каталог” (“use current directory”), что позволило мне указать директорий, в который я экспортировал файл данных.

Вы заметите, когда сделаете это, что на самом деле вы не указываете файл, а только директорий. Это происходит потому, что обычно база данных представляет набор файлов в одном директории (хотя она точно так же может быть представлениа и в другой форме). Каждый файл обычно содержит единственную таблицу базы данных, а SQL инструкции могут производить результат, который собирается из различных таблиц базы данных (это называется объединением (join)). База данных, содержащая только одну таблицу (как моя база даных “people”), обычно называется flat-file database. Большинство проблем, связанных с простым хранением и получением данных, обычно требуют нескольких таблиц, которые для получения желаемого результата должны быть связаны путем объединения, и это называется реляционной (relational) базой данных.

Шаг 3: Проверка конфигурации

Для проверки конфигурации вам необходим способ, с помощью которого вы можете определить видна ли база данных для программы, которая ее опрашивает. Конечно вы можете просто запустить пример JDBC программы, приведенный выше, и включить инструкцию:

Connection c = DriverManager.getConnection(
  dbUrl, user, password);

Если выброшено исключение, ваша конфигурация некорректна.

Однако в этом месте полезно причлечь инструмент генерации запросов. Я использовал Microsoft Query, который поставляется с Microsoft Office, но вы можете предпочесть что-то другое. Инструмент опроса должен знать где находится база данных, и Microsoft Query требовал, чтобы я запустил ODBC Администратор и в закладке “File DSN” добавил новый элемент, опять указав текстовый драйвер и дерикторий, в котором хранится моя база данных. Вы можете задать какое хотите имя элемента, но полезно использовать то же самое имя, которое задействовано в “System DSN”.

Как только вы сделаете это, вы увидите, что ваша база данных доступна при создании нового запроса с вашим инструментом запросов.

Шаг 4: Генерация вашего SQL запроса

Запрос, который я создал с помошью Microsoft Query, не только показал мне, что моя база даных на месте и впорядке, но также автоматически создал SQL код, который необходим мне для вставки в мою Java программу. Мне нужен был запрос, который искал бы записи, имеющие в поле имени значение, совпадающее с напечатанным в командной строке при запуске Java программы. Для начала я искал определенное имя: “Eckel”. Я также хотел отображать только те имена, которые ассоциированы с электронным адресом. Я сделал для генерации запроса следующее:

  1. Запустите новый запрос и используйте Query Wizard. Выберите базу данных “people”. (Это эквивалентно открытию соединения с базой данных при использовании соответствующего URL базы данных.)
  2. Выберите таблицу “people” из базы данных. Из таблицы выберите колонки FIRST, LAST и EMAIL.
  3. Под “Filter Data” выберите LAST и выберите “equals” с аргуменом “Eckel”. Нажмите радио кнопку “And”.
  4. Выберите EMAIL и выберите “Is not Null”.
  5. Под “Sort By” выберите FIRST.

Результат запроса покажет вам выбрали ли вы то, что хотели.

Теперь вы можете нажать кнопку SQL и, не проводя никаких исследований со своей стороны, получить корректный SQL код, готовый для употребления. Для этого запроса он выглядит так:

SELECT people.FIRST, people.LAST, people.EMAIL
FROM people.csv people
WHERE (people.LAST='Eckel') AND 
(people.EMAIL Is Not Null)
ORDER BY people.FIRST

При более сложных запросах легко ошибиться, но при использовании инструмента построения запроса вы можете интерактивно протестировать ваш запрос и автоматически сгенерировать корректный код. Трудно найти аргументы в пользу построения запросов в ручную.

Шаг 5: Изменеие и вставка в ваш запрос

Вы могли заметить, что приведенный выше код отличается от кода, использованного в программе. Это происходит потому, инструмент запросов использует полные квалификаторы для всех имен, даже если вовлечена только одна таблица. (Когда задействовано больше таблиц, квалификаторы предотвращают коллизии между колонками из разных таблиц, имеющих одинаковые имена.) Так как этот запрос использует только одну таблицу, вы можете по желанию удалить квалификатор “people” из большинства имен, как приведено ниже:

SELECT FIRST, LAST, EMAIL
FROM people.csv people
WHERE (LAST='Eckel') AND 
(EMAIL Is Not Null)
ORDER BY FIRST

Кроме того, вам не нужна программа, ищущая только одно имя. Вместо этого должны выбираться имена, заданные в командной стоке. Сделаем эти изменения и включим SQL выражение в динамическую генерацию String:

"SELECT FIRST, LAST, EMAIL " +
"FROM people.csv people " +
"WHERE " +
"(LAST='" + args[0] + "') " +
" AND (EMAIL Is Not Null) " +
"ORDER BY FIRST");

SQL имеет другой способ вставки имен в запрос, называемый хранимая процедура (stored procedures), которая используется для ускорения. Но для боьшинства ваших экспериментов с базой данных и для вашего первого опыта, построение ваших собственных запросов в Java удобно.

Из этого примера вы видите, что при использовании доступных в настоящее время — обычно это инструменты построения запросов — программирование с SQL и JDBC может быть достаточно простым.

GUI версия программы поиска

Более полезно оставлять программу поиска постоянно работающей все время и просто переключаться на нее и впечатывать то имя, которое вы хотите найти. Далее приведена программа поиска, выполненная как приложение/аппдет, также в нее добавлено свойство автоматического завершения ввода имени так, что вам нет необходимости набирать имя до конца:

//: c15:jdbc:VLookup.java
// GUI версия Lookup.java.
// <applet code=VLookup
// width=500 height=200></applet>
import javax.swing.*; 
import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import java.sql.*;
import com.bruceeckel.swing.*;

public class VLookup extends JApplet {
  String dbUrl = "jdbc:odbc:people";
  String user = "";
  String password = "";
  Statement s;
  JTextField searchFor = new JTextField(20);
  JLabel completion = 
    new JLabel("                        ");
  JTextArea results = new JTextArea(40, 20);
  public void init() {
    searchFor.getDocument().addDocumentListener(
      new SearchL());
    JPanel p = new JPanel();
    p.add(new Label("Last name to search for:"));
    p.add(searchFor);
    p.add(completion);
    Container cp = getContentPane();
    cp.add(p, BorderLayout.NORTH);
    cp.add(results, BorderLayout.CENTER);
    try {
      // Загружаем драйвер (регистрируем себя)
      Class.forName(
        "sun.jdbc.odbc.JdbcOdbcDriver");
      Connection c = DriverManager.getConnection(
        dbUrl, user, password);
      s = c.createStatement();
    } catch(Exception e) {
      results.setText(e.toString());
    }
  }
  class SearchL implements DocumentListener {
    public void changedUpdate(DocumentEvent e){}
    public void insertUpdate(DocumentEvent e){
      textValueChanged();
    }
    public void removeUpdate(DocumentEvent e){
      textValueChanged();
    }
  }
  public void textValueChanged() {
    ResultSet r;
    if(searchFor.getText().length() == 0) {
      completion.setText("");
      results.setText("");
      return;
    }
    try {
      // Завершение ввода имени:
      r = s.executeQuery(
        "SELECT LAST FROM people.csv people " +
        "WHERE (LAST Like '" +
        searchFor.getText()  + 
        "%') ORDER BY LAST");
      if(r.next()) 
        completion.setText(
          r.getString("last"));
      r = s.executeQuery(
        "SELECT FIRST, LAST, EMAIL " +
        "FROM people.csv people " +
        "WHERE (LAST='" + 
        completion.getText() +
        "') AND (EMAIL Is Not Null) " +
        "ORDER BY FIRST");
    } catch(Exception e) {
      results.setText(
        searchFor.getText() + "\n");
      results.append(e.toString());
      return; 
    }
    results.setText("");
    try {
      while(r.next()) {
        results.append(
          r.getString("Last") + ", " 
          + r.getString("fIRST") + 
          ": " + r.getString("EMAIL") + "\n");
      }
    } catch(Exception e) {
      results.setText(e.toString());
    }
  }
  public static void main(String[] args) {
    Console.run(new VLookup(), 500, 200);
  }
} ///:~

Большая часть логики работы с базой данных осталась прежней, но вы можете видеть, что добавлен DocumentListener, чтобы следить за JTextField (более детально смотрите javax.swing.JTextField в HTML документации по Java на java.sun.com), так что когда бы вы не напечатали новый сивол, сначала выполнятся попытка завершения имени путем поиска имени в базе данных по введенным первым символам. (Имя завершения помещается в completion JLabel и используется как текст поиска.) Таким образом, как только вы напечатаете достаточно символов, чтобы программа уникально нашала имя, которое вы хотите искать, вы можете остановиться.

Почему JDBC API выглядит так сложно

Когда вы просмотрите онлайн документацию по JDBC, она может испугать. В частности, интерфейс DatabaseMetaData, который просто огромен, в противоположность большинству интерфейсов, вивденных вами в Java. У него есть такие методы, как dataDefinitionCausesTransactionCommit( ), getMaxColumnNameLength( ), getMaxStatementLength( ), storesMixedCaseQuotedIdentifiers( ), supportsANSI92IntermediateSQL( ), supportsLimitedOuterJoins( ) и так далее. Что вы думаете об этом?

Как упоминалось ранее, базы данных от начала до конца выглядят беспорядочно, в основном поэтому требуются прилажения и инструменты по работе с базами данных. Только недавно появились общие соглашения по языку SQL (и в общем употреблении существует множество других языков работы с базами данных). Но даже при существовании “стандартного” SQL есть так много вариаций этой темы, из-за чего JDBC должен предоставлять такой огромный интерфейс DatabaseMetaData, чтобы ваш код мог обнаруживать совместимость подключенной в настоящее время базы данных с определенным “стандартом” SQL. Короче говоря, вы можете писать на простом, переносимом SQL, но если вы хотите оптимизировать скорость, ваш код черезвычайно расширится, если вы будете исследовать совместимость базы данных со свойствами определенного производителя.

Конечно, Java в этом не виноват. JDBC просто пытается компенсировать расхождения между базами данных разных производителей. Но держите в уме, что ваша жизнь будет проще, если вы сможете писать общие запросы и не будете беспокоиться так много о производительности, или, если вы должны настраивать производительность, знать платформу, для которой вы пишите, чтобы вам не нужно было писать весь исследующий код.

Более изощренный пример

Более интересный пример [73] задействует множественную базу данных, расположенную на сервере. Здесь в качестве базы данных используется хранилище для ведения журнала обращений, чтобы позволить людям регестрировать события, поэтому она называется Community Interests Database (CID). Этот пример будет обеспечивать только просмотр базы данных и ее реализации, и не предназначен быть полным руководством по разработке баз данных. Есть множество книг, семинаров и пакетов программ, которые помогут вам при проектировании и разработке базы данных.

Кроме того, этот пример предполагает, что проведена предварительная установка SQL базы данных на сервере (хотя это может быть запущено и на локальной машине), а так же опрос и обнаружение подходящего JDBC драйвера для базы данных. Существуют несколько бесплатных SQL баз данных, и некоторые из них автоматически устанавливаются с различными версиями Linux. Вы сами отвечаете за выбор базы данных и поиск JDBC драйвера. Приведенный здесь пример основывается на SQL базе данных системы, называемой “Cloudscape”.

Чтобы упростить изменения в информации о соединении, драйвер базы данных, URL базы данных, имя пользователя и пароль помещены в отдельный класс:

//: c15:jdbc:CIDConnect.java
// Информация для подключения к базе данных
// community interests database (CID).

public class CIDConnect {
  // Вся информация спецефична для CloudScape:
  public static String dbDriver = 
    "COM.cloudscape.core.JDBCDriver";
  public static String dbURL =
    "jdbc:cloudscape:d:/docs/_work/JSapienDB";
  public static String user = "";
  public static String password = "";
} ///:~

В этом примере нет пароля защиты базы данных, поэтому имя пользователя и пароль представлены пустыми строками.

База данных состоит из множества таблиц, структура которых показана ниже:


“Members” содержит информацию о пользователе, “Events” и “Locations” содержат информацию о подключении и откуда оно было сделано, а “Evtmems” объединяет события и пользователей, которые хотят знать о событиях. Можно видеть, что данные в одной таблице являются ключами в другой таблице.

Следующий класс содержит SQL строки, которые создают эти таблицы базы данных (обратитесь к руководству по SQL, чтобы узнать объяснения этого кода):

//: c15:jdbc:CIDSQL.java
// SQL строки для создания таблиц CID.

public class CIDSQL {
  public static String[] sql = {
    // Создание таблицы MEMBERS:
    "drop table MEMBERS",
    "create table MEMBERS " +
    "(MEM_ID INTEGER primary key, " +
    "MEM_UNAME VARCHAR(12) not null unique, "+
    "MEM_LNAME VARCHAR(40), " +
    "MEM_FNAME VARCHAR(20), " +
    "ADDRESS VARCHAR(40), " +
    "CITY VARCHAR(20), " +
    "STATE CHAR(4), " +
    "ZIP CHAR(5), " +
    "PHONE CHAR(12), " +
    "EMAIL VARCHAR(30))",
    "create unique index " +
    "LNAME_IDX on MEMBERS(MEM_LNAME)",
    // Создание таблицы EVENTS
    "drop table EVENTS",
    "create table EVENTS " +
    "(EVT_ID INTEGER primary key, " +
    "EVT_TITLE VARCHAR(30) not null, " +
    "EVT_TYPE VARCHAR(20), " +
    "LOC_ID INTEGER, " +
    "PRICE DECIMAL, " +
    "DATETIME TIMESTAMP)",
    "create unique index " +
    "TITLE_IDX on EVENTS(EVT_TITLE)",
    // Создание таблицы EVTMEMS
    "drop table EVTMEMS",
    "create table EVTMEMS " +
    "(MEM_ID INTEGER not null, " +
    "EVT_ID INTEGER not null, " +
    "MEM_ORD INTEGER)",
    "create unique index " +
    "EVTMEM_IDX on EVTMEMS(MEM_ID, EVT_ID)",
    // Создание таблицы LOCATIONS
    "drop table LOCATIONS",
    "create table LOCATIONS " +
    "(LOC_ID INTEGER primary key, " +
    "LOC_NAME VARCHAR(30) not null, " +
    "CONTACT VARCHAR(50), " +
    "ADDRESS VARCHAR(40), " +
    "CITY VARCHAR(20), " +
    "STATE VARCHAR(4), " +
    "ZIP VARCHAR(5), " +
    "PHONE CHAR(12), " +
    "DIRECTIONS VARCHAR(4096))",
    "create unique index " +
    "NAME_IDX on LOCATIONS(LOC_NAME)",
  };
} ///:~

Следующая программа использует информацию CIDConnect и CIDSQL для загрузки JDBC драйвера, создания соединения с базой данных и создания таблиц, структура которых показана на диаграмме. Для соединения с базой данных вы вызываете статический (static) метод DriverManager.getConnection( ), передавая в него URL базы данных, имя пользователя и пароль для доступа к базе данных. Назад вы получаете объект Connection, который вы можете использовать для опроса и манипуляций с базой данных. Как только соединение создано, вы можете просто поместить SQL в базу данных, в этом случае проходя по массиву CIDSQL. Однако при первом запуске этой программы команда “drop table” завершиться неудачей, что станет причиной исключения, которое будет поймано, объявлено и проигнорировано. Необходимость команды “drop table” легко понять из экспериментов: вы можете измнить SQL, который определяет таблицы, а затем вернуться в программу. При этом возникнет необходимость заменить старые таблицы новыми.

В этом примере есть смысл позволить исключениям отображаться на консоли:

//: c15:jdbc:CIDCreateTables.java
// Создание таблиц базы данных для
// community interests database.
import java.sql.*;

public class CIDCreateTables {
  public static void main(String[] args) 
  throws SQLException, ClassNotFoundException,
  IllegalAccessException {
    // Загрузка драйвера (саморегистрация)
    Class.forName(CIDConnect.dbDriver);
    Connection c = DriverManager.getConnection(
      CIDConnect.dbURL, CIDConnect.user, 
      CIDConnect.password);
    Statement s = c.createStatement();
    for(int i = 0; i < CIDSQL.sql.length; i++) {
      System.out.println(CIDSQL.sql[i]);
      try {
        s.executeUpdate(CIDSQL.sql[i]);
      } catch(SQLException sqlEx) {
        System.err.println(
          "Probably a 'drop table' failed");
      }
    }
    s.close();
    c.close();
  }
} ///:~

Обратите внимание, что все изменения в базе данных могут управляться путем изменения строк (String) в таблице CIDSQL, при этом CIDCreateTables не меняется.

executeUpdate( ) обычно возвращает число строк, которые были получены при воздействии SQL инструкции. executeUpdate( ) чаще всего используется для выполнения таких инструкций, как INSERT, UPDATE или DELETE, чтобы изменить одну или более строк. Для таких инструкций, как CREATE TABLE, DROP TABLE и CREATE INDEX, executeUpdate( ) всегда возвращает ноль.

Для проверки базы данных она загружается некоторыми простыми данными. Это требует нескольких INSERT'ов, за которыми следует SELECT для получения результирующего множества. Чтобы облегчить проверку добавления и изменения данных, тестовые данные представлены в виде двумерного массива типа Object, а метод executeInsert( ) затем может использовать информацию из одной строки для создания соответствующей SQL команды.

//: c15:jdbc:LoadDB.java
// Loads and tests the database.
import java.sql.*;

class TestSet {
  Object[][] data = {
    { "MEMBERS", new Integer(1),
      "dbartlett", "Bartlett", "David",
      "123 Mockingbird Lane",
      "Gettysburg", "PA", "19312",
      "123.456.7890",  "bart@you.net" },
    { "MEMBERS", new Integer(2),
      "beckel", "Eckel", "Bruce",
      "123 Over Rainbow Lane",
      "Crested Butte", "CO", "81224",
      "123.456.7890", "beckel@you.net" },
    { "MEMBERS", new Integer(3),
      "rcastaneda", "Castaneda", "Robert",
      "123 Downunder Lane",
      "Sydney", "NSW", "12345",
      "123.456.7890", "rcastaneda@you.net" },
    { "LOCATIONS", new Integer(1),
      "Center for Arts",
      "Betty Wright", "123 Elk Ave.",
      "Crested Butte", "CO", "81224",
      "123.456.7890",
      "Go this way then that." },
    { "LOCATIONS", new Integer(2),
      "Witts End Conference Center",
      "John Wittig", "123 Music Drive",
      "Zoneville", "PA", "19123",
      "123.456.7890",
      "Go that way then this." },
    { "EVENTS", new Integer(1),
      "Project Management Myths",
      "Software Development",
      new Integer(1), new Float(2.50),
      "2000-07-17 19:30:00" },
    { "EVENTS", new Integer(2),
      "Life of the Crested Dog",
      "Archeology",
      new Integer(2), new Float(0.00),
      "2000-07-19 19:00:00" },
    // Сопоставление людей и событий
    {  "EVTMEMS", 
      new Integer(1),  // Dave is going to
      new Integer(1),  // the Software event.
      new Integer(0) },
    { "EVTMEMS", 
      new Integer(2),  // Bruce is going to
      new Integer(2),  // the Archeology event.
      new Integer(0) },
    { "EVTMEMS", 
      new Integer(3),  // Robert is going to
      new Integer(1),  // the Software event.
      new Integer(1) },
    { "EVTMEMS", 
      new Integer(3), // ... and 
      new Integer(2), // the Archeology event.
      new Integer(1) },
  };
  // Use the default data set:
  public TestSet() {}
  // Use a different data set:
  public TestSet(Object[][] dat) { data = dat; }
}

public class LoadDB {
  Statement statement;
  Connection connection;
  TestSet tset;
  public LoadDB(TestSet t) throws SQLException {
    tset = t;
    try {
      // Load the driver (registers itself)
      Class.forName(CIDConnect.dbDriver);
    } catch(java.lang.ClassNotFoundException e) {
      e.printStackTrace(System.err);
    }
    connection = DriverManager.getConnection(
      CIDConnect.dbURL, CIDConnect.user, 
      CIDConnect.password);
    statement = connection.createStatement();
  }
  public void cleanup() throws SQLException {
    statement.close();
    connection.close();
  }
  public void executeInsert(Object[] data) {
    String sql = "insert into " 
      + data[0] + " values(";
    for(int i = 1; i < data.length; i++) {
      if(data[i] instanceof String)
        sql += "'" + data[i] + "'";
      else
        sql += data[i];
      if(i < data.length - 1)
        sql += ", ";
    }
    sql += ')';
    System.out.println(sql);
    try {
      statement.executeUpdate(sql);
    } catch(SQLException sqlEx) {
      System.err.println("Insert failed.");
      while (sqlEx != null) {
        System.err.println(sqlEx.toString());
        sqlEx = sqlEx.getNextException();
      }
    } 
  }
  public void load() {
    for(int i = 0; i< tset.data.length; i++)
      executeInsert(tset.data[i]);
  }
  // Выбрасываем исключение на консоль:
  public static void main(String[] args) 
  throws SQLException {
    LoadDB db = new LoadDB(new TestSet());
    db.load();
    try {
      // Получаем ResultSet из загруженной базы данных:
      ResultSet rs = db.statement.executeQuery(
        "select " +
        "e.EVT_TITLE, m.MEM_LNAME, m.MEM_FNAME "+
        "from EVENTS e, MEMBERS m, EVTMEMS em " +
        "where em.EVT_ID = 2 " +
        "and e.EVT_ID = em.EVT_ID " +
        "and m.MEM_ID = em.MEM_ID");
      while (rs.next())
        System.out.println(
          rs.getString(1) + "  " + 
          rs.getString(2) + ", " +
          rs.getString(3));
    } finally {
      db.cleanup();
    }
  }
} ///:~

Класс TestSet содержит множество данных по умолчанию, которое производится, если вы используете конструктор по умолчанию. Однако вы можете создать объект TestSet, используя альтернативный набор данных со вторым конструкторомo. Набор данных хранится в двумерном массиве типа Object, поскольку он может быть любого типа, включая String или числовые типы. Метод executeInsert( ) использует RTTI того, чтобы различать данные типа String (которые должны быть в кавычках) и данные не типа String, так как SQL команда строится из данных. После печати этой команды на консоль используется executeUpdate( ) для отсылки ее в базу данных.

Конструктор для LoadDB создает соединение и пошагово с помощью load( ) проходит по данным и вызывает executeInsert( ) для каждой записи. cleanup( ) закрывает инструкцию и соединение. Чтобы гарантировать этот вызов, он помещен в предложение finally.

Как только база данных будет загружена, инструкция executeQuery( ) производит простое результирующее множество. Так как запрос комбинирует несколько таблиц, он является примером объединения.

Более подробно о JDBC можно узнать в электронной документации, которая распространяется как часть пакета Java от Sun. Кроме того, вы можете найти дополнительную информацию в книге JDBC Database Access with Java (Hamilton, Cattel, and Fisher, Addison-Wesley, 1997). Другие книги, посвященные JDBC, появляются регулярно.

Сервлеты

Доступ клиентов из Интернета или корпоративной сети бесспорно является наиболее простым способом доступа многих пользователей к данным и ресурсам [74]. Этот тип доступа основывается на клиентах, использующих стандарт World Wide Web или Hypertext Markup Language (HTML) и Hypertext Transfer Protocol (HTTP). Servlet API устанавливает общую структуру решения для удовлетворения запросам HTTP.

Традиционным способом решения такой проблемы, как изменение Интернет-клиентом базы данных, было создание HTML страницы с текстовыми полями и кнопкой “submit”. Пользователь впечатывал соответствующую инфрмацию и текстовых полях и нажимал кнопку “submit”. Данные отправляются на URL, который говорит серверу что с ними делать, указывая местоположение Common Gateway Interface (CGI) программы, которую запускает сервер, обеспечивая программу данными при вызове. CGI программы обычно пишутся на Perl, Python, C, C++ или любом другом языке, который может читать со стандартного ввода и писать в стандартный вывод. Таким образом, все, что делает Web сервер, это запуск CGI программы, а для ввода и вывода используются стандартные потоки (или, иногда для ввода используются переменные окружения). За все остальное отвечает CGI программа. Сначала она проверяет данные и решает корректный ли у них формат. Если это не так, CGI программа должна создать HTML, чтобы указать на проблему; эта страница посылается Web серверу (через стандартный вывод из CGI программы), Который отсылает ее пользователю. Пользователь должен вернуться к предыдущей странице и попробовать вновь. Если данные корректны, CGI программа обрабатывает данные соответствующим способом и, возможно, вставляет их в базу данных. Затем она должна создать соответствующую HTML страницу для Web сервера, которая будет возвращена пользователю.

Это было бы идеально для перехода ан полностью Java-ориентированное решение такой проблемы — апплет на стороне клиента проверяет и отсылает данные, а сервлет на стороне сервера получает и обрабатывает их. К сожалению, хотя апплеты и поддерживают технологию с достаточной поддержкой, их проблематично использовать в Web, поскольку вы не можете рассчитывать, что определенная версия Java, поддерживается на клиентском Web броузере. Фактически, вы не можете полагаться, что Web броузер вообще поддерживает Java! В Интранет вы можете требовать определенный уровень поддержки, который позволит создать гораздо большую гибкость в том, что вы делаете, но для Web наиболее безопасным подходом является выполнение всей обработки на стороне сервера и возвращение клиенту просого HTML кода. При этом подходе никакой пользователь не будет отвергнут из-за того, что у него нет правильно установленного программного обеспечения.

Поскольку сервлеты обеспечивают великолепное решение для программирования на стороне сервера, они являются наиболее популярным объяснением перехода на Java. Не только потому, что они обеспечивают рабочее пространство, заменяющее CGI программирование (и снижают количество сложных CGI проблем), но и весь ваш код становится переносимым между платформами при использовании Java, а вы получаете доступ ко всему Java API (исключая, конечно, то, которое поизводит GUI, например Swing).

Основы сервлетов

Архитектура API сервлетов состоит в том, что классический сервис обеспечивается методом service( ), через который сервлету посылаются все клиентские запросы, и методами жизненного цикла init( ) и destroy( ), которые вызываются только при загрузке и выгрузке сервлета (это исключительные стуации).

public interface Servlet {
  public void init(ServletConfig config)
    throws ServletException;
  public ServletConfig getServletConfig();
  public void service(ServletRequest req,
    ServletResponse res) 
    throws ServletException, IOException;
  public String getServletInfo();
  public void destroy();
}

Изюминка getServletConfig( ) состоит в том, что он возвращает объект ServletConfig, который содержит параметры инициализации и запуска для этого сервлета. getServletInfo( ) возвращает строку, содержащую информацию о сервлете, такую, как имя автора, версию и авторское право.

Класс GenericServlet является реализацией оболочки этого интерфейса и обычно не используется. Класс HttpServlet является расширением GenericServlet и предназначен специально для обработки протокола HTTP — HttpServlet один из тех классов, которые вы будете использовать чаще всего.

Наиболее удобным инструментом сервлетного API является внешние объекты, которые идут вместе с классом HttpServlet для его поддержки. Если вы взглянете на метод service( ) из интерфейса Servlet, вы увидите, что он имеет два параметра: ServletRequest и ServletResponse. У класса HttpServlet эти два объекта расширяются на HTTP: HttpServletRequest и HttpServletResponse. Вот простой пример, который показывает использование HttpServletResponse:

//: c15:servlets:ServletsRule.java
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class ServletsRule extends HttpServlet {
  int i = 0; // Servlet "persistence"
  public void service(HttpServletRequest req, 
  HttpServletResponse res) throws IOException {
    res.setContentType("text/html");
    PrintWriter out = res.getWriter();
    out.print("<HEAD><TITLE>");
    out.print("A server-side strategy");
    out.print("</TITLE></HEAD><BODY>");
    out.print("<h1>Servlets Rule! " + i++);
    out.print("</h1></BODY>");  
    out.close();    
  }
} ///:~

ServletsRule настолько прост, насколько может быть прост сервлет. Сервлет инициализиуется только однажды путем вызова своего метода init( ) при загрузке сервлета после того, как сначала загрузиться контейнер сервлетов. Когда клиент делает запрос к URL, соответствующий представленному сервлету, контейнер сервлетов перехватывает этот запрос и делает вызов метода service( ), затем устанавливает объекты HttpServletRequest и HttpServletResponse.

Главная забота метода service( ) состоит во взаимодействии с HTTP запросом, посланным клиентом, и построение HTTP ответа, основываясь на аттрибутах, содержащихся в запросе. ServletsRule манипулирует объектом ответа не зависимот от того, что мог послать клиент.

После установки типа содержимого ответа (что всегда должно быть сделано перед созданием Writer или OutputStream), метод getWriter( ) объекта ответа производит объект PrintWriter, который используется для написания символьного ответа (другой подход: getOutputStream( ) производит OutputStream, используемы для бинарного ответа, который походит только для специальных решений).

Оставшаяся часть программы просто посылает клиенту HTML (тут предполагается, что вы понимаете HTML, так что эта часть не объясняется) в виде последователности String. Однако, обратите внимание на включение “счетчика посещений”, представленного переменной i. Здесь выполняется автоматическая конвертация в String в инструкции print( ).

Когда вы запустите программу, вы увидите, что значение i сохраняется между запросами к сервлету. Это важное свойство сервлетов: так как только один сервлет определенного класса загружается в контейнер, и он никогда не выгружается (только в случае, если контейнер сервлетов завершает работу, что обычно случается, если вы перезагружаете машину), любые поля сервлета этого класса загружаются в контейнер и становятся устойчивыми объектами! Это означает, что вы можете без особого труда сохранять значения между запросами к сервлету, а при использовании CGI вы должны записывать значения на диск, чтобы сохранить его, что требует некоторое количество искусственности, а в результате получаем ен кросс-платформенное решение.

Конечно иногда Web сервер и, соответственно, контейнер сервлетов должен быть перегружен как часть процесса поддержки или из-за проблем с питанием. Для предотвращения потери любой пристутствующей информации автоматически вызываются методы сервлета init( ) и destroy( ) при любой загрузке или выгрузке сервлета, что дает вам возможность сохранить данные при выключении и восстановить их после перезагрузки. Контейнер сервлетов вызывает метод destroy( ), как только он прекращает работу, так что вы всегда имеете удобный случай сохранить важные данные.

Есть еще одна проблема при использовании HttpServlet. Этот класс имеет методы doGet( ) и doPost( ), которые отличаются от метода “GET” CGI получения от клиента, и метода CGI “POST”. GET и POST отличаются только в деталях способами, которыми они передают данные, что лично я предпочитаю игнорировать. Однако чаще всего публикуется информация, из того, что видел я, которая одобряет создание отдельных методов doGet( ) и doPost( ) вместо единого общего метода service( ), который обрабатывает оба случая. Это предпочтение кажется достаточно общим, но я никогда не видел объяснения, которое заставило бы меня поверить, что это не просто интертность мышления CGI программистов, которые привыкли обращать внимание, используется ли GET или POST. Так что в духе “упрощения всего насколько это возможно”[75], я буду использовать метод service( ) в этих примерах, и пусть он сам заботиться о GET'ах и POST'ах. Однако держите в уме, что я что-то упустил, что, возможно, является хорошей причиной в польху использования doGet( ) и doPost( ).

В любое время, когда форма передается сервлету, HttpServletRequest предварительно загружает все данные формы, хранящиеся в виде пар ключ-значение. Если вы знаете имена полей, вы можете просто использовать их напрямую с помощью метода getParameter( ) для получения значения. Вы также можете получить Enumeration (старая форма Iterator) на имена полей, как показано в следующем примере. Этот пример также демонстрирует как один сервлет может быть использован для генерации страницы, которая содержит форму, и для ответа на страницу (более удобное решение будет показано позже, при рассмотрении JSP). Если Enumeration пустое, значит полей нет. Это значит, что никакой формы не было передано. В этом случае содается форма, а кнопка посылки повторно вызывает этот же сервлет. Если же поля существуют, они показываются.

//: c15:servlets:EchoForm.java
// Дамп пар имен-значений из любой формы HTML
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;

public class EchoForm extends HttpServlet {
  public void service(HttpServletRequest req, 
    HttpServletResponse res) throws IOException {
    res.setContentType("text/html");
    PrintWriter out = res.getWriter();
    Enumeration flds = req.getParameterNames();
    if(!flds.hasMoreElements()) {
      // Форма не передавалась - создание формы:
      out.print("<html>");
      out.print("<form method=\"POST\"" + 
        " action=\"EchoForm\">");
      for(int i = 0; i < 10; i++)
        out.print("<b>Field" + i + "</b> " +
          "<input type=\"text\""+
          " size=\"20\" name=\"Field" + i + 
          "\" value=\"Value" + i + "\"><br>");
      out.print("<INPUT TYPE=submit name=submit"+
      " Value=\"Submit\"></form></html>");
    } else {
      out.print("<h1>Your form contained:</h1>");
      while(flds.hasMoreElements()) {
        String field= (String)flds.nextElement();
        String value= req.getParameter(field);
        out.print(field + " = " + value+ "<br>");
      }
    }
    out.close();    
  }
} ///:~

Здесь виден один из недостатков, заключающийся в том, что Java не кажеться предназначенной для обработки строк в уме — форматирование возвращаемой страницы достаточно неприятно из-за переводов строки, выделение знаков кавычки и символов “+”, необходимых для построения объекта String. С большими HTML страницами становится неразумно вносить этот код прямо в Java. Одо из решений - держать страницу в виде отдельного текстового файла, затем открывать и обрабатывать его на Web сервере. Если вы выполняете любые подстановки в содержимом страницы, это не лучший подход, так как Java плохо выполняет обработку строк. В этом случае для вас, вероятно, лучше будет использовать более подходящее решение (Python может быть вашим выбором. Есть версии, которые встраиваются в Java, называемые JPython) для генерации ответной страницы.

Сервлеты и множественные процессы

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

Следующий пример помещает предложение synchronized вокруг метода процесса sleep( ). Это блокирует все другие методы на заданное время (пять секунда), которое используют все. Когда будете проверять, вы должны запустить несколько экземпляров окон броузера и обращаться к сервлету так часто, как это возможно в каждом окне — вы увидите, что каждое окно ждет, пока до него дойдет ход.

//: c15:servlets:ThreadServlet.java
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class ThreadServlet extends HttpServlet {
  int i;
  public void service(HttpServletRequest req, 
    HttpServletResponse res) throws IOException {
    res.setContentType("text/html");
    PrintWriter out = res.getWriter();
    synchronized(this) {
      try {
        Thread.currentThread().sleep(5000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
    out.print("<h1>Finished " + i++ + "</h1>");
    out.close();    
  }
} ///:~

Также возможно синхронизировать весь сервлет, поместив ключевое слово synchronized перед методом service( ). Фактически, разумно использовать блок synchronized вместо этого, если есть критическая секция при выполнении, которая может не получить управление. В этом случае вы можетеизбегать синхронизации верхнего уровня, используя предложение synchronized. В противном случае все процессы будут ожидать так или иначе, так что вы можете синхронизировать (synchronize) весь метод.

Управление сессиями с помощью сервлетов

HTTP является протоколом, не использующим сессии, так что вы не можете передать информацию из одного обращения к серверу в другое, если один и тот же человек постоянно обращается к вашему сайту, или если это совсем другой человек. Хорошим решением стало введение механизма, который позволял Web разработчикам отслеживать сессии. Напимер, компании не могли заниматься электронной коммерцией без отслеживания клиентов и предметов, которые они выбрали в свою карзину покупок.

Есть несколько методов отслеживания сессий, но наиболее общий метод связан с наличием “cookies”, что является интегрированной частью стандарта Internet. Рабочая Группа HTTP из Internet Engineering Task Force вписала cookies в официальный стандарт RFC 2109 (ds.internic.net/rfc/rfc2109.txt или проверьте на www.cookiecentral.com).

Cookie - это ничто иное, как маленький кусочек информации, посылаемый Web сервером броузеру. Броузер хранит cookie на локальном диске, и когда выполняется другой вызов на URL, с которым связано cookie, cookie спокойно отсылается вместе с вызовом, тем самым сервер обеспечивается необходимой информацией (обычно обеспечивается какой-то способ, чтобы сервер мог сказать, что это ваш вызов). Однако, клиенты могут выключить возможность броузера получать cookies. Если ваш сайт должен отслеживать клиентов с выключенными cookie, есть другой метод отслеживания сессий (запись URL или спрятанные поля формы), которые встраиваются в ручную, так как возможность отслеживания сессий встроена в API сервлетов и разработана с упором на cookies.

Класс Cookie

API сервлетов (версии 2.0 и выше) имеет класс Cookie. Этот класс объединяет все детали HTTP заголовка и позволяет устанавливать различные аттрибуты cookie. Использование cookie просто и есть смысл добавлять его в объекты ответов. Конструктор получает имя cookie в качестве первого аргумента, а значение в качестве второго. Cookies добавляются в объект ответа прежде, чем вы отошлете любое содержимое.

Cookie oreo = new Cookie("TIJava", "2000");
res.addCookie(cookie);

Cookies извлеваются путем вызова метода getCookies( ) объекта HttpServletRequest, который возвращают массив из объектов cookie.

Cookie[] cookies = req.getCookies();

Затем вы можете вызвать getValue( ) для каждого из cookie для получения строки (String) содержимого cookie. В приведенном выше примере getValue("TIJava") вернет строку (String), содержащую “2000”.

Класс Session

Сессия состоит из запроса клиентом одной или нескольких страниц Web сайта за определенный период времени. Например, если вы покупаете бакалею в режиме онлайн, вы хотите, чтобы сессия была подтверждена с того момента, когда вы добавили первый элемент в “свою карзину покупок” до момента проверки состояния карзины. При каждом добавлении в карзину покупок нового элемента в результате получается новое HTTP соединение, котоое не имеет инфрмации о предыдущих соединениях или элементах, уже находящихся в корзине покупок. Чтобы компенсировать утечку информации механики снабдили спецификацией cookie, позволяющей вашему сервлету следить за сессией.

Объект сервлета Session живет на стороне сервера соединительного канала. Он предназначен для сбора полезной информации о клиенте, его перемещении по сайту и взаимодействию с ним. Эти данные могут подходить для представления сессии, так например для элементов в корзине покупок, или это могут быть такие даные, как информация об авторизации, которая была введена при первом входе клиента на ваш Web сайт, и которую не нужно вводить повторно во время выполнения определенных обращений.

Класс Session из API сервлета использует класс Cookie, чтобы сделать эту работу. Однако всем объектам Session необходим уникальный идентификатор некоторого вида, хранимый на стороне клиента и передающийся на сервер. Web сайты могут также использовать другие типы слежения за сессиями, но эти механизмы более сложны для реализации, так как они не инкапсулированы в API сервлетов (так что вы должны писать их руками, чтобы разрешить ситуации, когда клиент отключает cookies).

Вот пример, который реализует слежение за сессиями с помощью сервлетного API:

//: c15:servlets:SessionPeek.java
// Использование класса HttpSession.
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SessionPeek extends HttpServlet { 
  public void service(HttpServletRequest req, 
  HttpServletResponse res)
  throws ServletException, IOException {
    // Получаем объект Session перед любой
    // исходящей посылкой клиенту.
    HttpSession session = req.getSession();
    res.setContentType("text/html");
    PrintWriter out = res.getWriter();
    out.println("<HEAD><TITLE> SessionPeek ");
    out.println(" </TITLE></HEAD><BODY>");
    out.println("<h1> SessionPeek </h1>");
    // Простой счетчик посещений для этой сессии.
    Integer ival = (Integer) 
      session.getAttribute("sesspeek.cntr");
    if(ival==null) 
      ival = new Integer(1);
    else 
      ival = new Integer(ival.intValue() + 1);
    session.setAttribute("sesspeek.cntr", ival);
    out.println("You have hit this page <b>"
      + ival + "</b> times.<p>");
    out.println("<h2>");
    out.println("Saved Session Data </h2>");
    // Цикл по всем данным сессии:
    Enumeration sesNames = 
      session.getAttributeNames();
    while(sesNames.hasMoreElements()) {
      String name = 
        sesNames.nextElement().toString();
      Object value = session.getAttribute(name);
      out.println(name + " = " + value + "<br>");
    }
    out.println("<h3> Session Statistics </h3>");
    out.println("Session ID: " 
      + session.getId() + "<br>");
    out.println("New Session: " + session.isNew()
      + "<br>");
    out.println("Creation Time: "
      + session.getCreationTime());
    out.println("<I>(" + 
      new Date(session.getCreationTime())
      + ")</I><br>");
    out.println("Last Accessed Time: " +
      session.getLastAccessedTime());
    out.println("<I>(" +
      new Date(session.getLastAccessedTime())
      + ")</I><br>");
    out.println("Session Inactive Interval: "
      + session.getMaxInactiveInterval());
    out.println("Session ID in Request: "
      + req.getRequestedSessionId() + "<br>");
    out.println("Is session id from Cookie: "
      + req.isRequestedSessionIdFromCookie()
      + "<br>");
    out.println("Is session id from URL: "
      + req.isRequestedSessionIdFromURL()
      + "<br>");
    out.println("Is session id valid: "
      + req.isRequestedSessionIdValid()
      + "<br>");
    out.println("</BODY>");
    out.close();
  }
  public String getServletInfo() {
    return "A session tracking servlet";
  }
} ///:~

Внутри метода service( ), getSession( ) вызывает ся для объекта запроса, который возвращает объект Session, связанный с этим запросом. Объект Session не перемещается по сети, а вместо этого он живет на сервере и ассоциируется с клиентом и его запросами.

getSession( ) существует в двух версиях: без параметров, как использовано здесь, и getSession(boolean). getSession(true) эквивалентно вызову getSession( ). Причина введения boolean состоит определении, когда вы хотите создать объект сессии, если он не найден. getSession(true) вызывается чаще всего, отсюда и взялось getSession( ).

объект Session, если он не новый, детально сообщает нам о клиенте из предыдущих визитов. Если объект Session новый, то программа начинает сбор информации об активности этого клиента в этом визите. Сбор информации об этом клиенте выполняется методами setAttribute( ) и getAttribute( ) объекта сессии.

java.lang.Object getAttribute(java.lang.String)
void setAttribute(java.lang.String name,
                  java.lang.Object value)

Объект Session использует простые пары из имени и значения для загрузки информации. Имя является объектом типа String, а значение может быть любым объектом, унаследованным от java.lang.Object. SessionPeek следит за тем, сколько раз клиент возвращался во время сессии. Это сделано с помощью объекта sesspeek.cntr типа Integer. Если имя не найдено, создается объект Integer с единичным значением, в противном случае Integer создается с инкрементированным значением, взятым от предыдущего Integer. Новый объект Integer помещается в объект Session. Если вы используете этот же ключ в вызове setAttribute( ), то новый объект запишется поверх старого. Инкрементируемый счетчик используется для отображения числа визитов клиента во время этой сесии.

getAttributeNames( ) относится как к getAttribute( ), так и к setAttribute( ); он возвращает enumeration из имен объектов, прикрепленных к объекту Session. Цикл while в SessionPeek показывает этот метод в действии.

Вы можете быть удивлены как долго сохраняется объект Session. Ответ зависит от используемого вами контейнера сервлетов; по умолчанию обычно это длится до 30 минут(1800 секунд), это вы можете увидеть при вызове getMaxInactiveInterval( ) из ServletPeek. Тесты показывают разные результаты, в зависимости от контейнера сервлетов. Иногда объект Session может храниться всю ночь, но я никогда не видел случаю, чтобы объект Session исчезал раньше указанного неактивного интервала времени. Вы можете попробовать установить интервал неактивномти с помощью setMaxInactiveInterval( ) до 5 секунд и проверить очистится ли ваш объект Session через соответствующее время. Это может быть тот аттрибут, который вы захотите исследовать при выборе контейнера сервлетов.

Запуск примеров сервлетов

Если вы раньше не работали с сервером приложений, который обрабатывает сервлеты от Sun и поддерживает JSP технологию, вы можете получить реализацию Tomcat для Java сервлетов и JSP, который является бесплатным, с открытым кодом реализации сервлетов, а официальная ссылка на реализацию санкционирована Sun. Его можно найти на jakarta.apache.org.

Следуйте инструкции по инсталяции реализации Tomcat, затем отредактируйте файл server.xml, чтобы он указывал на ваше дерево директориев, в котором помещены ваши сервлеты. Как только вы запустите программу Tomcat, вы сможете протестировать ваши сервлеты.

Это было только короткое введение в сервлеты. Есть много книг, посвещенных этой теме. Однако, это введение должно дать вам достаточно идей, чтобы позволить начать работать. Кроме того, многие из идей следующего раздела снова возвращают нас к сервлетам.

Java Server Pages

Java Server Pages (JSP) является стандартным расширением Java, который определен на основании сервлетного Расширения. Целью JSP является упрощение создания и управления динамическими Web страницами.

Как упоминалось ранее, свободно распространяемое ПО Tomcat, которую можно получить с jakarta.apache.org автоматически поддерживает JSP.

JSP позволяет вам комбинировать HTML код Web страницы с кусочками Java кода в одном и том же документе. Код Java окружатся специальными ярлыками, которые говорят JSP контейнеру, что он должен использовать этот код для генерации сервлета илил его части. Преимущество JSP в том, что вы можете иметь единый документ, который представляет и страницу, и Java код, который включается в нее. Недостатотк в том, что поддерживающий JSP страницу человек должен быть опытен и в HTML и в Java (однако разработчик GUI сред для JSP к этому приближается).

В первый раз JSP загружается JSP контейнером (который обычно связан с Web сервером, или является его частью), код сервлета, помеченный JSP ярлыками, автоматически генерируется, компилируется и загружатся и контейнер сервлетов. Статическая часть HTML страницы воспроизводится путем посылки статического объекта String в метод write( ). Динамическая часть включается прямо в сервлет.

Исходя из этого, пока исходный текст JSP страницы не изменяется, она ведет себя так, как будто это статическая HTML страница с ассоциированным сервлетом (однако, весь HTML код на самом деле генерируется сервлетом). Если вы изменяете исходный код для JSP, он автоматически перекомпилируется и перегружается при следующем запросе этой страницы. Конечно, из-за всей этой динамики вы увидите замедленый ответ на первый запрос этой JSP страницы. Но поскольку JSP использует немного больше, чем просто изменения, обычно вы не встретите этой задержки.

Структура JSP страницы состоит из перемешивания сервлета и HTML страницы. JSP ярлыки начинаются и заканчиваются угловыми скобками, так же как и ярлыки HTML, но эти ярлыки также включают символ процентов, так что все JSP ярлыки обозначаются

<% JSP code here %>

За первым знаком процента могут следовать другие символы, которые означают часть JSP кода в ярлыке.

Ниже приведен очень простой пример JSP, который использует стандартную вызов Java библиотеки для получения текущего времени в милисекундах, затем это значение делится на 1000, для получение времени в секундах. Так как используется JSP выражение ( <%= ), результат вычислений конвертируется в String, и помещается на генерируемую Web страницу:

//:! c15:jsp:ShowSeconds.jsp
<html><body>
<H1>The time in seconds is: 
<%= System.currentTimeMillis()/1000 %></H1>
</body></html>
///:~

В JSP примерах этой книги первая и последняя строки не будут включаться в файл реального кода, который помещен в архив исходного кода, прилагающийся к этой книге.

Когда клиент создает запрос к JSP странице, Web сервер должен быть сконфигурирован, чтобы соответствовать запросам JSP контейнера, который затем вызывает страницу. Как упоминалось ранее, при первом вызове страницы, компоненты, указанные на странице компоненты генерируются и компилируются JSP контенером в один или несколько сервлетов. В приведенном выше примере сервлет будет содержать код для конфигурирования объекта HttpServletResponse, производящего объект PrintWriter (который всегда называется out), а затем происходит вычисление String, которая посылается в out. Как вы можете видеть, все это выполняется с помощью очень краткой инструкции, но среднестатистический HTML программист/Web дизайнер не имеют опыта в написании такого кода.

Неявные объекты

Сервлеты включают классы, которые обеспечивают соответствующие утилиты, такие как HttpServletRequest, HttpServletResponse, Session и т.д. Объекты этих классов встроены в JSP спецификацию и автоматически доступны для использования в вашем JSP коде без написания дополнительных строчек кода Неявные объекты JSP сведены в приведенную ниже таблицу.

Неявная переменная

Тип (javax.servlet)

Описание

Границы

request

Зависящий от протокола тип, производный от HttpServletRequest

Запрос, который вызывает обращение к службе.

запрос

response

Зависящий от протокола тип, производный от HttpServletResponse

Ответ на запрос.

страница

pageContext

jsp.PageContext

Содержимое страницы включает зависящие от реализации особенности и обеспечивает удобные методы и доступ к пространству имен для JSP.

страница

session

Зависящий от протокола тип, производный от http.HttpSession

Объект сессии, созданный для клиентского запроса. Смотрите объект Session для сервлетов.

сессия

application

ServletContext

Контекст сервлета получается из конфигурирующего сервлет объекта (e.g., getServletConfig(), getContext( ).

приложение

out

jsp.JspWriter

Объект, который пишет в выходной поток.

страница

config

ServletConfig

ServletConfig для этого JSP.

страница

page

java.lang.Object

Экземпляр класса страницы, обрабатывающей этот запрос.

страница

Границы видимости каждого объекта могут значительно отличаться. Например, объект session имеет границы видимости, которые превышают страницу, так как он может отвечать за несколько клиентских запросов и страниц. Объект application может предоставить сервис для группы JSP страниц, которые совместно представляют Web приложение.

Директивы JSP

Директивы являются сообщениями для JSP контенера и указываются символом “@”:

<%@ directive {attr="value"}* %>

Директивы ничего не посылают в поток out, но они важны в настройке атрибутов ваших JSP страниц и взаиможействий с JSP контейнером. Например строка:

<%@ page language="java" %>

сообщает, что на JSP странице используется язык скриптов Java. На самом деле JSP спецификации только описывают, что семантика языка скриптов аналогична “Java”. Назначение этой директивы состоит в придании гибкости технологии JSP. В будующем, если вы выберите другой язык, скажем Python (хороший выбор для написания скриптов), и этот язык будет иметь поддержку Java Run-time Environment, с применением технологии Java объектыных моделей для среды скриптов, особенно это относится к определенным выше переменным, свойствам JavaBeans и публичным методам.

Наиболее важная директива - это директива страницы. Она определяет номер страницы в зависимости от атрибутов и взаимодействия между этими атрибутами и JSP контейнером. К таким атрибутам относятся: language, extends, import, session, buffer, autoFlush, isThreadSafe, info и errorPage. Например:

<%@ page session=”trueimport=”java.util.*” %>

Эта строка указывает, что страница требует участия HTTP сессии. Так как мы не установили директивы языка, JSP контейнер по умолчанию использует Java и подразумеваемую переменную языка скриптов, называемую session типа javax.servlet.http.HttpSession. Если бы директива была ложна, то неявная переменная session была бы недоступна. Если переменная session не указана, то используется значение по умолчанию “true”.

Аттрибут import описывает типы, которые доступны в окружении скриптов. Этот атрибут используется так, как если бы он использовался в языке программирования Java, т.е. это разделеный запятыми список, как и обычное выражение import. Этот список импортируется реализацией транслированной JSP страницы и доступен для среды скрипта. Опять таки, пока это определено, когда значение директивы языка - “java”.

Элемены JSP скриптов

После того, как были использованы директивы для настойки среды скриптов, вы можете использовать элементы языка скриптов. JSP 1.1 имеет три элемента языка скриптов — declarations, scriptlets, и expressions. Declaration декларирует элементы, Scriptlet - это фрагмент инструкций, а Еxpression - это законченное выражение языка. В JSP каждый элемент сценария начинается с “<%”. Синтаксис для каждого следующий:

<%! declaration %>
<%  scriptlet   %>
<%= expression  %>

Пробелы полсе “<%!”, “<%”, “<%=” и перед “%>” не обязательны.

Все эти ярлыки базируются на XML. Вы даже можете сказать, что JSP страница может быть преобразована в XML документ. Эквивалентный XML синтаксис для элементов скриптов, приведенных выше, должен быть:

<jsp:declaration> declaration </jsp:declaration>
<jsp:scriptlet>   scriptlet   </jsp:scriptlet>
<jsp:expression>  expression  </jsp:expression>

Кроме того, есть два типа коментариев:

<%-- jsp comment --%>
<!-- html comment -->

Первая форма позволяет вам добавлять компоненты в исходный текст JSP страницы, который не будет появляться в любой форме HTML страницы, посылаемой клиенту. Конечно, вторая форма коментариев не спечефично для JSP — это просто обычный коментарий HTML. Что интересно, вы можете вставить JSP код внутрь HTML коментария, и коментарий будет вставлен в рузультирующую страницу, включая результат работы JSP кода.

Declaration испоьзуется для объявления переменных и методов в языке скриптов (пока только в Java), используемых JSP страницей. Декларация должна быть завершенной инструкцией Java и не может совершать вывод в поток out. В приведенном ниже примере Hello.jsp декларация переменных loadTime, loadDate и hitCount является законченной инструкцией Java, которая объявляет и инициализирует новые переменные.

//:! c15:jsp:Hello.jsp
<%-- This JSP comment will not appear in the
generated html --%>
<%-- This is a JSP directive: --%>
<%@ page import="java.util.*" %>
<%-- These are declarations: --%>
<%!
    long loadTime= System.currentTimeMillis();
    Date loadDate = new Date();
    int hitCount = 0;
%>
<html><body>
<%-- The next several lines are the result of a 
JSP expression inserted in the generated html;
the '=' indicates a JSP expression --%>
<H1>This page was loaded at <%= loadDate %> </H1>
<H1>Hello, world! It's <%= new Date() %></H1>
<H2>Here's an object: <%= new Object() %></H2>
<H2>This page has been up 
<%= (System.currentTimeMillis()-loadTime)/1000 %>
seconds</H2>
<H3>Page has been accessed <%= ++hitCount %> 
times since <%= loadDate %></H3>
<%-- A "scriptlet" that writes to the server
console and to the client page. 
Note that the ';' is required: --%>
<%
   System.out.println("Goodbye");
   out.println("Cheerio");
%>
</body></html>
///:~

Когда вы запустите эту программу, вы увидите, что переменные loadTime, loadDate и hitCount сохраняют свои начения между обращениями к странице, так что, конечно, они являются полями, а не локальными переменными.

В конце примера скриплет пишет “Goodbye” на консоле Web сервера и “Cheerio” в неявный объект out типа JspWriter. Скриплеты могут содержать любой фрагмент кода, содержащий правильные Java инструкции. Скриплеты выполняются во время обработки запроса. Когда все фрагменты скриплетов в данном JSP скомбинированы так, как они введены в JSP страницу, они должны произвести дествительное выражение, которое определено в языке программирования Java. Будет или нет производиться какой-либо вывод в поток out зависит от кода скриплета. Вы должны быть уверены, что скриплеты могут производить какие-либо эффекты при изменении видимых для них объектов.

JSP выражения (expression) могут быть найдены среди HTML кода в средней части Hello.jsp. Выражения должны быть законченными Java инструкциями, которые вычисляют, приводят к String, и посылают в out. Если результат выражения не может быть приведен к String, выбрасывается ClassCastException.

Извлечение полей и значений

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

//:! c15:jsp:DisplayFormData.jsp
<%-- Fetching the data from an HTML form. --%>
<%-- This JSP also generates the form. --%>
<%@ page import="java.util.*" %>
<html><body>
<H1>DisplayFormData</H1><H3>
<%
  Enumeration flds = request.getParameterNames();
  if(!flds.hasMoreElements()) { // No fields %>
    <form method="POST" 
    action="DisplayFormData.jsp">
<%  for(int i = 0; i < 10; i++) {  %>
      Field<%=i%>: <input type="text" size="20"
      name="Field<%=i%>" value="Value<%=i%>"><br>
<%  } %>
    <INPUT TYPE=submit name=submit 
    value="Submit"></form>
<%} else { 
    while(flds.hasMoreElements()) {
      String field = (String)flds.nextElement();
      String value = request.getParameter(field);
%>
      <li><%= field %> = <%= value %></li>
<%  }
  } %>
</H3></body></html>
///:~

Наиболее интересная особенность этого примера состоит в том, что он демонстрирует, как код скриплета может быть смешан с HTML кодом, даже в точке генерации HTML с помощью цикла for из Java. Это особенно хорошо для построения всех видов форм, в которых присутствует повторяющийся HTML код.

Атрибуты JSP страницы и границы видимости

Просматривая HTML документацию по сервлетам и JSP, вы найдете возможность получать информацию относительно запущенного сервлета или JSP. Приведенный ниже пример отображает некоторые из возможных данных.

//:! c15:jsp:PageContext.jsp
<%--Viewing the attributes in the pageContext--%>
<%-- Note that you can include any amount of code
inside the scriptlet tags --%>
<%@ page import="java.util.*" %>
<html><body>
Servlet Name: <%= config.getServletName() %><br>
Servlet container supports servlet version:
<% out.print(application.getMajorVersion() + "."
+ application.getMinorVersion()); %><br>
<%
  session.setAttribute("My dog", "Ralph");
  for(int scope = 1; scope <= 4; scope++) {  %>
    <H3>Scope: <%= scope %> </H3>
<%  Enumeration e =
      pageContext.getAttributeNamesInScope(scope);
    while(e.hasMoreElements()) {
      out.println("\t<li>" + 
        e.nextElement() + "</li>");
    }
  }
%>
</body></html>
///:~

Этот пример также показывает использование внедрения в HTML и посылку в out для получения результирующей HTML страницы.

Первый кусок информации выдает имя сервлета, который, вероятно, будет просто “JSP”, но это зависит от вашей реализации. Вы также можете обнаружить текущую версию контейнера сервлетов, используя объект приложения. Наконец, после установки атрибутов сессии, отображается “attribute names” в определенных пределах видимости. Чаще всего вы не используете границы видимости в JSP программировании. Они просто показаны тут для придания примеру увлекательности Есть четыре границы видимости атрибута. Вот они: пределы страницы (граница 1), пределы запроса (граница 2), пределы сессии (граница — здесь только один элемент доступен в пределах сессии - это “My dog”, добавленный перед цыклом for), и пределы приложения (граница 4), основанные на объекте ServletContext. Существует единственный ServletContext для кадого “Web приложения” в каждой Java Virtual Machine. (“Web приложение” - это набор сервлетов и содержимого страничек, относящихся к определенному подмножеству пространства имен URL сервера, таких как /каталог. Обычно это устанавливается в конфигурационном файле.) В пределах приложения вы видите объекты, которые представляют путь к рабочему директорию и временному директорию.

Управление сессиями в JSP

Сессии были введены в предыдущем разделе о сервлетах и также доступны в JSP. Следующий пример исследует объект session и позволяет вам управлять промежутком времени после которого сессия становится недействительной.

//:! c15:jsp:SessionObject.jsp
<%--Getting and setting session object values--%>
<html><body>
<H1>Session id: <%= session.getId() %></H1>
<H3><li>This session was created at 
<%= session.getCreationTime() %></li></H1>
<H3><li>Old MaxInactiveInterval = 
  <%= session.getMaxInactiveInterval() %></li>
<% session.setMaxInactiveInterval(5); %>
<li>New MaxInactiveInterval= 
  <%= session.getMaxInactiveInterval() %></li>
</H3>
<H2>If the session object "My dog" is 
still around, this value will be non-null:<H2>
<H3><li>Session value for "My dog" =  
<%= session.getAttribute("My dog") %></li></H3>
<%-- Now add the session object "My dog" --%>
<% session.setAttribute("My dog", 
                    new String("Ralph")); %>
<H1>My dog's name is 
<%= session.getAttribute("My dog") %></H1>
<%-- See if "My dog" wanders to another form --%>
<FORM TYPE=POST ACTION=SessionObject2.jsp>
<INPUT TYPE=submit name=submit 
Value="Invalidate"></FORM>
<FORM TYPE=POST ACTION=SessionObject3.jsp>
<INPUT TYPE=submit name=submit 
Value="Keep Around"></FORM>
</body></html>
///:~

Объект session существует по умолчанию, так что он доступен без написания дополнительного кода. Вызовы getID( ), getCreationTime( ) и getMaxInactiveInterval( ) используются для отображения информации об объекте сессии.

Когда вы в первый получите эту сессию, вы увидите, что MaxInactiveInterval равен, например, 1800 секунд (30 минут). Это зависит от способа конфигурации вашего контейнера JSP/сервлетов. MaxInactiveInterval сокращается до 5 секунд, чтобы сделать предмет изучения более интересным. Если вы обновите страницу до того, как закончится интервал в 5 секунд, то вы увидите:

Session value for "My dog" = Ralph

Но если вы промедлите, “Ralph” станет равен null.

Чтобы посмотреть как информация о сессии может быть передана на другие страницы, а также посмотреть эффект недействительности объекта сессии, просто дайте ему устареть, будут созданы два других JSP. Первый из них (может быть получен при нажатии кнопки “invalidate” в SessionObject.jsp) читает информацию о сессии, а затем явно делает ее недействительной:

//:! c15:jsp:SessionObject2.jsp
<%--The session object carries through--%>
<html><body>
<H1>Session id: <%= session.getId() %></H1>
<H1>Session value for "My dog" 
<%= session.getValue("My dog") %></H1>
<% session.invalidate(); %>
</body></html>
///:~

Чтобы поэкспериментировать с этим, обновите SessionObject.jsp, затем сразу нажмите на кнопку “invalidate”, чтобы посмотреть SessionObject2.jsp. В этом случае вы все еще увидите “Ralph”, в противом случае (после того, как пройдет 5-ти секундный интервал), обновите SessionObject2.jsp, чтобы увидеть, что сессия действительно стаа недействительной, а “Ralph” исчез.

Если вы вернетесь к SessionObject.jsp, обновите страничку так, чтобы прошел 5-ти секундный интервал, затем нажмите кнопку “Keep Around”, вы получите следующую страницу, SessionObject3.jsp, которая НЕ делает сессию недействительной:

//:! c15:jsp:SessionObject3.jsp
<%--The session object carries through--%>
<html><body>
<H1>Session id: <%= session.getId() %></H1>
<H1>Session value for "My dog" 
<%= session.getValue("My dog") %></H1>
<FORM TYPE=POST ACTION=SessionObject.jsp>
<INPUT TYPE=submit name=submit Value="Return">
</FORM>
</body></html>
///:~

Поскольку эта страница не делает сессию недействительной, “Ralph” будет оставаться до тех пор, пока вы будете выполнять обновления до окончания 5 секундного интервала. Это похоже на “Tomagotchi” — пока вы играете с “Ralph”, он будет там, в противном случае он исчезнет.

Создание и изменение cookies

Cookies были введены и предыдущем разделе, посвященном сервлетам. Опять таки, краткость JSP делает работу с cookies очень простой, чем при использовании сервлетов. Следующий пример показывает это, получая cookies, которые приходят с запросом, читают и изменяют их максимальный возраст (дату устаревания) и присоединяют новый cookie, для помещения в ответ:

//:! c15:jsp:Cookies.jsp
<%--This program has different behaviors under
 different browsers! --%>
<html><body>
<H1>Session id: <%= session.getId() %></H1>
<%
Cookie[] cookies = request.getCookies();
for(int i = 0; i < cookies.length; i++) { %>
  Cookie name: <%= cookies[i].getName() %> <br>
  value: <%= cookies[i].getValue() %><br>
  Old max age in seconds: 
  <%= cookies[i].getMaxAge() %><br>
  <% cookies[i].setMaxAge(5); %>
  New max age in seconds: 
  <%= cookies[i].getMaxAge() %><br>
<% } %>
<%! int count = 0; int dcount = 0; %>
<% response.addCookie(new Cookie(
    "Bob" + count++, "Dog" + dcount++)); %>
</body></html>
///:~

Так как каждый броузер хранит свои cookies по-своемуin, вы можете видеть разное поведение у разных броузеров (не утверждаю точно, то это может быть некоторым ошибкам, которые могут быть уже устранены в от момент, когда вы читаете это). Также вы можете получить различные результаты, если вы закроете броузер и запустите его снова, или посетите другой сайт и вернетесь к Cookies.jsp. Обратите, что использование объекта сессий лучший подход, чем прямое использование cookies.

После отображения идентификатора сессий, отображается каждый cookie из массива cookies, пришедший с объектом request, наряду с его максимальным возростом. Максимальный возраст меняется и отображается вновь для проверки нового значения, затем новый cookie добавляется в ответ. Однако ваш броузер может игнорировать максимальный возраст, поигравшись с этой программой и изменяя значение возраста можно увидеть поведение различных броузеров.

Резюме о JSP

Этот раздел лишь коротко рассказывается о JSP, но даже с тем, что рассказано здесь (вместе с теми знаниями, которые вы получили о Java в оставшейся части книги, совместо с тем, что вы сами знаете об HTML) вы можете начать писать достаточно сложные Web страницы с помощью JSP. Синтаксис JSP специально не спрятан глубоко и не сложен, так что если вы поняли что показано в этом разделе, вы готовы к продуктивной работе с JSP. Вы можете найти более новую информацию во вновь вышеших книгах по сервлетам или на java.sun.com.

Особенно хорошо иметь поддержку JSP даже если вашей целью является разработка сервлетов. Вы обнаружите, что если у вас есть вопросы о поведении сервлета в будующем, то легко и просто написать тестовую JSP программу, отвечающую на этот вопрос. Часть выгоды состоит в том, что нужно писать меньше кода и можно смешивать отображаемый HTML код с Java кодом, но рычаги управления становятся особенно очевидными, когда JSP контейнер обрабатывает всю перекомпиляцию и перезагрузку JSP вместо вас, когда бы вы не изменили исходный код.

Недостаток JSP в том, что для создания JSP требуется более высокий уровень умения, чем уровень простого Java программиста или простого Web мастера. Кроме того, отладка JSP страниц с ошибками не так легка, как отладка Java программ, так как (в настоящее время) сообщения об ошибках слишком невразумительны. Это должно измениться при улучшении среды разработки, но мы можем также найти другую технологию, надстроеную над Java и Web, которая будет лучше адаптирована в знаниям дизайнера Web сайтов.

RMI (Удаленный вызов методов)

Традиционный подход к выполнению кода на любой машине по сети сбивал с толку , а так же был утомителен и подвержен ошибкам при реализации. Лучший способ представить эту проблемму - это думать, что какой-то объект живет на другой машине и что вы можете посылать сообщения удаленному объекту и получать результат, будто бы этот объект живет на вашей машине. Говоря простым языком, это в точности то, что позволяет делать Удаленный вызов методов (Remote Method Invocation (RMI) в Java. Этот раздел показывает шаги, необходимые для создания ваших собственных RMI.

Удаленный интерфейс

RMI делает тыжелым использование интерфейсов. Когда вы хотите создать удаленный объект, вы помечаете, что лежащую в основе раелизацию нужно передавать через интерфейс. Таким образом, когда клиент получает ссылку на удаленный объект, на самом деле он получаете ссылку на интерфейс, который выполняет соединение с определенныму местом кода,общающимся по сети. Но вы не заботитесь об этом, вы просто посылаете сообщения через ссылку на интерфейс.

Когда вы создаете удаленный интерфейс, вы должны следовать следующей иснтрукции:

  1. Удаленный интерфейс должен быь публичным - public (он не может иметь “доступ на уровне пакета”, так же он не может быть “дружественным”). В противном случае клиенты будут получать ошибку при попытке загрузки объекта, реализующего удаленный интерфейс.
  2. Удаленный интерфейс должен расширять интерфейс java.rmi.Remote.
  3. Каждый метод удаленного интерфейса должен объявлять java.rmi.RemoteException в своем предложении throws в добавок к любым исключениям, специфичным для приложения.
  4. Удаленный объект, передаваемый как аргумент или возвращаемое значение (либо напрямую, либо как к части локального объекта), должен быть объявлен как удаленный интерфейс, а не реализация класса.

Ниже приведен простой удаленный интерфейс, представляющий сервис точного времени:

//: c15:rmi:PerfectTimeI.java
// Удаленный интерфейс PerfectTime.
package c15.rmi;
import java.rmi.*;

interface PerfectTimeI extends Remote {
  long getPerfectTime() throws RemoteException;
} ///:~

Он выглядит как любой другой интерфейс, за исключением того, что расширяет Remote и все его методы выбрасывают RemoteException. Помните, что interface и все его методы автоматически становятся public.

Реализация удаленного интерфейса

Сервер должен содержать класс, который расширяет UnicastRemoteObject и реализует удаленный интерфейс. Этот класс также может иметь другие методы, но для клиента доступны только методы удаленного интерфейса, так как клиент получает тоько ссылку на интерфейс, а не на класс, который его реализует.

Вы должны явно определить конструктор для удаленого объекта, даже если вы определяете только конструктор по умолчанию, который вызывает конструктор базового класса. Вы должны написать его, так как он должен выбрасывать RemoteException.

Ниже приведена реализация удаленного интерфейса PerfectTimeI:

//: c15:rmi:PerfectTime.java
// Реализация удаленного объекта PerfectTime.
package c15.rmi;
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.net.*;

public class PerfectTime 
    extends UnicastRemoteObject
    implements PerfectTimeI {
  // Реализация интерфейса:
  public long getPerfectTime() 
      throws RemoteException {
    return System.currentTimeMillis();
  }
  // Должна быть реализация конструктора
  // для выбрасывания RemoteException:
  public PerfectTime() throws RemoteException {
    // super(); // Вызывается автоматически
  }
  // Регистрация для обслуживания RMI. Выбрасывает
  // исключения на консоль.
  public static void main(String[] args) 
  throws Exception {
    System.setSecurityManager(
    new RMISecurityManager());
    PerfectTime pt = new PerfectTime();
    Naming.bind(
      "//peppy:2005/PerfectTime", pt);
    System.out.println("Ready to do time");
  }
} ///:~

В этом примере main( ) обрабатывает все детали установки сервера. Когда вы обслуживаете RMI объект, в определенном месте вашей программы вы должны:

  1. Создать и установит менеджер безопасности, поддерживающий RMI. Как часть Java пакета, для RMI поддерживается только RMISecurityManager.
  2. Создать один или несколько экземпляров удаленного объекта. Здесь вы видите создание объекта PerfectTime.
  3. Зарегистрировать не менее одного удаленного объекта с помощью RMI удаленной регистрации объекта с целью загрузки Один удаленный объект может иметь методы, которые производят ссылки на другой удаленный объект. Это позволяет вам настроить так, чтобы клиент проходил регистрацию только один раз, при получении первого удаленного объекта.

Регистрация

В этом примере вы видите вызов статического метода Naming.bind( ). Однако этот вызов требует, чтобы регистрация была запущена отделным процессом на вашем компьютере. Имя сервера регистрации - это rmiregistry, и под 32-битной Windows вы говорите:

start rmiregistry

для запуска в фоновом режиме. Под Unix эта команда выглядит:

rmiregistry &

Как и многие другие сетевые программы, rmiregistry обращается по IP адресу машины, на которой она установлена, но она также слушает порт. Если вы вызовите rmiregistry как показано выше, без аргументов, будет использован порт по умолчанию 1099. Если вы хотите использовать другой порт, вы добавляете аргумент в командную строку, указывающий порт. Следующий пример устанавливает порт 2005, так что rmiregistry под управлением 32-битной Windows должна запускаться так:

start rmiregistry 2005

а подUnix:

rmiregistry 2005 &

Информаци о порте также должна передаваться в команде bind( ), наряду с IP адресом машины, где располагается регистрация. Но это может выявить огорчительную проблему, если вы хотите проверять RMI программы локально, как проверялись все программы до этой главы. В выпуске JDK 1.1.1, есть целая связка проблем:[76]

  1. localhost не работает с RMI. Поэтому для экспериментов с RMI на одной машине вы должны использовать имя машины. Чтобы найти имя вашей машины под управлением 32-битной Windows, перейдите в панель управления и выберите “Network”. Выберите закладку “Identification”, и посмотрите имя вашего компьютера. В моем случае я назвал свой компьютер “Peppy”. Регистр в имени игнорируется.
  2. RMI не работает, пока ваш компьютер имеет активные TCP/IP соединения, даже если все ваши компоненты просто общаются друг с другом на локальной машине. Это значит, что вы должны соединятся с вашим провайдером Internet до того, как попробуете запустить программу или будете огорчены неким сообщением об ошибке.

Если учесть все это, команда bind( ) принимает вид:

Naming.bind("//peppy:2005/PerfectTime", pt);

Если вы используете порт по умолчанию 1099, вам не нужно указывать порт, так что вы можете просто сказать:

Naming.bind("//peppy/PerfectTime", pt);

Вы можете выполнить локальную проверку оставив в покое IP адрес, а использовать только идентификатор:

Naming.bind("PerfectTime", pt);

Имя сервиса здесь произвольно. В данном случае PerfectTime выбрано просто как имя класса, но вы можете назвать так, как захоите. Важно, чтобы это было уникальное имя регистрации, чтобы клиент знал, когда будет искать что производит удаленные объекты. Если имя уже зарегистрировано, вы получите AlreadyBoundException. Чтобы предотвратить это, вы всегда можете использовать rebind( ) вместо bind( ), так как rebind( ) либо добавляет новый элемент, либо заменяет уже существующий.

Даже после завершения работы main( ), ваш объект будет оставаться созданным и зарегистрированным, ожидая, что прийдет клиент и выполнит запрос. Пока rmiregistry остается запущенным, и вы не вызовите Naming.unbind( ) на вашей машине, объект будет оставаться там. По этой причине, когда вы разрабатываете ваш код, вам необходимо выгружать rmiregistry и перезапускать его, когда скомпилируете новую версию вашего удаленного объекта.

Вам не обязательно запускать rmiregistry как внешний процесс. Если вы знаете, что только ваше приложение использует регистрацию, вы можете загрузить ее внутри вашей программы с помощью строки:

LocateRegistry.createRegistry(2005);

Как и раньше, 2005 - это номер порта, который мы использовали в этом примере. Это эквивалентно запуску rmiregistry 2005 из командной строки, но часто этот способ является более подходящим при разработке RMI кода, так как это снжает число необходимых действий при запуске и остановке регистрации После того, как вы выполните этот код, вы можете вызвать bind( ), используя Naming, как и ранее.

Создание якорей и скелетов

Если вы откомпилировали и запустили PerfectTime.java, оно не будет работать, даже если вы правильно запустили rmiregistry. Это происходит потому, что рабочее пространство для RMI еще не создано. Вы должны сначала создать якоря и скелеты, которые обеспечат работу сетевого соединения и позволит вам делать вид, что удаленный - это просто докальный объект на вашей машине.

То, что происходит за сценой - очень сложно. Любой объект, который вы передаете или получаете из удаленого объекта должен реализовывать(implement) Serializable (если вы хотите передавать удаленные ссылки вместо целых объектов, аргументы объектов могут реализовывать (implement) Remote), так что вы можете представить, что якоря и скелеты автоматически выполняют сериализацию и десериализацию, а так же “передают по очереди” все аргументы по сети и возвращают результат. К счастью, вам не нужно знать всего этого, но вы должны делать якоря и скелеты. Это простой процесс: вы вызываете инструмент rmic для вашего откомпилированного кода, а он создает необходимые файлы. Так что от вас требуется включить еще один шаг в процесс компиляции.

Однако инструмент rmic спецефичен относительно packages classpath. PerfectTime.java находится в пакете c15.rmi, и даже если вы вызовите rmic в том же самом директори, в котором находится PerfectTime.class, rmic не найдет файл, так как он ищет classpath. Так что вы должны указать путь к классу примерно так:

rmic c15.rmi.PerfectTime

Вам не нужно быть в директории, содержащим PerfectTime.class, когда вы исполняете эту команду, но результат будет помещен в текущий директоий.

Если запус rmic завершится успешно, вы найдете два новых класса в дректории:

PerfectTime_Stub.class
PerfectTime_Skel.class

соответствующих якорю и скелету. Теперь вы готовы запустить общение клиента с сервером.

Использование удаленных объектов

Главная цель RMI состоит в упращении использования удаленных объектов. Вы должны сделать только самую важную вещь в вашей клиентской программе: это поиск и получение удаленного интерфейса с сервера. Во всем остальном - это обычное программирование на Java: посылка сообщений объекту. Ниже приведена программа, использующая PerfectTime:

//: c15:rmi:DisplayPerfectTime.java
// Испольование удаленного объекта PerfectTime.
package c15.rmi;
import java.rmi.*;
import java.rmi.registry.*;

public class DisplayPerfectTime {
  public static void main(String[] args) 
  throws Exception {
    System.setSecurityManager(
      new RMISecurityManager());
    PerfectTimeI t = 
      (PerfectTimeI)Naming.lookup(
        "//peppy:2005/PerfectTime");
    for(int i = 0; i < 10; i++)
      System.out.println("Perfect time = " +
        t.getPerfectTime());
  }
} ///:~

Строка идентификатора такая же, как и та, что использовалась при регистрации объекта с помощью Naming, а первая часть представляет URL и номер порта. Так как вы используете URL, вы можете также указать машину в Internet.

То, что возвращается из Naming.lookup( ) должно быть преобразовано к удаленному интерфейсу, а не к классу. Если вы будите использовать класс, вы получите исключение.

Вы виите вызов метода

t.getPerfectTime()

так как вы имеете ссылку на удаленный объект, то с точки зрения программирования, это не отличается от работы с локальным объектом (с одним отличием: удаленные методы выбрасывают RemoteException).

CORBA

В огромных распределенных приложениях вы можете быть неудовлетворены изложенными выше подходами. Например, вам может понадобиться интерфейс с унаследованным хранилищем данных, или вам может понадобиться услуга от сервера объектов не зависимо от его физического расположения. Такие ситуации требуют некоторого рода Процедуры Удаленного Вызова (RPC), и, возможно, независимости от языка. Здесь может помочь CORBA.

CORBA - это не особенность языка, это технология интеграции. Это спецификация, которой могут следовать производители для реализации CORBA-совместимых интегрированных продуктов. CORBA - это одна из попыток OMG определить рабочее пространство для распределенных, независящих от языка способностей объекта.

CORBA предлагает возможность создания процедуры удаленного вызова Java объектов и не Java объектов для взаимодействия с системой наследования независящим от расположения способом. Java добавляет поддержку сети и великолепный объектно-оиентированный язык для построения графических и не графических приложений. Модельные карты объектов Java и OMG дополняют друг друга, например, и Java, и CORBA реализуют концепцию интерфейсов и модель ссылок на объекты.

Принципы CORBA

Спецификация взаимодействия объектов, разработанная OMG, часто называется, как Object Management Architecture (OMA). OMA определяет два компонента: Модель Ядра Объекта (Core Object Model) и Архитектура Ссылок OMA (OMA Reference Architecture). Модель Ядра Объекта устанавливает основную концепцию объекта, интерфейса, операции и т.п. (CORBA является улучшением Core Object Model.) Архитектура Ссылок OMA определяет лежащую в основе ифраструктуру сервисов и механизма, который позволяет объектам взаиможействовать. Архитектура Ссылок OMA включает Object Request Broker (ORB), Object Services (также известный, как CORBA сервис), и общие средства обслуживания.

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

Нет спецификации о том, как должно реализовываться ядро ORB, но для обеспечения совместимости с различными производителями ORB, OMG определяет набор сервисов, которые доступны через стандартные интерфейсы.

Язык Определения Интерфейсов CORBA(CORBA Interface Definition Language) - IDL

CORBA предназначена для независимости от языков: объект клиента может вызывать методы серверного объекта различных классов, не зависимо от языка реализации этих объектов. Конечно, клиентский объект должен знать имена и сигнатуру методов, прадоставляемых серверным объеком. Для этого сделан IDL. CORBA IDL - это не зависимый от языков способ указания типов данных, атрибутов, операций, интерфейсов и многого другого. Синтаксис IDL схож с синтаксисом C++ или Java. Следующая таблица показывает соответствия между некоторыми общими концепциями этих трех языков, которые можно указать в CORBA IDL:

CORBA IDL

Java

C++

Module

Package

Namespace

Interface

Interface

Pure abstract class

Method

Method

Member function

Концепция наследования поддерживается так же, как испоьзование оператора двоеточие в C++. Прогаммист создает IDL описание атрибутов, методов и интерфейсов, которые реализуются и используются сервером и клиентом. Затем IDL компилируется предоставляемым производителем IDL/Java компилятором, читающим исходный IDL код и генерирующим Java код.

IDL компилятор очень полезный инструмент: он не просто генерирует Java код, эквивалентный IDL, он также генерирует код, который будет использоваться при передаче аргументов методов и при произведении удаленных вызовов. Эот код, называемый кодом якорей и скелетов, разбит на несколько файлов Java программы, и обычно является частью одного Java пакета.

Служба Указания Имен

Служба указания имен является одной из фундаментальных служб CORBA. Объекты CORBA ассоциируются по ссылке, эта часть информации ничего не значит для человека. Но ссылкам можно назначать определенные программой строковые имена. Эта операция известа как именование ссылок (stringifying the reference) и один из OMA компонент, Служба Указания Имен (Naming Service), предназначена для выполнения преобразования строки в объект и объекта в строку. Так как Служба Указания Имен действует как телефонная книга, в которой и клиент и сервер могут получит консултацию, она работает как отдельный процесс. Создание преобразования объекта в строку называется привязыванием объекта (binding an object), а удаления преобразования называется отвязыванием (unbinding). Получение ссылки на объект по переданной строке называется разрешением имени (resolving the name).

Например, при запуске сервер приложений должен создать серверный объект, связать объект с именем сервиса, а затем подождать пока клиент выполнит запрос. Клиент сначала получает ссылку на серверный объект, разрешает строковое имя, а затем может выполнить обращение к серверу, используя ссылку.

Спецификация Сервиса Указания Имен является частью CORBA, но приложения, которые реализуют его обеспечиваются производителем ORB. Способ получения доступа к Сервису Указания Имен функционально может различаться в зависимости от производителя.

Пример

Приведенные здесь код не продуман до конца, потому что различные ORB имеют различные способы доступа к сервису CORBA, так что примеры специфичны для производителя. (Приводимые ниже примеры используют JavaIDL - это бесплатный продукт от Sun, поставляемый с облегченной версией ORB, службой укaхзания имен и компилятором IDL-to-Java.) Кроме того, так как Java еще очень молод и продолжает развиваться, не все особенности CORBA представлены в различных Java/CORBA продуктах.

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

Написание исходного кода IDL

Первый шаг состоит в написании IDL описания разрабатываемого сервиса. Это обычно делает программист сервера, который после этого свободен реализовывать сервер на любом языке, для которго существует CORBA IDL компилятор. IDL файл передается программисту клиентской стороны и становится мостом между языками.

Приведенный ниже пример показывает IDL описание для нашего сервера ExactTime:

//: c15:corba:ExactTime.idl
//# Вы должны установить idltojava.exe с
//# java.sun.com и отрегулировать установки для
//# использования вашего локального C препроцессора
//# чтобы откомпилировать этот.
//# Смотрите докуметацию на java.sun.com.
module remotetime {
   interface ExactTime {
      string getTime();
   };
}; ///:~

Это декларация интерфейса ExactTime внутри просранства имен remotetime. Интерфейс состоит из единственного метода, который возвращает текущее время в формате string.

Создание якорей и скелетов

Второй шаг состоит в компиляции IDL для создания кода якорей и скелетов Java, который будет использоваться для реализации клиента и сервера. Инструмент, поставляемый с JavaIDL нащывается idltojava:

idltojava remotetime.idl

Это автоматически сгенерирует код и для якорей и для скелетов. Idltojava сгенерирует Java package с названием IDL модуля: remotetime, и сгенерирует Java файлы, поместив их в поддиректорий remotetime. _ExactTimeImplBase.java - это скелет, который мы будем использовать для реализации объекта сервера, а _ExactTimeStub.java будет использован для клиента. Существует Java представление IDL интерфейса в ExactTime.java и набор других файлов поддержки, например, для облегчения доступа к операции сервиса указания имен.

Реализация сервера и клиента

Ниже вы можете видеть код серверной стороны. Реализация серверного объекта выполнена в классе ExactTimeServer. RemoteTimeServer является приложением, которое создает объект сервера, регистрирует его с помошью ORB, дает имя ссылке на объект, а затем мирно ожидает клиентского запроса.

//: c15:corba:RemoteTimeServer.java
import remotetime.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import org.omg.CORBA.*;
import java.util.*;
import java.text.*;

// Реализация серверного объекта
class ExactTimeServer extends _ExactTimeImplBase {
  public String getTime(){
    return DateFormat.
        getTimeInstance(DateFormat.FULL).
          format(new Date(
              System.currentTimeMillis()));
  }
}

// Реализация удаленного приложения
public class RemoteTimeServer {
  // Выброс исключений на консоль
  public static void main(String[] args) 
  throws Exception {
    // Создание и реализация ORB:
    ORB orb = ORB.init(args, null);
    // Создание серверного объекта и регистрция:
    ExactTimeServer timeServerObjRef = 
      new ExactTimeServer();
    orb.connect(timeServerObjRef);
    // Получение корневого контекста имен:
    org.omg.CORBA.Object objRef = 
      orb.resolve_initial_references(
        "NameService");
    NamingContext ncRef = 
      NamingContextHelper.narrow(objRef);
    // Присвоение строкового имени
    // для ссылки на объект (связывание):
    NameComponent nc = 
      new NameComponent("ExactTime", "");
    NameComponent[] path = { nc };
    ncRef.rebind(path, timeServerObjRef);
    // Ожидание запроса клиента:
    java.lang.Object sync =
      new java.lang.Object();
    synchronized(sync){
      sync.wait();
    }
  }
} ///:~

Как вы можете видеть, реализация серверного объекта достаточно проста. Это обычный Java класс, унаследованный от кода скелета, сгенерированного IDL компилятором. Вещи становятся много сложнее, когда происходит взаимодействие с ORB и другими службами CORBA.

Некоторые службы CORBA

Это короткое описание того, что делает JavaIDL код (в основном игнорируется часть CORBA кода, зависящая от производителя). Первая строка main( ) запускает ORB, это необходимо потому, что нашему серверу необходимо взаимодействовать с ним. Сразу после инициализации ORB создается серверный объект. На самом деле правильнее называть временный обслуживающий объект (transient servant object): объект, который принимает запросы от клиентов, и чье время жизни равно совпадает с временем жизни породившего процесса. Как только временный бслуживающий объект создан, он регистрируется с помошь ORB, что означает, что ORB знает о его наличи и теперь может перенаправлять к нему запросы.

До этого момента все, что мы имели - это timeServerObjRef - указатель на объект, который известен только внутри текущего серверного процесса. Следующий шаг состоит в присвоении строкового имени эому обслуживающему объекту. Клиент будет использовать имя для нахождения обслуживающего объекта. Мы совершили эту операцию, используя Сервис Указания Имен. Во-первых, нам необходима ссылка на Службу Указания Имен. Метод resolve_initial_references( ) принимает значимую ссылку на объект Службы Указания Имен, в случае JavaIDL “NameService”, и возвращает ссылку на объект. Он приводит сылку к специфичному типу NamingContext, используя метод narrow( ). Теперь мы можем использовать службу указания имен.

Для связывания обслуживающих объектов со ссылками на строковые объекты, мы сначала создаем объект NameComponent, инициализирует его значением “ExactTime” - строка имени, которую мы хотим связать с обслуживающим объектом. Затем, используя метод rebind( ), строковая ссылка связывается со ссылкой на объект. Мы испоьзуем rebind( ) для присвоения ссылки, даже если она уже существует, тогда как bind( ) выбрасывает исключение, если ссылка уже существует. Имя состоит в CORBA из последовательности NameContexts — поэтому мы используем массив для связывания имени со ссылкой на объект.

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

Теперь, когда мы имеем представление о том, что делает серверный код, давайте взглянем на код клиента:

//: c15:corba:RemoteTimeClient.java
import remotetime.*;
import org.omg.CosNaming.*;
import org.omg.CORBA.*;

public class RemoteTimeClient {
  // Выбрасываем исключение на консоль:
  public static void main(String[] args) 
  throws Exception {
    // Создание и инициализация ORB:
    ORB orb = ORB.init(args, null);
    // Получение контекста наименования:
    org.omg.CORBA.Object objRef = 
      orb.resolve_initial_references(
        "NameService");
    NamingContext ncRef = 
      NamingContextHelper.narrow(objRef);
    // Получение (разрешение) ссылки на строковый
    //  объект для сервера времени:
    NameComponent nc = 
      new NameComponent("ExactTime", "");
    NameComponent[] path = { nc };
    ExactTime timeObjRef = 
      ExactTimeHelper.narrow(
        ncRef.resolve(path));
    // Выполнение запроса к серверу:
    String exactTime = timeObjRef.getTime();
    System.out.println(exactTime);
  }
} ///:~

Первые несколько строк делают то же, что они делали в серверном процессе: инициализируют ORB и разрешают указатель на сервис указания имен. Далее, нам нужна ссылка на объект для обслуживающего объекта, поэтому мы передаем ссылку на строковый объект в метод resolve( ), и приводим результат к ссылке на интерфейс ExactTime, используя метод narrow( ). В конце мы вызываем getTime( ).

Активация процесса указания имен

Теперь м ыимеем серверное и клиентское приложения, готовые к взаимодействию. Вы видели, что оба они нуждаются в службе указания имен для связывания и разрешения указателя на строковый объект. Вы должны запустить процесс указания имен до запуска сервера и клиента. В JavaIDL служба указания имен является Java приложением, которое поставляется в пакете с продуктом, но для других продуктов это может быть не так. Служба указания имен JavaIDL запускается внутри экземпляра JVM и слушает по умолчанию сетевой порт 900.

Активация сервера и клиента

Теперь вы готовы запустить ваше серверное и клиентское приложение (в этом порядке, так как наш сервер временный). Если вы все установили правильно, то, что вы получите - это единственная строка вывода в клиентской консоли, сообщающая вам текущее время. Конечно это не очень волнующе само по себе, но вы должны принять во внимание одну вещь: даже если они располагаются на одной и той де машине, клиентское и серверное приложения запускаются внутри разных виртуальных машин и они могут общаться через лежащий в основе интегрирующий уровень, ORB и Сервис Указания Имен.

Этот простой пример предназначен ля работы без сети, но ORB обычно конфигурируется для независимости от местоположения. Когда сервер и клиент находятся на разных машинах, ORB может разрешать удаленные строковые ссылки, используя компонент, известный как Implementation Repository. Хотя Implementation Repository является частью CORBA, для него нет спецификации, так что он различен у разных производителей.

Как вы можете видеть, о CORBA есть много больше информации, чем было рассмотрено тут, но вы должны получить основную идею. Если вы хотите получить более подробную информацию относительно CORBA, начните с Web страницы OMG, на www.omg.org. Там вы найдете документацию, белые страницы, работы и ссылки на другие исходные тексты и продукты CORBA.

Java Апплеты и CORBA

Java апплеты могут выступать в роли CORBA клиентов. Таким образом апплеты могут получать доступ к удаленной информации и службам, существующим, как CORBA объекты. Но апплеты могут соединяться только с тем сервером, с которого их загрузили, так что все CORBA объекты, с которыми взаимодействует апплет, должны быть помещены на сервере. Это противоречит тому, для чего предназначен CORBA: дать вам полную независимость от местоположения.

Это особенность сетевой безопасности. Если вы в Интранете, одним из решений является отказ от системы безопасности броузера. Или в установки firewall на соединения со внешних серверов.

Некоторые из продуктов Java ORB предлагают потенциальное решение этой пролемы. Например, некоторые реализуют то, что называется тунелированием HTTP Tunneling, а другие имеют свои собственные особенности для firewall.

Это слишком сложный вопрос, чтобы он был освещен в приложении, но это то, в чем вы должны быть уверены.

CORBA против RMI

Вы видели, что одной из главных особенностей CORBA являтся поддержка RPC, которая позволяет вашим локальным объектам вызывать методы удаленного объекта. Конечно, есть родное свойство Java, которое делает то же самое: RMI (смотрите Главу 15). При использовании RMI возможным RPC между объектами Java, CORBA делает возможным RPC между объектами, реализованными на любом языке. В этом огромное различие.

Однако RMI может быть использовано для вызова сервисов удаленного не Java кода. Все, что вам нужно - это некоторый Java объект-оболочка, включающий в себя не Java код на стороне сервера. Объект-оболочка присоединяется внешним образом к Java клиенту по RMI, и внутренним образом соединяется с не Java кодом, используя одну из технологий, таких как JNI или J/Direct.

Такой подход требует от вас написания некоторого рода интеграционного уровня, который явно делает то, что CORBA делает за вас, но в этом случае у вас нет неоходимости использовать ORB сторонних разработчиков.

Enterprise JavaBeans

Предположим, [77] вам нужно разработать многоярусное приложение для просмотра и обновления записей в базе данных через Web интерфейс. Вы можете написать приложение для баз данных, используя JDBC, а Web интерфейс использует JSP/сервлеты, а распределенная система использует CORBA/RMI. Но какие дополнительные соображения вы должны принять во внимание при разработке системы распределенных объектов кроме уже известного API? Вот основные соображения:

Производительность: Распределенные объекты, которые вы создаете, должны хорошо работать, так как потенциально они должны обслуживать много клиентов одновременно. Вам надо использовать оптимизационные технологии, такие как кеширование и объединение таких ресурсов, как соединение с базой данных. Вам также понадобится управлять продолжительностью жизни ваших распределенных объектов.

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

Безопасность: Распределенный объект часто должен управлять авторизацией доступа клиента. В идеале вы добавляете новых пользователей и политики без перекомпиляции.

Распределенные Транзакции: Распределенные объекты должны быть способны прозрачно ссылаться на распределенные транзакции. Например, если вы работаете с двумя разными базами данных, вы должны быть способны обновить их одновременно в одной трензакции и отменить изменения, если не был выполнен определенный критерий.

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

Доступность:Если одна из машин вашей системы выключается, клиенты должны автоматически перейти к резервной копии объектов, работающих на другой машине.

Эти соображения, наряду с проблемами бизнеса, которые вы собираетесь решить, могут застопорить весь процесс разработки. Однако все эти проблемы, исключая проблемы вашего бизнеса, излишни — решения должны быть придуманы для каждого распределенного бизнес-приложения.

Sun, наряду с другими лидирующими производителями распределенных объектов, определила, что рано или поздно каждая команда разработчиков найдет обчные решения, поэтому она создала спецификацию Enterprise JavaBeans (EJB). EJB описывает модель компонент стороны сервера, принимающую во внимание все упомянутые выше соображения и стандартные подходы, которые позволят разработчикам создавать бизнес-компоненты, называемые EJB, которые будут изолированы от низкоуровневого “служебного” кода, а будут полностью сфокусированы на обеспечении бизнесс-логики. Поскольку EJB определены стандартным способом, они могут быть не зависимы от производителя.

JavaBeans против EJB

Из-за схожести имен часто путаются между моделью компонент JavaBeans и спецификацией Enterprise JavaBeans. JavaBeans и спецификация Enterprise JavaBeans разделяют одинаковые цели: продвижения повторного использования, компактность Java кода при разработке и инструменты разработки с использованием стандартных шаблонов, но мотивы спецификации больше подходят для решения различных проблем.

Стандарт, определенный в модели компонент JavaBeans предназначен для создания повторного использования компонент, которые обычно используются в интегрированной среде разработки и часто, но не всегда, являются визуальными компонентами.

Спецификация Enterprise JavaBeans определяет модель компонентов для разработки Java кода стороны сервера. Поскольку EJB могут потенциально запускаться на различных серверных платформах — включая центральные машины, которые не имеют визуальных дисплеев — EJB не может использовать графические библиотеки, типа AWT или Swing.

Спецификация EJB

Спецификация Enterprise JavaBeans описывает модель компонентов стороны сервера. Она определяет шесть ролей, которые используются для выполнения задач при разработке и развертывании, так же определяет компоненты системы. Эти роли используются в разработке, развертывании и запуске распределенных систем. Производители, администраторы и разработчики играют разные роли, позволяя разделять технологию и область знаний. Продавец обеспечивает техническое рабочее пространство, а разработчик создает специфичные для данной области компоненты, например, компонент “счет”. Та же сама компания может выполнять одну или несколько ролей. Роли, определенные в спецификации EJB сведены в следующую таблицу:

Роль

Отвественность

Поставщик Enterprise Bean

Разработчик отвечает за создание EJB компонент повторного использования. Эти компоненты упакованы в специальный jar файл (ejb-jar файл).

Сборщик приложения

Создает и собирает приложение из набора ejb-jar файлов. Это включает написание приложений, которые утилизируют набор EJB (напимер, сервлетов, JSP, Swing и т.д., и т.п.).

Установщик

Берет набор ejb-jar файлов от сборщика и/или Поставщика Bean и разворачивает их в среде времени выплнения: один или несколько EJB Контейнеров.

EJB Контейнер/Поставщик сервера

Предоставляет среду времени выполнения и инструменты, используемые для развертывания, администрирования и запуска EJB компонент.

Системный администратор

Управляет различными компонентами и службами, чтобы они были сконфигурированы и правильно взаимодействовали, также следит, чтобы система работала корректно.

EJB компоненты

EJB компоненты являются многократно используемыми элементами бизнес-логики, которые жестко следуют стандартам и шаблонам разработки, определенным в спецификации EJB. Это позволяет компонентам быть переносимыми. Это также позволяет другим службам — таким как безопасность, кэширование и распределенные транзакции — работать с пользо для компонент. Поставщик Enterprise Bean отвечает за разработку EJB компонент.

EJB контейнер и сервер

EJB Контейнер представляет из себя среду времени выполнения, которая содержит и запускает EJB компоненты и предоставляет набор стандартных служб для этих компонент. Обязанности EJB Контейнера четко определены в спецификации, чтобы обеспечить нейтралитет производителя. EJB контейнер предоставляет низкоуровневое “обслуживание” EJB, включая распределенные транзакции, безопасность, управление циклом жизни компонента, кэширование, нити процессов и управление сессиями. Поставщик EJB Конетйнера отвечает за предоставление EJB Контейнера.

EJB Сервер определяется как Сервер Приложений, который содержит и запускает один или несколько EJB Контейнеров. Поставщик EJB Сервера отвечает за предоставление EJB Сервера. Вы можете полагать, что EJB Контейнер и EJB Сервер это одно и то же.

Java Naming и Directory Interface (JNDI)

Java Naming and Directory Interface (JNDI) используется в Enterprise JavaBeans в качестве службы указания имен для EJB компонент в сети и других службах контейнера, таких как транзакции. JNDI работает очень похоже с другими стандартами, такими как CORBA CosNaming, и может на самом деле быть реализован в виде надстройки над ним.

Java Transaction API/Java Transaction Service (JTA/JTS)

JTA/JTS используются в Enterprise JavaBeans в качестве API транзакции. Поставщик Enterprise Bean может использовать JTS для создания кода транзакции, хотя EJB Контейнер чаще всего реализует транзакцию в EJB на полезных EJB компонентах. Установщик может определить атрбуты транзакции EJB компонента во время развертывания. EJB Контейнер отвечает за обработку тразакции не зависимо от того, является ли она локальной или распределенной. Спецификация JTS является Java отображением на CORBA OTS (Object Transaction Service).

CORBA и RMI/IIOP

Спецификация EJB определяет взаимодействие с CORBA через совместимость с CORBA протоколами. Это достигнуто путем совмещения EJB служб, таких как JTS и JNDI, с соотвествующими службами CORBA и реализацией RMI поверх IIOP протокола CORBA.

Использование CORBA и RMI/IIOP в Enterprise JavaBeans реализовано в EJB Контейнере и за это отвечает поставщик EJB Котейнера. Использование CORBA и RMI/IIOP в EJB Контейнере спрятано от самого EJB Контейнера. Это означает, что Поставщик Enterprise Bean может написать свой EJB Компонент и развернуть его в любом EJB Контейнере не заботясь о том, какие коммуникационные прооколы он использует.

Составные части EJB компонента

EJB состоит из нескольких частей, включая сам компонент, реализацию некоторых интерфейсов и информационный файл. Все это пакуется вместе в специальный jar файл.

Enterprise Bean

Enterprise Bean является Java классом, разработанным Поставщиком Enterprise Bean. Он реализует интерфейс Enterprise Bean и обеспечивает реализацию бизнес-методов, которые выполняет компонент. Класс не реализует никакую авторизацию, многопоточность или код транзакции.

Домашний интерфейс

Каждый содающийся Enterprise Bean должен иметь ассоциированный домашний интерфейс. Домашний интерфейс используется как фабрика для вашего EJB. Клиент использует Домашний интерфейс для нахождения экземпляра вашего EJB или создания нового экземпляра вашего EJB.

Удаленный интерфейс

Удаленный интерфейс является Java Интерфейсом, который отображает через рефлексию те методы вашего Enterprise Bean, которые вы хотите показывать внешнему миру. Удаленный интрфейс играет ту же роль, что и IDL интерфейс в CORBA.

Описатель развертывания

Описатель развертываия является XML файлом, который содержит информацию относительно вашего EJB. Исползование XML позволяет установщику легко менять атрибуты вашего EJB. Конфигурационные атрибуты, определеные в описателе развертывания, включают:

EJB-Jar файл

EJB-Jar файл - это обычный java jar файл, который содержит ваш EJB, Домашний и Удаленный интерфейсы наряду с описателем развертывания.

EJB операции

После того, как вы получили EJB-Jar файл, содержащий компонент, Домашний и Удаленный интерфейсы и описатеь развертывания, вы можете сложить все части вместе и в процессе понять, для чего нужны Домашний и Удаленный интерфейсы и как EJB Контейнер использует их.

EJB Контейнер реализует Домашний и Удаленный интерфейсы, которые есть в EJB-Jar файле. Как упоминалось ранее, Домашний интерфейс обеспечивает методы для создания и нахождения вашего EJB. Это означает, что EJB Контейнер отвечает за уравление жизненным циклом вашего EJB. Этот уровень ненаправленности позволяет учитывать происходящую оптимизацию. Например, 5 клиентов могут одновременно запросить определенный EJB через Домашний интерфейс, а EJB Контейнер должен ответить созданием только одого EJB и распределением его между 5 клиентами. Это достигается через Удаленный интерфейс, который так же реализуется через EJB Контейнер. Реализованный Удаленный объект играет роль довертельного объекта для EJB.

Все вызовы EJB ‘проксирубтся(proxied)’ через EJB Контейнер посредством Домашнего и Удаленного интерфейса. Этот обходной путь является причиной того, что EJB контейнер может управлять безопасностью и поведением транзакций.

Типы EJB

Спецификация Enterprise JavaBeans определяет различные типы EJB, которые имеют разичные характеристики и поведение. В спецификации определены две категории EJB: Сессионный Компонент и Сущностный Компонент. Каждая категория имеет свои варианты.

Сессионный компонент

Сессионный компонент используется для представления случаев использования или порядока выполняеых действий с поьзой для клиента. Они представляют операции с постоянными данными, но не сами постоянные данные. Есть два типа Сессионных Компонентов: Без Состояния(Stateless) и Полного Состояния(Stateful). Все Сессионные Компоненты должны реализовывать интерфейс javax.ejb.SessionBean. EJB Контейнер управляет жизнью Сессионного Компонента.

Сессионный Компонент Без Состояния - это самый простой в реализации тип EJB компонента . Он не содержит никаких значимых состояний для клиента между вызовами методов, так что они легко используются повторно на стороне сервера и поэтому они могут кэшироваться, они легко масштабируются при необходимости. Когда используете Сессионные Компоненты Без Состояния, все состояния можно хранить вне EJB.

Сессионный Компонент Полного Состояния содержит состояние между вызовами. Они имеют логику один к одному для клиентов и могут содержать состояния в себе. EJB Клнтейнер ответственен за объединение и кэширование Сессионных Компонентов Полного Состояния, что достигается через Неактивность и Активность. Если EJB Контейнер рушиться, данные всех EJB Сессионных Компонентов Полного Состояния могут быть потеряны. Некоторые высокоуровневые EJB Контейнеры обеспечивают восстановление Сессионных Компонентов Полного Состояния.

Сущностные компоненты

Сущностные компоненты являются компонентами, представляющие постоянные даные и их поведение. Сущностные компоненты могут быть разделены между многоми клиентами, точно так же как могут разделяться данные в базе данных. EJB Контейнер отвечает за кэширование Сущностных Объектов и за поддержкой интегрирования Сущностных Компонентов. Существование Сущностных Компонентов определяется EJB Контейнером, так что если EJB Контейнер рушится, Сущностные Компоненты будут доступны только тогда, когда будет доступен EJB Контейнер.

Есть два типа Сущностных Компонент: существующие с Управлением Контейнера (Container Managed persistence) и существующие с Управлением Компонентами (Bean-Managed persistence).

Container Managed Persistence (CMP). CMP Сущностные Компоненты реализованы с выгодой для EJB Контейнера. Через указанные в описании развертывания спецификации, EJB Контейнер связывает атрибуты Сущностных Компонент с некоторым постоянным хранилищем (обычно — но не всегда — это база данных). CMP снижает время разработки для EJB, так же, как и значительно снижает число требуемого кода.

Bean Managed Persistence (BMP). BMP Сущностные Компоненты реализовываются Поставщиком Enterprise Bean. Поставщик Enterprise Bean отвечает за реализацию логики, требуемой для создания новых EJB, изменения некоторых атрибутов EJB, удаление EJB и нахождение EJB в постоянном хранилище. Обычно для этого требуется написание JDBC кода для взаимодействия с базой данных или другим постоянным хранилищем. С помощью BMP разработчик имеет полный контроль над управлением существования Сущностного Объекта.

BMP также дает гибкость в тех местах, где реализация CMP не может быть использована. Например, если вы хотите создать EJB, который включает в себя некий код существующей главной системы, вы должны написать вашу устойчивось, используя CORBA.

Разработка EJB

В качестве примера будет реализован EJB компонент “Perfect Time” из предыдущего раздела, посвященного RMI. Пример будет выполнен как Сессионный Компонент без Состояния.

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

  1. Удаленный интерфейс должен быть публичным (public).
  2. Удаленный интерфейс должен расширять интерфейс javax.ejb.EJBObject.
  3. Каждый метод удаленного интерфейса должен декларировать java.rmi.RemoteException в предложении throws помимо всех исключений, спецефичных для приложения.
  4. Лбой объект, передаваемый в качестве аргумента или возвращаемого значения (встроенный, либо содержащийся внутри локального объекта) должен быть действительным с точки зрения RMI-IIOP типом данных (это относится и к другим EJB объектам).

Вот простой удаленный интерфейс для PerfectTime EJB:

//: c15:ejb:PerfectTime.java
//# Вы должны установить J2EE Java Enterprise 
//# Edition с java.sun.com и добавить j2ee.jar
//# в вашу переменную CLASSPATH, чтобы скомпилировать
//# этот файл. Подробности смотрите на java.sun.com.
// Удаленный интерфейс для PerfectTimeBean
import java.rmi.*;
import javax.ejb.*;

public interface PerfectTime extends EJBObject {
  public long getPerfectTime() 
    throws RemoteException;
} ///:~

Домашний интерфейс является фабрикой для создания компонента. Он может определить метод create, для создания экземпляра EJB, или метод finder, который находит существующий EJB и используется олько для Сущностных Компонент. Когда вы создаете Домашний интерфейс для EJB, вы должны следовать следующим принципам:

  1. Домашний интерфейс должен быть публичным (public).
  2. Домашний интерфейс должен расширять интерфейс javax.ejb.EJBHome.
  3. Каждый метод create Домашнего интерфейса должен декларировать java.rmi.RemoteException в преложении throws наряду с javax.ejb.CreateException.
  4. Возвращаемое значение метода create должно быть Удаленным интерфейсом.
  5. Возвращаемое значение метода finder (только для Сущностных Компонент) должно быть удаленным интерфейсом или java.util.Enumeration, или java.util.Collection.
  6. Любые объекты, передаваемые в качесвте аргумента (либо напрямую, либо внутри локального объекта) должны быть действительными с точки зрения RMI-IIOP типом данным (включая другие EJB объекты).

Стандартное соглашение об именах Домашних интерфейсов состоит в прибавлении слова “Home” в конец имени Удаленного интерфейса. Вот Домашний интерфейс для PerfectTime EJB:

//: c15:ejb:PerfectTimeHome.java
// Домашний интерфейс PerfectTimeBean.
import java.rmi.*;
import javax.ejb.*;

public interface PerfectTimeHome extends EJBHome {
  public PerfectTime create() 
    throws CreateException, RemoteException;
} ///:~

Теперь вы можете реализовать бизнес логику. Когда вы создаете вышу реализацию EJB класса, вы должны следовать этим требованиям (обратите внимание, что вы должны обратиться к спецификации EJB, чтобы получить полный список требований при разработке Enterprise JavaBeans):

  1. Класс должен быть публичным (public).
  2. Класс должен реализовывать EJB интерфейс (либо javax.ejb.SessionBean, либо javax.ejb.EntityBean).
  3. Класс должен определять методы, которые напрямую связываются с методами Удаленного интерфейса. Обратите внимание, что класс не реализует Удаленный интерфейс. Он отражает методы удаленного интерфейса, но не выбрасывает java.rmi.RemoteException.
  4. Определите один или несколько методов ejbCreate( ) для инициализации вашего EJB.
  5. Возвращаемое значение и аргументы всех методов должны иметь действительны тип данных с точки зрения RMI-IIOP.
//: c15:ejb:PerfectTimeBean.java
// Простой Stateless Session Bean,
// возвращающий текущее системное время.
import java.rmi.*;
import javax.ejb.*;

public class PerfectTimeBean 
  implements SessionBean {
  private SessionContext sessionContext;
  //возвращае текущее время
  public long getPerfectTime() { 
     return System.currentTimeMillis();
  }
  // EJB методы
  public void ejbCreate() 
  throws CreateException {}
  public void ejbRemove() {}
  public void ejbActivate() {}
  public void ejbPassivate() {}
  public void 
  setSessionContext(SessionContext ctx) {
    sessionContext = ctx;
  }
}///:~

Из-за простоты этого примера EJB методы (ejbCreate( ), ejbRemove( ), ejbActivate( ), ejbPassivate( )) оставлены пустыми. Этиметоды вызываются EJB Контейнером и используются для управления состоянием компонента. Метод setSessionContext( ) передает объект javax.ejb.SessionContext, который содержит информацию относительно контекста компонента, такую как текущая транзакция и информация безопасности.

После того, как мы создали Enterprise JavaBean, нам нужно создать описатель развертывания. Описатель развертывания - это XML файл, котрый описывает EJB компонент. Описатель развертывания должен хранится в файле, называемом ejb-jar.xml.

//:! c15:ejb:ejb-jar.xml
<?xml version="1.0" encoding="Cp1252"?>
<!DOCTYPE ejb-jar PUBLIC '-
//Sun Microsystems, Inc.
//DTD Enterprise JavaBeans 1.1
//EN' 'http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd'>

<ejb-jar>
  <description>Example for Chapter 15</description>
  <display-name></display-name>
  <small-icon></small-icon>
  <large-icon></large-icon>
  <enterprise-beans>
    <session>
      <ejb-name>PerfectTime</ejb-name>
      <home>PerfectTimeHome</home>
      <remote>PerfectTime</remote>
      <ejb-class>PerfectTimeBean</ejb-class>
      <session-type>Stateless</session-type>
      <transaction-type>Container</transaction-type>
    </session>
  </enterprise-beans>
  <ejb-client-jar></ejb-client-jar>
</ejb-jar>
///:~

Вы можете видеть, что Компонент, Удаленный интерфейс и Домашний интерфейс определены внури ярлыка <session> этого описателя развертывания. Описатель развертывания может быть сгенерирован автоматически при использовании инструментов разработки EJB.

Наряду со стандартным описателем развертывания ejb-jar.xml, спецификация EJB устанавливает, что любые ярлыки, специфичные для производитея, должны хранится в отдельном файле. Это обеспечивает высокую совместимость между компонентами и EJB контейнерами различных марок.

Файлы должны быть заархивированы внутри стандартного Java Archive (JAR) файла. Описатель развертывания должен помещаться внутри поддиректории /META-INF Jar.

После того, как EJB компонент определен в описателе развертывания, установщик может развернуть EJB компонент в EJB Контейнере. В то время ,когда это писалось, процесс установки был достаточно “GUI визуализированным” и специфичным для каждого индивидуального EJB Контейнера, так что этот обзор не документирует этот процесс. Однако каждый EJB Контейнер имеет хорошую документацию для равзертывания EJB.

Поскольку EJB компоненты являются распределенными компонентами, процесс установки должен также создавать некотые клиентские якоря для вызова EJB компонент. Эти классы должны помещаться в classpath клиентского приложения. Поскольку EJB компоненты могут реализовываться поверх RMI-IIOP (CORBA) или RMI-JRMP, генерируемые якоря могут различаться в зависимости от EJB Контейнера, тем не менее, они являются генерируемыми классами.

Когда клиентмкая программа хочет вызвать EJB, она должна найти EJB компонент внутри JNDI и получить ссылку на домашний интерфейс EJB компонента. Домашний интерфейс используется для создания экземпляра EJB.

В этом примере клиентская программа - это простая Java программа, но вы должны помнить, что она так же легко может быть сервлетом, JSP или даже распределенным объектом CORBA или RMI.

//: c15:ejb:PerfectTimeClient.java
// Клиентская программа для PerfectTimeBean

public class PerfectTimeClient {
public static void main(String[] args) 
throws Exception {
  // Получение контекста JNDI с помощью 
  // JNDI службы Указания Имен:
  javax.naming.Context context = 
    new javax.naming.InitialContext();
  // Поиск Домашнего интерфейса в
  // службе JNDI Naming:
  Object ref = context.lookup("perfectTime");
  // Приведение удаленного объекта к домашнему итерфейсу:
  PerfectTimeHome home = (PerfectTimeHome)
    javax.rmi.PortableRemoteObject.narrow(
      ref, PerfectTimeHome.class);
  // Создание удаленного объекта из домашнего интерфейса:
  PerfectTime pt = home.create();
  // Вызов getPerfectTime()
  System.out.println(
    "Perfect Time EJB invoked, time is: " + 
    pt.getPerfectTime() );
  }
} ///:~

Последовательность выполняемых действий поясняется комментариями. Обратите внимание на использование метода narrow( ) для совершения приведения объекта перед выполнением Java приведения. Это очень похоже на то, что происходит в CORBA. Также обратите внимание, что Домашний объект становится фабрикой для объекта PerfectTime.

Резюме о EJB

Спецификация Enterprise JavaBeans это кординальный шаг к стандартизации распределенных объектов и упрощению распределенных вычислений. Это основной элемент платформы Java 2 Enterprise Edition (J2EE), который пинимает на сбя основную поддержку общения распределенных объектов. В настоящее время существуют или появятся в бижайшем будущем многие инструменты, помогающие ускорить разработку EJB компонентов.

Этот обзор является только коротким туром по EJB. Более поробно о спецификации EJB вы можете посмотреть на официальной странице Enterprise JavaBeans по адресу java.sun.com/products/ejb/, где вы можете загрузиь последнюю спецификацию и ссылку на реализацию J2EE. Они могут быть использованы для разработки и развертывания ваших собственных EJB.

Jini: распределенные сервисы

Этот раздел [78] дает вам обзор технологии Jini от Sun Microsystems. Здесь описаны некоторые элементы Jini и показано как Jini архитектура помогает увеличить уровень абстракции в распределенной системе программирования, эффективно включая сетевое програмирование в объектно-ориентированное программирование.

В контексте Jini

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

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

Построенная на Java, сериализации объектов и RMI (все вместе это позволяет перемещать объекты по сети от одной виртуальной машины к другой) Jini пробует расширять выгоды объектно-ориентированного программирования в сети. Вместо того, чтобы требовать от производителей согласия на поддержку сетевых протоколов, через которые их устройства могли бы взаиможействовать, Jini позволяет устройсвтам говорить друг с другом через интерфейсы объектов.

Что такое Jini?

Jini - это набор API и сетевых протоколов, которые могут помочь вам построить и развернуть распределенную систему, организованную, как федерация сервисов. Сервисы могут быть всем, что сидит в сети и готово выполнить полезную функцию. Аппаратные устройства, программы, каналы связи — даже сами пользователи - люди — могут быть сервисами. Например Jini-совместимые дисководы могут предлагать сервис “хранения”. Jini-совместимые принтеры могут прелагать сервис “распечатки”. Таким образом, федерация служб является набором сервисов, доступных в сети в данный момент, которыми могут воспользываться клиенты (под клиентами подразумевается программы, службы или пользователи) для достижения некоторой цели.

Для выполнения задачи клиенты привлекают на помощь сервисы. Например, клиентские программы могут загружать кртинки из хранилища изображений цифровой камеры, передавать картинки службе постоянного хранения, предаставляемой дисководом, и посылать страницу эскизов фотографий различных размеров для распечатки на цветном принтере. В этом примере клиентская программа построена, как распределенная система, содержащая в себе сервис хранения изображений, сервис постоянного хранения и сервис цветной печати. Клиенты и сервисы этой распределеной системы работают совместно для выполнения задач: выгрузки хранимых изображений из цифровой камеры и распечатка страниц эскизов.

Идея, стоящая за миром федерации, состоит в том, что Jini просматривает сеть и не вовлекает центр управления. Поскольку не один из сервисов не загружается, набор всех сервисов доступен в сети из федерации — группы, состоящей из равных элементов. Вместо централной власти, инфраструктура Jini времени выполнения просто обеспечивает способ для клиентов и сервисов находить друг друга (через службу поиска, которая хранит справочник поддерживаемых в настоящее время сервисов). После того, как услуги найдут друг друга, они становятся собственными. Клиент и привлеченные им сервисы выполняют свою задачу независимо от инфраструктуры Jini времени выполнения. Если служба поиска Jini потерпит крушение, любая распределенная система, собранная через сервис поиска до его падения, продолжит свою работу. Jini даже включает сетевой протокол, который могут использовать клиенты для нахождения служб при отсутствии службы поиска.

Как работает Jini

Jini определяет ифраструктуру времени выполнения, которая располагается в сети и обеспечивает механизм, позволяющий вам добавлять, удалять, находить и получать доступ к сервисам. Инфраструктура времени выполнения располагается в трех местах: в сервисе поиска, которая находится в сети, в поставщиках услуг (таких как Jini-совместимые устройства) и в клиентах. Служба поиска является механизмом центральной организации для систем, базирующихся на Jini. Когда в сети становится доступным новый сервис, он регистрируется в службе поиска. Когда клиент хочет найи службу для выполнения какой-либо работы, он консультируется у службы поиска.

Инфроструктура времени выполнения использует один протокол сетевого уровня, называемый обнаружение(discovery) и два протокола объектного уровня, называемые объединение(join) и поиск(lookup). Обнаружение позволяет клиентам и службам обнаружить службу поиска. Объединение позволяет службам регистрироваться в службе поиска. Поиск позволяет клиенту опрашивать сервисы, в поисках тех, которые могут помочь в достижении цели.

Процесс обнаружения

Обнаружение работает следующим образом: предположим у вас есть Jini-совместимый дисковод, который предоставляет услугу постоянного хранения. Как только вы подключите дисковод к сети, он пошлет оповещение присутствия по средством группового пакета через хорошо знакомый порт. В оповещение присутствия включается IP адрес и номер порта, через который дисковод может общаться со службой поиска.

Сервис поиска следит хорошо знакомый порт в ожидании пакетов оповещения присутствия. Когда служба поиска получает оповещение присутствия, она открывает и инспектирует пакет. Пакет содержит информацию, которая позволяет службе поиска определить должна ли она связаться с отправителем пакета. Если это так, она соединяется с отправителем напрямую, создавая TCP соединение по IP адресу и номеру порта, полученному из пакета. Используя RMI, сервис поиска посылает к источнику пакета объект, называемый регистратором. Назначение объекта-регистратора заключается в содействии будующему взаимодействию со службой поиска. Вызывая методы этого объекта отправитель оповещающего пакета может выполнить объединение и поиск службы поиска. В случае дисковода, служба поиска должна создать TCP соединение с дисководом и послать ему объект-регистратор, через который дисковод смог бы зарегистрировать свою службу постоянного хранения через процес объединеия.

Процесс объединения

Как только поставщик сервиса получает объект-регистратор - конечный продукт регистрации - он готов выполнить объединение — стать частью федерации сервисов, зарегистрированных в службе поиска. Чтобы выполнить объединение, поставщик сервиса вызывает метод register( ), принадлежащий объекту-регистратору, передавая в качестве параметра объект, назваемый элемент службы, являющийся пакетом объектов, описывающих службу. Метод register( ) посылает копию элемента службы сервису поиска, где хранится эолемент службы. Когда это будет выполнено, поставщик сервиса завершает процесс объединения: его служба становится зарегистрированной в службе поиска.

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

Объекты службы обычно реализованы одним или несколькими интерфейсами, через которые клиенты взаимодействуют со службой. Напимер, служба поиска является Jini службой, а соответствующий объект службы - это объект-регистратор. Метод register( ), вызываемый поставщиком службы во время объединения, объявляется в интерфейсе ServiceRegistrar (член пакета net.jini.core.lookup), который реализуют все объекты-регистраторы. Клиенты и поставщики услуг общаются со службой поиска через объект-регистратор, вызывая методы, объявленные в интерфейсе ServiceRegistrar. Точно так же, дисковод будет предоставлять объект службы, который реализует некоторый хорошо известный интерфейс службы хранения. Клиенты будут искать и взаимодействовать с дисководом посредством интерфейса службы хранения.

Процесс поиска

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

Для выполнения поиска, клиент задействует метод lookup( ) объекта-регистратора. (Клиент, такой как поставщик службы, получает объект-регистратор в процессе обнаружения, описанном ранее.) Клиент передает в качестве аргумента метода lookup( ) шаблон службы - объект, являющийся критерием поиска при опросе. Шаблон службы может включать ссылки на массив объектов типа Class. Эти объекты типа Class указывают ищущей службе тип (или типы) Java объекта службы, требуемые клиенту. Шаблон службы также может включать ID службы, который уникально идентифицирует службу и атрибуты, которые точно должны соответствовать атрибутам, загружаемым поставщиком службы в элемент службы. Шаблон службы также может содержать подстановки ля любого из этих полей. Например, подстановки в поле ID службы будет совпадать с любым ID службы. метод lookup( ) посылает шаблон службы службе поиска, которая выполняет опрос и посылает назад ноль любому соответствующему объекту службы. Клиент получает ссылку на совпавший объект обслуживания в качестве возвращаемого значения метода lookup( ).

В общем случае клиент ищет сервис по типу Java, обычно это интерфейс. Например, если клиенту необходимо использовать принтер, он должен сравнивать шаблон сервиса, чтобы он включал объект Class с хорошо знакомым интерфейсом служб печати. Все службы печати должны реализовывать этот хорошо знакомый интерфейс. Служба поиска должна возвращать объект службы (или объекты), которые реализуют этот интерфейс. Атрибуты могут включаться в шаблон службы, чтобы сузить число совпадений для поиска, основывающегося на типе. Клиент должен использовать службу печати, вызывая методы хорошо знакомого интерфейса службы обслуживающего объекта.

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

Архитиктура Jini превносит объектно-ориентированное программирование в сеть, позволяя получать доступ к сетевым службам используя одно из фундаментальных свойств объектов: разделение интерфейса и реализации. Например, обслуживающий объект может предоставить клиенту доступ к службе многими способами. Объект может на самом деле представлять целую службу, которая загружается клиентом во время поиска, а затем выполняется локально. С другой стороны, обслуживающий объект может лишь замещать удаленную службу. Затем, когда клиент вызывает методы обслуживающего объекта, он посылает запрос по сети к серверу, который выполняет реальную работу. Третий вариант - это локальный обслуживающий объект и удаленный сервер, каждый из которых выполняет часть работы.

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


Клиент общается с сервером через хорошо знакомый интерфейс

Разные реализации одного и того же интерфейса службы могут использовать полностью различный подход и разные сетевые протоколы. Служба может использовать специализированные аппаратные средства для полного удовлетворения клиентским запросам или может делать всю работу в програмной части. Фактически, подход при реализации одной службы может измениться по прошествии времени. Клиент может быть уверен, что он имеет обслуживающий объект, который понимает текущую реализацию службы, поскольку клиент принимает обслуживающий объект (посредством службы поиска) от самого поставщика службы. С точки зрения клиента служба выглядит, как хорошо знакомый интерфейс, независимо от того, как эта служба реализована.

Абстрагирование распределенной системы

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

Резюме

Наряду с Jini для локальных сетевых устройств, эта глава ввела некоторые, но не все, компоненты, которые Sun называет J2EE: Java 2 Enterprise Edition. Целью J2EE является построение множества инструментов, которые позволят Java разработчикам строить приложения, основывающиеся на сервере, много быстрее и делать их платформонезависимыми. Строить такие приложения не только сложно и долго, но особенно сложно строить их так, чтобы они легко переносились на другие платформы, а также сохранить бизнес-логику, отделив ее от лежащих в основе деталей реализации. J2EE обеспечивает рабочее пространство, помогающее в создании приложений, работающих с сервером, такие приложения теперь в моде, и потребность в них будет возрастать.

Упражнения

Решения для выбранных управжнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.

  1. Скомпилируйте и запустите программы JabberServer и JabberClient из этой главы. Теперь измените файлы, удалив всю буфферизацию ввода и вывода, затем скомпилируйте и запустите программу снова, чтобы увидеть результат.
  2. Создайте сервер, который спрашивает пароль, а затем открывает файл и посылает его содержимое по сети. Создайте клиента, который соединяется с сервером, выдает соответствующий пароль, затем получает и записывает файл. Проверьте пару программ на вашей машине, используя localhost (IP адрес заглушки 127.0.0.1 производится вызовом InetAddress.getByName(null)).
  3. Измените сервер из Упражнения 2 так, чтобы он использовал множественные потоки для обслуживания множественных клиентов.
  4. Измените JabberClient.java так, чтобы не происходил сброс буфера и пронаблюдайте эффект.
  5. Измените MultiJabberServer так, чтобы он использовал накопление нитей вместо выбрасывания нити при каждом отключении клиента, нити должны помещать себя в “доступный пул” нитей. Когда новый клиент хочет подключится, сервер должен искать в доступном пуле нить для обработки запроса, и если нить не найдена, создавать новую. Таким образом число необходимых нитей на самом деле будет больше необходимого количества. Число накопленных нитей такое, что не требуется изишнего создания и разрушения нити для каждого нового клиента.
  6. Начав с ShowHTML.java, создайте апплет, который является защищенным паролем шлюзом к определенной части вашего Web сайта.
  7. Измените CIDCreateTables.java так, чтобы он читал SQL строки из текстового файла вместо CIDSQL.
  8. Сконфигурируйте вашу систему так, чтобы вы могли полностью удовлетворить CIDCreateTables.java и LoadDB.java.
  9. Измените ServletsRule.java, переписав метод destroy( ), чтобы он записывал значение i в файл, и метод init( ), чтобы он восстанавливал значение. Продемонстрируйте, что он работает при перезапуске контейнера сервлетов. Если у вас нет контейнера сервлетов, вы можете загрузить, установить и запустить Tomcat jakarta.apache.org, чтобы запускать сервлеты.
  10. Создайте сервлет, который добавляет cookie в объект ответа, таким образом сохраняя их на стороне клиента. Добавьте в сервлет код, который находит и отображает cookie. Если у вас нет контейнера сервлетов, вы можете загрузить, установить и запустить Tomcat jakarta.apache.org, чтобы запускать сервлеты.
  11. Создайте сервлет, который использует объект Session для хранения информации о сессии по вашему выбору. В том же сервлете найдите и отобразите эту информацию о сессии. Если у вас нет контейнера сервлетов, вы можете загрузить, установить и запустить Tomcat jakarta.apache.org, чтобы запускать сервлеты.
  12. Создайте сервлет, который изменяет интервал неактивности сессии на 5 секунд, с помощью вызова getMaxInactiveInterval( ). Проверьте, чтобы убедится, что сессия не продолжается после 5 секунд. Если у вас нет контейнера сервлетов, вы можете загрузить, установить и запустить Tomcat jakarta.apache.org, чтобы запускать сервлеты.
  13. Создайте JSP страницу, печатающую строку текста, используя ярлык <H1>. Установите цвет этого текста случайным образом, используя код Java, встроенный в JSP страницу. Если у вас нет JSP контейнера, вы можете загрузить, установить и запустить Tomcat jakarta.apache.org, чтобы запускать JSP.
  14. Измените значение максимального возраста в Cookies.jsp и пронаблюдайте поведение в двух разных браузерах. Также обратите внимание на разницу между повторным посещением страницы и закрытием и перезапуском броузера. Если у вас нет JSP контейнера, вы можете загрузить, установить и запустить Tomcat jakarta.apache.org, чтобы запускать JSP.
  15. Создайте JSP с полями, которая позволит пользователю вводить время действительности сессии, а второе поле, которое содержит данные, хранимые в сессии. Кнопка отсылки обновляет страницу и показывает текущее время истечения и данные сессии, затем помещает их в качестве значений по умолчанию вышеупомянутых полей. Если у вас нет JSP контейнера, вы можете загрузить, установить и запустить Tomcat jakarta.apache.org, чтобы запускать JSP.
  16. (Повышенной сложности) Возьмите программу VLookup.java и изменте ее так, чтобы когда вы щелкали на результирующее имя, она автоматически брала имя и копировала его в буфер обмена (чтобы вы могли просто вставить его в ваш электронный адрес). Вам нужно вновь обратиться к Главе 13, чтобы вспомнить как использовать буфер обмена в JFC.

[72] Это означает более четырех миллиардов чисел, которые появляются повторно. Новый стандарт IP адресов будет использовать 128-битовый номер, который должен производить достаточно уникальных IP адресов в обозримом будующем.

[73] Создано Dave Bartlett.

[74] Dave Bartlett помогал в разработке этого материала, а также раздела JSP.

[75] Главная догма Эксремального Программирования (Extreme Programming (XP)). Смотрите www.xprogramming.com.

[76] Многие клетки мозга умирают в агонии при обнаружении этой информации.

[77] Этот раздел вышел при содействии Robert Castaneda с помошью Dave Bartlett.

[78] Этот раздел вышел при содействии Bill Venners (www.artima.com).

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]