日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

转转支付网关之注解式HTTP客户端

發布時間:2024/3/13 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 转转支付网关之注解式HTTP客户端 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1. 背景

轉轉支付中心與多家第三方支付平臺、金融機構存在合作,例如微信、支付寶、分期樂、合利寶、平安銀行等。

在收單、打款、退款等業務上,大部分接口都需要通過HTTP協議與第三方進行交互。

目前業界上或轉轉內部都有封裝好HttpUtil工具類提供使用,但開發人員在接入三方渠道時,不同渠道方提供的文檔有所差異且內部研發人員變動等原因,實現時自然會存在一些問題:

  • 缺少統一的設計流程,代碼復雜臃腫、耦合度高
  • 開發人員水平參差不齊,不同人的設計風格千差萬別
  • 抽象程度不夠,復用性較低

由此,支付中心研發了統一設計風格、注解式的HTTP客戶端,建立一套面向“使用HTTP協議與三方渠道交互“的“設計規約”。

圖1 轉轉APP收銀臺

2. 實踐思路

2.1 自定義注解

目標:

  • 通過自定義注解,將一些通用參數信息直接附加在接口上,達到接口即文檔的效果。
  • 新增方法時,按文檔接口內容,簡單配置即可使用。
  • 接口代碼變得簡潔,減少樣板代碼。
  • 圖2 注解式HTTP接口

    2.2 動態代理增強接口方法

    目標:

  • 通過動態代理,可以屏蔽這些復雜或存在差異的實現細節,讓使用者面向純接口編程。
  • 結合注解,代理類實現無侵入式的代碼擴展。
  • 圖3 代理類增強視圖

    2.3 將代理類Bean注入到Spring容器

    目標:

  • 支持Spring IOC特性。
  • 保證代理類實現和普通接口實現的調用方式無差別,用戶無感知。
  • 3. 實現

    整體流程:

  • 在Spring啟動初始化時,通過@Import({HTTPMethodScannerRegistrar.class})來驅動ImportBeanDefinitionRegistrar接口的實現類進行定制化Bean的注冊。
  • 實現ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法,主要是獲取帶有@HTTPController注解的接口,使用這些接口的元數據的注解信息來構建HTTPControllerFactoryBean的Bean,然后注冊進Spring容器中。
  • 從HTTPControllerFactoryBean中實際獲取的Bean,是調用“實現FactoryBean接口的getObject()方法”獲取的,該方法就是使用Proxy.newProxyInstance來實例化代理類,從而達到將目標接口的增強Bean注冊到Spring容器中。
  • 圖4 整體實現流程圖

    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客户端的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。