通過JMS監聽Oracle AQ,在數據庫變化時觸發執行Java程序

環境說明

本實驗環境基於Oracle 12C和JDK1.8,其中Oracle 12C支持多租戶特性,相較於之前的Oracle版本,使用‘C##用戶名‘表示用戶,例如如果數據庫用戶叫kevin,則登陸時使用C##kevin進行登陸。


一、Oracle高級消息隊列AQ

Oracle AQ是Oracle中的消息隊列,是Oracle中的一種高級應用,每個版本都在不斷的加強,使用DBMS_AQ系統包進行相應的操作,是Oracle的默認組件,只要安裝了Oracle數據庫就可以使用。使用AQ可以在多個Oracle數據庫、Oracle與Java、C等系統中進行數據傳輸。

下面分步驟說明如何創建Oracle AQ

1. 創建消息負荷payload

Oracle AQ中傳遞的消息被稱爲有效負荷(payloads),格式可以是用戶自定義對象或XMLType或ANYDATA。本例中我們創建一個簡單的對象類型用於傳遞消息。

create type demo_queue_payload_type as object (message varchar2(4000));

2. 創建隊列表

隊列表用於存儲消息,在入隊時自動存入表中,出隊時自動刪除。使用DBMS_AQADM包進行數據表的創建,只需要寫表名,同時設置相應的屬性。對於隊列需要設置multiple_consumers爲false,如果使用發佈/訂閱模式需要設置爲true。

begin
  dbms_aqadm.create_queue_table(
    queue_table   => 'demo_queue_table',
    queue_payload_type => 'demo_queue_payload_type',
    multiple_consumers => false
  );
end;

執行完後可以查看oracle表中自動生成了demo_queue_table表,可以查看影響子段(含義比較清晰)。

3. 創建隊列並啓動

創建隊列並啓動隊列:

begin
  dbms_aqadm.create_queue (
    queue_name  => 'demo_queue',
    queue_table => 'demo_queue_table'
  );

  dbms_aqadm.start_queue(
    queue_name  =>  'demo_queue'
  );
end;

至此,我們已經創建了隊列有效負荷,隊列表和隊列。可以查看以下系統創建了哪些相關的對象:

SELECT object_name, object_type FROM user_objects WHERE object_name != 'DEMO_QUEUE_PAYLOAD_TYPE';

OBJECT_NAME OBJECT_TYPE
------------------------------ ---------------
DEMO_QUEUE_TABLE TABLE
SYS_C009392 INDEX
SYS_LOB0000060502C00030$$ LOB
AQ$_DEMO_QUEUE_TABLE_T INDEX
AQ$_DEMO_QUEUE_TABLE_I INDEX
AQ$_DEMO_QUEUE_TABLE_E QUEUE
AQ$DEMO_QUEUE_TABLE VIEW
DEMO_QUEUE QUEUE

我們看到一個隊列帶出了一系列自動生成對象,有些是被後面直接用到的。不過有趣的是,創建了第二個隊列。這就是所謂的異常隊列(exception queue)。如果AQ無法從我們的隊列接收消息,將記錄在該異常隊列中。

消息多次處理出錯等情況會自動轉移到異常的隊列,對於異常隊列如何處理目前筆者還沒有找到相應的寫法,因爲我使用的場景並不要求消息必須一對一的被處理,只要起到通知的作用即可。所以如果消息轉移到異常隊列,可以執行清空隊列表中的數據

delete from demo_queue_table;

4. 隊列的停止和刪除

如果需要刪除或重建可以使用下面的方法進行操作:

BEGIN
   DBMS_AQADM.STOP_QUEUE(
      queue_name => 'demo_queue'
      );
   DBMS_AQADM.DROP_QUEUE(
      queue_name => 'demo_queue'
      );
   DBMS_AQADM.DROP_QUEUE_TABLE(
      queue_table => 'demo_queue_table'
      );
END;

5. 入隊消息

入列操作是一個基本的事務操作(就像往隊列表Insert),因此我們需要提交。

declare
  r_enqueue_options DBMS_AQ.ENQUEUE_OPTIONS_T;
  r_message_properties DBMS_AQ.MESSAGE_PROPERTIES_T;
  v_message_handle RAW(16);
  o_payload demo_queue_payload_type;
