赞
踩
本文中将教会您如何快速使用Openfeign,包括Opengfeign的基础配置、接口调用、接口重试、拦截器实现、记录接口日志信息到数据库
OpenFeign 是一个基于 Spring 的声明式、模板化的 HTTP 客户端,它简化了编写 Web 服务客户端的过程。用户只需创建一个接口并添加相应的注解,即可实现对远程服务的调用。OpenFeign 是 Spring Cloud 的一部分,它支持 Spring MVC 的注解,如 @RequestMapping
,使得使用 HTTP 请求访问远程服务就像调用本地方法一样直观和易于维护。Openfeign底层默认使用JDK提供的HttpURLConnection进行通信(源码参考类feign.Default
),使用Openfeign可以快速的帮我们完成第三方接口调用的实现,简化开发流程。
因为我使用的SpringBoot是2.2.10.RELEASE版本,因此在引入Openfeign依赖时,也就引入与SpringBoot一致的版本,具体版本号根据您的SpringBoot版本来定。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
启动类增加 @EnableFeignClients 用于在启动时加载我们定义的所有使用注解 @FeignClient 定义的feign客户端,并把feign客户端注册到 IOC 容器中。@EnableFeignClients注解中的basePackages用于配置扫描哪些包下来的类,这里我配置com.hl.by.remote包下的类
此时openfeign最基础的配置就已经配置完成了,现在可以自定义feign客户端,使用openfeign调用远程接口。
feign的客户端需要使用 @FeignClient 注解进行标识,这样在扫描时才知道这是一个feign客户端。@FeignClient 最常用的就两个属性,一个是name,用于给客户端定义一个唯一的名称,另一个就是url,用于定义该客户端调用的远程地址。
这里的 ${open-feign.api-url} 是通过springBoot通过读取yaml或者properties文件获取的配置。这里我是在yaml中配置了该值:
因此客户端feign-remote-service调用的远程地址就是http://127.0.0.1:8080。在定义好客户端后,可以在客户端中加入对应的接口,比如我在客户端feign-remote-service中增加了一个saveSku的方法,当调用该方法时,将发送一个请求方式是POST,requestBody的类型是FeignRemoteRequestVO,返回对象类型是ResponseResult,请求地址是http://127.0.0.1:8080/hl-template/mock/saveSku 的请求。
由于我定义的feign客户端的调用地址是本地,因此我在本地定义了一个saveSku接口,用于演示feign远程调用的效果
我使用Postman 调用接口feign,然后通过feign这个接口使用刚刚定义的feign客户端进行远程调用saveSku方法。
通过feignRemoteService.saveSku将调用到第三方接口,可以看到已经通过feign客户端调用到了远程方法saveSku
现在您已经学会了使用Openfegn来进行最基础的远程调用。可以看出使用Openfeign以后,我们只用定义客户端即可,剩下的具体调用实现交给Openfeign就可以了。
本小节将对Openfeign常用配置进行演示,包括连接超时配置、接口调用、接口重试、拦截器实现,其中会涉及部分少量源码,但重点是教会您如何使用
Openfeign采用动态代理的方式实现接口调用,通过ReflectiveFeign.invoke 方法找到具体SynchronousMethodHandler。SynchronousMethodHandler是每个feign客户端中每个具体调用方法的最终实现。通过调用SynchronousMethodHandler中的invoke方法进行远程接口调用。
Openfeign中常用的基础信息维护在抽象类Feign中,维护着Feign调用时的日志等级、客户端调用Client、重试机制、日志实现、请求编码器(encoder)、响应解码器(decoder)等。可以看到Feign采用Builder构建者模式对每个属性都设置了最初的值。而请求超时的配置则由类Options维护。
Options类中属性只有三个,connectTimeoutMillis配置连接超时时间,readTimeoutMillis连接成功后等待响应超时时间,followRedirects是否允许重定向。默认的Options设置连接超时是10秒,等待响应超时是60秒,不允许重定向。
因此如果我们要自定义Openfeign的连接响应超时时间,则只需要自己声明一个Options注入到spring容器中去即可替换Openfeign默认的配置。我在yaml中配置了openfeign的接口连接超时信息,只需要创建一个配置类,读取配置信息放入Options中即可。
代码如下:
@Data @Component @ConfigurationProperties(prefix = "open-feign") @RequiredArgsConstructor public class FeignConfig { private Integer connectTimeoutMillis; private Integer readTimeoutMillis; private boolean followRedirects; private Integer maxAttempts; private Long period; private Long maxPeriod; /** * 配置openfeign调用接口时的超时时间和等待超时时间 * * @return Request.Options */ @Bean public Request.Options options() { return new Request.Options(connectTimeoutMillis, readTimeoutMillis, followRedirects); }
通过启动项目打断点就可以看到我们自定义的Options是否生效,可以看到我们自定的配置已经生效并设置到了Openfeign中
Openfeign接口调用重试默认是由Retryer接口中的Default来实现,Default里默认设置每次接口调用失败随机休眠100毫秒~1000毫秒,最多重试5次。
如果我们要自定义Openfeign的重试机制的话,可以像Retryer.Default一样实现Retryer接口,这里我自定义了一个接口重试机制名叫FeignRetry,相比于Retryer.Default只是增加了错误信息打印。首先依然先在yaml配置文件中配置重试相关信息。配置了接口调用失败时休眠一秒进行重试,最多重试3次
open-feign:
api-url: http://127.0.0.1:8088
connectTimeoutMillis: 5000 #超时连接超时时间
readTimeoutMillis: 30000 #连接成功后等待接口响应时间超时时间
followRedirects: false #是否允许重定向
maxAttempts: 3 #接口调用失败重试次数,调用失败通常指的是接口网络异常
period: 1000 #接口调用失败后,周期多少毫秒后重新尝试调用
maxPeriod: 1000 #接口调用失败后,最大周期多少毫秒后重新尝试调用
通过FeignConfig读取配置信息将配置信息加载到FeignRetry中:
@Data @Component @ConfigurationProperties(prefix = "open-feign") @RequiredArgsConstructor public class FeignConfig { private Integer connectTimeoutMillis; private Integer readTimeoutMillis; private boolean followRedirects; private Integer maxAttempts; private Long period; private Long maxPeriod; private final InterfaceLogMapper interfaceLogMapper; /** * 配置将重试N次,每次间隔M秒钟,重试条件为连接超时或请求失败 * * @return Retryer */ @Bean public Retryer feignRetryer() { return new FeignRetry(period, maxPeriod, maxAttempts); } /** * 配置openfeign调用接口时的超时时间和等待超时时间 * * @return Request.Options */ @Bean public Request.Options options() { return new Request.Options(connectTimeoutMillis, readTimeoutMillis, followRedirects); }
FeignRetry代码实现如下:
import feign.FeignException; import feign.RetryableException; import feign.Retryer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static java.util.concurrent.TimeUnit.SECONDS; /** * @Author: Greyfus * @Create: 2024-03-10 00:56 * @Version: 1.0.0 * @Description:自定义feign请求时重试机制,与Default相比,只是增加了调用日志纪录机制 */ public class FeignRetry implements Retryer { private static final Logger LOGGER = LoggerFactory.getLogger(FeignRetry.class); //最大重试次数 private final int maxAttempts; //调用频率 private final long period; //最大频率 private final long maxPeriod; //尝试的次数 int attempt; //纪录失眠次数 long sleptForMillis; public FeignRetry() { this(1000, SECONDS.toMillis(1), 3); } public FeignRetry(long period, long maxPeriod, int maxAttempts) { this.period = period; this.maxPeriod = maxPeriod; this.maxAttempts = maxAttempts; this.attempt = 1; } protected long currentTimeMillis() { return System.currentTimeMillis(); } public void continueOrPropagate(RetryableException e) { if (e instanceof FeignException) { //打印异常 FeignException remoteException = e; LOGGER.error("Feign request【{}】 attempt 【{}】 times,status:【{}】,errorMessage:{}", remoteException.request().url(), attempt, remoteException.status(), remoteException.getMessage()); } if (attempt++ >= maxAttempts) { throw e; } long interval; if (e.retryAfter() != null) { interval = e.retryAfter().getTime() - currentTimeMillis(); if (interval > maxPeriod) { interval = maxPeriod; } if (interval < 0) { return; } } else { interval = nextMaxInterval(); } try { Thread.sleep(interval); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); throw e; } sleptForMillis += interval; } long nextMaxInterval() { long interval = (long) (period * Math.pow(1.5, attempt - 1)); return interval > maxPeriod ? maxPeriod : interval; } @Override public Retryer clone() { return new FeignRetry(period, maxPeriod, maxAttempts); } }
openfeign提供了一个RequestInterceptor接口,凡是实现该接口的类并注入到Spring容器中后,在feign进行远程调用之前会调用实现该接口的所有类。源码可以参考SynchronousMethodHandler中的targetRequest方法,targetRequest是在执行远程调用之前进行调用。Openfeign请求拦截器的作用通常是用于设置公共属性,比如给请求设置请求头认证信息等。
这里我自定义了一个名叫FeignRequestInterceptor 的拦截器并使用@Component注入到容器中。当我调用远程接口时,通过debug模式可以看到该拦截器的apply方法被调用。
/**
* @Author: DI.YIN
* @Date: 2024/3/13 13:43
* @Version: 1.0.0
* @Description: openfeign请求拦截器,在发送请求前进行拦截
**/
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
System.out.println("你好");
}
}
Openfeign提供了请求拦截器,但是没有提供响应拦截器,这导致我们无法纪录接口的响应信息到数据库。为了实现该功能我翻遍了SynchronousMethodHandler这个类的所有源码信息。我们先简单的对SynchronousMethodHandler源码进行分析,再来说如何实现将接口日志信息存入数据库。SynchronousMethodHandler的invoke是整个openfeign远程调用了的核心部分,其最核心的是executeAndDecode方法,用于构建请求参数、请求方式等信息。
进入executeAndDecode方法后,第一个执行targetRequest方法,targetRequest就是循环遍历RequestInterceptor请求拦截器,调用每个RequestInterceptor实现类的apply方法。
执行完拦截器后,判断当前日志等级是否为不为NONE,如果当前日志等级不为NONE,则通过logger.logRequest打印日志。此时可以通过request获取到请求信息、请求地址、请求头等信息。
response = client.execute(request, options); 则是真正调用远程接口的地方,openfeing默认采用HttpURLConnection进行远程接口调用。如果调用异常,则logger.logIOException打印异常信息,如果调用成功,则通过logger.logAndRebufferResponse打印异常信息。
通过对源码的分析,我们知道当日志等级不为NONE时,openfeign会打印接口请求信息到控制台,如果接口调用失败,调用logIOException将错误信息打印到控制台,接口调用成功则调用logAndRebufferResponse将响应信息打印到控制台。因此我打算自定义一个日志对象,将接口信息存入数据库。
第一步创建接口日志信息表,用于存放接口日志信息:
CREATE TABLE
interface_log
(
log_id
bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘日志主键’,
remote_sys_code
varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT ‘远程系统编码’,
remote_sys_name
varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT ‘远程系统名称’,
request_time
datetime NOT NULL COMMENT ‘请求时间’,
response_time
datetime DEFAULT NULL COMMENT ‘响应时间’,
request_method
varchar(10) COLLATE utf8mb4_bin NOT NULL COMMENT ‘请求方式 GET PUT POST’,
request_url
varchar(200) COLLATE utf8mb4_bin NOT NULL COMMENT ‘请求地址’,
request_headers_message
longtext COLLATE utf8mb4_bin COMMENT ‘请求头信息’,
request_message
longtext COLLATE utf8mb4_bin COMMENT ‘请求信息’,
response_message
longtext COLLATE utf8mb4_bin COMMENT ‘响应信息’,
status
varchar(50) COLLATE utf8mb4_bin NOT NULL COMMENT ‘接口状态’,
PRIMARY KEY (log_id
) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT=‘日志记录表’;
第二步创建接口日志对象:
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.hl.by.domain.base.BaseDomain; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.RequiredArgsConstructor; import java.util.Date; /** * @Author: DI.YIN * @Date: 2024/3/13 16:05 * @Version: 1.0.0 * @Description: 日志实体类 **/ @Data @RequiredArgsConstructor @TableName("interface_log") public class InterfaceLogDomain extends BaseDomain { @TableField(exist = false) private static final long serialVersionUID = 2588400601329175428L; @ApiModelProperty(value = "日志主键") @TableId(type = IdType.AUTO) private Long logId; @ApiModelProperty(value = "远程系统编码") @TableField(value = "remote_sys_code") private String remoteSysCode; @ApiModelProperty(value = "远程系统名称") @TableField(value = "remote_sys_name") private String remoteSysName; @ApiModelProperty(value = "请求时间") @TableField(value = "request_time") private Date requestTime; @ApiModelProperty(value = "响应时间") @TableField(value = "response_time") private Date responseTime; @ApiModelProperty(value = "请求方式") @TableField(value = "request_method") private String requestMethod; @ApiModelProperty(value = "请求地址") @TableField(value = "request_url") private String requestUrl; @ApiModelProperty(value = "请求头内容") @TableField(value = "request_headers_message") private String requestHeaders; @ApiModelProperty(value = "请求内容") @TableField(value = "request_message") private String requestMessage; @ApiModelProperty(value = "响应内容") @TableField(value = "response_message") private String responseMessage; @ApiModelProperty(value = "接口状态") @TableField(value = "status") private String status; }
第三步创建日志Mapper,包含对日志表的增删改查功能,这里我使用MyabtisPlus框架,因此实现相对简单。您可根据自己的实际情况实现Mapper。
/**
* @Author: DI.YIN
* @Date: 2024/3/13 16:34
* @Version: 1.0.0
* @Description: 接口日志
**/
public interface InterfaceLogMapper extends BaseMapper<InterfaceLogDomain> {
}
第四步自定义日志对象FeignRemoteLogger
package com.hl.by.common.feign; import com.hl.by.domain.log.InterfaceLogDomain; import com.hl.by.domain.log.InterfaceLogStatus; import com.hl.by.persistence.dao.log.InterfaceLogMapper; import feign.Logger; import feign.Request; import feign.Response; import feign.Util; import lombok.RequiredArgsConstructor; import java.io.IOException; import java.util.Collection; import java.util.Date; import java.util.Map; import static feign.Util.UTF_8; import static feign.Util.decodeOrDefault; /** * @Author: DI.YIN * @Date: 2024/3/13 15:36 * @Version: 1.0.0 * @Description: Feign接口远程调用接口日志记录 **/ @RequiredArgsConstructor public class FeignRemoteLogger extends Logger { private final InterfaceLogMapper logMapper; /** * 具体日志显示格式再次方法打印,因为要将日志写入数据库,因此该地方就不打印日志了 * * @param configKey * @param format * @param args */ @Override protected void log(String configKey, String format, Object... args) { } /** * 在调用之前调用该方法,将请求数据写入数据库 * * @param configKey * @param logLevel * @param request */ @Override protected void logRequest(String configKey, Level logLevel, Request request) { InterfaceLogDomain interfaceLogDomain = new InterfaceLogDomain(); //从header中获取系统编码 interfaceLogDomain.setRemoteSysCode(request.headers().get("SYS_CODE") == null || request.headers().get("SYS_CODE").isEmpty() ? "UNKNOWN" : request.headers().get("SYS_CODE").toArray()[0].toString()); //从header中获取系统名称 interfaceLogDomain.setRemoteSysName(request.headers().get("SYS_NAME") == null || request.headers().get("SYS_NAME").isEmpty() ? "UNKNOWN" : request.headers().get("SYS_NAME").toArray()[0].toString()); interfaceLogDomain.setRequestTime(new Date()); interfaceLogDomain.setStatus(InterfaceLogStatus.DOING.getCode()); interfaceLogDomain.setRequestMethod(request.httpMethod().name());//请求方式 interfaceLogDomain.setRequestUrl(request.url()); //请求地址 Map<String, Collection<String>> headers = request.headers();//请求头 if (request.requestBody() != null) { interfaceLogDomain.setRequestMessage(request.requestBody().asString());//请求内容 } //存入数据库 logMapper.insert(interfaceLogDomain); //放入线程缓存中,等待请求完成或者异常时,从线程缓存中拿出日志对象,回填数据 FeignRequestContextHolder.setThreadLocalInterfaceLog(interfaceLogDomain); } /** * 当配置了接口重试机制时,再接口重试之前调用该方法 * * @param configKey * @param logLevel */ @Override protected void logRetry(String configKey, Level logLevel) { } /** * 当接口调用异常时,会调用该接口,如果配置了重试机制,该方法在logRetry之前调用 * * @param configKey * @param logLevel * @param ioe * @param elapsedTime * @return */ @Override protected IOException logIOException(String configKey, Level logLevel, IOException ioe, long elapsedTime) { //从线程缓存中获取日志信息,用于信息回填存入数据库 InterfaceLogDomain threadLocalInterfaceLog = FeignRequestContextHolder.getThreadLocalInterfaceLog(); if (threadLocalInterfaceLog != null) { threadLocalInterfaceLog.setStatus(InterfaceLogStatus.ERROR.getCode());//回填响应状态 threadLocalInterfaceLog.setResponseTime(new Date()); //回填响应时间 threadLocalInterfaceLog.setResponseMessage(ioe.getMessage()); try { logMapper.updateById(threadLocalInterfaceLog); } catch (Throwable throwable) { //清除线程缓存 FeignRequestContextHolder.clear(); } } return ioe; } /** * 接口调用成功后,调用该方法记录日志信息 * * @param configKey * @param logLevel * @param response * @param elapsedTime * @return * @throws IOException */ @Override protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { //从线程缓存中获取日志信息,用于信息回填存入数据库 InterfaceLogDomain threadLocalInterfaceLog = FeignRequestContextHolder.getThreadLocalInterfaceLog(); if (threadLocalInterfaceLog != null) { try { threadLocalInterfaceLog.setResponseTime(new Date());//回写接口响应时间 threadLocalInterfaceLog.setStatus(InterfaceLogStatus.SUCCESS.getCode());//回写接口日志状态 int status = response.status(); int bodyLength; if (response.body() != null && !(status == 204 || status == 205)) { // HTTP 204 No Content "...response MUST NOT include a message-body" // HTTP 205 Reset Content "...response MUST NOT include an entity" //获取响应内容 byte[] bodyData = Util.toByteArray(response.body().asInputStream()); //内容长度 bodyLength = bodyData.length; if (bodyLength > 0) { String responseMessage = decodeOrDefault(bodyData, UTF_8, "Binary data"); threadLocalInterfaceLog.setResponseMessage(responseMessage); } return response.toBuilder().body(bodyData).build(); } } finally { logMapper.updateById(threadLocalInterfaceLog); FeignRequestContextHolder.clear(); } } return response; } }
FeignRequestContextHolder 用于缓存每个线程当前所拥有的日志信息
/** * @Author: Greyfus * @Create: 2024/3/13 15:36 * @Version: 1.0.0 * @Description: 用于维护openfeign当前线程请求的请求日志对象,该对象在请求之前进行缓存,在请求结束或者异常后进行拿出回写日志信息保存到数据库 */ package com.hl.by.common.feign; import com.hl.by.domain.log.InterfaceLogDomain; public class FeignRequestContextHolder { private static final ThreadLocal<InterfaceLogDomain> THREAD_LOCAL_INTERFACE_LOG = new ThreadLocal<>(); /** * 获取当前线程缓存的日志信息 * * @return */ public static InterfaceLogDomain getThreadLocalInterfaceLog() { return THREAD_LOCAL_INTERFACE_LOG.get(); } /** * 设置当前线程接口请求的日志信息 * * @param interfaceLogDomain */ public static void setThreadLocalInterfaceLog(InterfaceLogDomain interfaceLogDomain) { THREAD_LOCAL_INTERFACE_LOG.set(interfaceLogDomain); } /** * 清除数据 */ public static void clear() { THREAD_LOCAL_INTERFACE_LOG.remove(); } }
InterfaceLogStatus 枚举维护接口请求状态
package com.hl.by.domain.log; /** * @Author: DI.YIN * @Date: 2024/3/13 16:31 * @Version: 1.0.0 * @Description: 接口日志状态 **/ public enum InterfaceLogStatus { SUCCESS("S"), ERROR("E"), DOING("D"), ; private String code; InterfaceLogStatus(String code) { this.code = code; } public String getCode() { return code; } }
第五步设置日志等级并注入自定义日志对象
@Data @Component @ConfigurationProperties(prefix = "open-feign") @RequiredArgsConstructor public class FeignConfig { private Integer connectTimeoutMillis; private Integer readTimeoutMillis; private boolean followRedirects; private Integer maxAttempts; private Long period; private Long maxPeriod; @Autowired private InterfaceLogMapper interfaceLogMapper; /** * 参考SynchronousMethodHandler源码,executeAndDecode方法中,当日志等级不等于Logger.Level.NONE才触发日志显示 * * @return 返回日志等级,openfeign根据日志等级来显示请求响应日志 */ @Bean public Logger.Level level() { return Logger.Level.FULL; } /** * 配置openfeign自定义请求日志记录,将请求日志存入MQ或者数据库 * * @return */ @Bean public Logger feignRemoteLogger() { return new FeignRemoteLogger(interfaceLogMapper); } }
您仅需要将以上代码复制到您的项目中,就可以正常运行!!!现在让我们来测试一下效果。我用Postman调用本地接口,本地接口通过openfeign调用远程接口进行保存商品。
数据库的接口日志表已经成功保存本次请求的信息:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。