<<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); } }
当然了,基于上述的一个最后提交的事务,在提交成功后,系统应该自动的删除事务资源。
代码见附件