简介
本文为 DB2 提供一个 SOAP 驱动程序示例,以便使用 SOAP API 隐式地执行 DB2 存储过程,而不需要创建任何显式的映射。我相信下一代数据库驱动程序会让数据库连接抛弃 ODBC/JDBC 等低层 API,转而使用 SOAP 和 REST 等高层 API,这会使数据库成为 SOA 环境中的直接参与者。
为了突出这个体系结构中与 XML 数据模型相关的方面,示例程序的流程尽可能保持简单。GUI 也保持展示功能所需的最基本形式。
我使用 Open Travel Alliance XML 模式创建旅馆示例数据和搜索旅馆的 SOAP 调用。使用 PayPal API 处理来自应用服务器的信用卡交易。
本文后面提供了源代码,可以下载并编译这些源代码。您需要安装 DB2 9 并在 Tomcat 类路径中包含 DB2 JCC 和 XML jar 文件。如果希望测试信用卡交易,就需要安装 PayPal Java API 并在 Tomcat 类路径中包含相关的 jar 文件。还必须在 PayPal 沙箱中创建一个帐户并获得 API 凭证,详细说明参见 PayPal Integration Center。然后可以修改 article4.java 文件的 setupPaypal() 函数中的凭证信息。
场景
在这个场景中,一位客户要通过 Web 预订旅馆房间。他首先要登录,获得他的个人信息。然后,指定一个城市,获取这个城市中的旅馆及其房间价格列表。最后,选择一家旅馆并预订一个房间。
图 1. 特性级体系结构
客户机上的客户操作导致 Web 浏览器向应用服务器发出 REST 调用。然后,应用服务器:
- 使用 JDBC 直接连接内部数据库,获取客户的个人信息。
- 对另一个数据库执行 SOAP 调用,这个数据库在公司防火墙内,但是位于客房预订部门的内部防火墙后面。
- 对一个外部信用卡交易服务提供商(比如 PayPal)执行 REST 调用。
图 2. 设计级体系结构

细节
为了了解在预订过程的每个步骤幕后发生的情况,我们来看看信息流和相关代码。
步骤 1
客户在旅行代理商的 Web 站点上输入他的姓名并获得他的个人信息。为了简单,这个示例不要求输入密码,并假设客户个人信息已经在代理商数据库中存在。
图 3. 登录和获得个人信息的命令和数据流
客户个人信息是一个 XML 文档,存储在数据库中的 XML 列中。信用卡信息也是一个 XML 文档,但是为了安全,它被加密并存储为二进制格式。
清单 1. 创建 customers 表并插入记录
CREATE
TABLE
CUSTOMERS (CUSTID
CHARACTER
(
64
)
NOT
NULL
,
CC
VARCHAR
(
1024
)
for
bit
data
not
null
, INFO XML
NOT
NULL
)

insert
into
CUSTOMERS
values
(
'
hardeep
'
,
encrypt(
'
<CC type="visa" expirydate="12/2009" number="4721930402892796" cvv="808">
<name>hardeep singh</name></CC>
'
,
'
password
'
),
'
<Customer customerid="hardeep" firstname="hardeep" lastname="singh"/>
'
);
图 4. 登录
当单击 Login 按钮时,在客户机中调用 Javascript 函数 getCustomerInfo()。这个函数生成执行应用服务器中的 customerinfo 服务所需的 REST 调用。
清单 2. 用来获取客户个人信息的客户机调用
var
cid
=
document.getElementById(
"
userid
"
).value;
var
addr
=
servletpath
+
"
?cmd=customerinfo&msg=
"
+
cid;
var
xmlhttpObj
=
new
XMLHttpRequest();
xmlhttpObj.open(
'
GET
'
, addr,
true
);

