阅读 248

使用feign对接微信支付v3

feign不仅可以用来进行服务间的通信,还可以用来对接第三方api接口。

先来个示例

就像写controller一样向微信发送请求。帮你把签名之类的工作全给做了。


/**
 * 微信app支付feign
 *
 * @author yanghx
 */
@Component
@FeignClient(url = "https://api.mch.weixin.qq.com", name = "wechatAppPayFeign", configuration = WechatAppPayFeignConfig.class)
public interface WechatAppPayFeign {


    /**
     * 发起预支付
     *
     * @param params 预支付参数
     * @return 预支付返回信息
     */
    @PostMapping("/v3/pay/transactions/app")
    WechatAppPrepayResult prepay(WechatAppPrepayParams params);


    /**
     * 微信支付订单号查询.根据微信的订单id
     *
     * @param transactionId 微信支付订单号
     * @param mchId         商户id
     * @return o
     */
    @GetMapping("/v3/pay/transactions/id/{transaction_id}")
    WechatAppQueryResult queryForWechatOrderId(@PathVariable(name = "transaction_id") String transactionId, @RequestParam(name = "mchid") String mchId);


    /**
     * 微信支付订单号查询.根据商户的订单号
     *
     * @param outTradeNo 商户订单号
     * @param mchId      商户id
     * @return o
     */
    @GetMapping("/v3/pay/transactions/out-trade-no/{out_trade_no}")
    WechatAppQueryResult queryForOutTradeNo(@PathVariable(name = "out_trade_no") String outTradeNo, @RequestParam(name = "mchid") String mchId);


    /**
     * 取消订单
     *
     * @param outTradeNo 商户订单号
     * @param params     参数
     * @return o
     */
    @PostMapping("/v3/pay/transactions/out-trade-no/{out_trade_no}/close")
    feign.Response close(@PathVariable(name = "out_trade_no") String outTradeNo, @RequestBody WechatAppPayCloseParams params);


    @GetMapping("/v3/certificates")
    @Headers({
            "Accept: application/json"
    })
    public Object certificates();
}
复制代码

用到的技术点

wechatpay-apache-httpclient

微信支付Api v3 提供的http client 扩展,实现了请求签名的生成和应答签名的验证。

spring-cloud-starter-openfeign

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.2.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>10.1.0</version>
    <scope>compile</scope>
</dependency>
复制代码

这里用到了feign和feign-httpclient。

feign默认采用reastTemplate进行网路请求,但是也支持采用httpclient,OKhttp进行替换。这里就是通过将wechatpay-apache-httpclient提供的httpclient实例注入到feign中来进行整合的

feignwechatpay-apache-httpclient的整合

首先看下feign的代码(其实是spring-cloud-starter-openfeign提供的整合代码)

