feign不只能夠用來進行服務間的通訊,還能夠用來對接第三方api接口。java
先來個示例git
就像寫controller同樣向微信發送請求。幫你把簽名之類的工做全給作了。github
/** * 微信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
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。express
feign默認採用reastTemplate進行網路請求,可是也支持採用httpclient,OKhttp進行替換。這裏就是經過將wechatpay-apache-httpclient
提供的httpclient實例注入到feign中來進行整合的apache
feign
和wechatpay-apache-httpclient
的整合首先看下feign的代碼(實際上是spring-cloud-starter-openfeign
提供的整合代碼)json
/* * 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);
}
}
}
複製代碼
咱們要作的就是重寫httpClient
和feignClient
這兩個bean。 這樣項目啓動時就會以httpclient進行feign請求了。 而後咱們再根據微信wechatpay-apache-httpclient
提供的文檔進行支付信息配置就能夠了。 這樣子再進行支付相關請求時,httpclient就會自動接口認證和簽名了。api
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以後,寫第三方接口方便了,再也不像之前是一堆字符串,而後簽名之類的代碼都是寫在一塊兒的,改起來也方便。微信
說明:markdown
- 這只是一個demo項目,是我從項目代碼中抽出來的。目前只實現了微信app支付相關接口,若是有其它支付要對接,須要本身寫(搞懂模式後,寫起來很快的)。
- 再有就是實際開發中,建議採用starter的形式引用。咱們項目也是封裝的starter,只是拿來作demo就把一些代碼複製出來了。
- 這種作法只是一個快速開發的方式。若是項目中用到支付很頻繁,建議採用一些成熟的支付庫。
源碼地址
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-----"
複製代碼