xmlhttpObj.onreadystatechange
=
function
()
...
{ getCustomerInfoCallback(xmlhttpObj); }
;
xmlhttpObj.send(
""
);
应用服务器对本地数据库执行一个 SQL 查询,从 customers 表的 info 列中选择客户个人信息。
清单 3. 应用服务器查询数据库来获取客户个人信息
Connection conn
=
DriverManager.getConnection(
"
jdbc:db2:article4
"
);
Statement stmt
=
conn.createStatement();
stmt.setMaxRows(
1
);
ResultSet rs
=
stmt.executeQuery(
"
select info from customers where custid='
"
+
msg
+
"
'
"
);
if
(rs.next ()) retValue
=
rs.getString(
1
);
stmt.close();
conn.close();
将数据库查询所产生的客户数据以 XML 数据的形式发送回客户机。
清单 4. 在 HTTP 报头中返回的数据类型设置为 XML
_res.setContentType(
"
text/xml
"
);
_res.setHeader(
"
Cache-Control
"
,
"
no-cache
"
);
_res.getWriter().write(retValue);
当客户机从应用服务器接收到客户数据时,它调用 getCustomerInfoCallback 函数,这个函数使用 XMLParse 包装器类将客户 XML 数据解析为 DOM 树并保存在一个全局变量中。然后改变用户界面,让客户能够输入城市的编码。
清单 5. 解析 XML 数据并改变用户界面
customerinfo
=
new
xmlparse(xmlhttp.responseXML,
false
);
var
hstr
=
'
<table cellSpacing="0" width="100%" cellPadding="2" border="0" align="left">
'
;
hstr
+=
'
<tr><td align="right">City Code:</td><td><INPUT type="text" id="citycode"
SIZE=15 MAXLENGTH=50 value="msy" tabindex="1">
'
;
hstr
+=
'
<td><INPUT type=button value="submit" onClick="javascript:getRates()" >
'
;
document.getElementById(
"
canvas
"
).innerHTML
=
hstr;
步骤 2
客户现在要搜索旅行目的地城市中的旅馆。
图 5. 列出旅馆及其房间价格的命令和数据流
用户输入城市编码并单击提交按钮,从而调用客户机 Javascript 中的 getRates() 函数。
图 6. 搜索与城市编码对应的旅馆
getRates 函数使用 OTA_HotelAvailRQ XML 模式生成请求旅馆信息所需的应用服务器调用。
清单 6. 使用 OTA_HotelAvailRQ 模式创建的搜索旅馆消息
var
citycode
=
document.getElementById(
"
citycode
"
).value
var
req
=
'
OTA_HotelSearchRQ xmlns="http://www.opentravel.org/OTA/2003/05"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opentravel.org/OTA/2003/05 OTA_HotelSearchRQ.xsd"
EchoToken="HL" Target="Production" Version="1.003" PrimaryLangID="EN-US"
ResponseType="PropertyList">
'
+
'
<POS><Source AirlineVendorID="FG" PseudoCityCode="MIA" ISOCountry="US"
ISOCurrency="USD" AgentSine="A4444BM" AgentDutyCode="FR"></Source>
'
+
'
<Source><RequestorID Type="5" ID="12345675" ID_Context="IATA"/></Source></POS>
'
+
'
<Criteria><Criterion><RefPoint></RefPoint><CodeRef LocationCode="23"
CodeContext="OTA-REF code list"/>
'
+
'
<HotelRef HotelCityCode="
'
+
citycode
+
'
"/><Radius Distance="2" DistanceMeasure="MILES"/>
'
+
'
<RoomAmenity RoomAmenity="74"/><RoomAmenity RoomAmenity="123"/></Criterion>
</Criteria></OTA_HotelSearchRQ>
'
;
使用 AJAX API 将旅馆清单调用以 POST 请求的形式发送到应用服务器。
清单 7. 发送到应用服务器的搜索旅馆消息
var
msg
=
'
<request cmd="hotelrates">
'
+
req
+
'
</request>
'
;
var
xmlhttpObj
=
new
XMLHttpRequest();
xmlhttpObj.open(
'
POST
'
, servletpath,
true
);