/*
 * Copyright 2013-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.cloud.openfeign;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;

import javax.annotation.PreDestroy;

import feign.Client;
import feign.Feign;
import feign.httpclient.ApacheHttpClient;
import feign.okhttp.OkHttpClient;
import okhttp3.ConnectionPool;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.CloseableHttpClient;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.actuator.HasFeatures;
import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory;
import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
import org.springframework.cloud.openfeign.support.DefaultGzipDecoderConfiguration;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @author Spencer Gibb
 * @author Julien Roy
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({ FeignClientProperties.class,
      FeignHttpClientProperties.class })
@Import(DefaultGzipDecoderConfiguration.class)
public class FeignAutoConfiguration {

   @Autowired(required = false)
   private List<FeignClientSpecification> configurations = new ArrayList<>();

   @Bean
   public HasFeatures feignFeature() {
      return HasFeatures.namedFeature("Feign", Feign.class);
   }

   @Bean
   public FeignContext feignContext() {
      FeignContext context = new FeignContext();
      context.setConfigurations(this.configurations);
      return context;
   }

   @Configuration(proxyBeanMethods = false)
   @ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
   protected static class HystrixFeignTargeterConfiguration {

      @Bean
      @ConditionalOnMissingBean
      public Targeter feignTargeter() {
         return new HystrixTargeter();
      }

   }

   @Configuration(proxyBeanMethods = false)
   @ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
   protected static class DefaultFeignTargeterConfiguration {

      @Bean
      @ConditionalOnMissingBean
      public Targeter feignTargeter() {
         return new DefaultTargeter();
      }

   }

   // the following configuration is for alternate feign clients if
   // ribbon is not on the class path.
   // see corresponding configurations in FeignRibbonClientAutoConfiguration
   // for load balanced ribbon clients.
   @Configuration(proxyBeanMethods = false)
   @ConditionalOnClass(ApacheHttpClient.class)
   @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
   @ConditionalOnMissingBean(CloseableHttpClient.class)
   @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
   protected static class HttpClientFeignConfiguration {

      private final Timer connectionManagerTimer = new Timer(
            "FeignApacheHttpClientConfiguration.connectionManagerTimer", true);

      @Autowired(required = false)
      private RegistryBuilder registryBuilder;

      private CloseableHttpClient httpClient;

      @Bean
      @ConditionalOnMissingBean(HttpClientConnectionManager.class)
      public HttpClientConnectionManager connectionManager(
            ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
            FeignHttpClientProperties httpClientProperties) {
         final HttpClientConnectionManager connectionManager = connectionManagerFactory
               .newConnectionManager(httpClientProperties.isDisableSslValidation(),
                     httpClientProperties.getMaxConnections(),
                     httpClientProperties.getMaxConnectionsPerRoute(),
                     httpClientProperties.getTimeToLive(),
                     httpClientProperties.getTimeToLiveUnit(),
                     this.registryBuilder);
         this.connectionManagerTimer.schedule(new TimerTask() {
            @Override
            public void run() {
               connectionManager.closeExpiredConnections();
            }
         }, 30000, httpClientProperties.getConnectionTimerRepeat());
         return connectionManager;
      }

      @Bean
      public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
            HttpClientConnectionManager httpClientConnectionManager,
            FeignHttpClientProperties httpClientProperties) {
         RequestConfig defaultRequestConfig = RequestConfig.custom()
               .setConnectTimeout(httpClientProperties.getConnectionTimeout())
               .setRedirectsEnabled(httpClientProperties.isFollowRedirects())
               .build();
         this.httpClient = httpClientFactory.createBuilder()
               .setConnectionManager(httpClientConnectionManager)
               .setDefaultRequestConfig(defaultRequestConfig).build();
         return this.httpClient;
      }

      @Bean
      @ConditionalOnMissingBean(Client.class)
      public Client feignClient(HttpClient httpClient) {
         return new ApacheHttpClient(httpClient);
      }

      @PreDestroy
      public void destroy() throws Exception {
         this.connectionManagerTimer.cancel();
         if (this.httpClient != null) {
            this.httpClient.close();
         }
      }

   }

   @Configuration(proxyBeanMethods = false)
   @ConditionalOnClass(OkHttpClient.class)
   @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
   @ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
   @ConditionalOnProperty("feign.okhttp.enabled")
   protected static class OkHttpFeignConfiguration {

      private okhttp3.OkHttpClient okHttpClient;

      @Bean
      @ConditionalOnMissingBean(ConnectionPool.class)
      public ConnectionPool httpClientConnectionPool(
            FeignHttpClientProperties httpClientProperties,
            OkHttpClientConnectionPoolFactory connectionPoolFactory) {
         Integer maxTotalConnections = httpClientProperties.getMaxConnections();
         Long timeToLive = httpClientProperties.getTimeToLive();
         TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
         return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
      }

      @Bean
      public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
            ConnectionPool connectionPool,
            FeignHttpClientProperties httpClientProperties) {
         Boolean followRedirects = httpClientProperties.isFollowRedirects();
         Integer connectTimeout = httpClientProperties.getConnectionTimeout();
         Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
         this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation)
               .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
               .followRedirects(followRedirects).connectionPool(connectionPool)
               .build();
         return this.okHttpClient;
      }

      @PreDestroy
      public void destroy() {
         if (this.okHttpClient != null) {
            this.okHttpClient.dispatcher().executorService().shutdown();
            this.okHttpClient.connectionPool().evictAll();
         }
      }

      @Bean
      @ConditionalOnMissingBean(Client.class)
      public Client feignClient(okhttp3.OkHttpClient client) {
         return new OkHttpClient(client);
      }

   }

}
复制代码

我们要做的就是重写httpClientfeignClient这两个bean。 这样项目启动时就会以httpclient进行feign请求了。 然后我们再根据微信wechatpay-apache-httpclient提供的文档进行支付信息配置就可以了。 这样子再进行支付相关请求时,httpclient就会自动接口认证和签名了。

package cn.yanghx.pay.feign;



/**
 * 微信app支付feign配置
 *
 * @author yanghx
 */
public class WechatAppPayFeignConfig {

 
    @Resource
    private PayProperties payProperties;

    @Bean
    public CloseableHttpClient httpClient() {
        WechatAppPayProperties wechatAppPayProperties = payProperties.getWechat().getApp();

        if (wechatAppPayProperties.isEmpty()) {
            throw new WechatAppPayException("需要配置app支付相关配置");
        }

        String mchId = wechatAppPayProperties.getMchId();
        String mchSerialNo = wechatAppPayProperties.getMchSerialNo();
        String apiV3Key = wechatAppPayProperties.getApiV3Key();
        String privateKey = wechatAppPayProperties.getPrivateKey();
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
                new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8)));
        //不需要传入微信支付证书了
        AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
                new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),
                apiV3Key.getBytes(StandardCharsets.UTF_8));
        //微信提供的签名工具
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, merchantPrivateKey)
                .withValidator(new WechatPay2Validator(verifier));
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        return builder.build();
    }

    /**
     * 这两个bean的作用是。将feign的请求地址换成httpclient。 然后自己提供httpClient 。 并设置拦截器
     *
     * @param httpClient client
     * @return client
     */
    @Bean
    public Client feignClient(HttpClient httpClient) {
        return new ApacheHttpClient(httpClient);
    }
}
}
复制代码

结语

采用feign接入wechatpay-apache-httpclient其实是一种比较麻烦的做法,需要处理几个框架的兼容问题。 优点就是用了feign之后,写第三方接口方便了,不再像以前是一堆字符串,然后签名之类的代码都是写在一起的,改起来也方便。

demo项目展示

说明:

  • 这只是一个demo项目,是我从项目代码中抽出来的。目前只实现了微信app支付相关接口,如果有其它支付要对接,需要自己写(搞懂模式后,写起来很快的)。
  • 再有就是实际开发中,建议采用starter的形式引用。我们项目也是封装的starter,只是拿来做demo就把一些代码复制出来了。
  • 这种做法只是一个快速开发的方式。如果项目中用到支付很频繁,建议采用一些成熟的支付库。

源码地址

gitee.com/yanghx_git/…

配置信息说明

pay:
  config:
    wechat:
      app:
        appId: xxx
        # 商户id
        mchId: xxx
        # 商户证书序列号
        mchSerialNo: xxxx
        # api密钥
        apiV3Key: xxxx
        #回调地址
        notifyUrl: http://xxx/callback
        # 商户私钥。apiclient_key.pem中的文本
        privateKey: "-----BEGIN PRIVATE KEY-----   -----END PRIVATE KEY-----"
复制代码
文章分类
后端
文章标签