begin
  o_payload := demo_queue_payload_type('what is you name ?');

  dbms_aq.enqueue(
    queue_name  => 'demo_queue',
    enqueue_options => r_enqueue_options,
    message_properties => r_message_properties,
    payload => o_payload,
    msgid => v_message_handle
  );

  commit;
end;

通過SQL語句查看消息是否正常入隊:

select * from aq$demo_queue_table;
select user_data from aq$demo_queue_table;

6. 出隊消息

使用Oracle進行出隊操作,我沒有實驗成功(不確定是否和DBMS_OUTPUT的執行權限有關),代碼如下,讀者可以進行調試:

declare
  r_dequeue_options DBMS_AQ.DEQUEUE_OPTIONS_T;
  r_message_properties DBMS_AQ.MESSAGE_PROPERTIES_T;
  v_message_handle RAW(16);
  o_payload demo_queue_payload_type;
begin
  DBMS_AQ.DEQUEUE(
    queue_name => 'demo_queue',
    dequeue_options => r_dequeue_options,
    message_properties => r_message_properties,
    payload => o_payload,
    msgid => v_message_handle
  );


  DBMS_OUTPUT.PUT_LINE(
    '***** Browse message is [' || o_payload.message || ']****'
  );

end;

二、Java使用JMS監聽並處理Oracle AQ隊列

Java使用JMS進行相應的處理,需要使用Oracle提供的jar,在Oracle安裝目錄可以找到:在linux中可以使用find命令進行查找,例如

find `pwd` -name 'jmscommon.jar'

需要的jar爲:

  • app/oracle/product/12.1.0/dbhome_1/rdbms/jlib/jmscommon.jar
  • app/oracle/product/12.1.0/dbhome_1/jdbc/lib/ojdbc7.jar
  • app/oracle/product/12.1.0/dbhome_1/jlib/orai18n.jar
  • app/oracle/product/12.1.0/dbhome_1/jlib/jta.jar
  • app/oracle/product/12.1.0/dbhome_1/rdbms/jlib/aqapi_g.jar

1. 創建連接參數類

實際使用時可以把參數信息配置在properties文件中,使用Spring進行注入。

package org.kevin.jms;
/**
 * 
 * @author 李文鍇
 *  連接參數信息
 *
 */
public class JmsConfig {

    public String username = "c##kevin";
    public String password = "a111111111";
    public String jdbcUrl = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";
    public String queueName = "demo_queue";
}

2. 創建消息轉換類

因爲消息載荷是Oracle數據類型,需要提供一個轉換工廠類將Oracle類型轉換爲Java類型。

package org.kevin.jms;

import java.sql.SQLException;

import oracle.jdbc.driver.OracleConnection;
import oracle.jdbc.internal.OracleTypes;
import oracle.jpub.runtime.MutableStruct;
import oracle.sql.CustomDatum;
import oracle.sql.CustomDatumFactory;
import oracle.sql.Datum;
import oracle.sql.STRUCT;

/**
 * 
 * @author 李文鍇 
 * 數據類型轉換類
 *
 */
@SuppressWarnings("deprecation")
public class QUEUE_MESSAGE_TYPE implements CustomDatum, CustomDatumFactory {
    public static final String _SQL_NAME = "QUEUE_MESSAGE_TYPE";
    public static final int _SQL_TYPECODE = OracleTypes.STRUCT;

    MutableStruct _struct;
    // 12表示字符串
    static int[] _sqlType = { 12 };
    static CustomDatumFactory[] _factory = new CustomDatumFactory[1];
    static final QUEUE_MESSAGE_TYPE _MessageFactory = new QUEUE_MESSAGE_TYPE();

    public static CustomDatumFactory getFactory() {
        return _MessageFactory;
    }

    public QUEUE_MESSAGE_TYPE() {
        _struct = new MutableStruct(new Object[1], _sqlType, _factory);
    }

    public Datum toDatum(OracleConnection c) throws SQLException {
        return _struct.toDatum(c, _SQL_NAME);
    }