xmlhttpObj.onreadystatechange
=
function
()
...
{ getRatescallback(xmlhttpObj); }
;
xmlhttpObj.setRequestHeader(
'
content-type
'
,
'
text/xml
'
);
xmlhttpObj.send(msg);
应用服务器创建一个 SOAP 调用,调用 article4 数据库中的 getHotelRates 存储过程。这个数据库在部门内部防火墙的后面运行,位置是 http://localhost:8080/article4。从客户机接收的 OTA 搜索旅馆请求(msg)作为参数传递给这个存储过程。
注意:SOAP 消息的 SOAPAction 属性设置为数据库名。
清单 8. 应用服务器对 DB2 getHotelRates 存储过程执行一个 SOAP 调用
String body
=
"
<db:getHotelRates xmlns:db='http://ibm.com/db2/soap'>
"
+
"
<db:arg>
"
+
msg
+
"
</db:arg></db:getHotelRates>
"
;
return
sendURLMessage(
"
http://localhost:8080/article4/db2soapdriver
"
,
body,
"
http://ibm.com/db2/soap#article4
"
);
尽管有用来创建 SOAP 消息的 API,但是本文只使用基本的 URL 调用代码,以此说明 SOAP 调用仅仅是一种特殊的 HTTP POST 调用,其消息体符合一个标准化的 XML 模式。
注意:SOAPAction 设置为目标数据库名。
清单 9. 应用服务器对 DB2 getHotelRates 存储过程执行一个 SOAP 调用
URL u
=
new
URL(url);
URLConnection uc
=
u.openConnection();
HttpURLConnection connection
=
(HttpURLConnection) uc;
connection.setDoOutput(
true
);
connection.setDoInput(
true
);
connection.setRequestMethod(
"
POST
"
);
connection.setRequestProperty(
"
SOAPAction
"
, database_name);
OutputStream out
=
connection.getOutputStream();
Writer wout
=
new
OutputStreamWriter(out);
wout.write(
"
<?xml version='1.0'?>
"
);
>
wout.write(
"
xmlns:SOAP-ENV=
"
);
wout.write(
"
'http://schemas.xmlsoap.org/soap/envelope/'
"
);
wout.write(
"
xmlns:SOAP-ENC=
"
);
wout.write(
"
'http://schemas.xmlsoap.org/soap/encoding/'
"
);
wout.write(
"
SOAP-ENV:encodingStyle=
"
);
wout.write(
"
'http://schemas.xmlsoap.org/soap/encoding/'
"
);
wout.write(
"
xmlns:xsi=
"
);
wout.write(
"
'http://www.w3.org/2001/XMLSchema-instance'>
"
);
wout.write(
"
<SOAP-ENV:Body>
"
);
wout.write(msg);
wout.write(
"
</SOAP-ENV:Body>
"
);
wout.write(
"
</SOAP-ENV:Envelope>
"
);
因为应用服务器仅仅创建 SOAP 包装器并执行一个 URL 调用,所以如果 AJAX 的安全限制允许的话,也可以从客户机直接执行 SOAP 调用。尽管在 Web 客户机中使用 SOAP 驱动程序直接调用数据库是可能的,但是由于安全原因这种方式并不合适,应该改进 SOAP 驱动程序来防止这种做法。
getHotelRates 存储过程接受一个 XML 参数,其中包含 OTA 请求。XQuery 从输入的 XML 中提取出 HotelCityCode,并用它搜索和列出包含匹配的 HotelCityCode 属性的所有旅馆。
清单 10. getHotelRates 存储过程
CREATE
PROCEDURE
getHotelRates(
IN
request XML )
DYNAMIC RESULT SETS
1
LANGUAGE SQL
BEGIN
DECLARE
c_cur
CURSOR
WITH
RETURN
FOR
Select
XMLQuery(
'
declare namespace ns1 = "http://www.opentravel.org/OTA/2003/05";
$info//ns1:HotelDescriptiveContents
'
passing info
as
"info")
from
hotel
where
xmlexists(
'
declare namespace ns1 = "http://www.opentravel.org/OTA/2003/05";
$info//ns1:HotelDescriptiveContents[@HotelCityCode=$req//ns1:HotelRef/@HotelCityCode]
'
passing request
as
"req", info
as
"info" );
OPEN
c_cur;
END
然后,将从 db2soapdriver 返回给应用服务器的 SOAP 响应发送回客户机,而不做任何修改。这再次说明,在 XML 模型编程方式中,数据库成了重要的参与者,而且在许多情况下应用服务器仅仅作为交换信息的中介。
当客户机从应用服务器接收到响应时,它调用 getRatescallback。使用 DOM 解析器解析返回的 SOAP 响应。DOM 解析器会处理 SOAP 响应中的名称空间。
清单 11. 客户机解析来自应用服务器的 SOAP 响应
soapxml
=
new
xmlparse(xmlhttp.responseXML,
false
);
soapxml.xmlRoot.setProperty(
"
SelectionNamespaces
"
,
"
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/'
xmlns:db='http://ibm.com/db2/soap'
"
);

var
hstr
=
'
<table cellSpacing="0" width="100%" cellPadding="2" border="1" align="left">
'
;
hstr
+=
"
<tr><td>name<td>rate<td>rooms<td>
"
;
将数据库结果中的每一行提取到另一个 DOM 树中,然后使用 XPath 提取相关信息。为客户机创建一个新视图,显示返回的所有旅馆的列表。注意,XPath 调用中使用了一个名称空间别名。
清单 12. 客户机从 SOAP 体中提取数据库结果集
soapxml.find(
"
//SOAP-ENV:Body//db:row
"
,
null
,
true
);
for
(i
=
0
;soapxml.currentFind.length
>
i;i
++
)

