Доступ к базам данных из Java-программ и проблемы русификации

С. Б. Дунаев
Статья написана автором в октябре 1997 г и опубликована в ж-ле СУБД №1-1 1998

Введение

Разумеется, что организовать доступ к базам данных из современного языка программирования в наше время не представляет никакой сложности. Более того, и сами языки программирования более всего оцениваются разработчиками по типу и возможностям заложенных в них средств доступа к базам данных, удобству и полноте интерфейсов. В этом смысле Java не представляет исключения. Уже в версии JDK1.1 появился пакет классов java.sql, обеспечивающий больщинство функций, известных к тому времени разработчикам ODBC-приложений. В этом пакете содержится ряд замечательных классов, например: java.sql.CallableStatement, который обеспечивает выполнение на Java хранимых процедур; java.sql.DatabaseMetaData, который исследует базу данных на предмет ее реляционной полноты и целостности с получением самых разнообразных данных о типах и содержимом таблиц, колонок, индексов, ключей и т.д.; наконец, - java.sql.ResultSetMetaData, с помощью которого можно выводить в удобном виде всю необходимую информацию из таблиц базы данных или печатать сами метаданные в виде названий таблиц и колонок.

Однако, коренное отличие Java от других традиционных языков программирования заключается в том, что одни и те же функции доступа к базам данных, с помощью универсальности и кроссплатформенности Java, можно организовать чрезвычайно гибко, используя все преимущества современных объектно-ориентированных технологий, WWW и Intranet/Internet. Рассмотрим по порядку все варианты использования Java-программ при взаимодействии с базами данных.

1. Java-программы и апплеты с интерфейсом JDBC-ODBC

JDBC (Java Database Connectivity) является не протоколом, а интерфейсом и основан на спецификациях SAG CLI (SQL Access Group Call Level Interface - интерфейс уровня вызова группы доступа SQL).

Сам по себе JDBC работать не может и использует основные абстракции и методы ODBC. Хотя в стандарте JDBC API и предусмотрена возможность работы не только через ODBC, а и через использование прямых линков к базам данных по двух- или трех-звенной схеме (см. Рис.1), эту схему используют гораздо реже, чем повсеместно используемый JDBC-ODBC-Bridge занимающий центральное место в общей схеме взаимодействия интерфейсов (см. Рис. 2)

Рис. 1. Непосредственный доступ к базе данных по 3-х-звенной схеме.

Рис. 2. Схема взаимодействия интерфейсов.

Даже беглого взгляда на Рис. 2 вполне достаточно, чтобы понять - общая схема взаимодействия интерфейсов в Java удивительным образом напоминает столь всем знакомую схему ODBC с ее гениальным изобретением драйвер-менеджера к различным СУБД и единого универсального пользовательского интерфейса. JDBC Driver Manager - это основной ствол JDBC-архитектуры. Его первичные функции очень просты - соединить Java-программу и соответствующий JDBC драйвер и затем выйти из игры. Естественно, что ODBC был взят в качестве основы JDBC из-за его популярности среди независимых поставщиков программного обеспечения и пользователей. Но тогда возникает законный вопрос - а зачем вообще нужен JDBC и не легче ли было организовать интерфейсный доступ к ODBC-драйверам непосредственно из Java? Ответом на этот вопрос может быть только однозначное нет. Путь через JDBC-ODBC-Bridge, как ни странно, может оказаться гораздо короче.

  1. ODBC нельзя использовать непосредственно из Java, поскольку он основан на C-интерфейсе. Вызов из Java C-кода нарушает целостную концепцию Java, пробивает брешь в защите и делает программу трудно-переносимой.
  2. Перенос ODBC C-API в Java-API нежелателен. К примеру, Java не имеет указателей, в то время как в ODBC они используются.
  3. ODBC слишком сложен для понимания. В нем смешаны простые и сложные вещи, причем сложные опции иногда применяются для самых простых запросов.
  4. Java-API необходим, чтобы добиться абсолютно чистых Java решений. Когда ODBC используется, то ODBC-драйвер и ODBC менеджер должны быть инсталлированы на каждой клиентской машине. В то же время, JDBC драйвер написан полностью на Java и может быть легко переносим на любые платформы от сетевых компьютеров до мэйнфреймов.

JDBC API - это естественный Java-интерфейс к базовым SQL абстракциям и, восприняв дух и основные абстракции концепции ODBC, он реализован, все-таки, как настоящий Java-интерфейс, согласующийся с остальными частями системы Java.

В отличие от интерфейса ODBC, JDBC организован намного проще. Главной его частью является драйвер, поставляемый фирмой JavaSoft для доступа из JDBC к источникам данных. Этот драйвер является самым верхним в иерархии классов JDBC и называется DriverManager. Согласно, установившимся правилам Internet, база данных и средства ее обслуживания идентифируются при помощи URL.

jdbc::

где под понимается имя конкретного драйвера, или некоего механизма установления соединения с базой данных, например, ODBC. В случае применения ODBC, в URL-строку подставляется именно эта аббревиатура, а в качестве используется обычный DSN (Data Source Name), т.е. имя ODBC-источника из ODBC.INI файла. Например:

jdbc:odbc:dBase

В некоторых случаях вместо ODBC может быть использовано имя прямого сетевого сервиса к базе данных, например:

