пятница, 20 июня 2014 г.

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

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

До сих пор мы создавали Java классы в схеме Oracle из исходного кода с помощью команды CREATE JAVA SOURCE. Альтернатива в том, чтобы загрузить в базу данных готовые Java классы с помощью утилиты loadjava.

Вначале создам файл DeepThought.java, содержащий одноименный класс, и откомпилирую его для JVM версии 1.5:


$ cat DeepThought.java

// DeepThought.java

public class DeepThought 
{
    private static int theAnswer = 42;
    
 public static void setTheAnswer(int answer)
 {
     theAnswer = answer;
 }
 
 public static int getTheAnswer()
 {
     return theAnswer;
 }
}

$ javac -target 1.5 DeepThought.java

$ ls
DeepThought.class    DeepThought.java

Успешно. Теперь загружу полученный DeepThought.class в базу данных утилитой loadjava. Опция -verbose позволяет увидеть, что же делает утилита:


$ loadjava -user ay/ay@may -resolve -verbose DeepThought.class
arguments: '-user' 'ay/***@may' '-resolve' '-verbose' 'DeepThought.class'
creating : class DeepThought
loading  : class DeepThought
resolving: class DeepThought
Classes Loaded: 1
Resources Loaded: 0
Sources Loaded: 0
Published Interfaces: 0
Classes generated: 0
Classes skipped: 0
Synonyms Created: 0
Errors: 0

Утилита loadjava позволяет загружать (и компилировать) java-файлы c исходным кодом, class-файлы (и разрешать внешние зависимости классов) и файлы ресурсов.

Создам обертку для метода DeepThought.getTheAnswer() и получу ответ на главный вопрос о жизни, Вселенной и обо всем:


SQL> CREATE OR REPLACE FUNCTION deep_thought_get_the_answer RETURN NUMBER
  2  AS LANGUAGE JAVA NAME 'DeepThought.getTheAnswer() return int';
  3  /
Function created

SQL> show error
No errors 

SQL> SELECT deep_thought_get_the_answer FROM dual;

DEEP_THOUGHT_GET_THE_ANSWER
---------------------------
                         42

(Сейчас можно взять паузу и помедитировать над числом 42 :-)

В нашем классе DeepThought имеется метод setTheAnswer(int), который позволяет изменить значение статической переменной theAnswer. Воспользуемся этим методом, чтобы продемонстрировать два свойства встроенной JVM Oracle:

  • каждый сеанс Oracle имеет дело с собственным экземпляром JVM и собственными экземплярами загруженных классов,
  • статические данные классов сохранются между вызовами методов Java на протяжении сеанса Oracle.

Создам обертку для метода DeepThought.setTheAnswer() и изменю с его помощью значение переменной theAnswer:


SQL> CREATE OR REPLACE PROCEDURE deep_thought_set_the_answer(answer NUMBER)
  2  AS LANGUAGE JAVA NAME 'DeepThought.setTheAnswer(int)';
  3  /
Procedure created

SQL> show error
No errors

SQL> exec deep_thought_set_the_answer(43)
PL/SQL procedure successfully completed

SQL> SELECT deep_thought_get_the_answer FROM dual;
DEEP_THOUGHT_GET_THE_ANSWER
---------------------------
                         43

В другом сеансе.


SQL> SELECT deep_thought_get_the_answer FROM dual;
DEEP_THOUGHT_GET_THE_ANSWER
---------------------------
                         42

SQL> exec deep_thought_set_the_answer(41)
PL/SQL procedure successfully completed

SQL> SELECT deep_thought_get_the_answer FROM dual;
DEEP_THOUGHT_GET_THE_ANSWER
---------------------------
                         41

И снова в первом сеансе.


SQL> SELECT deep_thought_get_the_answer FROM dual;
DEEP_THOUGHT_GET_THE_ANSWER
---------------------------
                         43

Как видим, в разных сеансах функция deep_thought_get_the_answer дает разный ответ, и этот ответ остается неизменным от вызова к вызову (если он не изменен процедурой deep_thought_set_the_answer). Последнее доказывает, что время жизни JVM и статических переменных не ограничивается единственным вызовом Java метода. Статические переменные Java, как и переменные пакетов PL/SQL, сохраняют свои значения между обращениями к ним в течение всего сеанса работы с Oracle.

