原创文章,转载请注明出处
之前有提到过在年后做的项目中有一个集中刻录机管理的系统。简单来说,这是一个典型的 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
如果发现文章中有错误和疏漏之处,或者表述不明确,亦或是您有更好的设计,欢迎在评论中进行回复_