代理模式的理解:
- 在實(shí)際的生活中代理情況是無處不在的,找代理買東西,請朋友幫忙拿東西,在這個過程中引入了新的角色即"第三者"的角色來幫助你完成某一件事,這里的第三者我們稱之為代理者
- 通過引用一個新的對象來實(shí)現(xiàn)對真實(shí)對象的一個替身,這種實(shí)現(xiàn)的機(jī)制就是代理模式,通過引用代理對象來訪問真實(shí)對象就是代理模式的設(shè)計動機(jī)
代理模式的定義
- 代理模式就是給一個對象提供一個代理,并由==對象控制對原對象的引用==
代理模式應(yīng)用的場景
- 當(dāng)我們無法或者不想直接訪問某一個對象,那么可以通過一個代理對象間接訪問,同時為了保證客戶端透明性,委托對象與代理對象需要同時實(shí)現(xiàn)相同接口
代理模式的結(jié)構(gòu)(角色的劃分)
- Subject:抽象角色,目標(biāo)接口,聲明真實(shí)對象和代理對象的共同接口
- RealSubject:真實(shí)對象(即目標(biāo)對象),是我們最終要引用的對象
- Proxy:代理類.代理對象與真實(shí)對象實(shí)現(xiàn)相同的接口,所以它能夠在任何時刻都能夠代理真實(shí)對象.代理角色內(nèi)部包含有對真實(shí)對象的引用,所以她可以操作真實(shí)的對象,同時也可以附加其他的操作,相當(dāng)于對真實(shí)對象進(jìn)行封裝
- Client:客戶端,直接調(diào)用代理
模式的實(shí)現(xiàn)
- 服務(wù)器支付的集成(微信支付 銀聯(lián)支付)
- 首先來整體介紹使用代理模式實(shí)現(xiàn)服務(wù)器端的設(shè)計步驟:
1.支付對象設(shè)計Charge(專門用于返回給客戶端)
//付款對象
public class Charge {
// 定義支付錯誤碼
public enum ChargeError {
// 支付憑證或者成功
credentialGetSuccess("0", "支付憑證獲取成功"),
// 支付憑證或者失敗
credentialGetError("1", "支付憑證獲取錯誤"),
// 支付憑證請求錯誤
credentialRequestError("2", "支付憑證請求錯誤"),
// 支付簽名成功
sineSuccess("10", "支付憑證請求錯誤"),
// 支付簽名錯誤
sineError("11", "支付憑證請求錯誤");
private String code;
private String msg;
private ChargeError(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
// 解決共性問題:支付統(tǒng)一參數(shù)(20個參數(shù)不等)
// 付款I(lǐng)D
private String id;
// 支付創(chuàng)建時間
private Long created;
// 支付渠道(微信支付渠道穷遂、銀聯(lián)支付渠道函匕、支付寶支付渠道、蘋果支付渠道)
private String channel;
// 訂單號(每一個支付渠道:訂單號長度不相同)
private String orderNo;
// 客戶端IP
private String clientIP;
// 訂單的金額(單位:分)
private int amount;
// 商品標(biāo)題
private String subject;
// 商品描述
private String body;
// 訂單錯誤碼
private String failCode;
// 訂單錯誤信息
private String failMsg;
// 解決差異問題:客戶端支付憑證
// 分析
// 微信支付(返回支付憑證:appid蚪黑、partnerid盅惜、prepayid、package忌穿、noncestr抒寂、timestamp、sign)
// 銀聯(lián)支付(返回支付憑證:tn掠剑、mode)
// 支付寶支付(返回支付憑證:sign)
// Map類似于iOS中字典Dictionary
private Map<String, Object> credential;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Long getCreated() {
return created;
}
public void setCreated(Long created) {
this.created = created;
}
public String getOrderNo() {
return orderNo;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public String getClientIP() {
return clientIP;
}
public void setClientIP(String clientIP) {
this.clientIP = clientIP;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getFailCode() {
return failCode;
}
public void setFailCode(String failCode) {
this.failCode = failCode;
}
public String getFailMsg() {
return failMsg;
}
public void setFailMsg(String failMsg) {
this.failMsg = failMsg;
}
public Map<String, Object> getCredential() {
return credential;
}
public void setCredential(Map<String, Object> credential) {
this.credential = credential;
}
public String getChannel() {
return channel;
}
public void setChannel(String channel) {
this.channel = channel;
}
public void setFail(ChargeError error) {
this.setFailCode(error.getCode());
this.setFailMsg(error.getMsg());
}
}
- 從上面可以看出,微信支付和銀聯(lián)支付有一些共性參數(shù)和一些差異問題,對于差異問題可以直接使用Map集合來處理
2.支付框架中(代理模式的設(shè)計)
- 對代理模式角色的分析:
- 1.角色一(Subject):抽象角色(目標(biāo)接口)-->支付的接口-->IPay
//代理模式:目標(biāo)接口(類似于iOS中協(xié)議)
public interface IPay {
/**
* 支付
* @param req 請求
* @param resp 響應(yīng)
* @param params 請求第三方支付服務(wù)器參數(shù)
* @return
*/
Charge pay(HttpServletRequest req, HttpServletResponse resp,Map<String,Object> params);
}
2.角色二(RealSubject):目標(biāo)的對象
- 第一個實(shí)現(xiàn)類:目標(biāo)對象-->銀聯(lián)支付-->UnionPay 實(shí)現(xiàn)接口<IPay>
- 第二個實(shí)現(xiàn)類:目標(biāo)對象-->微信支付-->WxPay 實(shí)現(xiàn)接口<IPay>
銀聯(lián)UnionPay
//銀聯(lián)支付:被代理對象(目標(biāo)對象)
public class UnionPay implements IPay {
@Override
public Charge pay(HttpServletRequest req, HttpServletResponse resp, Map<String, Object> params) {
String amount = params.get("amount").toString();
String channel = params.get("channel").toString();
// 商戶號(注冊銀聯(lián)賬號屈芜,并且申請開通銀聯(lián)支付賬號)
// 測試賬號:777290058110097
String merId = "777290058110048";
Date date = new Date();
String format = new SimpleDateFormat("yyyyMMddHHmmss").format(date);
// 交易金額
String txnAmt = amount;
// 商戶訂單號
String orderId = format;
// 訂單發(fā)送時間
String txnTime = format;
// 第一步:拼接訂單參數(shù)列表
Map<String, String> contentData = new HashMap<String, String>();
contentData.put("version", DemoBase.version);
contentData.put("encoding", DemoBase.encoding);
// 簽名方法(01: 表示采用RSA簽名)
contentData.put("signMethod", SDKConfig.getConfig().getSignMethod());
// 交易類型 01:消費(fèi)(支付)
// 取值:00:查詢交易,01:消費(fèi)朴译,02:預(yù)授權(quán)井佑,03:預(yù)授權(quán)完成,
// 04:退貨眠寿,05:圈存,11:代收盒发,12:代付迹辐,13:賬單支付,14:轉(zhuǎn)賬(保留)殷费,
// 21:批量交易仍律,22:批量查詢水泉,31:消費(fèi)撤銷草则,32:預(yù)授權(quán)撤銷炕横,33:預(yù)授權(quán)完成撤銷份殿,
// 71:余額查詢卿嘲,72:實(shí)名認(rèn)證-建立綁定關(guān)系,73:賬單查詢放前,74:解除綁定關(guān)系糯彬,
// 75:查詢綁定關(guān)系似扔,77:發(fā)送短信驗證碼交易炒辉,78:開通查詢交易黔寇,79:開通交易缝裤,
// 94:IC卡腳本通知 95:查詢更新加密公鑰證書
contentData.put("txnType", "01");
// 交易子類 01:消費(fèi)
contentData.put("txnSubType", "01");
// 產(chǎn)品類型
// 依據(jù)實(shí)際業(yè)務(wù)場景填寫(目前僅使用后 4 位嬉挡,簽名 2 位 默認(rèn)為 00)
// 默認(rèn)取值:000000 具體取值范圍: 000201:B2C 網(wǎng)關(guān)支付
// 000301:認(rèn)證支付 2.0 000302:評級支付 000401:代付 000501:代收
// 000601:賬單支付 000801:跨行收單 000901:綁定支付 001001:訂購 000202:B2B
// 填寫000201
contentData.put("bizType", "000201");
// 05:語音 07:互聯(lián)網(wǎng) 08:移動 16:數(shù)字機(jī)頂盒
// 渠道類型 08手機(jī)
contentData.put("channelType", "08");
/*** 商戶接入?yún)?shù) ***/
// 商戶號碼,請改成自己申請的商戶號或者open上注冊得來的777商戶號測試
// 測試賬號:777290058110048
contentData.put("merId", merId);
// 接入類型噪馏,商戶接入填0 ,不需修改(0:直連商戶厘擂, 1: 收單機(jī)構(gòu) 2:平臺商戶)
contentData.put("accessType", "0");
// 商戶訂單號驴党,8-40位數(shù)字字母港庄,不能含“-”或“_”鹏氧,可以自行定制規(guī)則
contentData.put("orderId", orderId);
// 訂單發(fā)送時間把还,取系統(tǒng)時間吊履,格式為YYYYMMDDhhmmss艇炎,必須取當(dāng)前時間缀踪,否則會報txnTime無效
contentData.put("txnTime", txnTime);
// 賬號類型 01:銀行卡 02:存折 03:IC卡帳號類型(卡介質(zhì))
contentData.put("accType", "01");
// 交易金額 單位為分驴娃,不能帶小數(shù)點(diǎn)(服務(wù)器規(guī)定金額單位,客戶端統(tǒng)一傳遞)
// 一般情況:采用分為基本單位(一般支付服務(wù)器開發(fā)標(biāo)準(zhǔn))
contentData.put("txnAmt", txnAmt);
// 境內(nèi)商戶固定 156 人民幣
contentData.put("currencyCode", "156");
// 請求方保留域厚棵,
// 透傳字段婆硬,查詢彬犯、通知谐区、對賬文件中均會原樣出現(xiàn)宋列,如有需要請啟用并修改自己希望透傳的數(shù)據(jù)炼杖。
// 出現(xiàn)部分特殊字符時可能影響解析坤邪,請按下面建議的方式填寫:
// 1. 如果能確定內(nèi)容不會出現(xiàn)&={}[]"'等符號時艇纺,可以直接填寫數(shù)據(jù)黔衡,建議的方法如下盟劫。
// contentData.put("reqReserved", "透傳信息1|透傳信息2|透傳信息3");
// 2. 內(nèi)容可能出現(xiàn)&={}[]"'符號時:
// 1) 如果需要對賬文件里能顯示捞高,可將字符替換成全角&={}【】“‘字符(自己寫代碼,此處不演示)型檀;
// 2) 如果對賬文件沒有顯示要求胀溺,可做一下base64(如下)仓坞。
// 注意控制數(shù)據(jù)長度无埃,實(shí)際傳輸?shù)臄?shù)據(jù)長度不能超過1024位侦镇。
// 查詢壳繁、通知等接口解析時使用new String(Base64.decodeBase64(reqReserved),
// DemoBase.encoding);解base64后再對數(shù)據(jù)做后續(xù)解析闹炉。
// contentData.put("reqReserved",
// Base64.encodeBase64String("任意格式的信息都可以".toString().getBytes(DemoBase.encoding)));
// 后臺通知地址(需設(shè)置為外網(wǎng)能訪問 http
// https均可),支付成功后銀聯(lián)會自動將異步通知報文post到商戶上送的該地址昵观,【支付失敗的交易銀聯(lián)不會發(fā)送后臺通知】
// 后臺通知參數(shù)詳見open.unionpay.com幫助中心 下載 產(chǎn)品接口規(guī)范 網(wǎng)關(guān)支付產(chǎn)品接口規(guī)范 消費(fèi)交易 商戶通知
// 注意:1.需設(shè)置為外網(wǎng)能訪問,否則收不到通知 2.http https均可 3.收單后臺通知后需要10秒內(nèi)返回http200或302狀態(tài)碼
// 4.如果銀聯(lián)通知服務(wù)器發(fā)送通知后10秒內(nèi)未收到返回狀態(tài)碼或者應(yīng)答碼非http200或302壁查,那么銀聯(lián)會間隔一段時間再次發(fā)送∮镉總共發(fā)送5次,銀聯(lián)后續(xù)間隔1碉纺、2耿导、4、5
// 分鐘后會再次通知狮荔。
// 5.后臺通知地址如果上送了帶有晚树?的參數(shù)慨亲,例如:http://abc/web?a=b&c=d
// 在后臺通知處理程序驗證簽名之前需要編寫邏輯將這些字段去掉再驗簽刑棵,否則將會驗簽失敗
contentData.put("backUrl", DemoBase.backUrl);
/** 對請求參數(shù)進(jìn)行簽名并發(fā)送http post請求,接收同步應(yīng)答報文 **/
// 第二步:簽名處理
// 報文中certId,signature的值是在signData方法中獲取并自動賦值的碍舍,只要證書配置正確即可。
// 參數(shù)一:需要簽名的參數(shù)集合
// 參數(shù)二:簽名的字符編碼
Map<String, String> reqData = AcpService.sign(contentData, DemoBase.encoding);
// 交易請求url從配置文件讀取對應(yīng)屬性文件acp_sdk.properties中的
// acpsdk.backTransUrl
String requestAppUrl = SDKConfig.getConfig().getAppRequestUrl();
// 第三步:發(fā)起銀聯(lián)服務(wù)器請求(獲取tn:支付憑證)
// 發(fā)送請求報文并接受同步應(yīng)答(默認(rèn)連接超時時間30秒邑雅,讀取返回結(jié)果超時時間30秒);
// 這里調(diào)用signData之后片橡,調(diào)用submitUrl之前不能對submitFromData中的鍵值對做任何修改,
// 如果修改會導(dǎo)致驗簽不通過
Map<String, String> rspData = AcpService.post(reqData, requestAppUrl, DemoBase.encoding);
// 創(chuàng)建支付憑證對象
Charge charge = new Charge();
charge.setAmount(Integer.valueOf(amount));
charge.setBody("愛學(xué)寶");
charge.setChannel(channel);
charge.setClientIP(req.getRemoteAddr());
charge.setCreated(date.getTime());
charge.setOrderNo(orderId);
if (!rspData.isEmpty()) {
// 數(shù)據(jù)庫處理(保存訂單信息)
// 判斷簽名驗證是否通過
if (AcpService.validate(rspData, DemoBase.encoding)) {
LogUtil.writeLog("驗證簽名成功");
String respCode = rspData.get("respCode");
if (("00").equals(respCode)) {
// 狀態(tài)碼 = 00 說明獲取到了支付憑證
// 成功,獲取tn號
String tn = rspData.get("tn");
Map<String, Object> credential = new HashMap<String, Object>();
credential.put("tn", tn);
credential.put("mode", "01");
// TODO
// 將銀聯(lián)服務(wù)器返回數(shù)據(jù)--->Json數(shù)據(jù)格式+自己服務(wù)器屬性
// 目前這個數(shù)據(jù)格式不是標(biāo)準(zhǔn)的json淮野,所以說你直接解析捧书,會報錯
// 設(shè)置支付憑證
charge.setCredential(credential);
charge.setFail(ChargeError.sineSuccess);
} else {
// 其他應(yīng)答碼為失敗請排查原因或做失敗處理
charge.setFail(ChargeError.sineError);
}
} else {
LogUtil.writeErrorLog("驗證簽名失敗");
charge.setFail(ChargeError.sineError);
}
} else {
charge.setFail(ChargeError.credentialRequestError);
// 未返回正確的http狀態(tài)
LogUtil.writeErrorLog("未獲取到返回報文或返回http狀態(tài)碼非200");
}
return charge;
}
}
微信WxPay
//微信支付:目標(biāo)對象(被代理對象)
public class WxPay implements IPay {
// 首先:定義支付應(yīng)用ID、商戶ID等等...
// 應(yīng)用ID
public static String APP_ID = ConstantUtil.APP_ID;
// 商戶ID
public static String PARTNER_ID = ConstantUtil.PARTNER_ID;
// 商戶好對應(yīng)的密鑰
public static String PARTNER_KEY = ConstantUtil.PARTNER_KEY;
// 統(tǒng)一下單的接口(微信支付服務(wù)器提供的)
public static String URL_UNIFIEDORDER = ConstantUtil.URL_UNIFIEDORDER;
@Override
public Charge pay(HttpServletRequest req, HttpServletResponse resp, Map<String, Object> params) {
String amount = params.get("amount").toString();
String channel = params.get("channel").toString();
// 第1步-簽名訂單信息
// 第一點(diǎn):獲取客戶端傳遞過來的參數(shù)
// req:客戶端請求
// resp:響應(yīng)客戶端請求
// 第二點(diǎn):設(shè)置訪問微信支付服務(wù)器請求數(shù)據(jù)類型
resp.reset();
resp.setHeader("ContentType", "text/xml");
// 第三點(diǎn):創(chuàng)建請求微信支付服務(wù)器參數(shù)集合
// 做了請求封裝
// 微信寫好一個封裝案例,你可以根據(jù)服務(wù)器需求骤星,自己定義網(wǎng)絡(luò)請求框架
PrepayIdRequestHandler handler = new PrepayIdRequestHandler(req, resp);
// 統(tǒng)一下單的接口(調(diào)用微信支付服務(wù)器需要的接口)--->公開的
handler.setGateUrl(ConstantUtil.URL_UNIFIEDORDER);
// 設(shè)置密鑰
handler.setKey(PARTNER_KEY);
// 設(shè)置應(yīng)用的ID
handler.setParameter("appid", APP_ID);
// 商戶號
handler.setParameter("mch_id", PARTNER_ID);
// 隨機(jī)字符串
handler.setParameter("nonce_str", WXUtil.getNonceStr());
// 商品描述(例如:天天愛消除-游戲充值)
handler.setParameter("body", "愛學(xué)寶-筆記本");
// 商戶訂單號(自己服務(wù)器生成訂單號)
String out_trade_no = OrderUtils.getOrderNumber();
handler.setParameter("out_trade_no", out_trade_no);
// 總金額
handler.setParameter("total_fee", amount);
// e8ab8a8d4bc8be24ccac804344ef0ef3
// 終端IP(客戶端IP)
handler.setParameter("spbill_create_ip", req.getRemoteAddr());
// 通知地址(微信服務(wù)器回調(diào)商戶服務(wù)器頁面)
handler.setParameter("notify_url", ConstantUtil.NOTIFY_URL);
// 交易類型
handler.setParameter("trade_type", "APP");
// 第四點(diǎn):對我們訂單信息進(jìn)行簽名
String sign = handler.createMD5Sign();
// 設(shè)置簽名
// 問題一(排序:可能不會在最后,隱藏潛在危機(jī))
// handler.setParameter("sign", sign);
// 不參與排序,并且將簽名參數(shù)放置最后
handler.setSign(sign);
// 第2步-調(diào)用微信統(tǒng)一下單接口(目的:獲取prepay_id)
// command+1
Charge charge = new Charge();
charge.setAmount(Integer.valueOf(amount));
charge.setBody("愛學(xué)寶");
charge.setChannel(channel);
charge.setClientIP(req.getRemoteAddr());
charge.setOrderNo(out_trade_no);
try {
Map paramsMap = handler.sendPrepay();
String prepay_id = (String) paramsMap.get("prepay_id");
if (prepay_id != null && !"".equals(prepay_id)) {
// 統(tǒng)一下單接口調(diào)用成功
// 第3步-處理接口返回信息進(jìn)行二次簽名
// 調(diào)用支付接口:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_12&index=2
// 二次簽名和一次簽名敌卓,參數(shù)不一樣(復(fù)用之前參數(shù)集合)
// 請求集合列表(情空:不影響新的參數(shù)簽名)
// 微信服務(wù)器要求
handler.clear();
String noncestr = (String) paramsMap.get("nonce_str");
String timestamp = WXUtil.getTimeStamp();
charge.setCreated(Long.valueOf(timestamp));
// 密鑰
handler.setKey(PARTNER_KEY);
// 設(shè)置應(yīng)用的ID
handler.setParameter("appid", APP_ID);
// 預(yù)付單ID
handler.setParameter("prepayid", prepay_id);
// 擴(kuò)展字段
handler.setParameter("package", "Sign=WXPay");
// 商戶號
handler.setParameter("partnerid", PARTNER_ID);
// 隨機(jī)字符串
handler.setParameter("noncestr", noncestr);
// 時間戳
handler.setParameter("timestamp", timestamp);
// 進(jìn)行二次簽名(簽名參數(shù)不一樣)
// 第一次簽名:對訂單信息簽名蕾盯,獲取prepay_id
// 第二次簽名:對支付信息進(jìn)行簽名
sign = handler.createMD5Sign();
// 第4步-將簽名信息返回客戶端(xml挫鸽、json都可以)
// 采用Json解析(構(gòu)建json--->返回客戶端)
Map<String, Object> credential = new HashMap<String, Object>();
credential.put("appid", APP_ID);
credential.put("noncestr", noncestr);
credential.put("packageValue", "Sign=WXPay");
credential.put("partnerid", PARTNER_ID);
credential.put("prepayid", prepay_id);
credential.put("tradeType", (String) paramsMap.get("trade_type"));
credential.put("sign", sign);
credential.put("timestamp", timestamp);
charge.setCredential(credential);
charge.setFail(ChargeError.sineSuccess);
} else {
charge.setFail(ChargeError.sineError);
}
} catch (Exception e) {
e.printStackTrace();
charge.setFail(ChargeError.credentialRequestError);
}
return charge;
}
}
2.角色三(Proxy):代理對象--->PayEngine
//代理對象(同時也是一個工廠類)
public class PayEngine implements IPay {
private static PayEngine payEngine;
private Map<String, IPay> channelPay;
private PayEngine() {
registerPayChannel();
}
public static PayEngine getInstance() {
if (payEngine == null) {
payEngine = new PayEngine();
}
return payEngine;
}
private void registerPayChannel() {
// 注冊支付渠道
channelPay = new HashMap<String, IPay>();
channelPay.put("wxpay", new WxPay());
channelPay.put("unionpay", new UnionPay());
}
@Override
public Charge pay(HttpServletRequest req, HttpServletResponse resp, Map<String, Object> params) {
// 根據(jù)支付的憑證調(diào)用具體的支付渠道
// if判斷
String channel = params.get("channel").toString();
return channelPay.get(channel).pay(req, resp, params);
}
}
- 這個代理對象PayEngine這里是通過Map集合來持有目標(biāo)對象的引用,在實(shí)現(xiàn)的接口方法中,根據(jù)傳入的渠道來判斷調(diào)用哪一個支付
4.角色四(Client):客戶端
- 實(shí)現(xiàn)一個接口來調(diào)用
- 客戶端-->接口-->PayServlet
//支付統(tǒng)一接口
public class PayServlet extends HttpServlet {
private static final long serialVersionUID = 6717470124590741929L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 第一步:將客戶端數(shù)據(jù)轉(zhuǎn)成json
try {
Map<String, Object> params = JsonReader.jsontoMap(req);
// 第二步:調(diào)用支付
// 返回支付憑證
Charge charge = PayEngine.getInstance().pay(req, resp, params);
// 第三步:返回給客戶端
resp.getWriter().write(new Gson().toJson(charge));
} catch (JSONException e) {
e.printStackTrace();
}
}
}