写点什么

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

  • 2022-12-29
    北京
  • 本文字数:7804 字

    阅读完需:约 26 分钟

1. 背景

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


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


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


  • 缺少统一的设计流程,代码复杂臃肿、耦合度高

  • 开发人员水平参差不齐,不同人的设计风格千差万别

  • 抽象程度不够,复用性较低


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



<center>图 1 转转 APP 收银台</center>

2. 实践思路

2.1 自定义注解

目标:


  1. 通过自定义注解,将一些通用参数信息直接附加在接口上,达到接口即文档的效果。

  2. 新增方法时,按文档接口内容,简单配置即可使用。

  3. 接口代码变得简洁,减少样板代码。



<center>图 2 注解式 HTTP 接口</center>

2.2 动态代理增强接口方法

目标:


  1. 通过动态代理,可以屏蔽这些复杂或存在差异的实现细节,让使用者面向纯接口编程。

  2. 结合注解,代理类实现无侵入式的代码扩展。



<center>图 3 代理类增强视图</center>

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 容器中。



<center>图 4 整体实现流程图</center>

3.1 自定义注解

HTTPController 注解

该注解属于运行时的 TYPE 注解,作用在一个类或接口上。


用途:标识该接口为某个三方渠道的 HTTP 网关接口,可以配置渠道基础信息、代理类等信息。


@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})@Documentedpublic @interface HTTPController {    // 三方渠道描述    String desc() default "";     // 三方渠道类型    ThirdPartEnum thirdPart();     // 请求Url    String baseUrl() default "";     // 代理类    Class<?> invocationHandlerClass();}
复制代码

HTTPMethod 注解

该注解属于运行时的 METHOD 注解,作用在一个方法上。


用途:标识该方法为三方渠道的某个特定的文档接口,可以配置接口路径、请求方式、重试次数等信息。


@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})@Documentedpublic @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 动态代理)。


以下代码是核心流程代码,细节有所缩减,主要是一些边界判断、特殊处理等,不影响理解。


@Slf4jpublic class WeiXinPayInvocationHandler implements InvocationHandler {    @Override    public 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];        }                // 验签+解密resData        String 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 ();} @Setterpublic class XiaoMing implements FactoryBean<Object>, Person {    private String regards;    @Override    public Object getObject() {        return new ZhangSan(regards);    }    @Override    public Class<?> getObjectType() {        return ZhangSan.class;    }    @Override    public void sayHello() {        System.out.println("Greetings from XiaoMing: " + regards);    }} public class ZhangSan implements Person {    String regards;    public ZhangSan(String regards) {        this.regards = regards;    }    @Override    public void sayHello() {        System.out.println("Greetings from ZhangSan: " + regards);    }} public class BeanDefinitionBuilderExample {    public static void main (String[] args) {        // 定义Bean        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(XiaoMing.class).getBeanDefinition();        beanDefinition.getPropertyValues().add("regards", "Hello World");         // 注册Bean        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();        beanFactory.registerBeanDefinition("person", beanDefinition);         // 获取Bean        Person bean = (Person) beanFactory.getBean("person");        bean.sayHello();    }}
复制代码



<center>图 5 Demo 运行结果</center>


上述例子:将实现 FactoryBean 的 XiaoMing 类,将其注入到 Spring 容器中。获取 Bean 时,调用 sayHello 方法,输出的是“Greetings from ZhangSan : Hello World”。<br/>


结论:根据“person”从 BeanFactory 中获取的 Bean,实际上是 FactoryBean 的 getObeject()返回的对象。

FactoryBean 存在意义和使用场景

FactoryBean 是一个能生产或修饰对象生成的 Bean,类似于设计模式中的工厂模式和装饰器模式。


存在意义


  • 通过实现 FactoryBean 这个接口,用户可以自定义实例化 Bean 的逻辑,并且在创建时才去实现具体的功能。


使用场景


  • Spring 中 FactoryBean 最典型的应用就是创建 AOP 代理对象-ProxyFactoryBean。

  • MyBatis 中使用 MapperFactoryBean 来创建 Mapper,最终得到是由 Proxy.newProxyInstance 创建的代理实例。

HTTPControllerFactoryBean 实现

@Setter@Slf4jpublic class HTTPControllerFactoryBean implements FactoryBean<Object> {    // 目标接口    private Class<?> targetClass;     private ThirdPartEnum thirdPart;     private String baseUrl;     private Class<InvocationHandler> invocationHandlerClass;     // 返回工厂生产的对象,这是 Spring 容器将使用的对象    @Override    public 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生成的对象类型    @Override    public Class<?> getObjectType() {        return targetClass;    }     // 表示此FactoryBean生成的对象是否为单例    @Override    public boolean isSingleton() {        return true;    }}
复制代码

HTTPMethodScannerRegistrar 实现

在 ImportBeanDefinitionRegistrar 接口中,有一个 registerBeanDefinitions()方法,通过该方法可以向 Spring 容器中注册 Bean 实例。


实现该接口的类都会被 ConfigurationClassPostProcessor 后置处理器,因此在 ImportBeanDefinitionRegistrar 中注册的 Bean 可以比依赖它的 Bean 更早初始化(有兴趣可自行查阅资料)。


public class HTTPMethodScannerRegistrar implements ImportBeanDefinitionRegistrar {    /**     * 注入对象到Spring     * @param annotationMetadata 注解元数据     * @param beanDefinitionRegistry 它定义了关于 BeanDefinition 的注册、移除、查询等一系列的操作     */    @Override    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata            , BeanDefinitionRegistry beanDefinitionRegistry) {        // ClassPathScanningCandidateComponentProvider是Spring提供的工具,可以按自定义的类型,查找classpath下符合要求的class文件。        ClassPathScanningCandidateComponentProvider classScanner = new ClassPathScanningCandidateComponentProvider(false) {            @Override            protected 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.WeiXinPayRequestGateway        String className = metadata.getClassName();         // 生成一个HTTPControllerFactoryBean的BeanDefinition        AbstractBeanDefinition 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})@SpringBootApplicationpublic 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/




作者简介


曹志鑫,转转中台支付中心研发工程师


转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。

关注公众号「转转技术」(综合性)、「大转转 FE」(专注于 FE)、「转转 QA」(专注于 QA),更多干货实践,欢迎交流分享~

用户头像

还未添加个人签名 2019-04-30 加入

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」,各种干货实践,欢迎交流分享~

评论

发布
暂无评论
转转支付网关之注解式HTTP客户端_后端_转转技术团队_InfoQ写作社区