Удалить теперь уже ненужный класс DeepThought можно либо с помощью команды DROP JAVA CLASS "DeepThought";, либо с помощью утилиты dropjava. Воспользуюсь утилитой dropjava:


$ dropjava -user ay/ay@may -verbose DeepThought
dropping: class DeepThought

PL/SQL обертки методов этого класса также больше нам не нужны:


SQL> DROP PROCEDURE deep_thought_set_the_answer;
Procedure dropped

SQL> DROP FUNCTION deep_thought_get_the_answer;
Function dropped

Теперь обратимся к задаче записи и чтения внешних файлов из СУБД Oracle. Обычно эта задача решается при помощи пакета UTL_FILE. (Да вот беда, время от времени с ним случаются необъяснимые сбои! UTL_FILE явно не любит некоторые символы, особенно, если они стоят в конце строки. При интенсивной эксплуатации UTL_FILE сбои возникают регулярно.) Создадим альтернативное решение для работы с внешними файлами - на базе стандартной библиотеки Java.

Класс TextFileWizard, код которого представлен ниже, пишет и читает текстовые файлы в указанной кодировке с помощью java.io.BufferedWriter и java.io.BufferedReader, соответственно. Приведенная реализация позволяет одновременно писать не более одного файла и читать не более одного файла (из-за того, что ссылки на открытые файлы хранятся в статических переменных класса bufferedWriter и bufferedReader). Желающие снять это ограничение могут доработать код самостоятельно.


CREATE OR REPLACE AND COMPILE JAVA SOURCE NAMED "TextFileWizard" AS
public class TextFileWizard
{
    private static java.io.BufferedWriter bufferedWriter = null;
    private static java.io.BufferedReader bufferedReader = null;

    
    public static int openOutputFile(String file) throws java.io.IOException
    {
        return openOutputFile(file, "UTF-8");
    }
    
    public static int openOutputFile(String file, String encoding) throws java.io.IOException
    {
        if (bufferedWriter == null) {
            java.io.OutputStreamWriter osw = 
                new java.io.OutputStreamWriter(new java.io.FileOutputStream(file), encoding);
            bufferedWriter = new java.io.BufferedWriter(osw);
            return bufferedWriter.hashCode();
        } else {
            return -1;
        }
    }
    
    public static int closeOutputFile(int fileHandler) throws java.io.IOException 
    {
        if (bufferedWriter != null && bufferedWriter.hashCode() == fileHandler) {
            bufferedWriter.close();
            bufferedWriter = null;
            return 1;
        } else {
            return -1;
        }
    }

    public static int openInputFile(String file) throws 
        java.io.FileNotFoundException, java.io.UnsupportedEncodingException 
    {
        return openInputFile(file, "UTF-8");
    }
    
    public static int openInputFile(String file, String encoding) throws
        java.io.FileNotFoundException, java.io.UnsupportedEncodingException
    {
        if (bufferedReader == null) {
            java.io.InputStreamReader fr = 
                new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding);
            bufferedReader = new java.io.BufferedReader(fr);
            return bufferedReader.hashCode();
        } else {
            return -1;
        }
    }

    public static int closeInputFile(int fileHandler) throws java.io.IOException
    {
        if (bufferedReader != null && bufferedReader.hashCode() == fileHandler) {
            bufferedReader.close();
            bufferedReader = null;
            return 1;
        } else {
            return -1;
        }
    }

    public static void write(int fileHandler, String line) throws java.io.IOException 
    {
        if (bufferedWriter != null && bufferedWriter.hashCode() == fileHandler) {
            bufferedWriter.write(line);
        }
    }

    public static String read(int fileHandler) throws java.io.IOException 
    {
        if (bufferedReader != null && bufferedReader.hashCode() == fileHandler) {
            return bufferedReader.readLine();
        } else {
            return null;
        }
    }
}
/

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


SQL> begin
  2      dbms_java.grant_permission(user, 'SYS:java.io.FilePermission', '/home/oracle/folder/-', 'read,write');
  3  end;
  4  /
PL/SQL procedure successfully completed

Теперь текущий пользователь может читать и писать файлы в директории /home/oracle/folder и ее поддиректориях.

PL/SQL пакет tfw предоставляет методы-обертки для методов класса TextFileWizard, а также содержит процедуру testme для тестирования его основной функциональности:


CREATE OR REPLACE PACKAGE tfw IS

    FILEPATH CONSTANT VARCHAR2(255) := '/home/oracle/folder';
    CHARSET CONSTANT VARCHAR2(50) := 'CP1251';
    
    CR CONSTANT CHAR(1) := chr(13);
    LF CONSTANT CHAR(1) := chr(10);
    CRLF CONSTANT CHAR(2) := CR||LF;
    NL CONSTANT VARCHAR2(2) := CR||LF;

    -- open file p_filename for write
    -- in p_dir
    -- using charset p_charset
    FUNCTION open_output_file(
        p_filename VARCHAR2,
        p_dir IN VARCHAR2 DEFAULT tfw.FILEPATH,
        p_charset IN VARCHAR2 DEFAULT tfw.CHARSET)
    RETURN BINARY_INTEGER;
    
    PROCEDURE write_line(
        p_filehandler BINARY_INTEGER,
        p_line VARCHAR2,
        p_nl VARCHAR2 DEFAULT tfw.NL);
    
    FUNCTION close_output_file(p_filehandler BINARY_INTEGER) RETURN BINARY_INTEGER;

    -- open file p_filename for read
    -- in p_dir
    -- using charset p_charset
    FUNCTION open_input_file(
        p_filename VARCHAR2,
        p_dir IN VARCHAR2 DEFAULT tfw.FILEPATH,
        p_charset IN VARCHAR2 DEFAULT tfw.CHARSET)
    RETURN BINARY_INTEGER;

    FUNCTION read_line(p_filehandler BINARY_INTEGER) RETURN VARCHAR2;

    FUNCTION close_input_file(p_filehandler BINARY_INTEGER) RETURN BINARY_INTEGER;

    PROCEDURE testme;
END tfw;
/
CREATE OR REPLACE PACKAGE BODY tfw IS

    FUNCTION j_open_output_file(p_filename VARCHAR2, p_encoding VARCHAR2) RETURN BINARY_INTEGER
    AS LANGUAGE JAVA
    NAME 'TextFileWizard.openOutputFile(java.lang.String, java.lang.String) return int';

    FUNCTION open_output_file(
        p_filename VARCHAR2,
        p_dir IN VARCHAR2 DEFAULT tfw.FILEPATH,
        p_charset IN VARCHAR2 DEFAULT tfw.CHARSET)
    RETURN BINARY_INTEGER
    IS
        l_filename VARCHAR2(1000) :=
            CASE
            WHEN p_dir IS NOT NULL THEN
                p_dir || '/' || p_filename
            ELSE
                p_filename
            END;
    BEGIN
        RETURN j_open_output_file(l_filename, p_charset);
    END open_output_file;

    PROCEDURE j_write_to_file(p_filehandler BINARY_INTEGER, p_line VARCHAR2)
    AS LANGUAGE JAVA
    NAME 'TextFileWizard.write(int, java.lang.String)';
    
    PROCEDURE write_line(
        p_filehandler BINARY_INTEGER,
        p_line VARCHAR2,
        p_nl VARCHAR2 DEFAULT tfw.NL)
    IS
    BEGIN
        j_write_to_file(p_filehandler, p_line || p_nl);
    END write_line;

    FUNCTION close_output_file(p_filehandler BINARY_INTEGER) RETURN BINARY_INTEGER
    AS LANGUAGE JAVA
    NAME 'TextFileWizard.closeOutputFile(int) return int';

    
    FUNCTION j_open_input_file(p_filename VARCHAR2, p_encoding VARCHAR2) RETURN BINARY_INTEGER
    AS LANGUAGE JAVA
    NAME 'TextFileWizard.openInputFile(java.lang.String, java.lang.String) return int';

    FUNCTION open_input_file(
        p_filename VARCHAR2,
        p_dir IN VARCHAR2 DEFAULT tfw.FILEPATH,
        p_charset IN VARCHAR2 DEFAULT tfw.CHARSET)
    RETURN BINARY_INTEGER
    IS
        l_filename VARCHAR2(1000) :=
            CASE
            WHEN p_dir IS NOT NULL THEN
                p_dir || '/' || p_filename
            ELSE
                p_filename
            END;
    BEGIN
        RETURN j_open_input_file(l_filename, p_charset);
    END open_input_file;

    FUNCTION read_line(p_filehandler BINARY_INTEGER) RETURN VARCHAR2
    AS LANGUAGE JAVA
    NAME 'TextFileWizard.read(int) return java.lang.String';

    FUNCTION close_input_file(p_filehandler BINARY_INTEGER) RETURN BINARY_INTEGER
    AS LANGUAGE JAVA
    NAME 'TextFileWizard.closeInputFile(int) return int';
        

    PROCEDURE p(message VARCHAR2) IS BEGIN dbms_output.put_line(message); END;

    PROCEDURE testme IS
        fh BINARY_INTEGER := NULL;
        line VARCHAR2(10000);
        FILENAME1251 VARCHAR2(100) := 'testme_1251.txt';
        FILENAMEUTF8 VARCHAR2(100) := 'testme_utf8.txt';
    BEGIN
        -- writing to file in default encoding
        fh := tfw.open_output_file(FILENAME1251);
        p('out : ' || fh);
        FOR r IN (SELECT username FROM all_users WHERE rownum < 6) LOOP
            tfw.write_line(fh, 'Привет, ' || r.username);
        END LOOP;
        fh := tfw.close_output_file(fh);
        p('out : ' || fh);

        -- writing to file in UTF-8 encoding
        fh := tfw.open_output_file(FILENAMEUTF8, tfw.FILEPATH, 'UTF-8');
        p('out : ' || fh);
        FOR r IN (SELECT username FROM all_users WHERE rownum < 6) LOOP
            tfw.write_line(fh, 'Привет, ' || r.username);
        END LOOP;
        fh := tfw.close_output_file(fh);
        p('out : ' || fh);

        -- reading from file in default encoding
        fh := tfw.open_input_file(FILENAME1251);
        p(' in : ' || fh);
        LOOP
            line := tfw.read_line(fh);
            EXIT WHEN line IS NULL;
            p(line);
        END LOOP;
        fh := tfw.close_input_file(fh);
        p(' in : ' || fh);

        -- opening non-existent file for read
        BEGIN
            fh := tfw.open_input_file('non-existant');
        EXCEPTION
        WHEN OTHERS THEN
            p(SQLERRM(SQLCODE));
        END;
    END;