jdbc:dcenaming:accounts-payable,
или
jdbc:dbnet://ultra1:1789/state

В последнем случае часть URL //ultra1:1789/state представляет собой и описывает имя хоста, порт и соответствующий идентификатор для доступа к соответствующей базе данных.

Однако, как уже говорилось выше, чаще всего, все-таки используется механизм ODBC благодаря его универсальности и доступности. Программа взаимодействия между драйвером JDBC и ODBC разработана фирмой JavaSoft в сотрудничестве с InterSolv и называется JDBC-ODBC-Bridge. Она реализована в виде JdbcOdbc.class (для платформы Windows JdbcOdbc.dll) и входит в поставку JDK1.1. Помимо JdbcOdbc-библиотек должны существовать специальные драйвера (библиотеки), которые реализуют непосредственный доступ к базам данных через стандартный интерфейс ODBC. Как правило эти библиотеки описываются в файле ODBC.INI. На внутреннем уровне JDBC-ODBC-Bridge отображает медоды Java в вызовы ODBC и тем самым позволяет использовать любые существующие драйверы ODBC, которых к настоящему времени накоплено в изобилии.

Рассмотрим типичное приложение на Java c доступом к типичному реляционному серверу или даже к обычной dBase-таблице.

// Следующий код на Java используется как пример. Простой подстановкой
// соответствующих значений url, login, и password, и, затем подстановкой
// SQL операторов вы можете посылать их в базу данных.
//--------------------------------------
//
// Module: SimpleSelect.java
//
// Описание: Эта программа для ODBC API интерфейса. Java-приложение
// будет присоединяться к JDBC драйверу, посылать select оператор
 // и показывать результаты в таблице
 //
 // Продукт: JDBC к ODBC Мост
 //
 // Автор: Karl Moss (С.Дунаев модификация для работы с кириллицей)
 //
 // Дата: Апрель 1997
 //
 // Copyright: 1990-1996
INTERSOLV, Inc.
 // This software contains
confidential and proprietary
 // information of INTERSOLV, Inc.
 //--------------------------------------
import java.net.URL;
import java.sql.*;
import java.io.*;
class SimpleSelect {
 public static void main (String args[]) {
 String url = _jdbc:odbc:dBase_;
 String query = _SELECT * FROM my_table_;
 try {
 // Загрузка jdbc-odbc-bridge драйвера
 Class.forName (_sun.jdbc.odbc.JdbcOdbcDriver_);
 DriverManager.setLogStream(System.out);
 // Попытка соединения с драйвером. Каждый из
 // зарегистрированных драйверов будет загружаться, пока
 // не будет найден тот, который сможет обработать этот URL
 Connection con = DriverManager.getConnection (
 url, __, __);
 // Если не можете соединиться, то произойдет exception
 // (исключительная ситуация). Однако, если вы попадете
 // в следующую строку программы, значит вы успешно соединились с URL
 // Проверки и печать сообщения об успешном соединении
 //
 checkForWarning (con.getWarnings ());
 // Получить DatabaseMetaData объект и показать
 // информацию о соединении
 DatabaseMetaData dma = con.getMetaData ();
 //System.out.println(_\nConnected to _ + dma.getURL());
 //System.out.println(_Driver _ +
 //dma.getDriverName());
//System.out.println(_Version _ +
 //dma.getDriverVersion());
 //System.out.println(__);
 // Создать Оператор-объект для посылки
 // SQL операторов в драйвер
 Statement stmt = con.createStatement ();
 // Образовать запрос, путем создания ResultSet объекта
 ResultSet rs = stmt.executeQuery (query);
 // Показать все колонки и ряды из набора результатов
 dispResultSet (rs);
 // Закрыть результирующий набор
 rs.close();
 // Закрыть оператор
 stmt.close();
 // Закрыть соединение
 con.close();
 }
 catch (SQLException ex) {
 // Случилось SQLException. Перехватим и
 // покажем информацию об ошибке. Заметим, что это
 // может быть множество ошибок, связанных вместе
 //
 //System.out.println (_\n*** SQLException caught ***\n_);
 while (ex != null) {
 //System.out.println (_SQLState: _ +
 // ex.getSQLState ());
 //System.out.println (_Message: _ + ex.getMessage ());
 //System.out.println (_Vendor: _ +
 //ex.getErrorCode ());
 ex = ex.getNextException ();
 //System.out.println (__);
 }
 }
 catch (java.lang.Exception ex) {
 // Получив некоторые другие типы exception, распечатаем их.
 ex.printStackTrace ();
 }
 }
 //----------------------------------
 // checkForWarning
 // Проверка и распечатка предупреждений. Возврат true если
 // предупреждение существует
 //----------------------------------
 private static boolean checkForWarning (SQLWarning warn)
 throws SQLException {
 boolean rc = false;
 // Если SQLWarning объект был получен, показать
 // предупреждающее сообщение.
 if (warn != null) {
 System.out.println (_\n *** Warning ***\n_);
 rc = true;
 while (warn != null) {
 //System.out.println (_SQLState: _ +
 //warn.getSQLState ());
 //System.out.println (_Message: _ +
 //warn.getMessage ());
 //System.out.println (_Vendor: _ +
 //warn.getErrorCode ());
 //System.out.println (__);
 warn = warn.getNextWarning ();
 }
 }
 return rc;
 }
 //----------------------------------
 // dispResultSet
 // Показать таблицу полученных результатов
 //----------------------------------
 private static void dispResultSet (ResultSet rs)
 throws SQLException, IOException
 {
         // Объявление необходимых переменных и
         // константы для желаемой таблицы перекодировки данных
 int i, length, j;
 String cp1 = new String(_Cp1251_);
 // Получить the ResultSetMetaData. Они будут использованы
 // для печати заголовков
 ResultSetMetaData rsmd = rs.getMetaData ();
 // Получить номер столбца в результирующем наборе
 int numCols = rsmd.getColumnCount ();
 // Показать заголовок столбца
 for (i=1; i<=numCols; i++) {
 if (i > 1) System.out.print(_,_);
 //System.out.print(rsmd.getColumnLabel(i));
 }
 System.out.println(__);
 // Показать данные, загружая их до тех пор, пока не исчерпается
 // результирующий набор
 boolean more = rs.next ();
 while (more) {
 // Цикл по столбцам
 for (i=1; i<=numCols; i++) {
// Следующая группа операторов реализует функции перекодировки
// строк из таблицы базы данных в желаемый формат, потому что в
// различных базах символы могут быть закодированы произвольным
// образом. Если использовать стандартный метод - getString - на выходе
// получается абракадабра. Строки нужно сначала перевести в Unicode,
// затем конвертировать в строку Windows и убрать лидирующие нули
InputStream str1 = rs.getUnicodeStream(i);
         byte str2[];
         byte str3[];
         int sizeCol = rsmd.getColumnDisplaySize(i);
         str2 = new byte[sizeCol+sizeCol];
         str3 = new byte[sizeCol+sizeCol];
         length = str1.read(str2);
 // Здесь нужно убрать нули из строки, которые предваряют каждый
 // перекодированный символ
         k=1;
         for (j=1; j<sizeCol*2; j++) {
                if (str2[j] != 0) {
                        str3[k]=str2[j]; k=k+1; } }
         String str = new String(str3,cp1);
         System.out.print(str);
 }
 System.out.println(__);
 // Загрузка следующего ряда в наборе
 more = rs.next ();
 }
 }
}

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

