如何通过 PL/SQL 调用 Java 方法

Posted by mingfer on September 23, 2019

前言

当我们加载并发布了使用 Java 实现的存储过程(包括过程和函数)之后,我们可以通过 SQL 语句调用它们。本文将说明如何在各自的上下文中 SQL 如何去调用 Java 实现的存储过程,以及在 SQL 中如何处理 Java 存储过程中产生的异常。

相关文章参见:

Hello World

我们首先通过一个简单的打印 Hello World 字符串的过程和函数来说明如何使用 Java 存储过程。在下面的 Java 代码中我们实现了两个不同的用于打印 Hello World 的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HelloWorld {

    // 过程
    public static void sayHelloWorld() {
        System.out.println("Hello World.");
    }

    // 函数
    public static String sayHelloWorld(String text) {
        System.out.println("Hello World.");
        return text;
    }

}

注意:所有的 Java 存储过程方法都应该是 static 修饰的静态方法。

重定向输出

在 Oracle 数据库中默认的输出设备是一个 trace file 而不是用户屏幕。也就是说 Java 中 System.outSystem.err 的输出会输出到当前的 trace file 中。我们可以重定向输出到当前屏幕,并通过 dbms_java.set_output() 设置缓冲区大小:

1
2
SQL> SET SERVEROUTPUT ON
SQL> CALL dbms_java.set_output(2000);

缓冲区的默认大小和最小值均为 2000 字节,最大值为 1,000,000 字节。我们可以通过如下方式更改缓冲区的大小:

1
2
SQL> SET SERVEROUTPUT ON SIZE 5000
SQL> CALL dbms_java.set_output(5000);

配置之后当存储过程运行完毕输出将会打印到屏幕上面。

上面是使用 sqlplus 命令行工具的示例,如果使用 Oracle SQL Developer 工具,可以在当前连接下输入上面的代码并执行这些代码:

1569207655476

如果需要打印 System.outSystem.err 输出的内容,需要将 【查看】–> 【DBMS 输出】连接到当前数据库。

1569216787385

注意:所有过程或函数需要在 【SQL 工作表】下执行,才会打印 System.outSystem.err 输出的内容到【脚本输出】和【DBMS 输出】。

加载 Java 存储过程

可以使用 loadjava 工具将 Java 类源码,编译后的 .class 文件或资源文件加载到数据库实例中,它们将被存储为 Java Schema Object。当我们在命令行或应用中运行 loadjava 命令的时候,可以指定解析器在内的一些可选项。

在下面的例子中,loadjava 在命令行使用 JDBC OCI 驱动连接 Oracle 数据库实例,它需要我们在连接的时候指定用户和密码。默认情况下,导入的 Java 过程函数将存储在登录用户的 schema 下面。

1
2
3
$ javac HelloWorld.java
$ loadjava -user HR HelloWorld.class
Password: password

当我们调用 Java 存储过程中提供的方法时,解析器会去查找支持的 Java 类。默认的解析器会现在当前用户的 schema 中查找,然后在系统的 schema (包含所有 Java 的核心库)中查找。必要情况下,我们可以指定需要的解析器。

如果使用 Oracle SQL Developer ,直接在 Java 目录导入 Java 文件即可:

1569209720935

发布 Java 存储过程

一般来说,无返回的 Java 方法发布为 PL/SQL 过程,有返回的 Java 方法发布为 PL/SQL 函数。在下面的例子中,我们通过 sqlplus 连接到 Java 存储过程所在的用户,并发布一个 Java 方法。

1
2
3
4
5
6
SQL> connect HR
Enter password: password

SQL> CREATE FUNCTION say_hello_world RETURN VARCHAR2
2 AS LANGUAGE JAVA
3 NAME 'HelloWorld.sayHelloWorld(java.lang.String) return java.lang.String';

如果使用 Oracle SQL Developer ,可以选择在过程和函数目录发布过程或函数,也可以在 Package 目录同时发布过程或函数。

