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

  • 1. 背景

  • 2. 实践思路

    • 2.1 自定义注解

    • 2.2 动态代理增强接口方法

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

  • 3. 实现

    • 3.1 自定义注解

    • 3.2 动态代理增强接口方法

    • 3.3 将代理类Bean注入到Spring容器

  • 4. 总结

  • 5. 参考

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();
 
    // 请求Url
    String 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 {
    @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 ();
}
 
@Setter
public 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();
    }
}

图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 容器将使用的对象
    @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})
@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的生命周期和后置处理器

研发人员可以通过学习和实践这类“轮子式”代码,举一反三,提高自己的编程水平。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值