2. Спец-ориентированные Java-приложения

2.1 RMI-приложения

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

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

Рассмотрим основные шаги для построения работающего RMI-приложения:

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

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

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

rmi://localhost/LookupServer

Клиент, используя эту запись, будет пытаться отыскать объект с данным именем, и получить удаленную ссылку к нему. Затем вызванный метод будет обработан с помощью RMI компилятора и преобразован из пользовательского кода в последовательную форму объекта, который передается пользователю с помощью TCP/IP.

Разработка удаленного объектного кода

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

// Lookup.java
import java.rmi.*;
public interface Lookup extends Remote {
public String findInfo(String info) throws RemoteException;
 }

Java.rmi.Remote - пустой интерфейс, который указывает, что это удаленный объект, - так объекты класса, выполняющие Поиск(_Lookup_) отмечены удаленными ссылками. Все методы в удаленном интерфейсе должны быть объявлены через исключение типа Java.rmi.RemoteException, которое активизируется всякий раз, когда метод удаленного вызова дает сбои.

Разработка серверного кода

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

Для нашего примера это будет выглядеть следующим образом:

// LookupServer.java
 import java.io.*;
 import java.util.*;
 import java.rmi.*;
 import java.rmi.server.*;
 public class LookupServer extends UnicastRemoteObject
 implements Lookup {
 private Vector save = new Vector();
 public LookupServer(String db) throws RemoteException
 {
 try {
 FileReader fr = new FileReader(db);
 BufferedReader br = new BufferedReader(fr);
 String s = null;
 while ((s = br.readLine()) != null)
 save.addElement(s);
 fr.close();
 }
 catch (Throwable e) {
 System.err.println(_exception_);
 System.exit(1);
 }
 }
 public String findInfo(String info)
 {
 if (info == null)
 return null;
 info = info.toLowerCase();
 int n = save.size();
 for (int i = 0; i < n; i++) {
 String dbs = (String)save.elementAt(i);
 if (dbs.toLowerCase().indexOf(info) != -1)
 return dbs;
 }
 return null;
 }
 public static void main(String args[])
 {
 try {
 RMISecurityManager security =
 new RMISecurityManager();
 System.setSecurityManager(security);
 String db = args[0];
 LookupServer server = new LookupServer(db);
 Naming.rebind(_LookupServer_, server);
 System.err.println(_LookupServer ready..._);
 }
 catch (Throwable e) {
 System.err.println(_exception: _ + e);
 System.exit(1);
 }
 }
 }

Сервер читает в текстовой базе номера телефонов и имена и сохраняет их внутренне. Метод findInfo ищет затем нужное имя и телефон.

Пример базы данных:

Ivanov, Ivan 295-0083
Petrov, Peter 775-9958
Romanov, Alexander 555-7779

Заметим, что LookupServer является расширением стандартного класса java.rmi.server.UnicastRemoteObject и выполняет Lookup. Один из этих классов обеспечивает некоторые базисные реквизиты, необходимые для удаленных объектов, а другой определяет методы, которые будут вызваны дистанционно.

