среда, 11 июня 2014 г.

Java в СУБД Oracle, часть I

Мне редко приходится писать на Java хранимые процедуры для СУБД Oracle. А когда приходится, всякий раз я трачу некоторое время на "восстановление контекста", чтобы вспомнить особенности встроенной JVM и работы с ней. Чтобы в следующий раз восстановление контекста прошло быстрее, в этой статье в конспективной форме я собрал самые необходимые сведения и примеры кода на Java для встроенной Oracle JVM.

Вот так можно посмотреть, установлена ли поддержка Java в СУБД Oracle:


SQL> SELECT * FROM all_registry_banners WHERE UPPER(banner) LIKE '%JAVA%';

BANNER
--------------------------------------------------------------------------------
JServer JAVA Virtual Machine Release 11.2.0.2.0 - Development
Oracle Database Java Packages Release 11.2.0.2.0 - Development

Запрос был выполнен на Oracle 11gR2 Enterprise Edition. Но какая же версия Java поддерживается?


SQL> CREATE OR REPLACE FUNCTION get_java_property (prop IN VARCHAR2)
  2  RETURN VARCHAR2 IS LANGUAGE JAVA
  3  name 'java.lang.System.getProperty(java.lang.String) return java.lang.String';
  4  /
Function created

SQL> SELECT get_java_property('java.version') FROM dual;