...
{
var result=soapxml.getValue("db:col/text()",i);
rateslist=new xmlparse(result,true);
rateslist.xmlRoot.setProperty("SelectionNamespaces",
"xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:x='http://www.opentravel.org/OTA/2003/05' ");
var id=rateslist.getValue("//x:HotelDescriptiveContents/@HotelCode",null);
var name=rateslist.getValue("//x:HotelName/@HotelShortName",null);
var rooms=rateslist.getValue("//x:GuestRoomInfo/@Quantity",null);
var charge=rateslist.getValue("//x:Charge/@Amount",null);
hstr+="<tr><td>"+name+"<td>"+charge+"<td>"+rooms+"<td>
<input type='button' onClick="javascript:bookRoom('"+id+"','"+charge+"');
" value='select'/>";
}
document.getElementById(
"
canvas
"
).innerHTML
=
hstr;
步骤 3
在最后一步中,客户选择一家旅馆并预订房间。从数据库中保存的客户个人信息中获得信用卡信息。
图 7. 预订房间的命令和数据流
客户现在可以在列表中选择一家旅馆,从而在这家旅馆预订房间。
图 8. 从旅馆列表中选择旅馆
当客户单击 Select 时,调用客户机 Javascript 中的 bookroom 函数。使用 AJAX API 将一个 XML 消息(请求预订房间)以 POST 请求的形式发送到应用服务器。这个请求包含旅馆 ID、客户名、要预订的房间数量和客户信用卡需要支付的数额。
清单 13. 客户机向应用服务器发送 REST 调用来预定房间
function
bookRoom(hotelid,amount)