END tfw;
/

Протестирую запись-чтение файлов:


SQL> set serveroutput on

SQL> exec tfw.testme
out : -1403543160
out : 1
out : -1762102103
out : 1
 in : 462025015
Привет, SYS
Привет, SYSTEM
Привет, OUTLN
Привет, DIP
Привет, ORACLE_OCM
 in : 1
ORA-29532: Java call terminated by uncaught Java exception: java.io.FileNotFoundException: No such file or directory

PL/SQL procedure successfully completed

Итак, благодаря встроенной JVM прорублено окошко из СУБД Oracle в файловую систему, альтернативное UTL_FILE.

Документация Oracle сообщает интересную подробность, относящуюся к работе с внешними файлами:

Files, threads, and sockets persist across calls when you use a dedicated mode server. In shared server mode, files, threads, and sockets terminate when the call ends.

Это значит, что если сервер Oracle сконфигурирован для работы в режиме shared server mode (когда пользовательские запросы выполняются серверным процессом, произвольно выбираемым из пула предварительно запущенных процессов-серверов), то файл, открытый в Java методе, будет принудительно закрыт, как только запрос пользователя будет обработан процессом-сервером. Отсюда вывод: в режиме shared server mode открывайте, пишите/читайте и закрывайте файл в пределах одного запроса к серверу, например, в пределах вызывемой PL/SQL процедуры. И не рассчитывайте, что файл, открытый в одном запросе(вызове), все еще открыт при следующем запросе(вызове) в том же самом сеансе.

В следующий я раз рассмотрю работу хранимого Java кода с данными и объектами БД при помощи серверного JDBC драйвера. Исходный код будет использовать стандарт SQLJ и будет обработан SQLJ препроцессором. Попутно познакомимся с работой резолвера (resolver) и разберемся, с какими правами выполняется Java код внутри СУБД. Будет интересно запустить один и тот же код внутри СУБД и на традиционной JVM, с традиционным JDBC драйвером.

Комментариев нет:

Отправить комментарий