GET_JAVA_PROPERTY('JAVA.VERSIO
--------------------------------------------------------------------------------
1.5.0_10

Кстати, в Oracle 11gR2 Express Edition встроенная JVM отсутствует:


SQL> SELECT * FROM all_registry_banners WHERE UPPER(banner) LIKE '%JAVA%';

BANNER
--------------------------------------------------------------------------------

Следующая таблица содержит ряд отличий стандартной JVM и встроенной в СУБД Oracle JVM.

Стандартная JVMВстроенная Oracle JVM
Исполнямый код запускается в сеансе ОС командной строкой 'java <classname>' Исполнямый код запускается в сеансе СУБД Oracle вызовом PL/SQL процедуры или функции - обертки метода Java.
При запуске вызывается метод public static void main(String args[]) Может быть вызван любой из public static методов класса, опубликованный через PL/SQL процедуру или функцию - обертку.
JVM живет, пока выполняется 'java <classname>' JVM живет на протяжении сеанса работы с СУБД Oracle.
Исходный код, байт-код и ресурсы содержатся в файлах ОС (.java, .class, .properties), компилируются и загружаются JVM из файлов ОС. Исходный код, байт-код и ресурсы хранятся в БД как объекты схемы (JAVA SOURCE, JAVA CLASS, JAVA RESOURCE), компилируются и загружаются для исполнения JVM из объектов схемы.
Пространство имен конструируется из разделенных точками имен пакетов, например, java.util.regex или ru.trofimov.helloworld Стандартное пространство имен Java вкладывается в схемы Oracle. Классы стандартной библиотеки Java находятся в схеме SYS и доступны в других схемах через публичные синонимы.
Имя класса: ru.trofimov.helloworld.Hello Имя класса (объекта схемы): ru/trofimov/helloworld/Hello
См. примечание * внизу таблицы.
Единственный CLASSPATH задает список директорий и jar-файлов, где JVM ищет классы для загрузки. Resolver, свой для каждого класса, задает список схем Oracle для поиска Java-объектов-схемы.
Поддерживает графический интерфейс (GUI) и взаимодействие с пользователем посредством GUI. Не поддерживает графический интерфейс (GUI) и взаимодействие с пользователем посредством GUI.
Поддерживает воспроизведение и запись звука с помощью звуковых средств компьютера. Не поддерживает воспроизведение и запись звука с помощью звуковых средств компьютера.
Поддерживает эффективное параллельное выполнение задач с помощью Java потоков (threads). Синтаксис Java потоков (threads) поддерживается, но распараллеливания потоков, на самом деле, не происходит. Эффективное распараллеливание в СУБД Oracle происходит на уровне пользовательских сеансов.
Для работы с БД Oracle используется драйвер JDBC. Для работы с БД Oracle используется так называемый Server-Side JDBC Internal Driver, взаимодействующий с БД минуя сетевой протокол и исключая накладные расходы.
Опции компиляции задаются в командной строке компилятора javac. Опции компиляции задаются в командной строке утилиты loadjava, либо в таблице JAVA$OPTIONS (при помощи пакета DBMS_JAVA), причем опции loadjava приоритетнее сохраненных в таблице.

* Имя Java класса может быть до 4000 символов длиной, а имя объекта схемы данных - не более 30 символов. В связи с этим пакет dbms_java предоставляет функции shortname(class_name) и longname(object_name), которые возращают длинное и короткое имена Java классов, соответственно. Пример:

SQL> -- Java classes and their shortnames
SQL> SELECT dbms_java.shortname(c.name), c.name
  2  FROM dba_java_classes c
  3  WHERE rownum <= 3;

DBMS_JAVA.SHORTNAME(C.NAME)                                                      NAME
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
/1000323d_DelegateInvocationHa                                                   com/sun/corba/se/spi/orbutil/proxy/DelegateInvocationHandlerImpl$1
/1000e8d1_LinkedHashMapValueIt                                                   java/util/LinkedHashMap$ValueIterator
/1005bd30_LnkdConstant                                                           oracle/aurora/util/classfile/Lnkd$Constant

SQL> -- Java objects and their longnames
SQL> SELECT object_name, dbms_java.longname(object_name) longname, status
  2  FROM all_objects
  3  WHERE object_type IN ('JAVA CLASS')
  4      AND rownum <= 3;

OBJECT_NAME                    LONGNAME                                                                         STATUS
------------------------------ -------------------------------------------------------------------------------- -------
/1000323d_DelegateInvocationHa com/sun/corba/se/spi/orbutil/proxy/DelegateInvocationHandlerImpl$1               VALID
/1000e8d1_LinkedHashMapValueIt java/util/LinkedHashMap$ValueIterator                                            VALID
/1005bd30_LnkdConstant         oracle/aurora/util/classfile/Lnkd$Constant                                       VALID

Теперь перейду к практике. Создам JAVA SOURCE, откомпилирую его в JAVA CLASS, опубликую и выполню.

SQL> -- the simplest start: hello world
SQL> 
SQL> CREATE OR REPLACE AND COMPILE JAVA SOURCE NAMED "ru/trofimov/helloworld/Hello" AS
  2  package ru.trofimov.helloworld;
  3  public class Hello
  4  {
  5      public static void hello()
  6      {
  7          System.out.println("Hello world!");
  8      }
  9  }
 10  /

Java created

Строки 2 - 9 содержат исходный код на Java. Найдем созданные объекты JAVA SOURCE и JAVA CLASS в системном словаре Oracle:


SQL> SELECT line, text
  2  FROM dba_source
  3  WHERE type='JAVA SOURCE'
  4      AND name = 'ru/trofimov/helloworld/Hello'
  5  ;

      LINE TEXT
---------- --------------------------------------------------------------------------------
         1 package ru.trofimov.helloworld;
         2 public class Hello
         3 {
         4     public static void hello()
         5     {
         6         System.out.println("Hello world!");
         7     }
         8 }
         9 
        10 
10 rows selected

SQL> SELECT dbms_java.shortname(c.name), c.name
  2  FROM dba_java_classes c
  3  WHERE name = 'ru/trofimov/helloworld/Hello'
  4  ;

DBMS_JAVA.SHORTNAME(C.NAME)              NAME
---------------------------------------- ----------------------------------------
ru/trofimov/helloworld/Hello             ru/trofimov/helloworld/Hello

Создадим PL/SQL процедуру-обертку для метода hello класса Hello и выполним ее:


SQL> CREATE OR REPLACE PROCEDURE hello AS LANGUAGE JAVA
  2  NAME 'ru.trofimov.helloworld.Hello.hello()';
  3  /
Procedure created

SQL> show error
No errors

SQL> set serveroutput on
SQL> call dbms_java.set_output(1000000);
Method called

SQL> BEGIN hello; END;
  2  /
Hello world!
PL/SQL procedure successfully completed

По умолчанию вывод System.out.println() идет в трейс-файлы в директории udump. Вызов dbms_java.set_output позволяет перенаправить вывод на консоль в рамках текущей сессии.

Продемонстрирую передачу аргумента методу Java:

SQL> -- calling java with argument(s)
SQL> 
SQL> set define off
SQL> 
SQL> CREATE OR REPLACE AND COMPILE JAVA SOURCE NAMED "Hello" AS
  2  public class Hello
  3  {
  4      public static void p(String message) {
  5          System.out.println(message);
  6      }
  7  
  8      public static void hello()
  9      {
 10          p("Hello world!");
 11      }
 12  
 13      public static void helloUser(String name)
 14      {
 15          p("Hello, " + (name != null && name != "" ? name : "user") + "!");
 16      }
 17  }
 18  /
Java created

SQL> show error
No errors

SQL> 
SQL> CREATE OR REPLACE PROCEDURE hello_user(username VARCHAR2) AS
  2  LANGUAGE JAVA NAME 'Hello.helloUser(java.lang.String)';
  3  /
Procedure created

SQL> show error
No errors

SQL> exec hello_user(null)
Hello, user!
PL/SQL procedure successfully completed

SQL> exec hello_user('Andrey')
Hello, Andrey!
PL/SQL procedure successfully completed

Кстати, можно найти метод Hello.helloUser и его параметр в системном словаре с помощью следующих запросов:


SELECT * 
FROM dba_java_methods
WHERE name = 'Hello'
    AND method_name = 'helloUser';

SELECT * 
FROM dba_java_arguments
WHERE name = 'Hello'
    AND method_name = 'helloUser';

А теперь я хочу вернуть значение из метода Java в вызывающий PL/SQL код.

SQL> -- getting values from java
SQL> 
SQL> CREATE OR REPLACE AND COMPILE JAVA SOURCE NAMED "Hello" AS
  2  public class Hello
  3  {
  4      private static void p(String message)
  5      {
  6          System.out.println(message);
  7      }
  8  
  9      public static void hello()
 10      {
 11          p("Hello world!");
 12      }
 13  
 14      public static String helloString(String name)
 15      {
 16          return "Hello, " + (name != null && name != "" ? name : "user") + "!";
 17      }
 18  
 19      public static void helloUser(String name)
 20      {
 21          p(helloString(name));
 22      }
 23  }
 24  /
Java created

SQL> show error
No errors

SQL> 
SQL> CREATE OR REPLACE FUNCTION hello_string(username VARCHAR2) RETURN VARCHAR2
  2  AS LANGUAGE JAVA
  3  NAME 'Hello.helloString(java.lang.String) return java.lang.String';
  4  /
Function created

SQL> show error
No errors

SQL> select hello_string('Andrey') from dual;

HELLO_STRING('ANDREY')
-----------------------------------------------------------
Hello, Andrey!

SQL> exec hello_user('Андрей')
Hello, Андрей!
PL/SQL procedure successfully completed

При передаче аргумента из PL/SQL в Java метод происходит автоматическое преобразование типа данных SQL в тип данных Java. А при возврате значения из Java метода в PL/SQL код происходит автоматическое преобразование из типа данных Java в тип данных SQL.

Приведу таблицу соответствий основных типов данных SQL и Java (Подробнее см. в Oracle Database Java Developer's Guide):

Тип данных SQLТип данных Java
CHAR, VARCHAR2 java.lang.String
oracle.sql.CHAR
NUMBER boolean
char
byte
short
int
long
float
double
java.lang.Byte
java.lang.Short
java.lang.Integer
java.lang.Long
java.lang.Float
java.lang.Double
java.math.BigDecimal
oracle.sql.NUMBER
BINARY_INTEGER boolean
char
byte
short
int
long
DATE oracle.sql.DATE
RAW oracle.sql.RAW
BLOB oracle.sql.BLOB
CLOB oracle.sql.CLOB
BFILE oracle.sql.BFILE
ROWID oracle.sql.ROWID
TIMESTAMP oracle.sql.TIMESTAMP
TIMESTAMP WITH TIME ZONE oracle.sql.TIMESTAMPTZ
TIMESTAMP WITH LOCAL TIME ZONE oracle.sql.TIMESTAMPLTZ

На этом пока закончу, устранив из базы данных следы своих экспериментов:

SQL> DROP FUNCTION hello_string;
Function dropped

SQL> DROP PROCEDURE hello_user;
Procedure dropped

SQL> DROP PROCEDURE hello;
Procedure dropped

SQL> DROP JAVA SOURCE "Hello";
Java dropped

SQL> DROP JAVA SOURCE "ru.trofimov.helloworld.Hello";
Java dropped

В следующий раз я собираюсь исследовать поведение статических переменных класса Java в контексте сеансов СУБД Oracle и поставить несколько других экспериментов с Java. А также разработать PL/SQL пакет-обертку для чтения и записи внешних файлов на базе стандартной библиотеки Java.

2 комментария:

  1. Здравствуйте, Андрей.
    Прошу уточнить пару моментов
    1) Что имеется в виду под сеансом работы с СУБД? Сессия пользователя или транзакция? Или что то другое?
    2) Насколько легко может быть "убит" экземпляр Oracle "неправильным" java кодом? Насколько использование java-ХП безопасно?
    3) Какие правила нужно соблюдать для написания безопасных java-ХП?
    Заранее спасибо

    ОтветитьУдалить
  2. Добрый день, Sergius.
    1) Сеанс работы пользователя = сессия пользователя.
    2) Я не слышал, чтобы экземпляр Oracle был убит неправильным java-кодом. Использование хранимых процедур на java в Oracle не опасней, чем использование хранимых процедур на PL/SQL. (Хотя и те, и другие способны, например, заполнить файловую систему ненужными файлами или иначе навредить.)
    3) Java и JVM изначально проектировались как безопасный язык и безопасная среда выполнения. О специальных правилах для написания безопасных хранимых процедур на Java мне не известно. Пишите на Java, а компилятор и JVM сделают все, чтобы не допустить фатальных вещей.

    ОтветитьУдалить