Установка службы безопасности

Наиболее сложная часть этого кода - то, что происходит в процедуре main(). Первым делом нужно установить защиту. RMI принужден загружать удаленные .class файлы и в этом смысле напоминает какой-нибудь web-браузер с его операциями по загрузке апплетов, что само по себе всегда небезопасно. Если вы не установили защиту, то по умолчанию должны загружаться только локальные файлы, и RMI по определению не может работать с такими ограничениями. Так что вы должны установить security manager, чтобы сделать возможной загрузку удаленных .class файлов.

Образец LookupServer затем регистрируется с помощью службы Naming.rebind и становится доступным клиенту по имени.

Вы могли бы задаться вопросом, как удаленный метод фактически становится вызываемым, если сервер не содержит никакого сетевого кода и никаких TCP/IP примитивов? Это происходит за сценой, поскольку сервер и клиент используют так называемые скелетоны и стабы для коммуникации между собой. Соответствующие .class файлы генерируются из серверного .class файла через RMI транслятор, описанный ниже.

Концептуально, класс stub(заглушка) выглядит так:

public class LookupServer_Stub extends java.rmi.server.RemoteStub
implements Lookup, java.rmi.Remote { ... }

и скелетон - так:

public class LookupServer_Skel implements
java.rmi.server.Skeleton { ... }

Использование команды:

Javap -c LookupServer_Stub

будет показывать байт-код и иллюстрировать то, что происходит за сценой.

Стаб(stub) -это суррогат для удаленного объекта, и скелетон - некая сущность на сервере, которая обрабатывает удаленные вызовы.

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

Разработка клиентского кода

// LookupClient.java
import java.rmi.*;
import java.rmi.server.*;
public class LookupClient {
public static void main(String args[])
{
try {
RMISecurityManager security =
new RMISecurityManager();
System.setSecurityManager(security);
String host = _localhost_;
String server = _LookupServer_;
String name = _rmi://_ + host + _/_ + server;
Lookup look_obj = (Lookup)Naming.lookup(name);
String results = look_obj.findInfo(args[0]);
if (results == null)
System.err.println(_** not found **_);
else
System.out.println(results);
}
catch (Throwable e) {
System.err.println(_exception: _ + e);
System.exit(1);
}
}
}

Если вы активизируете сервер, выполняя прямой клиентский запрос, то защита для пользователя определяется такая же как и на сервере. URL при этом определяется как:

Rmi://localhost/LookupServer

где localhost - имя локального компьютера (IP, адрес = 127.0.0.1), используемого как сервер. Клиент располагается на той же самой машине. Вы можете также использовать и удаленную главную ЭВМ. Когда вызов к нужному методу сделан, результаты незамедлительно передаются клиенту.

Компиляция кода

Три файла - Lookup.java, LookupServer.java, и LookupClient.java компилируются как и обычно в Java:

javac Lookup.java
javac LookupServer.java
javac LookupClient.java

Выполнение RMI компилятора

После того, как вы откомпилируете эти файлы, выполните RMI Compiler (rmic):

rmic LookupServer

чтобы получить LookupServer_Skel.class и LookupServer_Stub.class файлы. Вы должны переместить клиентские файлы (Lookup.class, LookupClient.class, и LookupServer_Stub.class) в директорию, откуда вы их желаете выполнять как клиент.

Вы должны переместить серверные файлы (Lookup.class, LookupServer.class, LookupServer_Skel.class, и LookupServer_Stub.class) в директорию, где они станут доступными для публичного доступа.

Регистрация

Объект, к которому обращаются дистанционно, должен быть введен в регистр объектов, т.е. зарегистрирован. В JDK 1.1 имеется специальная программа

Rmiregistry

Rmiregistry может выполняться или в отдельном окне или как фоновый процесс на сервере.

Старт сервера

Вы должны стартовать сервер по команде:

java LookupServer database_name

К примеру, если ваша база данных с набором имен и телефонов находится в файле C:\PHONE.TXT, то вы должны дать команду:

java LookupServer C:\PHONE.TXT

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

Клиентская программа стартуется по команде:

java LookupClient Ivanov

_Ivanov_ - это искомое имя в базе.

Таким образом, RMI дает возможность создавать распределенные Java-to-Java прикладные программы, в которых методы удаленных объектов Java вызываются из других Java-программ на различных главных ЭВМ так как, если бы эти методы вызывались локально. Естественно, что подобные возможности можно эффективно использовать при работе с SQL-серверами, которые предоставляют Java-API интерфейс для доступа к данным. Например, в группе продуктов Informix в Informix Client SDK дано описание Informix Object Interface for Java, где приводятся многочисленные примеры, как организовать взаимодействующие RMI-приложения с доступом к базам данных Informix. Более того, имеется и соответствующий RMI-сервер, который содержит массу удобных и полезных методов, которые можно вызывать дистанционно. В сущности, приведенный выше пример можно приспособить для работы с любыми базами данных и SQL-серверами, если вы знаете каким образом устроен Java-API интерфейс для доступа к базам данных. В крайнем случае не возбраняется и использование JDBC в RMI-приложениях, хотя вряд ли это будет в достаточной степени эффективно. В Informix, например, для непосредственного взаимодействия с базами данных существуют два RMI-пакета: informix.api.remote.rmi - для удаленных клиентов и informix.api.remote.rmi.server - для rmi-сервера. При этом в клиентском приложении используется интерфейс к DBMSManager, который накапливает информацию обо всех серверах Informix и базах данных, и вы можете установить либо локальное, либо удаленное соединение с базой через RMI сервер. Для локального соединения создается DirectDBMSManager объект. Для удаленного соединения создается RMIDBMS-Manager объект и передается к соответствующему RMI серверу. Спецификация RMI сервера осуществляется в форме:

rmi://hostname[:port]/
//Создание DBMSManager объекта
DBMSManager getDBMSManager() throws Exception
{
// based on RMI Checkbox, get appropriate DBMSManager
DBMSManager dbmsManager;
if (RMIcheckbox.getState())
dbmsManager = new RMIDBMSManager(rmiServerTextField.getText());
else
dbmsManager = new DirectDBMSManager();
return dbmsManager;

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

2.1 Java, инкапсулированная в СУБД

Java может быть встроена в СУБД множеством различных способов и при этом всегда достигается решение сразу нескольких задач:

Следующий пример вставляет новую запись в таблицу:

INSERT INTO employees (id, name, Address)
VALUES(1789, _Serg Dunaev_, new Rus_Address(_58 Gagarin Street_, _153038_)
)

Следующий запрос с использованием Java-объектов возвращает список сотрудников, живущих на указанной улице:

SELECT name
FROM employees
WHERE Rus_Address.street="Gagarin Street"

Единая программная модель. Впервые прикладные программные компоненты можно будет перемещать между клиентскими программами, серверами приложений и СУБД. Разработчики смогут использовать единую программную модель на всех уровнях информационной системы. Произвольная программа на Java состоит из набора классов. Для их использования необходимо провести инсталляцию Java-классов в СУБД. Классы должны быть откомпилированы в байт-код и тем самым, готовы для использования в любой виртуальной машине. В связи с использованием виртуальной машины для исполнения методов на Java и встроенной поддержке интерфейса JDBC, один и тот же объект на языке Java может быть использован как внутри, так и вне СУБД.

3. Java-сервлеты

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

3.1 Сервлеты на базе Java Web Server

Сервлеты являются объектами, которые образуют специфический интерфейс, с помощью которого их можно свободно встраивать в Java-ориентированные Web-сервера. В отличие от апплетов, они являются облегченными Java-объектами (без графики и GUI-компонентов), но зато, будучи кроссплатформенными и динамически загружаемыми и, используя все возможности Java, они могут использовать практически любой HTML и взаимодействовать с любым браузером. Естественно, что сервлеты можно использовать и используют для доступа к базам данных, программируя все удаленные операции с базой из браузера. В настоящее время большинство Web серверов поддерживают технологию сервлетов. Java Web-Server хорош тем, что что он наиболее полно реализует эту технологию. Стандартный Servlet-API реализован как в самом Java Web Server, так и в отдельном продукте JSDK, но начиная с версии 1.2, он включен в состав JDK. Характерной особенностью сервлетов является то, что они не требуют создания новых процессов при каждом новом запросе. Множество сервлетов выполняются параллельно в рамках одного процесса на сервере и по своей производительности превосходят как CGI, так и Fast-CGI приложения (см. Рис.3).


Рис. 3. Возможные процессы на Web-сервере.

Несмотря на то, что сервлеты используют HTTP-протокол, им нет необходимости перезагружать процесс при каждом новом запросе и это также повышает их быстродействие. К сожалению, когда вы используете сервлеты, опять-таки, возникает проблема использования кириллических символов. Дело в том, что стандартный путь прохождения данных тут не действует, потому что Web-сервер при старте запускает так называемый Java-handler, которому передает на выполнение все Java-сервлеты и обменивается вводом-выводом именно с ним через специальную библиотеку классов. Поэтому, если вы хотите получать в браузере от вашего Web-сервера, который исполняет сервлеты, кириллические строки, вам надо использовать вместо стандартных примитивов ввода/вывода некоторые специальные методы для работы со строками, например:

// Определение необходимого кодификатора
// в зависимости от используемого типа кодировки
String dos = new String(_Cp866_);
String win = new String(_Cp1251_);
String iso = new String(_8859_5_);
String im = new String(_Кириллический текст_);
.......................................................
out.println(_<body>_);
//out.write(im.getBytes(dos));
out.write(im1.getBytes(win));
//out.write(im2.getBytes(iso));
out.println(_</body></html>_);

Но и весьма эффективная функция Java - getBytes() не действует, когда вы извлекаете данные при помощи сервлетов из баз данных. Здесь необходимо применить метод, который мы уже использовали при работе с базами данных при помощи интерфейса JDBC. Кроме того, файл font.properties в каталоге $JDK_HOME/lib или в специальном каталоге вашего браузера должен быть соответствущим образом настроен. Во многих пакетах Java имеются образцы файла font.properties.ru, который можно взять в качестве основы. Если вы работаете с уже русифицированным Web-сервером, например, русской версией Apache, то он позаботится за вас и сам перекодирует данные, поступающие из базы данных от сервлета. Каким образом программируются операции с базой данных в сервере? Сервлет не накладывает никаких ограничений на использование интерфейсов с базой данных. Можно использовать обыкновенный JDBC-ODBC, или специфические драйвера ориентированные на конкретные SQL-сервера и базы данных. Рассмотрим типичный DBServlet.java, который с успехом будет выполняться как на Java Web Server, так и Web сервере Apache.

/*
* @(#)DBServlet.java    1.6 97/06/13
*/
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.net.*;
import java.sql.*;
/**
* DBServlet
*
* Этот сервлет демонстрирует как использовать JDBC стек
* и получать доступ к базам
данных при помощи сервлетов. Для того, чтобы выполнить этот
* сервлет, переменные CLASSPATH, LD_LIBRARY_PATH, ODBCINI
* должны быть соответствующим образом установлены.
*/
public class DBServlet extends HttpServlet {
/**
* init method
*/
public void init(ServletConfig conf) throws ServletException {
super.init(conf);
}
/**
* service method
*/
public void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
String stack, username,
password, query;
Connection con;
PrintStream out;
/* ServletOutputStream out = res.getOutputStream();*/
// Загрузка параметров. Именно эти параметры должны будут
вводиться
// в специально прорисованном окне браузера, с которым будет
// взаимодействовать наш сервлет
stack = req.getParameter(_stack_);
username = req.getParameter(_username_);
password = req.getParameter(_password_);
query = req.getParameter(_query_);
res.setContentType(_text/html_);
out = new PrintStream(res.getOutputStream());
printPageHeader(out);
// в том случае, если параметры отсутствуют
if (stack == null || username == null || password == null ||
query == null) {
printPageFooter(out);
return;
}
String url = _jdbc:odbc:_+stack;
out.println(_<hr><h3>Previous Query</h3>_);
out.println(_<pre>_);
out.println(_Database Stack : _+stack);
out.println(_ User ID : _+username);
out.println(_ Password : _+password);
out.println(_ Query : _+query);
out.println(_</pre>_);
try {
// Найти jdbc стек. Может
существовать несколько
зарегистрированных драйверов
Class.forName(_sun.jdbc.odbc.JdbcOdbcDriver_);
//Class.forName(_jdbc.odbc.JdbcOdbcDriver_);
// Получить соединение с базой данных
con = DriverManager.getConnection(url, username, password);
out.println(_<hr>_);
 out.println(_<h3>ODBC Driver and Database Messages</h3>_);
checkForWarning(con.getWarnings(), out);
 DatabaseMetaData dma = con.getMetaData();
 out.println(_Connected to _ + dma.getURL() + _<br>_);
 out.println(_Driver _ + dma.getDriverName() + _<br>_);
 out.println(_Version _ + dma.getDriverVersion() + _<br>_);
 // Создать и выполнить запрос. Конкретный оператор SQL
 // вводится удаленным
пользователем в окне его браузера
 Statement stmt = con.createStatement();
 ResultSet rs = stmt.executeQuery(query);
 // Печать результатов.
Они переназначаются на стандартный вывод
 // и поступают в браузер клиента
 dispResultSet(rs, out);
 rs.close();
 stmt.close();
 con.close();
 out.println(_<hr>_);
}
catch (SQLException ex) {
 out.println(_<hr>*** SQLException caught ***_);
 while (ex != null) {
 out.println(_SQLState: _ + ex.getSQLState() + _<br>_);
 out.println(_Message: _ + ex.getMessage() + _<br>_);
 out.println(_Vendor: _ + ex.getErrorCode() + _<br>_);
 ex = ex.getNextException();
 }
}
catch (java.lang.Exception ex) {
 ex.printStackTrace();
}
printPageFooter(out);
}
 /**
 * возврат запрошенной
информации
 */
 public String getServletInfo() {
return _Это простой сервлет,
который показывает как
использовать JDBC_;
 }
 /*
 * проверка и печать информации сервера
 */
 private void checkForWarning(SQLWarning warn, PrintStream out)
 throws SQLException
 {
 boolean rc = false;
 if (warn != null) {
 out.println(_<hr>*** Warning ***_);
 rc = true;
 while (warn != null) {
 out.println(_SQLState: _ + warn.getSQLState() + _<br>_);
 out.println(_Message: _ + warn.getMessage() + _<br>_);
 out.println(_Vendor: _ + warn.getErrorCode() + _<br>_);
 warn = warn.getNextWarning();
 }
 }
 }
 /*
 * Показ результатов запроса в табличном html формате
 */
 private void dispResultSet(ResultSet rs, PrintStream out)
 throws SQLException, IOException
 {
 int i;
 // метаданные используются для получения информации о схеме
 ResultSetMetaData rsmd = rs.getMetaData();
 int numCols = rsmd.getColumnCount();
 out.println(_<hr>_);
 out.println(_<h3>Database Columns and Data</h3>_);
 out.println(_<table border=3>_);
 out.println(_<tr>_);
 for (i=1; i<=numCols; i++) {
 out.println(_<th>_ + rsmd.getColumnLabel(i) + _</th>_);
 }
 out.println(_</tr>_);
 // для всех данных
 while (rs.next()) {
 out.println(_<tr>_);
 // for one row
 for (i=1; i<=numCols; i++) {
 dispElement(rs, rsmd.getColumnType(i), out, i);
 }
 out.println(_</tr>_);
 }
 out.println(_</table>_);
 }
 // печать одного элемента
 private void dispElement(ResultSet rs, int dataType,
 PrintStream out, int col)
 throws SQLException, IOException
 {
 String cp1 = new String(_Cp1251_);
 // в зависимости от типа
данных, определяем различные типы обработки
switch(dataType) {
case Types.DATE:
 java.sql.Date date = rs.getDate(col);
 out.println(_<th>_ + date.toString() + _</th>_);
 break;
case Types.TIME:
 java.sql.Time time = rs.getTime(col);
 out.println(_<th>_ + time.toString() + _</th>_);
 break;
case Types.TIMESTAMP:
 java.sql.Timestamp timestamp = rs.getTimestamp(col);
 out.println(_<th>_ +
timestamp.toString() + _</th>_);
 break;
case Types.CHAR:
case Types.VARCHAR:
case Types.LONGVARCHAR:
 String str = rs.getString(col);
// Возможно, что здесь вам
понадобятся кириллические
преобразования
 out.println(_<th>_ + str + _</th>_);
 break;
case Types.NUMERIC:
case Types.DECIMAL:
 java.math.BigDecimal numeric = rs.getBigDecimal(col, 10);
 out.println(_<th>_ + numeric.toString() + _</th>_);
 break;
case Types.BIT:
 boolean bit = rs.getBoolean(col);
 out.println(_<th>_ + new Boolean(bit) + _</th>_);
 break;
case Types.TINYINT:
 byte tinyint = rs.getByte(col);
 out.println(_<th>_ + new Integer(tinyint) + _</th>_);
 break;
case Types.SMALLINT:
 short smallint = rs.getShort(col);
 out.println(_<th>_ + new Integer(smallint) + _</th>_);
 break;
case Types.INTEGER:
 int integer = rs.getInt(col);
 out.println(_<th>_ + new Integer(integer) + _</th>_);
 break;
case Types.BIGINT:
 long bigint = rs.getLong(col);
 out.println(_<th>_ + new Long(bigint) + _</th>_);
 break;
case Types.REAL:
 float real = rs.getFloat(col);
 out.println(_<th>_ + new Float(real) + _</th>_);
 break;
case Types.FLOAT:
case Types.DOUBLE:
 double longreal = rs.getDouble(col);
 out.println(_<th>_ + new Double(longreal) + _</th>_);
 break;
case Types.BINARY:
case Types.VARBINARY:
case Types.LONGVARBINARY:
 byte[] binary = rs.getBytes(col);
 out.println(_<th>_ + new String(binary, 0) + _</th>_);
 break;
}
}
private void printPageHeader(PrintStream out) {
out.println(_<html>_);
out.println(_<head>_);
out.println(_<tltle>
Типичный сервлет для работы
с базами данных
</title>_);
out.println(_</head>_);
out.println(_<body>_);
out.println(_<center><font size=5>_ +
 _<b>Jeeves Database Servlet</b>_ +
 _</font></center>_);
out.println(_<hr>_);
out.println(_<form action=\_/servlet/dbServlet\_ method=\_get\_>_);
out.println(_<pre>_);
out.println(_ODBC DSN : <input type=textarea name=stack>_);
out.println(_ User ID : <input type=textarea name=username>_);
out.println(_ Password : <input type=textarea name=password>_);
out.println(_ SQL Query :
<input type=textarea name=query>_);
out.println(_</pre>_);
out.println(_<input type=
submit>_);
out.println(_</form>_);
}
 private void printPageFooter(PrintStream out) {
 out.println(_</body>_);
out.println(_</html>_);
out.flush();
}
}

