feign调用支持三方接口改造记录
平时项目里面的feign都是用在自己的微服务中的,但是有时候需要调用三方的接口。之前项目是用的Retrofit封装的,但是如果超时或者异常了,日志没有打印出来,我们也获取不到返回结果。这两天就整理了下feign开启三方调用。
因为feign是用了okhttpclient的,只是默认配置没有开启日志,以及没有使用okhttpclient.所以调用三方接口需要额外配置。
先是去git上下载了feign的源码,大概看下了。里面好多看的不是很懂,不过从一些常用的@Configuration、@ConditionalOnClass大概也能猜到。然后去网上看了下别人已经写好的feign源码解读(参考:https://zhuanlan.zhihu.com/p/526427027),之后就开始着手思考怎么替换掉项目的retrofit。
网上继续搜索,发现别人已经实现好了的。好的 ,按照网上的思路来吧。参考:https://www.zhangshengrong.com/p/AvN6Y8dWam/。其实就是自己实现okhttpclient的配置,因为springboot的默认装配机制,feign已经把okhttpclient装配进去了,而且用的是@ConditionalOnMissingBean({okhttp3.OkHttpClient.class})注解,只会注册一次。导致后面重新注册时候注册不进去。因为思路就是自己重写okhttp,不使用自动装配的。之后使用@AutoConfigureAfter(FeignAutoConfiguration.class) 。自己实现了的代码如下:
package com.gwm.marketing.restfulfeign; import feign.Feign; import feign.Logger; import feign.RequestInterceptor; import okhttp3.ConnectionPool; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; import org.springframework.cloud.openfeign.FeignAutoConfiguration; import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.TimeUnit; /** * @author fanht * @descrpiton 使用配置类声明http接口 * @AutoConfigureBefore(FeignAutoConfiguration.class) 不使用默认的自动配置,手动实现okhttpclient配置到spring容器 * @date 2022/7/25 16:00:30 * @versio 1.0 */ @Configuration @ConditionalOnClass(Feign.class) @AutoConfigureAfter(FeignAutoConfiguration.class) public class OraFeignConfig { private static final int DEFAULT_TIMEOUT = 10; /** * 注入自定义okHttpClient * @return */ @Bean public okhttp3.OkHttpClient okHttpClient(){ return new okhttp3.OkHttpClient().newBuilder(). readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS). connectTimeout(DEFAULT_TIMEOUT,TimeUnit.SECONDS). writeTimeout(DEFAULT_TIMEOUT,TimeUnit.SECONDS). connectionPool(new ConnectionPool()).build(); } @Bean public RequestInterceptor requestInterceptor(){ return new OraFeignRequestIntercepter(); } @Bean public Interceptor oraInterceptor(){ return new OraClientAlermIntercepter(); } @Bean public Logger.Level feignLoggerLevel(){ return Logger.Level.FULL; } @Bean public Logger logger(){ return new OraFeignLogger(); } @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); } /** * 自定义请求日志拦截器 * @param httpClientFactory * @param connectionPool * @param httpClientProperties * @return */ @Bean public OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) { Boolean followRedirects = httpClientProperties.isFollowRedirects(); Integer connectTimeout = httpClientProperties.getConnectionTimeout(); Boolean disableSslValidation = httpClientProperties.isDisableSslValidation(); return httpClientFactory.createBuilder(disableSslValidation) .connectTimeout((long)connectTimeout, TimeUnit.MILLISECONDS) .followRedirects(followRedirects) .connectionPool(connectionPool) .addInterceptor( new OraClientAlermIntercepter()) .build(); } }
先按照作者的思路,写出来后本地要调试。因为考虑到效果和之前的retrofit效果是一样的,于是就用同样的接口在本地测试,看看和之前的返回结果是不是一样的。嗯,又有新问题了:feign怎么支持传多个header 以及传对象?
参考了下这篇介绍:https://blog.csdn.net/hkk666123/article/details/113964715
最终用了@RequestHeader 和 @RequestBody达到了和之前一样的效果。
package com.gwm.marketing.restfulfeign.sso; import com.gwm.marketing.common.vo.beantech.BeanTechResponse; import com.gwm.marketing.common.vo.beantech.BeanTechUserInfo; import com.gwm.marketing.dto.beantech.LoginAccountDto; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import java.util.Map; /** * SSO接口调用-通过feign的方式来远程调用 测试使用 * @author fanht * @version V1.0.0 * @description * @date 2022-07-26 **/ @FeignClient(name = "feignSsoLoginTest",url = "${gwm.sso.ssoUrl}") public interface OraFeignSsoLoginClient { /** * 通过feign调用仙豆RPC * @param headerMap * @param loginAccountDto * @return */ @PostMapping(value = "/app-api/api/v1.0/userAuth/loginWithSMS") BeanTechResponse<BeanTechUserInfo> loginAccountWithSms(@RequestHeader Map<String, String> headerMap, @RequestBody LoginAccountDto loginAccountDto); }
继续调试,又碰到了新的问题:调试通过后,发现在本地debug模式下,总是会概率出现这个问题:
Stream is closed
网上搜了下原因,说是在debug模式下 ,toString默认会关闭流,而我自己是重写了feign的logger日志,读取流时候用的是feign的关闭流的方式。那种方式会关闭所有的流,用apache的common包的io流关闭解决了。参考: https://www.it1352.com/150589.html
代码如下:
package com.gwm.marketing.restfulfeign; import com.alibaba.fastjson.JSONObject; import com.gwm.marketing.restfulfeign.alerm.OraRpcDingdingConfiguration; import feign.Logger; import feign.Request; import feign.Response; import org.apache.commons.io.IOUtils; import org.springframework.context.annotation.Configuration; import javax.annotation.Resource; import java.io.IOException; import java.text.MessageFormat; /** * @author fanht * @descrpiton * @date 2022/7/26 14:09:39 * @versio 1.0 */ @Configuration public class OraFeignLogger extends Logger { private static final int PASS_STATUS= 200; @Resource private OraRpcDingdingConfiguration oraRpcDingdingConfiguration; @Override protected Response logAndRebufferResponse(String configKey, Level logLevel, feign.Response response, long elapsedTime) throws IOException { System.out.println("===========远程RPC调用耗时============" + elapsedTime); int status = response.status(); if (elapsedTime > oraRpcDingdingConfiguration.getRpc().getRpcTimeOut()) { Request request = response.request(); String requestMsg = request.httpMethod().name() + " " + request.url() + " HTTP/1.1"; String bodyMsg = request.body() != null ? new String(request.body()): ""; byte[] bodyData = null; String message = ""; if(response.body() != null){ try { bodyData = IOUtils.toByteArray(response.body().asInputStream()); } catch (IOException e) { e.printStackTrace(); } message = MessageFormat.format("调用三方接口耗时告警:项目:{0},环境:{1},IP:{2},接口请求方式以及请求地址:{3},耗时时间:{4}毫秒,请求入参:{5},三方返回信息:{6}", OraRpcDingdingConfiguration.applicationName, OraRpcDingdingConfiguration.env, OraIpUtil.initIp() , requestMsg, elapsedTime, bodyMsg,""); } else { message = MessageFormat.format("调用三方接口耗时告警:项目:{0},环境:{1},IP:{2},接口请求方式以及请求地址:{3},耗时时间:{4}毫秒,请求入参:{5}", OraRpcDingdingConfiguration.applicationName, OraRpcDingdingConfiguration.env, OraIpUtil.initIp() , requestMsg, elapsedTime,bodyMsg); } sendDingdingAlerm(message, oraRpcDingdingConfiguration.getToken().getUrl()); }else if(PASS_STATUS != status){ Request request = response.request(); String requestMsg = request.httpMethod().name() + " " + request.url() + " HTTP/1.1"; String bodyMsg = request.body() != null ? new String(request.body()): ""; byte[] bodyData = null; String message = ""; if(response.body() != null){ bodyData = IOUtils.toByteArray(response.body().asInputStream()); message = MessageFormat.format("调用三方接口异常告警:项目:{0},环境:{1},IP:{2},接口请求方式以及请求地址:{3},耗时时间:{4}毫秒,请求入参:{5},三方返回信息:{6}", OraRpcDingdingConfiguration.applicationName, OraRpcDingdingConfiguration.env, OraIpUtil.initIp() , requestMsg, elapsedTime, bodyMsg,response.toBuilder().body(bodyData).build()); } else { message = MessageFormat.format("调用三方接口异常告警:项目:{0},环境:{1},IP:{2},接口请求方式以及请求地址:{3},耗时时间:{4}毫秒,请求入参:{5}", OraRpcDingdingConfiguration.applicationName, OraRpcDingdingConfiguration.env, OraIpUtil.initIp() , requestMsg, elapsedTime,bodyMsg); } sendDingdingAlerm(message, oraRpcDingdingConfiguration.getToken().getUrl()); } log(configKey, "logLevel【%s】, body【%s】, response【%s】", JSONObject.toJSON(logLevel), JSONObject.toJSON(response), elapsedTime); return response; } @Override protected void log(String configKey, String format, Object... args) { } /** * 发送钉钉消息 */ public static void sendDingdingAlerm(String message, String dingdingTokenUrl) { JSONObject jsonObject = new JSONObject(); jsonObject.put("msgtype", "text"); JSONObject content = new JSONObject(); content.put("content", message); jsonObject.put("text", content); OraHttpClient oraHttpClient = new OraHttpClient(); String response = null; try { String[] dingdingArr = dingdingTokenUrl.split(","); if (dingdingArr != null && dingdingArr.length > 0) { for (int i = 0; i < dingdingArr.length; i++) { response = oraHttpClient.post(dingdingArr[i], jsonObject.toJSONString()); } } } catch (IOException e) { e.printStackTrace(); } } }
最后一个问题就是feign默认开启日志的问题,直接设置解决吧。
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}