转转支付网关之注解式HTTP客户端
1. 背景
轉轉支付中心與多家第三方支付平臺、金融機構存在合作,例如微信、支付寶、分期樂、合利寶、平安銀行等。
在收單、打款、退款等業務上,大部分接口都需要通過HTTP協議與第三方進行交互。
目前業界上或轉轉內部都有封裝好HttpUtil工具類提供使用,但開發人員在接入三方渠道時,不同渠道方提供的文檔有所差異且內部研發人員變動等原因,實現時自然會存在一些問題:
- 缺少統一的設計流程,代碼復雜臃腫、耦合度高
- 開發人員水平參差不齊,不同人的設計風格千差萬別
- 抽象程度不夠,復用性較低
由此,支付中心研發了統一設計風格、注解式的HTTP客戶端,建立一套面向“使用HTTP協議與三方渠道交互“的“設計規約”。
圖1 轉轉APP收銀臺2. 實踐思路
2.1 自定義注解
目標:
2.2 動態代理增強接口方法
目標:
2.3 將代理類Bean注入到Spring容器
目標:
3. 實現
整體流程:
3.1 自定義注解
HTTPController注解
該注解屬于運行時的TYPE注解,作用在一個類或接口上。
用途:標識該接口為某個三方渠道的HTTP網關接口,可以配置渠道基礎信息、代理類等信息。
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented public @interface HTTPController {// 三方渠道描述String desc() default "";// 三方渠道類型ThirdPartEnum thirdPart();// 請求UrlString baseUrl() default "";// 代理類Class<?> invocationHandlerClass(); }HTTPMethod注解
該注解屬于運行時的METHOD注解,作用在一個方法上。
用途:標識該方法為三方渠道的某個特定的文檔接口,可以配置接口路徑、請求方式、重試次數等信息。
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface HTTPMethod {// 請求路徑String url();// Http請求方式HTTPRequestType requestType() default HTTPRequestType.POST;// 重試次數int retryCount() default 0;// GET、POST請求enum HTTPRequestType {GET,POST} }使用示例
@HTTPController(desc = "微信支付", thirdPart = ThirdPartEnum.WeiXinPay, baseUrl = "https://api.mch.weixin.qq.com", invocationHandlerClass = WeiXinPayInvocationHandler.class) public interface WeiXinPayRequestGateway {// 個人用戶注冊接口@HTTPMethod(url = "/ea/pCustomerReg.action", requestType = HTTPRequestType.POST, retryCount = 2)ThirdPartResponse<CustomerRegResponse> pCustomerReg(CustomerRegV2Request request);// 轉賬@HTTPMethod(url = "/ea/transfer", requestType = HTTPRequestType.POST)ThirdPartResponse<TransferResponse> transfer(TransferRequest request);// 轉賬查詢@HTTPMethod(url = "/ea/transferQuery", requestType = HTTPRequestType.GET, retryCount = 2)ThirdPartResponse<TransferQueryResponse> transferQuery(TransferQueryRequest request); }3.2 動態代理增強接口方法
針對“微信支付”渠道,實現HTTP請求的動態代理(使用JDK動態代理)。
以下代碼是核心流程代碼,細節有所縮減,主要是一些邊界判斷、特殊處理等,不影響理解。
@Slf4j public class WeiXinPayInvocationHandler implements InvocationHandler {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) {WeiXinPayBaseResponse response = null;try {// Http處理邏輯response = realLogic(method, args[0]);} catch (Exception ex) {// 請求失敗處理return ThirdPartResponse.of(ThirdPartTransferResultEnum.UNCLEAR_FAILURE);}// 請求結果返回return ThirdPartResponse.of(response);}private WeiXinPayBaseResponse realLogic(Method method, Object args) {// 獲取方法注解HTTPMethod httpMethod = method.getAnnotation(HTTPMethod.class);// 重試參數是網絡連接重試HttpOptions httpOptions = httpOptionsBuild(httpMethod.retryCount());// 根據url和方法參數構建請求體HttpRequest httpRequest = httpRequestBuild(httpMethod.url(),(WeiXinPayBaseRequest)args);// 獲取請求類型HTTPMethod.HTTPRequestType httpRequestType = httpMethod.requestType();// 執行請求HttpResponse httpResponse = executeHttpRequest(httpOptions, httpRequest, httpRequestType);Type genericReturnType = method.getGenericReturnType();// 獲取返回值的泛型參數if (genericReturnType instanceof ParameterizedType) {Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();genericReturnType = actualTypeArguments[0];}// 驗簽+解密resDataString decodedData = DecodeUtil.decode(httpResponse.getResult());return GsonUtil.fromJson(decodedData, genericReturnType);}/*** 構建http 請求參數并且設置簽名* 簽名方式 對 data使用signType簽名類型進行簽名,目前僅支持 SHA256。*/private HttpRequest httpRequestBuild(String url, WeiXinPayBaseRequest args) {// Apollo配置WeiXinPayConfig config = WeiXinPayConfig.getConfig();HttpRequest httpRequest = new HttpRequest();httpRequest.setUrl(config.getBaseUrl + url);// 加密 + 簽名String data = EncodeUtil.encode(JSON.toJSONString(args));String sign = SignUtil.sign(data);WeiXinPayCommonRequest weiXinPayCommonRequest = WeiXinPayCommonRequest.builder().data(data).sign(sign).build();httpRequest.setParam(weiXinPayCommonRequest);return httpRequest;}/*** HttpClientUtil工具的httpGet、httpPost是平時大家常見的封裝方法,不再贅述。**/private HttpResponse executeHttpRequest(HttpOptions httpOptions, HttpRequest httpRequest, HTTPMethod.HTTPRequestType httpRequestType) {HttpResponse httpResponse = null;try {switch (httpRequestType) {case GET:httpResponse = HttpClientUtil.httpGet(httpRequest, httpOptions);break;case POST:httpResponse = HttpClientUtil.httpPost(httpRequest.getUrl(), JSONObject.toJSONString(httpRequest.getParam()), httpOptions);break;default:throw new ThirdPartHttpException(ThirdPartEnum.WeiXinPay, ReturnCodeEnum.HTTP_REQUEST_METHOD_NOT_MATCH);}} catch (Exception e) {throw new RuntimeException("[WeiXinPayInvocationHandler http execute error ]", e);}return httpResponse;} }3.3 將代理類Bean注入到Spring容器
我們是基于FactoryBean和ImportBeanDefinitionRegistrar的方案將代理類Bean動態注入到Spring容器中。
認識FactoryBean
這里通過一個簡單的Demo,來說明使用FactoryBean的效果。
public interface Person {public void sayHello (); }@Setter public class XiaoMing implements FactoryBean<Object>, Person {private String regards;@Overridepublic Object getObject() {return new ZhangSan(regards);}@Overridepublic Class<?> getObjectType() {return ZhangSan.class;}@Overridepublic void sayHello() {System.out.println("Greetings from XiaoMing: " + regards);} }public class ZhangSan implements Person {String regards;public ZhangSan(String regards) {this.regards = regards;}@Overridepublic void sayHello() {System.out.println("Greetings from ZhangSan: " + regards);} }public class BeanDefinitionBuilderExample {public static void main (String[] args) {// 定義BeanAbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(XiaoMing.class).getBeanDefinition();beanDefinition.getPropertyValues().add("regards", "Hello World");// 注冊BeanDefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();beanFactory.registerBeanDefinition("person", beanDefinition);// 獲取BeanPerson bean = (Person) beanFactory.getBean("person");bean.sayHello();} } 圖5 Demo運行結果上述例子:將實現FactoryBean的XiaoMing類,將其注入到Spring容器中。獲取Bean時,調用sayHello方法,輸出的是“Greetings from ZhangSan : Hello World”。
結論:根據“person”從BeanFactory中獲取的Bean,實際上是FactoryBean的getObeject()返回的對象。
FactoryBean存在意義和使用場景
FactoryBean是一個能生產或修飾對象生成的Bean,類似于設計模式中的工廠模式和裝飾器模式。
存在意義
- 通過實現FactoryBean這個接口,用戶可以自定義實例化Bean的邏輯,并且在創建時才去實現具體的功能。
使用場景
- Spring中FactoryBean最典型的應用就是創建AOP代理對象-ProxyFactoryBean。
- MyBatis中使用MapperFactoryBean來創建Mapper,最終得到是由Proxy.newProxyInstance創建的代理實例。
HTTPControllerFactoryBean實現
@Setter @Slf4j public class HTTPControllerFactoryBean implements FactoryBean<Object> {// 目標接口private Class<?> targetClass;private ThirdPartEnum thirdPart;private String baseUrl;private Class<InvocationHandler> invocationHandlerClass;// 返回工廠生產的對象,這是 Spring 容器將使用的對象@Overridepublic Object getObject() {InvocationHandler invocationHandler;try {invocationHandler = invocationHandlerClass.newInstance();} catch (Exception e) {throw new RuntimeException("[HTTPControllerFactoryBean-invocationHandlerClass-newInstance] error", e);}// 通過Proxy將代理類對象轉成目標接口return Proxy.newProxyInstance(HTTPControllerFactoryBean.class.getClassLoader(), new Class[]{targetClass}, invocationHandler);}// 返回此FactoryBean生成的對象類型@Overridepublic Class<?> getObjectType() {return targetClass;}// 表示此FactoryBean生成的對象是否為單例@Overridepublic boolean isSingleton() {return true;} }HTTPMethodScannerRegistrar實現
在ImportBeanDefinitionRegistrar接口中,有一個registerBeanDefinitions()方法,通過該方法可以向Spring容器中注冊Bean實例。
實現該接口的類都會被ConfigurationClassPostProcessor后置處理器,因此在ImportBeanDefinitionRegistrar中注冊的Bean可以比依賴它的Bean更早初始化(有興趣可自行查閱資料)。
public class HTTPMethodScannerRegistrar implements ImportBeanDefinitionRegistrar {/*** 注入對象到Spring* @param annotationMetadata 注解元數據* @param beanDefinitionRegistry 它定義了關于 BeanDefinition 的注冊、移除、查詢等一系列的操作*/@Overridepublic void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {// ClassPathScanningCandidateComponentProvider是Spring提供的工具,可以按自定義的類型,查找classpath下符合要求的class文件。ClassPathScanningCandidateComponentProvider classScanner = new ClassPathScanningCandidateComponentProvider(false) {@Overrideprotected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {// 只掃描接口,且帶有@HTTPController注解if (beanDefinition.getMetadata().isInterface()) {try {return beanDefinition.getMetadata().hasAnnotatedMethods(HTTPController.class.getName());} catch (Exception ex) {throw new RuntimeException("[isCandidateComponent error]", ex);}}return false;}};// 指定掃描的包名,在該包路徑下帶有@HTTPController注解的接口Set<BeanDefinition> beanDefinitionSet = classScanner.findCandidateComponents("com.zhuanzhuan.zzpaycore.gateway");for (BeanDefinition beanDefinition : beanDefinitionSet) {if (beanDefinition instanceof AnnotatedBeanDefinition) {// 注入處理registerBeanDefinition((AnnotatedBeanDefinition) beanDefinition, beanDefinitionRegistry);}}}// 將掃描到的接口放置DefaultListableBeanFactory的beanDefinitionMap中private void registerBeanDefinition(AnnotatedBeanDefinition beanDefinition, BeanDefinitionRegistry registry) {// 接口元數據AnnotationMetadata metadata = beanDefinition.getMetadata();// 接口全類名,例如:com.zhuanzhuan.zzpayaccount.gateway.WeiXinPayRequestGatewayString className = metadata.getClassName();// 生成一個HTTPControllerFactoryBean的BeanDefinitionAbstractBeanDefinition factoryBeanBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(HTTPControllerFactoryBean.class).getBeanDefinition();AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(HTTPController.class.getName()));// requiredType: java.lang.Class,convertedValue: "com.zhuanzhuan.zzpayaccount.gateway.WeiXinPayRequestGateway"// FactoryBean的targetClass是Class<?>類型,但這里可以用“類全路徑”字符串表示,// 是因為Spring在初始化bean的時候可以根據setTargetClass方法的參數來判斷類型,進而將“類全路徑”字符串轉為Class<?>類型factoryBeanBeanDefinition.getPropertyValues().add("targetClass", className);factoryBeanBeanDefinition.getPropertyValues().add("baseUrl", annotationAttributes.getString("baseUrl"));factoryBeanBeanDefinition.getPropertyValues().add("thirdPart", annotationAttributes.get("thirdPart"));factoryBeanBeanDefinition.getPropertyValues().add("invocationHandlerClass", annotationAttributes.get("invocationHandlerClass"));// className作為beanName,可以自定義前后綴,如className + "$ByScanner"registry.registerBeanDefinition(className, factoryBeanBeanDefinition);} }Spring初始化
這里給出的是在SpringBoot啟動程序上加上@Impot注解,來驅動HTTPMethodScannerRegistrar的流程邏輯。
轉轉有自研的SCF框架,初始化工作是自定義一個Init類,然后把該Init類路徑寫在scf.init配置項上。
// 使用@Import注解,配置實現ImportBeanDefinitionRegistrar的類,可以高度配置化加載Bean @Import({HTTPMethodScannerRegistrar.class}) @SpringBootApplication public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);} }4. 總結
以上就是注解式HTTP客戶端的實現過程,總體思路簡單清晰,大致就是“注解+動態代理+Spring的Bean后置處理器”一套公式,可謂常用的輪子式代碼。
可以通過本例,延伸一些知識點:
- 自定義注解、注解處理器、Spring注解驅動開發
- JDK動態代理、Cglib動態代理
- FactoryBean和BeanFactory區別、Spring Bean的生命周期和后置處理器
研發人員可以通過學習和實踐這類“輪子式”代碼,舉一反三,提高自己的編程水平。
5. 參考
https://blog.51cto.com/u_15162069/2820375
https://developpaper.com/beanfactory-and-factorybean-in-spring-is-enough/
作者簡介
曹志鑫,轉轉中臺支付中心研發工程師
總結
以上是生活随笔為你收集整理的转转支付网关之注解式HTTP客户端的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 查询Microsoft Visual C
- 下一篇: ESPRIT 2019车铣复合编程基础到