1569217725527

Package 实现:

CREATE OR REPLACE 
PACKAGE PACKAGE_HELLO_WORLD AS 

  /* TODO enter package declarations (types, exceptions, methods etc) here */ 
  PROCEDURE say_hello_world;
  
  FUNCTION say_hello_world(TEXT IN VARCHAR2) RETURN VARCHAR2;
  
END PACKAGE_HELLO_WORLD;

-- package body
CREATE OR REPLACE
PACKAGE BODY PACKAGE_HELLO_WORLD AS

  PROCEDURE say_hello_world AS
    LANGUAGE JAVA NAME 'HelloWorld.sayHelloWorld()';

  FUNCTION say_hello_world(TEXT IN VARCHAR2) RETURN VARCHAR2 AS
    LANGUAGE JAVA NAME 'HelloWorld.sayHelloWorld(java.lang.String) return java.lang.String';

END PACKAGE_HELLO_WORLD;

过程实现:

CREATE OR REPLACE PROCEDURE PROC_SAY_HELLO_WORLD AS 
LANGUAGE JAVA NAME 'HelloWorld.sayHelloWorld()';

函数实现:

CREATE OR REPLACE FUNCTION FUNC_SAY_HELLO_WORLD 
(
  TEXT IN VARCHAR2 
) RETURN VARCHAR2 AS 
LANGUAGE JAVA NAME 'HelloWorld.sayHelloWorld(java.lang.String) return java.lang.String';

使用 Jar 包中的方法

既然可以使用 Java 中的方法,那么打包成 Jar 包的 Java 方法也是可以被 SQL 调用的。但是需要注意,不同版本 Oracle 支持的 JDK 版本是不一样的。

功能 Oracle 版本
JDK1.5 11.1+
JDK1.6 12.1+
JDK1.7 12.1+
JDK1.8 12.2+

导入 Jar 包

导入 Jar 包或导入 Java 源码,.class 文件都需要使用 loadjava 工具。该工具在 Oracle 完整版本的安装包中有提供(注意不是 instant client),下载地址参见 Oracle 11g Realease 2 Client

使用下面的命令导入 .java.jar.class 文件:

1
$ loadjava -u user/password@database -r -o -verbose xxx.jar

注意

  • -u 指定的 database 名称可以在 tnsnames.ora 中取得
  • 指定了 -r 选项或去解析每一个类的依赖情况,如果没有提前导入其依赖的类会导致报错。
  • -o 是使用本地的 Oracle OCI 驱动连接 Oracle 数据库。

已经导入的内容可以使用 dropjava 进行卸载:

1
$ dropjava -u user/password@database -o -verbose xxx.jar

查看导入的内容

使用 sqlplus 可以通过下面的 SQL 语句进行查看:

1
SQL> select * from user_java_classes

在 Oracle SQL Developer 中可以通过上述语句查看,也可以刷新数据库之后在 【Java】下面查看:

1569316044152

导入 jar 包之后,将 jar 包里面需要使用的存储过程映射成对应的 PL/SQL 过程或函数即可。这部分内容在上面的教程中已经详细说明了,这里不再赘述。

异常处理

Java 异常是一个包含名称和继承关系的对象,也就是说我们可以把子类异常对象转换为他的父类异常。所有的 Java 异常都实现了 toString() 方法,该方法会返回异常类的全限定名称和相关的异常信息,该信息通常在异常的构造函数中填写。

当在 SQL 语句中运行 Java 存储过程的时候,所有该存储过程抛出的异常都应该是 java.sql.SQLException 的子类。这个类提供了 getErrorCode()getMessage() 方法,用于返回 Oracle 数据库的错误码和错误信息。

如果在 SQL 或 PL/SQL 调用的 Java 存储过程抛出了未被抓取的异常,Oracle 将提示下面的错误信息:

1
ORA-29532 Java call terminated by uncaught Java exception

这就是提示所有未抓取的异常,包括非 java.sql.SQLException 子类异常的方式。