1. 背景

转转支付中心与多家第三方支付平台、金融机构存在合作,例如微信、支付宝、分期乐、合利宝、平安银行等。

在收单、打款、退款等业务上,大部分接口都需要通过HTTP协议与第三方进行交互。

目前业界上或转转内部都有封装好HttpUtil工具类提供使用,但开发人员在接入三方渠道时,不同渠道方提供的文档有所差异且内部研发人员变动等原因,实现时自然会存在一些问题:

  • 缺少统一的设计流程,代码复杂臃肿、耦合度高
  • 开发人员水平参差不齐,不同人的设计风格千差万别
  • 抽象程度不够,复用性较低

由此,支付中心研发了统一设计风格、注解式的HTTP客户端,建立一套面向“使用HTTP协议与三方渠道交互“的“设计规约”。

图1 转转APP收银台

2. 实践思路

2.1 自定义注解

目标:

  1. 通过自定义注解,将一些通用参数信息直接附加在接口上,达到接口即文档的效果。
  2. 新增方法时,按文档接口内容,简单配置即可使用。
  3. 接口代码变得简洁,减少样板代码。

图2 注解式HTTP接口

2.2 动态代理增强接口方法

目标:

  1. 通过动态代理,可以屏蔽这些复杂或存在差异的实现细节,让使用者面向纯接口编程。
  2. 结合注解,代理类实现无侵入式的代码扩展。

图3 代理类增强视图

2.3 将代理类Bean注入到Spring容器

目标:

  1. 支持Spring IOC特性。
  2. 保证代理类实现和普通接口实现的调用方式无差别,用户无感知。

3. 实现

整体流程:

  1. 在Spring启动初始化时,通过@Import({HTTPMethodScannerRegistrar.class})来驱动ImportBeanDefinitionRegistrar接口的实现类进行定制化Bean的注册。
  2. 实现ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法,主要是获取带有@HTTPController注解的接口,使用这些接口的元数据的注解信息来构建HTTPControllerFactoryBean的Bean,然后注册进Spring容器中。
  3. 从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客户端相关推荐

  1. mqtt 发送消息过多_阿里云MQTT服务端注解式消息处理分发与同步调用实践小结

    一.前言 前段时间公司预研了设备app端与服务端的交互方案,出于多方面考量最终选用了阿里云的微服务队列MQTT方案,基于此方案,本人主要实践有: 1. 封装了RocketMQ实现MQTT订阅与发布的实 ...

  2. 中国电信翼支付网关接口接入

    最近在做中国电信的翼支付网关接口的接入,正好拿Java练练手.到目前为止,唯一不太适应的就是自己的Java积累几乎为0,什么都要重头写起,不像C#有这么多年的沉淀,可以随手拿来用.   废话先不多说. ...

  3. 【编程不良人】快速入门Spring学习笔记08---事务属性、Spring整合Structs2框架(SM)、Spring整合Mybatis+Struts2(SSM)、Spring注解、SSM注解式开发

    1. 事务属性 1.1 事务传播属性 配套视频:[编程不良人]快速入门Spring,SpringBoot.SpringCloud学不好完全是因为Spring没有掌握!_哔哩哔哩_bilibili # ...

  4. Braintree PayPal 支付网关开发(一)

    一般网上消费流程: 消费者 > 商户网站 > 消费者账户银行 > 支付网关 > 支付处理系统 > 商户收款银行 Braintree 就是一种支付方式. Braintree ...

  5. 【学习笔记】- 支付网关的设计

    大部分内容,来自大佬[凤凰牌老熊]博客:http://doc.cocolian.cn/essay/,外加我乱加的一些标签和乱找的一些注解. 文章目录 支付网关的设计 功能概述 支付(API)网关 支付 ...

  6. Braintree PayPal 支付网关开发(二)

    开发准备在上篇文章已经介绍 >>看这里 << 这篇文章说下Demo示例. 1. 开发流程图这里再贴一下(很重要): 2. 前端页面     2.1 代码 <div cla ...

  7. 四种电子商务支付模式:支付网关模式、网上银行模式、第三方支付模式和手机支付模式。

    网站浏览 分为 游客浏览模式(即无用户登录)四种电子商务支付模式:支付网关模式.网上银行模式.第三方支付模式和手机支付模式. 四种电子商务支付模式:支付网关模式.网上银行模式.第三方支付模式和手机支付 ...

  8. 【支付系统学习笔记】-二支付系统设计(支付网关设计)

    前言: 本文属于学习笔记,首先感谢原作者:凤凰牌老熊,博客链接:http://blog.lixf.cn/ 一 概述 在支付系统中,支付网关和支付渠道的对接是最核心的功能.其中支付网关是对外提供服务的接 ...

  9. 支付网关的设计:核心模块的功能需求、软件架构设计以及注意要点

    2019独角兽企业重金招聘Python工程师标准>>> 在支付系统中,支付网关和支付渠道的对接是最核心的功能.其中支付网关是对外提供服务的接口,所有需要渠道支持的资金操作都需要通过网 ...

最新文章

  1. 地球自转减速影响世界时 格林尼治时间或成历史
  2. 【SAP】PO中“交货已完成”的功能解析
  3. java+stream+源码分析_java8学习之Stream源码分析
  4. 每周分享第8期(2019.5.25)
  5. d3.js html显示图片,d3.js v4:如何在鼠标点击节点后显示图像
  6. mysql8.0.13 32位下载_MySQL8.0下载-MySQL数据库8.0下载 v8.0.11官方版(32位/64位)--pc6下载站...
  7. 宽带路由器常见故障排除
  8. C#中时间格式的转换
  9. (随机|批量)梯度下降法、(拟)牛顿法、共轭梯度法、启发式算法
  10. 清闲逛论坛,发个我们团队常用的开发资源整理,跟兄弟们共享
  11. cad导出pdf_CAD手机看图软件中导出的CAD图纸为什么没有颜色?
  12. jsBarcode生成条形码
  13. 互联网装修O2O模式是否可行?
  14. 蓝牙GATT和GAP层
  15. 3dsmax建模总结
  16. oracle lsnrctl命令,oracle 中的lsnrctl命令
  17. Jupyter lab add kernel Python+Julia+R 【jupyter Notebook 切换Python环境】and【在jupyter Notebook中安装第三方库】
  18. asp毕业设计—— 基于asp+access的人事管理系统设计与实现(毕业论文+程序源码)——人事管理系统
  19. JVM上篇_15-垃圾回收相关算法_尚硅谷
  20. 线上展厅打造视觉亮点

热门文章

  1. day015异常捕获和正则
  2. IC卡16个扇区简介
  3. 男人40岁后的健康生活方式
  4. 90后CEO率图鸭投身开源,视频通信也成“隐形”红海
  5. roboone机器人_ROBOONE机器人这个品牌怎么样?是否可以加盟投资?
  6. 别瞎学了,我的MySQL学习之路(超详细超硬核)
  7. 使用大华NetSDK对接大华相机
  8. spring注解开发配置spring父子容器
  9. JAVA中startwith函数的用法
  10. vertica java_Vertica数据查询优化