    public CustomDatum create(Datum d, int sqlType) throws SQLException {
        if (d == null)
            return null;
        QUEUE_MESSAGE_TYPE o = new QUEUE_MESSAGE_TYPE();
        o._struct = new MutableStruct((STRUCT) d, _sqlType, _factory);
        return o;
    }

    public String getContent() throws SQLException {
        return (String) _struct.getAttribute(0);
    }

}

3. 主類進行消息處理

package org.kevin.jms;

import java.util.Properties;

import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.Session;

import oracle.jms.AQjmsAdtMessage;
import oracle.jms.AQjmsDestination;
import oracle.jms.AQjmsFactory;
import oracle.jms.AQjmsSession;

/**
 * 
 * @author 李文鍇 消息處理類
 *
 */
public class Main {

    public static void main(String[] args) throws Exception {
        JmsConfig config = new JmsConfig();

        QueueConnectionFactory queueConnectionFactory = AQjmsFactory.getQueueConnectionFactory(config.jdbcUrl,
                new Properties());

        QueueConnection conn = queueConnectionFactory.createQueueConnection(config.username, config.password);
        AQjmsSession session = (AQjmsSession) conn.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);

        conn.start();

        Queue queue = (AQjmsDestination) session.getQueue(config.username, config.queueName);
        MessageConsumer consumer = session.createConsumer(queue, null, QUEUE_MESSAGE_TYPE.getFactory(), null, false);

        consumer.setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
                System.out.println("ok");

                AQjmsAdtMessage adtMessage = (AQjmsAdtMessage) message;

                try {
                    QUEUE_MESSAGE_TYPE payload = (QUEUE_MESSAGE_TYPE) adtMessage.getAdtPayload();
                    System.out.println(payload.getContent());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        Thread.sleep(1000000);
    }

}

使用Oracle程序塊進行入隊操作,在沒有啓動Java時看到隊列表中存在數據。啓動Java後,控制檯正確的輸出的消息;通過Oracle程序塊再次寫入消息,發現控制檯正確處理消息。Java的JMS監聽不是立刻進行處理,可能存在幾秒中的時間差,時間不等。


三、監控表記錄變化通知Java

下面的例子創建一個數據表,然後在表中添加觸發器,當數據變化後觸發器調用存儲過程給Oracle AQ發送消息,然後使用Java JMS對消息進行處理。

1. 創建表

創建student表,包含username和age兩個子段,其中username時varchar2類型,age時number類型。

2. 創建存儲過程

創建send_aq_msg存儲過程,因爲存儲過程中調用dbms數據包,系統包在存儲過程中執行需要進行授權(使用sys用戶進行授權):

grant execute on dbms_aq to c##kevin;

注意存儲過程中包含commit語句。

create or replace 
PROCEDURE send_aq_msg (info IN VARCHAR2) as
  r_enqueue_options DBMS_AQ.ENQUEUE_OPTIONS_T;
  r_message_properties DBMS_AQ.MESSAGE_PROPERTIES_T;
  v_message_handle RAW(16);
  o_payload demo_queue_payload_type;
begin
  o_payload := demo_queue_payload_type(info);

  dbms_aq.enqueue(
    queue_name  => 'demo_queue',
    enqueue_options => r_enqueue_options,
    message_properties => r_message_properties,
    payload => o_payload,
    msgid => v_message_handle
  );

  commit;
end send_aq_msg;

3. 創建觸發器

在student表中創建觸發器,當數據寫入或更新時,如果age=18,則進行入隊操作。需要調用存儲過程發送消息,但觸發器中不能包含事物提交語句,因此需要使用pragma autonomous_transaction;聲明自由事物:

CREATE OR REPLACE TRIGGER STUDENT_TR 
AFTER INSERT OR UPDATE OF AGE ON STUDENT FOR EACH ROW 
DECLARE
pragma autonomous_transaction;
BEGIN
  if :new.age = 18 then
      send_aq_msg(:new.username);  
  end if;  
END;

創建完觸發器後向執行插入或更新操作:

insert into student (username,age) values ('jack.lee.3k', 18);
update student set age=18 where username='jack003';

Java JMS可以正確的處理消息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章