В этой программе содержатся пакеты классов для поддержки сервлетов - javax.servlet.* и javax.servlet.http.*, которые находятся в специальном продукте фирмы JavaSoft JSDK1.0.1 или непосредственно в JDK1.2.

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

public void service (HttpServletRequest req,
HttpServletResponse res) throws
ServletException,
IOException
{
...
}

Специальный метод request содержит список параметров, которые могут быть извлечены при помощи HttpServletRequest::getParameterNames метода.

stack = req.getParameter(_stack_);

возвращает вам значение вами же определенного в htm-форме параметра stack, в котором вы можете задавать с клавиатуры имя ODBC-источника (ODBC DSN).

Можно установить специальный цикл обработки для извлечения каждого параметра, переданного из HTML-формы, когда она направлена для обработки к сервлету:

while ( values.hasMoreElements() )
{
...
}

Имя и значение каждого извлекаемого параметра можно получать во время работы цикла.

String name, value;
name = (String)values.nextElement();
value = req.getParameter (name);

Этих знаний вполне достаточно для того, чтобы начать писать простые сервлеты, расширяющие функциональность JavaServer"а для обработки форм, вместо того, чтобы писать cложные CGI. Имеется, впрочем, еще одна проблема, которую также необходимо изучить - обработка событий и исключительных ситуаций. Убедитесь, что вы специфицировали этот сервис и init методы в вашем подклассе HttpServlet как ServletException:

public void init
(ServletConfig conf) throws ServletException
 ...
public void service (HttpServletRequest req,
HttpServletResponse res)
throws ServletException, IOException
 ...

Внутри DBServlet сервлета метод service может использовать один или несколько блоков try/catch, которые обеспечивают пользователю или программисту обработку событий, связанных с некоторыми неверными действиями или ситуациями. В первом блоке try пользователь может обработать ситуацию при возникновении проблемы соединения с сервером базы данных или выполнении sql-запроса. Второй блок try можно использовать по своему усмотрению, например, при записи результатов в файл и последующей проверке - должным ли образом он закрыт перед выходом из обслуживающей процедуры. Если нет - в стандартное устройство вывода выдается соответствующая трассировка.

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

3.2 Сервлеты на базе Apache

Для тех, кто имеет дело с различными компьютерными платформами и стремится к универсализму, выбор Apache HTTP-сервера является, пожалуй, самым подходящим. Разве плохо, когда написанные вами модули могут работать и на ПК и на многопроцессорных высокопроизводительных системах, причем инсталляция и компоновка рабочих программ, как правило не требуется, дело сводится к простому копированию. Можно, не выходя из офиса, перемещать рабочие программы в специальные каталоги Web-сервера, находящегося за тридеять земель, и они мгновенно могут быть востребованы десятками пользователей, которым нет необходимости загружать их на свои компьютеры. До недавнего времени все Web-серверы могли вызывать на выполнение только три типа процессов: CGI, ISAPI или NSAPI, причем два последних чаще всего были реализованы на Wintel-платформе. Так, с появлением Delphi-3 многие стали предлагать свои разработки в виде Web-модулей, но аппетиты пользователей умерялись разработчиками, которые могли предложить только вариант Web, работающий под Windows-95/NT, и для связи с СУБД на больших машинах приходилось применять некое подобие Gateway-ODBC. Такой вариант не всегда оказывался эффективным, тем более, что протокол HTTP при каждом новом запросе требовал перезагрузки модуля Применение сервлетов в связке с СУБД решает эту проблему. Для Apache существует специальный программный компонент JServ, который также как и сам сервер в исходных кодах распространяется совершенно бесплатно. При запуске Apache автоматически стартует Java-обработчик, которому и будут передаваться на исполнение все запросы, связанные с запуском Java-программ. Наоборот, весь вывод из Java переадресуется непосредственно в Web, который аккуратным образом передает его запросившему браузеру. Скорость выполнения Java-приложений зависит от производительности компьютера, на котором сконфигурирован Web-сервер Apache. Если это, к примеру UltraSparc-3000 c Java-виртуальной машиной HotSpot, то скорость может быть очень большой, в десятки, а то и в сотни раз быстрее, чем на обычной NT-машине. Apache, в отличие от браузеров не имеет собственной виртуальной машины Java, а настраивается на ту, которая сгенерирована для данной платформы. Такой подход чрезвычайно гибок, ибо позволяет постоянно обновлять Java-среду, наращивать библиотеку классов и ничего не изменять в настройках Apache. Впрочем, и в самих конфигурационных файлах Apache для программистов имеется масса полезных вещей, они свободно корректируются, открыты для добавлений и предоставляют гораздо больше простора для маневра по сравнению с жестко запрограммированными системами для администрирования Web-узлов, такими как Fastrack, или Java Web Server. Для того, чтобы использовать Apache во взаимодействии с Java, необходимо инсталлировать продукт Jserv, содержащий необходимые библиотеки Java-классов, затем изменить конфигурационный файл Apache Configuration, добавив в него строку:

Module jserv_module mod_jserv.o

(модуль mod_jserv.c должен находиться при этом в src/директории Apache). После этого необходимо рекомпилировать Apache с помощью программы make. Затем можно изменить основной конфигурационный файл Apache httpd.conf, добавив в него строки вида:

<IfModule mod_jserv.c>
ServletProperties conf/servlet.properties
ServletPort 8007
ServletBinary /opt1/jdk1.1.5/bin/java
ServletClassPath /opt1/jdk1.1.5/lib/classes.zip
ServletClassPath /opt1/local/etc/httpd/jserv/lib/servclasses.zip
ServletClassPath /opt1/local/etc/httpd/jserv/classes
ServletClassPath /opt1/JSDK1.0.1/lib/classes.zip
ServletErrorLog
logs/jserv_log
ServletAlias
/servlets /opt1/local/etc/httpd/
jserv/servlets
ServletAuthExport Off
# AddHandler
jhtml-parser .jhtml
# Action jhtml-parser
/servlets/
org.apache.jserv.JServSSI
</IfModule>

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

Заключение

Перечисленные методы не исчепывают возможности использования Java-программ при работе с базами данных. Следует учесть, что технологии постоянно развиваются, совершенствуются и пополняются новыми стандартами. Совсем недавно Microsoft объявила о создании новых стандартов RDO, ADO и OLE DB. Эти разработки, также как JDBC движутся в направлении объектно-ориентированных технологий и основаны на классах, которые могут быть применены в JDBC. В то же время и JDBC развивается и в скором времени появится язык JSQL, который уже анонсировали некоторые компании. В этом случае, SQL операторы можно будет встраивать в Java программы, а не передавать их как строковые переменные в Java-методы. Встроенный SQL-препроцессор позволит программистам использовать Java-переменные в SQL операторах.

Сергей Борисович Дунаев, Ивановский государственный энергетический университет.