文章目錄
- 1、需求背景
- 2、接口+抽象類+具體實現類
- 3、疑問
- 4、存在的問題
- 5、通過反射加載SDK并完成調用
- 5、補充:關于業務網關
- 7、補充:關于SDK的開發
關鍵點:
- 接口+抽象類(半抽象半實現)+具體實現類
- 業務網關
- 反射加載SDK,完成統一調用
半路接手一個需求,需要從自己系統出發,經過業務網關的統一校驗和轉發,來請求第三方供應商系統的接口,整理下看同事代碼學到的一點思路。
1、需求背景
第三方供應商需要上架自己的產品到公司的交易平臺,但用戶使用產品時,最后一步請求的自然是供應商自己的服務器資源和API。關于這個需求的實現思路,大致是在交易平臺需要做接口有效性校驗、服務實例有效性校驗等,以及消費數據記錄落庫,最后轉發到供應商接口去請求資源(既然是請求別人的系統,那就涉及到怎么通過人家的鑒權系統)。
@PostMapping("/data/{platform}/{apiId}")
public Object redirect(@PathVariable String platform, @PathVariable String apiId,@RequestBody Map<String, Object> parameterMap, HttpServletRequest request) {//直接把API的ID放進請求參數里,后面用完了,再調三方API時去掉就行parameterMap.put("apiId", apiId);return redirectHandler.redirect(platform, ServletUtils.getHeaders(request), parameterMap);
}
這里給訪問所有三方系統接口一個統一的入口,做為業務網關(后面展開說),接口傳參中:
- paltform:確定是哪個第三方系統
- apiID:用來標識想請求第三方系統哪個的API接口,通過這個ID,可以在庫里查到API的路徑、三方系統的host、密鑰、以及后面會提到的SDK的存儲路徑、SDK里的核心方法名等信息
- parameterMap:用戶傳入的請求參數
- request:Http請求對象
其中用工具類獲取下HTTP請求的全部請求頭信息存入Map。
public class ServletUtils{/*** 獲取Http請求的請求頭信息*/public static Map<String, String> getHeaders(HttpServletRequest request) {Map<String, String> map = new LinkedHashMap<>();Enumeration<String> enumeration = request.getHeaderNames();if (enumeration != null) {while (enumeration.hasMoreElements()) {String key = enumeration.nextElement();String value = request.getHeader(key);map.put(key, value);}}return map;}}
2、接口+抽象類+具體實現類
既然需要對接很多第三方供應商系統,去調用第三方系統的API,那就考慮定義一個接口,里面抽象出一個做鑒權、轉發的方法,對接不同的供應商系統時,去實現這個接口,然后走不同的實現。
public interface ApiRedirectHandler {/*** @param headerMap 請求頭參數Map* @param paramMap 對第三方接口的請求參數* @return 返回第三方接用調用的結果*/Object redirect(Map<String, String> headerMap, Map<String, Object> paramMap);}
前面提到,在交易平臺要做一些校驗和消費記錄落庫的操作,這些是對接所有三方系統的公共步驟,而后面請求第三方系統接口肯定要做的鑒權認證以及轉發或者調用,則屬于各個三方系統的定制化行為(因為一個系統有一個系統的認證方式,A系統用APP密鑰、B系統可能用sign驗簽)。因此,考慮在接口下面墊一個抽象類,抽象類中,實現接口中的轉發方法,里面做校驗、記錄落庫等操作,同時調用本抽象類自己的抽象request方法(這個方法里做第三方系統的定制化的認證和轉發或調用)。這樣,對接不同的三方系統,只需就繼承這個抽象類,實現里面的request方法,做自己的認證和轉發即可。
總結
:全抽象的接口,過渡到半抽象的抽象類,抽象類中實現接口的抽象方法時,方法體中寫一部分公共邏輯 + 調用本抽象類自己的一個抽象方法B,這個抽象方法B就給以后的普通類去繼承和重寫。
@Slf4j
public abstract class AbstractRedirectHandler implements ApiRedirectHandler {//抽象類中實現接口的方法@Overridepublic Object redirect(Map<String, String> headerMap, Map<String, Object> paramMap) {//todo: 1.請求有效性驗證//從請求參數paramMap中拿到你要調用APIId,然后查到的三方系統接口的路徑、host等信息ApiInfo apiDetailVo = queryApiInfo(paramMap);//API的ID用完了,它不是三方系統接口需要的請求參數,移除paramMap.remove("apiId"); //todo: 2.服務實例有效性驗證//request中去寫不同三方系統的鑒權、轉發或調用邏輯val responseData = request(headerMap, paramMap, apiDetailVo);//todo: 3.記錄消費記錄//返回第三方接口的響應結果return responseData;}/*** API轉發請求,對接時,針對不同的三方系統去定制化實現** @param headerMap 頭信息* @param paramMap 請求參數* @Param apiDetailVo 接口信息,如接口路徑、服務器host* @return 返回第三方接用調用的結果*/protected abstract Object request(Map<String, String> headerMap, Map<String, Object> paramMap, ApiDetailVo apiDetailVo);}
比如現在對接001號系統:按它們系統支持的方式做認證,比如header中添加APP密鑰,然后組裝請求URL成一個HttpRequest對象,發送Http請求即可完成對三方系統API的調用。
public class System001RedirectHandler extends AbstractRedirectHandler {@Overridepublic Object request(Map<String, String> headerParam, Map<String, Object> paramMap, ApiDetailVo apiDetailVo) {//拿到三方系統的服務器HOST以及接口路徑String url = apiDetailVo.getHost() + apiDetailVo.getPath();//拿到三方系統接口的請求方式,POST還是GET...val method = Method.valueOf(apiDetailVo.getRequestMethod());//使用Hutool工具類的HTTP請求對象,方便后面調用現成的方法來發送HTTP請求HttpRequest request = null;//如果是GETif (method.equals(Method.GET)) {String headerBody = "";StringBuffer body = new StringBuffer();StringBuffer param = new StringBuffer();for (String key : paramMap.keySet()) {body.append(key).append("=").append(paramMap.get(key)).append("&");param.append(key).append("=").append(URLEncoder.encode((String) paramMap.get(key), StandardCharsets.UTF_8)).append("&");}//拼接出一個GET請求的完整路徑if (param.length() > 0) {headerBody = body.substring(0, body.length() - 1);url = url + "?" + param.substring(0, param.length() - 1);}//創建request請求對象request = HttpUtil.createGet(url);//請求頭中加入001系統的認證秘鑰,以便通過認證request.addHeaders(getHeader(headerBody, apiDetailVo.getAppSecret()));} else {//POST請求request = HttpUtil.createPost(url).contentType("application/json");String body = JSON.toJSONString(paramMap);//組裝請求頭和請求體request.addHeaders(getHeader(body, apiDetailVo.getAppSecret())).body(body);}//庫里存的API有要求超時時間if (apiDetailVo.getTimeout() > 0) {request.timeout(apiDetailVo.getTimeout());}//發送HTTP請求,拿到響應val httpResponse = request.execute();return JSON.parseObject(httpResponse.body());}}
3、疑問
給所有三方系統接口的調用一個統一的請求入口,怎么實現根據傳入的第三方系統類型platformType,來選擇不同的實現類對象:考慮把轉發接口ApiRedirectHandler的所有實現類放進一個List,遍歷去匹配傳入的platformType,匹配,則找到了三方系統對應的處理器實現類。找不到,就給個默認的處理器實現類。
@RequiredArgsConstructor
public class CompositeRedirectHandler {private ArrayList<ApiRedirectHandler> handlers = new ArrayList<>();public CompositeRedirectHandler(ArrayList<ApiRedirectHandler> redirectHandlerList) {handlers = redirectHandlerList;}public Object redirect(String platform, Map<String, String> headerMap, Map<String, Object> paramMap) {//給一個默認的通用執行器實現類對象ApiRedirectHandler execHandler = handlers.get(0);//根據平臺信息匹配到ApiRedirectHandler接口的三方系統的實現類for (ApiRedirectHandler handler : handlers) {if (handler.isMatched(platform)) {execHandler = handler;break;}}//用實現類去調用轉發方法 ==> 抽象類(包含抽象方法request) ==> 各個三方系統對抽象類的實現 ==> 完成三方系統API的請求return execHandler.redirect(headerMap, paramMap);}}
上面的接口中注入這個CompositeRedirectHandler對象,調用它的redirect方法,即可全部串起來。
private final CompositeRedirectHandler redirectHandler;@PostMapping("/data/{platform}/{apiId}")
public Object redirect(@PathVariable String platform, @PathVariable String apiId,@RequestBody Map<String, Object> parameterMap, HttpServletRequest request) {parameterMap.put("apiId", apiId);return redirectHandler.redirect(platform, ServletUtils.getHeaders(request), parameterMap);
}
4、存在的問題
如此,以后每對接一個三方系統,就得開發一個新的實現類,去按照他們系統支持的認證方式來做認證以及轉發或調用。相當繁瑣,現在考慮把這個認證的事交給三方系統自己去完成,比如讓他們開發一個SDK,SDK里他們按照自己系統支持的認證方式,做能通過鑒權的操作(到底是header里放密鑰還是做驗簽,我就不再關心了),以及組裝HTTP請求,而我們只需要load這個SDK里的內容,傳入請求參數和路徑,做一個調用即可。
@Slf4j
public class CommonRedirectHandler extends AbstractRedirectHandler {@Overridepublic Object request(Map<String, String> headerMap, Map<String, Object> paramMap, ApiDetailVo apiDetailVo) {//根據apiDetailInfo加載對應的SDK,完成調用//....}
}
如此,我就只需要一個通用的實現類CommonRedirectHandler就可以實現對所有三方系統的對接,這個通用類中也實現了上面的抽象類的request方法,request方法中只需load SDK里三方系統開發者寫的方法,傳入請求路徑和請求參數即可完成三方系統接口的調用。
5、通過反射加載SDK并完成調用
現在問題成了如何加載SDK,完成調用。 ? 通過反射拿到核心類的對象,以及負責認證和轉發請求的核心方法,最后完成調用即可。這里的反射直接用hutool這個強大的第三方依賴庫。
<!--引入hutool-->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.3.9</version>
</dependency>
加載SDK的示意代碼:
@Slf4j
public class CommonRedirectHandler extends AbstractRedirectHandler {/*** 動態加載sdk,調用里面已經完成鑒權和轉發的方法,以實現轉發請求** @param headerMap 頭信息* @param paramMap 請求參數* @return 三方系統接口的返回數據*/@Overridepublic Object request(Map<String, String> headerMap, Map<String, Object> paramMap, ApiDetailVo apiDetailVo) {//SDK的路徑、類名、核心方法名val jarFilePath = apiDetailVo.getSdkJarFilePath(); val classFullName = apiDetailVo.getClassFullName();val invokeMethodName = apiDetailVo.getInvokeMethodName();val httpMethod = apiDetailVo.getRequestMethod().toUpperCase();//拼接完整的三方系統接口的URLString apiUrl = apiDetailVo.getHost() + apiDetailVo.getPath();log.info("file = {}", new File(jarFilePath));//hutool工具類加載SDK成class對象Class<?> clazz = ClassLoaderUtil.loadClass(new File(jarFilePath), classFullName);//反射拿到構造方法對象final val constructors = ReflectUtil.getConstructor(clazz);Object instance = null;try {//SDK核心類的對象instance = constructors.newInstance();final val requestMethod = ReflectUtil.getMethodByName(clazz, invokeMethodName);//調用return requestMethod.invoke(instance, apiUrl, httpMethod, headerMap, paramMap);} catch (InstantiationException e) {log.error(e.getMessage());throw new ServiceException(ExceptionCodeEnum.API_GATEWAY_REQUEST_API_ERROR);} catch (IllegalAccessException e) {log.error(e.getMessage());throw new ServiceException(ExceptionCodeEnum.API_GATEWAY_REQUEST_API_ERROR);} catch (InvocationTargetException e) {log.error(e.getCause().getMessage());throw new ServiceException(ExceptionCodeEnum.API_GATEWAY_REQUEST_API_ERROR);}}
}
5、補充:關于業務網關
本需求里,給請求第三方系統接口資源提供了一個統一的API入口,比如:
@POSTMapping(/data/{platformType}/{API_ID})
public Object redirect(@PathVariable String platformType, @PathVariable String API_ID, @RequestBody Map<String, Object> requestParam, HttpServletRequset requset){//.....
}
有了這個統一入口,請求三方系統資源就都從這個接口過,前面說的各種合法性、有效性校驗、記錄落庫、轉發等就可以在這里完成了,由此可見,其雖然不比常規的Gateway服務,比如SpringCloudGateway,但干的活兒是類似的,即校驗和轉發(路由),因此,稱業務網關。
思路:給所有三方系統的api調用提供一個統一的入口(Api)
7、補充:關于SDK的開發
SDK,Software Development Kit,即軟件開發工具包。簡單說就是造輪子,實現一個小功能,別人引入,就能使用。往大了說,如Java開發工具包JDK,使用import引入相關的包:
import java.util.*;
往小了說,如文件上傳的SDK,其他系統引入后就可用。關于SDK的開發,需要注意:
- 易用性:提供統一調用,用戶不用關心內部實現的細節,只需知道調誰、傳什么、能返回什么即可
//常見方式1.直接調用
FileManage.upload(String filePath);//常見方式2.需要new對象
FileManage fileManage = new FileManage();
fileManage.upload(String filePath);
- 輕量依賴:盡量減少SDK本身對其他類庫的依賴,以減少用戶項目中的已有依賴和SDK依賴的沖突
- 結構清晰:如maven項目下,service包下編寫業務邏輯、constant包下存放常量、utils包下放工具類
- 見名知意:不用看說明文檔也知道這個方法是干啥用的
- 可擴展:提供接口或者抽象類對外,支持用戶自己繼承和按需寫實現類,如密碼相關SDK,做加密解密,起名PasswordHandler,其加密方法encode需要傳入一個密碼,一個加密器,這個加密器就可以提供成接口,用戶可通過實現這個接口來自定義加密方式。
//加密器對象:按照非對稱算法加密
Encoder encoder = new SignEncoder();
String password = PasswordHandler.encode("daihao9527", encoder);
//用戶自己實現加密器接口
public MyEncoder implements Encoder {@Overridevoid doEncode(){//...用戶自己寫,如采用時間戳、或自定義的MD5工具類Calendar calendar = Calendar.getInstance();Long timestamp = calendar.getTime.getTime();String sign = MD5Utils.getMD5Str(timestamp + secretKey);//..}
}
//此時,用戶可以自己指定加密器
String password = PasswordHandler.encode("daihao9527", new MyEncoder());
SDK中的內容一般包括:
- 功能模塊:實現功能
- API:SDK的門面,調用和使用功能的入口
- 文檔:附相關使用說明和指引
- Demo:使用示例,運行Demo,直觀體驗SDK功能