<<Restful Web Service>>这本书再第8章REST和ROA最佳实践的事务章节对如何实现事务是这么写的:
可以把简单的事务暴露为批量操作,或者采用重载的POST,不过还有一种做法,你猜对了,就是事务本身暴露为资源。
并结合一个例子给出具体的操作步骤描述:
比如把资金从支票帐户转移到储蓄帐户,比方说“支票帐户”暴露于/accounts/checking/11处,
“储蓄帐户”资源暴露于/accounts/saving/55. 这两个帐户都有50余额。
首先通过向一个事务工厂资源发送POST请求来创建事务:
POST /transactions/account-transfer HTTP/1.1
Host: example.com
HTTP响应里给出了我新创建的事务资源的URI:
201 Created
Location: /transactions/account-transfer/11a5
用PUT请求构造事务的第一部分:新的、余额增加的帐户。
PUT /transactions/account-transfer/11a5/accounts/checking/11 HTTP/1.1
Host: example.com
再用PUT请求构在事务的第二部分:新的余额增加的储蓄帐户
PUT /transactions/account-transfer/11a5/accounts/saving/55 HTTP/1.1
Host: example.com
在进行下一步之前,可以随时用DELETE请求删除事务资源,以回滚该事务,不过为了展示例子,
选择提交事务:
PUT /transactions/account-transfer/11a5
Host: example.com
如果不太明白也没关系,下面我就从服务器和客户端两个点来看看使用Restlet如何实现事务。实现的是上面的转帐的例子。
在我的环境中,baseUri是:http://localhsot:8080/restlet/resources
创建了四个资源:
1. TransactionsResource 对应的请求的Uri是/transaction, 这个资源是接到客户端发送的请求,来创建一个事务。
2. TransactionResource 对应的Uri是/transaction/{transactionId}, 这个资源是用作提交事务或者删除事务的。
3. TxnCheckingResource 对应的Uri是/transaction/{transactionId}/accounts/checking/{accountId},这个资源是接收客户端请求,对支票帐户进行相应的映象操作。
4. TxnAccountResource 对应的Uri是/transaction/{transactionId}/accounts/saving/{accountId},这个资源是接收客户端请求,对储蓄帐户进行相应的映象操作。
下面模拟客户端请求以及服务器应答的过程,来看看如何实现事务的。
首先客户端请求一个事务资源:
Client client = new Client(Protocol.HTTP);
Reference itemsUri = new Reference("http://localhost:8080/restlet/resources/transaction");
Response response = client.post(itemsUri, null);
请求的资源为TransactionsResource, 我们知道,如果是POST请求,则在Restlet中Resource的acceptRepresentation将被访问到
@Override
public void acceptRepresentation(Representation entity)
throws ResourceException {
String transactionId = this.generateTransactionId();
List<Transaction> transactions = Collections
.synchronizedList(new ArrayList<Transaction>());
getServletContext().setAttribute(transactionId, transactions);
getResponse().setStatus(Status.SUCCESS_CREATED);
getResponse().setLocationRef(getTransactionURI(transactionId));
}
上面这段代码,首先产生一个事务Id,然后,构在一个list列表,放到Servlet Context里,至于作用是什么,往下看就慢慢明白。构造完成,设置响应状态为成功创建(201),然后在location header里放置一个事务资源的URI,基于我的测试环境,此URI大概是http://localhost:8080/restlet/resources/transaction/xxxxxxxxx。xxxxxxxx其实就是产生的事务Id,这个Id应该具有唯一性。
ok,看看客户端怎么拿到这个事务资源的URI的,接上面客户端的代码:
assertEquals(Status.SUCCESS_CREATED.getCode(), response.getStatus().getCode());
Reference transactionUri = response.getLocationRef();
好了,如果没有出现问题的话,进行下一步,客户端请求从支票帐户减少50RMB,客户端代码如下:
Reference checkingAccountUri = new Reference(transactionUri + "/accounts/checking/11");
StringBuffer entity = new StringBuffer();
entity
.append("<account id=\"11\">")
.append("<type>checking</type>")
.append("<amount>-50</amount>")
.append("<currency>RMB</currency>")
.append(
"<description>debit from checking account which id is 11</description>")
.append("</account>");
Representation representation = new StringRepresentation(entity
.toString(), MediaType.TEXT_PLAIN);
response = client.put(checkingAccountUri, representation);
这时候,按照上面我们提到的,这次被请求到的资源是TxnCheckingResource,使用的PUT请求,看看服务器是如何处理的:
@SuppressWarnings("unchecked")
@Override
public void storeRepresentation(Representation entity) throws ResourceException {
String transactionId = (String)getRequest().getAttributes().get("transactionId");
Transaction tran = getTransactionFromRepresentation(entity);
List<Transaction> trans = (List<Transaction>)getServletContext().getAttribute(transactionId);
if(trans == null){
getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
}else{
trans.add(tran);
getServletContext().setAttribute(transactionId, trans);
getResponse().setStatus(Status.SUCCESS_ACCEPTED);
}
}
/*
* Note: we can receive entity of representation like
* <account id="11">
* <type>checking</type>
* <amount>-50</amount>
* <currency>RMB</currency>
* <description>debit from checking account which id is 11</description>
* </account>
*/
private Transaction getTransactionFromRepresentation(Representation entity){
//Note: we should handle xml body sent from client,
//then put the relevant info into transaction in product environment.
//but here we hard code just for testing.
String accountId = (String)getRequest().getAttributes().get("accountId");
Transaction tran = new Transaction();
//tran.setAccountId(accountId);
tran.setAccountId("11");
tran.setAccountType("checking");
tran.setAmount("-50");
return tran;
}
是的,服务器接收客户端请求后,分析传过来的表示(Xml),并从中解析出需要的信息,如帐户类型,交易的金额、交易的货币、交易描述等,统一封装到一个Transaction Java bean里。然后将这个Bean放到之前我们放到Servlet Context的一个List里,明白了吧,list的用处就在这里,实际上它也正是《REST Web Service》里提到的动作队列。在这里如果成功的话,返回相应代码为接受(代码为202)。
当然客户端接收到的代码应该是202,使用Junit测试返回响应值:
assertEquals(Status.SUCCESS_ACCEPTED.getCode(), response.getStatus().getCode());
接着客户端再次发送一个请求,请求往储蓄帐户里加入50RMB:
Reference savingAccountUri = new Reference(transactionUri + "/accounts/saving/55");
entity = new StringBuffer();
entity
.append("<account id=\"55\">")
.append("<type>saving</type>")
.append("<amount>50</amount>")
.append("<currency>RMB</currency>")
.append(
"<description>credit into saving account which id is 55</description>")
.append("</account>");
representation = new StringRepresentation(entity.toString(),
MediaType.TEXT_PLAIN);
response = client.put(savingAccountUri, representation);
服务器端被访问到的资源是TxnAccountResource,过程跟访问checking account非常类似,不多做解释,不同的代码如下:
/*
* Note: we can receive entity of representation like
* <account id="55">
* <type>saving</type>
* <amount>50</amount>
* <currency>RMB</currency>
* <description>Credit into saving account which id is 55</description>
* </account>
*/
private Transaction getTransactionFromRepresentation(Representation entity){
//Note: we should handle xml body sent from client,
//then put the relevant info into transaction in product environment.
//but here we hard code just for testing.
String accountId = (String)getRequest().getAttributes().get("accountId");
Transaction tran = new Transaction();
//tran.setAccountId(accountId);
tran.setAccountId("55");
tran.setAccountType("saving");
tran.setAmount("50");
return tran;
}
所有关于帐户的请求已经完成,下面提交事务,首先要客户端发送提交事务的请求:
response = client.put(transactionUri, null);
请求的URI是/transaction/{transactionId}, 对应的资源如上所述TransactionResource。所以服务器端Resource中代码:
@Override
public void storeRepresentation(Representation entity) throws ResourceException {
HttpServletRequest hsr = ServletCall.getRequest(getRequest());
Object obj = hsr.getSession().getServletContext().getAttribute(transactionId);
if(obj != null){
List<Transaction> trans = (List<Transaction>)obj;
//TODO validation should be done here
// needs to check the resource status and the balance between
// debit and credit and whether this transaction has been authrized.
//handleTransaction(trans);
getResponse().setStatus(Status.SUCCESS_OK);
}else{
getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
}
}
private void handleTransaction(List<Transaction> trans) throws ResourceException{
DBAccess dba = new DBAccess();
try {
dba.setAutoCommit(false);
dba.stmtBatchUpdate(buildSqls(trans));
dba.commit();
} catch (SQLException e) {
log.error("Excepiotn happened when excuting accounting transfer" + e.getMessage());
throw new ResourceException(Status.SERVER_ERROR_INTERNAL);
}
}
private String[] buildSqls(List<Transaction> trans){
List<String> sqls = new ArrayList<String>();
for(Iterator ite = trans.iterator(); ite.hasNext();) {
Transaction tran = (Transaction)ite.next();
String sql;
if(tran.getAccountType().equalsIgnoreCase("checking")){
sql = "UPDATE checking_account SET amount = amount-"
+ tran.getAmount() + "WHERE id = "
+ tran.getAccountId();
sqls.add(sql);
}else{
// should be saving account
sql = "UPDATE saving_account SET amount = amount-"
+ tran.getAmount() + "WHERE id = "
+ tran.getAccountId();
sqls.add(sql);
}
}
return (String[])sqls.toArray(new String[sqls.size()]);
}
恩,是不是已经明白了,之前的几步都是在根据请求来构造一个动作队列,当客户端请求提交事务时候,这个时候会真正的去启动一个数据库事务,然后执行队列里的动作,最后试图提交数据库事务,如果数据库事务出错,则Web事务也不会成功。
至此,一个正常的事务流程就完美的结束了,等等,问题来了,如果我请求了事务资源,然后我也请求了对帐户进行操作,但是最后我又不需要了,换句话说,我想删除事务,恩,同样请求的URI是:/transaction/{transactinId}, 使用DELETE方法:
@Override
public void removeRepresentations() throws ResourceException {
Object obj = getServletContext().getAttribute(getTransactionURI(transactionId));
if(obj != null){
getServletContext().removeAttribute(getTransactionURI(transactionId));
}
getResponse().setStatus(Status.SUCCESS_OK);
}
}
当然了,基于上述的一个最后提交的事务,在提交成功后,系统应该自动的删除事务资源。
代码见附件