...
{
var cid=document.getElementById("userid").value;
var msg='<request cmd="bookroom"><message><ccinfo units="1" invoice=""
amount="'+amount+'"/><username>'+cid+'</username>
<hotelid>'+hotelid+'</hotelid></message></request>';
var xmlhttpObj= new XMLHttpRequest();
xmlhttpObj.open('POST', servletpath, true);

xmlhttpObj.onreadystatechange = function() ...{ bookRoomcallback(xmlhttpObj); };
xmlhttpObj.setRequestHeader('content-type', 'text/xml');
xmlhttpObj.send(msg);
}
应用服务器解析收到的消息并从其中提取出客户 ID。然后,调用本地数据库,从 customers 表中获取信用卡信息和客户个人信息。
注意:信用卡信息存储在 customers 表的一个加密列中。尽管在这个示例中密码是硬编码的,但是在真实的场景中密码可能是客户用来登录的密码。
清单 14. 应用服务器向数据库查询加密的信用卡信息
接下来,应用服务器创建一个 SOAP 调用,调用在部门内部防火墙后面运行 article4 数据库中的 bookaroom 存储过程。旅馆 ID 和客户个人信息作为参数传递给这个存储过程。
清单 15. 应用服务器对 bookaroom 存储过程执行一个 SOAP 调用
String hotelid
=
msgxml.getValue(
"
//hotelid/text()
"
);
String body
=
"
<db:bookaroom xmlns:db='http://ibm.com/db2/soap'>
"
+
"
<db:arg>
"
+
hotelid
+
"
</db:arg>
"
+
"
<db:arg>
"
+
custinfo
+
"
</db:arg>
"
+
"
<db:arg></db:arg>
"
+
"
</db:bookaroom>
"
;
String soapstr
=
sendURLMessage(
"
http://localhost:8080/article4/db2soapdriver
"
,
body,
"
http://ibm.com/db2/soap#article4
"
);
bookaroom 存储过程只是一个伪过程,它总是在输出参数中返回一个固定的发票号。它的用途是说明 db2soapdriver 如何处理输出参数并在 SOAP 响应中表示输出参数。
注意:如果它是一个真正预订房间的存储过程,那么还需要日期和房间类型等信息。作为一个练习,您可以改进客户机代码,让它接收日期和房间信息,并生成 OTA 房间预订消息。还需要添加另一个表来处理预订信息。
清单 16. bookaroom 存储过程
create
procedure
bookaroom (
in
hotelid
varchar
(
12
),
in
userinfo xml,
out invoice
varchar
(
64
))
language SQL
begin
set
invoice
=
'
INV001
'
;
return
1
;
end
使用 DOM 包装器类将 SOAP 响应字符串解析为 DOM 对象,并使用 XPath 调用提取出发票号。请查看 db2soapdriver.java,了解 DB2 存储过程调用产生的 SOAP 响应的模式。
清单 17. 应用服务器从 SOAP 响应中提取发票号
XMLParse soapXML
=
new
XMLParse(
true
);
soapXML.createDOM(soapstr,
false
);
String namespaces
=
"
SOAPENV=http://schemas.xmlsoap.org/soap/envelope/;
db2
=
http:
//
ibm.com/db2/soap";
soapXML.setNamespaces(namespaces,
"
=
"
);
String invoice
=
soapXML.getValue(
"
//db2:out/text()
"
,
null
);
在客户机消息中设置发票号并传递给信用卡交易函数。将信用卡交易的结果和发票信息发送回客户机。
清单 18. 应用服务器将信用卡交易的结果发送回客户机
if(invoice!=null)
{
msgxml.setValue("//ccinfo/@invoice",invoice);
retValue="<invoice='"+invoice+"'>"+makePayment(msgxml,CCInfo)+"</invoice>";
|
在信用卡交易函数中,从客户机消息中提取出信用卡和购物车信息。
清单 19. 从 DOM 中提取信用卡和购物车信息
XMLParse ccinfo=new XMLParse(cc);
String units=msg.getValue("//ccinfo/@units");
String invno=msg.getValue("//ccinfo/@invoice");
String amount=msg.getValue("//ccinfo/@amount");
String ctype=ccinfo.getValue("//CC/@type");
String cnumber=ccinfo.getValue("//CC/@number");
String cexpdate=ccinfo.getValue("//CC/@expirydate");
String ccvv=ccinfo.getValue("//CC/@cvv");
String cname=ccinfo.getValue("//CC/name/text()");
String note="Paid by "+cname+" for "+units+" room(s) ";
|
使用 PayPal 信用卡服务的名称-值对(NVP)API 将这些信息传递给服务。
NVP API 是 PayPal 的业务功能、风险管理和业务逻辑的简单接口。NVP API 最基本的使用方法是通过到 PayPal 服务器的 HTTPS 连接发送一个 NVP 字符串,然后处理响应(也是一个 NVP 字符串)。执行 NVP API 调用的基本步骤如下:
- 为特定的 API 方法构造一个请求参数字符串。
- 通过 HTTPS 连接向 PayPal 服务器发送这个参数字符串。
- 处理服务器响应中的 NVP。
清单 20. 对 PayPal Web 服务执行 NVP API 调用
NVPEncoder encoder = new NVPEncoder();
encoder.add("METHOD","DoDirectPayment");
encoder.add("PAYMENTACTION","Sale");
encoder.add("AMT",amount);
encoder.add("CREDITCARDTYPE",ctype);
encoder.add("ACCT",cnumber);
encoder.add("CVV2",ccvv);
encoder.add("EXPDATE",cexpdate);
encoder.add("FIRSTNAME",cname);
encoder.add("CURRENCYCODE","USD");
encoder.add("INVNUM",invno);
encoder.add("CUSTOM",note);
String NVPString = encoder.encode();
String ppresponse = (String) caller.call(NVPString);
NVPDecoder resultValues = new NVPDecoder();
resultValues.decode(ppresponse);
String transactionId = (String)resultValues.get("TRANSACTIONID");
String amt = (String)resultValues.get("AMT");
String avsCode = (String)resultValues.get("AVSCODE");
String cvv2Match = (String)resultValues.get("CVV2MATCH");
String strAck = resultValues.get("ACK");
String strAckSMSG = resultValues.get("L_SHORTMESSAGE0");
String strAckLMSG = resultValues.get("L_LONGMESSAGE0");
String strAckLERR= resultValues.get("L_ERRORCODE0");
if(strAck !=null && !(strAck.equals("Success") || strAck.equals("SuccessWithWarning")))
{
return("PAYPAL Failed "+strAckLERR+" "+strAckSMSG+" "+strAckLMSG );
}
|
结束语
基于面向 Web 体系结构的应用程序可以使用 XML 数据模型减少设计和代码的复杂性。当使用 SOAP 和 REST 等高层 API 访问数据库时,能够轻松地集成来自不同数据库的数据。在当今的企业环境中经常出现合并和安全事件,所以以这种黑盒方式将公司数据隐藏在 Web 服务背后已经成为一项关键需求。XML 是连接这种体系结构中各个组件的桥梁,而 XML 数据模型是这个环境中最自然的编程模型。