之前有提到過在年後做的項目中有一個集中刻錄機管理的系統。簡單來說,這是一個典型的 B/S 系統,用戶在頁面上需要及時瞭解到刻錄機的狀態(各個光驅的使用情況等),並且需要進行刻錄附件、錄像刻錄等操作(從數據庫中獲取存放路徑,使用 samba 共享進行遠程下載)
系統結構
由於刻錄機提供的 SDK 需要使用本地調用的方式,同時考慮到今後可能有多個上層業務系統需要使用相同的刻錄機,所以採用了 業務系統---中間件---刻錄機的方式進行實現,業務系統服務器與中間件服務器通過 WebService 進行數據交換。
劃分好系統結構以後,可以看到業務系統不必再考慮刻錄機 SDK 的調用過程,只需要通過 WebService 與中間件服務器進行通信即可。
WebService 需求分析
首先通過與中間件開發人員的溝通,確定了 WebService的調用過程:中間件只提供一個 doCommand(String str)
方法,根據參數 json 字符串的字段信息進行不同的操作。
對於業務系統來說,則需要封裝一個調用 WebService 服務的底層實現,它所實現的功能非常簡單,就是調用 WebService 方法接收、返回數據。考慮到面向對象的編程方式,將這個 WebService 客戶端組件分成三個部分:
-
Client 描述一個 WebService 客戶端,進行
doCommand(String str)
方法的調用。 -
Request描述一次 WebService 請求參數並最終由 Client調用其
convertToString()
方法將其轉換爲String
類型參數。 -
Response描述一次請求的返回結果,使用
parseString(in String str)
方法將返回的字符串轉換爲自身屬性。
同時,由於 Response 與 Request應該一一對應(實際使用中有多種請求),所以Request需提供getResponse()
方法告知其對應返回的 Response 類型。
根據以上分析,可以畫出WSClient 的 UML 圖:
其中WSRequest與WSRespnose爲抽象類,這裏主要考慮到在 拼接 和 解析 String 參數時會有許多相同字符串的操作,此類重複的代碼可以在基類中進行處理,例如返回結構中的錯誤信息與錯誤碼,每一次都是相同的格式。所有自行封裝的 Request 和 Response 都需要繼承自 WSRequest 和 WSResponse。
代碼實現
先看看目錄結構:
主要看紅框以內的 WSClient 部分(框外的類在後續會提到),WSConfig 是 WSClient 所需的配置項實體,exception 中包含自定義的異常,爲了方便這裏只寫了一個,external 中放的是通過 axis2的 wsdl2java 工具生成的 java 類,接下來是Client、Request 和 Response三個頂級接口,最後是實現了 Client接口的 WSClient 。
三個頂級接口的定義:
//...
public interface Client {
Response sendRequest(Request req) throws WebServiceClientException;
}
//...
public interface Request {
Response getResponse();
String convertToString();
}
//...
public interface Response {
void parseJson(JSONObject jsonObject) throws JSONException;
int getErrorCode();
String getResult();
}
主要的處理邏輯,WSClient 類:
package com.yzhang.webservice;
import com.yzhang.webservice.entity.WSConfig;
import com.yzhang.webservice.exception.WebServiceClientException;
import com.yzhang.webservice.external.GwslibStub;
import org.apache.axis2.AxisFault;
import org.apache.axis2.client.Options;
import org.apache.log4j.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import java.rmi.RemoteException;
/**
* Created by yzhang on 2017/4/9.
*/
public class WSClient implements Client{
private static final Logger logger = Logger.getLogger(WSClient.class);
private static final long TIMEOUT = 15000;
private WSConfig config;
public WSClient(String targetIp, int targetPort){
config = new WSConfig();
config.setTargetIp(targetIp);
config.setTargetPort(targetPort);
}
/**
* send a request and parse response
* @param request
* @return
* @throws WebServiceClientException
*/
public Response sendRequest(Request request) throws WebServiceClientException {
// 1. 將 request 轉換爲 string,準備傳輸
String req = request.convertToString();
logger.info("<< WSClient發送>> -----> "+ req);
// 2. 調用接口處理,根據實際調用的WebService 進行修改
GwslibStub.DoCommand cmd = new GwslibStub.DoCommand();
cmd.setStrXMLReq(req);
GwslibStub stub;
GwslibStub.DoCommandResponse res;
try {
stub = getGwslibStub();
Options opts = stub._getServiceClient().getOptions();
opts.setTimeOutInMilliSeconds(TIMEOUT);
// res = stub.doCommand(cmd);
} catch (AxisFault axisFault) {
logger.error("AxisFault", axisFault);
throw new WebServiceClientException("連接刻錄機服務失敗:"+ config.getTargetEndpoint());
} catch (RemoteException e) {
logger.error("RemoteException", e);
throw new WebServiceClientException("連接刻錄機服務失敗:"+ config.getTargetEndpoint());
} catch (Exception e){
logger.error("UnknownException", e);
throw new WebServiceClientException("連接刻錄機服務失敗:"+ config.getTargetEndpoint());
}
// 3. 解析返回的字符串數據
// String ret = res.getStrResp();
String ret ="{ \"result\": ok, \"errorCode\": 0, \"param\": {\"customParam\": don't reapeat yourself}}";
logger.info("<<WSClient接收>> <----- "+ ret);
Response response = request.getResponse();
try{
JSONObject jsonObject = new JSONObject(ret);
response.parseJson(jsonObject);
} catch (JSONException e) {
logger.error("JSONException", e);
throw new WebServiceClientException(config.getTargetEndpoint()+ "解析返回結果錯誤:"+ config.getTargetEndpoint());
}
// 4. 處理遠端返回錯誤碼
int errorCode = response.getErrorCode();
if (errorCode > 0){
throw new WebServiceClientException(config.getTargetEndpoint()+ "遠端返回錯誤碼 errorCode: "+ errorCode);
}
return response;
}
/**
* 獲取遠程調用接口
* @return
* @throws AxisFault
*/
private GwslibStub getGwslibStub() throws AxisFault{
GwslibStub stub = new GwslibStub(config.getTargetEndpoint());
return stub;
}
public String getTargetIp(){
return this.config.getTargetIp();
}
public int getTargetPort(){
return this.config.getTargetPort();
}
}
在 sendRequest()
中將主要操作劃分爲了四個部分:
- 將 request 對象轉換爲 String 類型
- 調用實際的 WebService 接口
- 解析返回的數據
- 處理遠端返回的錯誤碼
除了第二步的調用 WebService 需要使用到具體的類,其他的地方全部都是針對接口進行編程,也就是說整個 WSClient 並不依賴於任何類的具體實現(生成的WebService類除外),而其中的request.convertToString()
、response.parseJson(jsonObject)
等接口函數則需要使用者自己針對不同的業務進行編寫。
在 Demo 中寫了一個 LongPollingRequest 和與之對應的 LongPollingResponse,其作用是向 WebService 服務端發送一次拉取信息的請求。根據前文的設計,在外部先使用了 WSRequest 、 WSResponse 實現 Request 和 Response 接口,他們的作用是處理一些通用的字段,例如接下來會看到的 errorCode
和 result
。LongPollingRequest 、 LongPollingResponse 則繼承自 WSRequest 和 WSResponse,他們在各自的函數中處理參數的轉換。
WSRequest 和WSResponse:
//...
public abstract class WSRequest implements Request{
}
//...
public abstract class WSResponse implements Response{
private String result;
private int errorCode;
public void parseJson(JSONObject jsonObject) throws JSONException {
try{
errorCode = jsonObject.getInt("errorCode");
result = jsonObject.getString("result");
}catch (NullPointerException e){
throw new JSONException("未找到指定字段 errorCode或 result", e);
}
}
public int getErrorCode() {
return errorCode;
}
public String getResult() {
if (result == null) result = "還爲收到返回結果";
return result;
}
}
LongPollingRequest:
//...
public class LongPollingRequest extends WSRequest {
private static final long DEFAULT_TIMEOUT = 15;
private String customParam;
public Response getResponse() {
return new LongPollingResponse();
}
public String convertToString() {
JSONObject json = new JSONObject();
json.putOpt("request", "longpolling");
JSONObject param = new JSONObject();
param.putOpt("timeout", DEFAULT_TIMEOUT);
if (customParam !=null) {
param.putOpt("customParam", customParam);
}
json.putOpt("param", param);
return json.toString();
}
/************getter and setter************/
public String getCustomParam() {
return customParam;
}
public void setCustomParam(String customParam) {
this.customParam = customParam;
}
}
LongPollingResponse:
public class LongPollingResponse extends WSResponse {
private String customParam;
public void parseJson(JSONObject jsonObject) throws JSONException {
super.parseJson(jsonObject);
try{
JSONObject param = jsonObject.getJSONObject("param");
customParam = param.getString("customParam");
}catch (NullPointerException e){
throw new JSONException("未找到指定字段 customParam", e);
}
}
public String getCustomParam() {
return customParam;
}
}
Demo客戶端調用:
public static void main( String[] args ) {
LongPollingRequest requestOne = new LongPollingRequest();
LongPollingResponse response = null;
//only use WSClient to send a request
requestOne.setCustomParam("test");
WSClient singleClient = new WSClient("172.16.136.98", 9999);
try {
response = (LongPollingResponse) singleClient.sendRequest(requestOne);
} catch (WebServiceClientException e) {
logger.error("WebService請求發送失敗", e);
}
System.out.println(response.getCustomParam());
}
控制檯輸出:
注:爲了方便調試,在sendRequest()
中將實際發送的代碼註釋了,直接人爲拼接了字符串作爲返回結果。在整理代碼的時候對parseJson(JSONObject jsonObject)
函數進行了調整,但是 UML 圖還沒來得及修改。
完整代碼以及後續更新可以參考:https://github.com/KevinZY/WSServer
如果發現文章中有錯誤和疏漏之處,或者表述不明確,亦或是您有更好的設計,歡迎在評論中進行回覆_