什么是Feign?
Feign是一个开源的Java库,用于简化Web请求的过程。
它通过提供更高级别的抽象来简化RESTful Web服务的实现,消除了样板代码的需要,使代码库更具可读性和可维护性。
Feign是一个流行的Java HTTP客户端库,具有多种优势和特性,是开发基于HTTP的微服务和应用程序的不错选择。
什么是声明式HTTP客户端?
声明式HTTP客户端是一种通过编写Java接口来发起HTTP请求的方式。
Feign会根据我们提供的注解,在接口背后生成实际的实现。
为何使用Feign?
如果我们需要调用大量的API,手动编写HTTP代码或使用难以维护的代码生成方式并不理想。
使用Feign,我们可以通过一个简单的小接口来描述API,让Feign在运行时解释和实现该接口,这样更加简便且易于维护。
谁适合使用Feign?
如果我们在Java代码中需要发起HTTP请求,并且不想编写样板代码,或者不想直接使用像Apache httpclient这样的库,那么Feign是一个很好的选择。
创建基本的Feign客户端
步骤0:添加bom
我们依然使用feign-bom作为版本管理:
<project>
……
<properties>
<openfeign.version>13.4</openfeign.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-bom</artifactId>
<version>${openfeign.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
步骤1:添加Feign依赖
引入feign-core:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
步骤2:定义客户端接口
该接口通常包含带有Feign注解的方法声明。
我们将为服务器上要调用的每个REST端点声明一个客户端接口方法,这些只是声明,无需实现这些方法,Feign会为我们完成。方法签名应包括HTTP方法以及所有必需的数据。
假设我们要定义一个表示计算器服务的接口,它有执行加法、减法、乘法和除法等计算的简单API方法:
public interface CalculatorService {
/**
* 两整数相加。
*
* @param firstNumber 第一个整数
* @param secondNumber 第二个整数
* @return 两数之和
*/
@RequestLine("POST /operations/add?firstNumber={firstNumber}&secondNumber={secondNumber}")
Long add(@Param("firstNumber") Long firstNumber,
@Param("secondNumber") Long secondNumber);
/**
* 两整数相减。
*
* @param firstNumber 第一个整数
* @param secondNumber 第二个整数
* @return 两数之差
*/
@RequestLine("POST /operations/subtract?firstNumber={firstNumber}&secondNumber={secondNumber}")
Long subtract(@Param("firstNumber") Long firstNumber,
@Param("secondNumber") Long secondNumber);
/**
* 两整数相乘。
*
* @param firstNumber 第一个整数
* @param secondNumber 第二个整数
* @return 两数之积
*/
@RequestLine("POST /operations/multiply?firstNumber={firstNumber}&secondNumber={secondNumber}")
Long multiply(@Param("firstNumber") Long firstNumber,
@Param("secondNumber") Long secondNumber);
/**
* 两整数相除。
*
* @param firstNumber 第一个整数
* @param secondNumber 第二个整数,不应为零
* @return 两数之商
*/
@RequestLine("POST /operations/divide?firstNumber={firstNumber}&secondNumber={secondNumber}")
Long divide(@Param("firstNumber") Long firstNumber,
@Param("secondNumber") Long secondNumber);
}
@RequestLine
定义了请求的HTTP方法和url模板,而@Param
定义了一个模板变量。
❝
不用担心,稍后我们将详细了解OpenFeign提供的注解。
步骤3:创建客户端对象
我们使用Feign的builder()方法来准备客户端:
final CalculatorService target = Feign
.builder()
.decoder(new JacksonDecoder())
.target(CalculatorService.class, HOST);
准备客户端的方式有很多种,具体取决于我们的需求。
上面给出的代码片段只是准备客户端的一种简单方式。
我们注册了用于解码JSON响应的解码器,解码器可以根据服务返回的响应的内容类型进行更改,稍后我们将详细了解解码器。
步骤4:使用客户端进行API调用
现在让我们调用客户端的add()方法:
final Long result = target.add(firstNumber, secondNumber);
与其他HTTP客户端相比,使用Feign HTTP客户端调用服务相当简单。
Feign注解
OpenFeign使用一组注解来定义HTTP请求及其参数,常用的OpenFeign注解及其示例如下:
注解 | 描述 | 示例 |
@RequestLine | 指定HTTP方法和路径 | @RequestLine("GET /resource/{id}") |
@Headers | 指定请求的HTTP头 | @Headers("Authorization: Bearer {token}") |
@QueryMap | 将查询参数映射到请求 | @QueryMap Map<String, Object> queryParams |
@Body | 发送特定对象作为请求体 | @Body RequestObject requestObject |
@Param | 向请求添加查询参数 | @Param("id") long resourceId |
@Path | 替换路径中的模板变量 | @Path("id") long resourceId |
@RequestHeader | 向请求添加头 | @RequestHeader("Authorization") String authToken |
@Headers | 指定请求的其他头 | @Headers("Accept: application/json") |
这些注解可以用来定义OpenFeign客户端接口,使其易于使用OpenFeign与远程服务进行交互。我们可以根据特定的API要求混合和匹配这些注解。
处理响应
Feign还提供了一种声明式的API集成方法。
与手动编写处理响应或错误的样板代码不同,Feign允许我们定义自定义处理程序并将其注册到Feign构建器中。
这不仅减少了我们需要编写的代码量,还提高了可读性和可维护性。
让我们看一个解码器的示例:
final CalculatorService target = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(CalculatorService.class, HOST);
这段给定的代码片段展示了创建一个Feign客户端,使用Jackson进行请求编码和响应解码:
- encoder(new JacksonEncoder()):在这里,为Feign客户端设置了一个JacksonEncoder。JacksonEncoder是Feign Jackson模块的一部分,用于将Java对象编码为JSON格式的HTTP请求体。当需要在请求体中发送对象时,这特别有用。
- decoder(new JacksonDecoder()):同样,为Feign客户端设置了一个JacksonDecoder。JacksonDecoder负责将服务器的JSON响应解码为Java对象,它将JSON响应反序列化为相应的Java对象。
处理错误
错误处理是构建健壮可靠应用程序的关键方面,尤其是在进行远程API调用时。Feign提供了强大的功能来帮助有效地处理错误。
Feign让我们对处理意外响应有更多的控制。我们可以通过构建器注册一个自定义的ErrorDecoder。
final CalculatorService target = Feign.builder()
.errorDecoder(new CalculatorErrorDecoder())
.target(CalculatorService.class, HOST);
以下是一个错误处理的示例:
public class CalculatorErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
ExceptionMessage message = null;
try (InputStream bodyIs = response.body().asInputStream()) {
ObjectMapper mapper = new ObjectMapper();
message = mapper.readValue(bodyIs, ExceptionMessage.class);
} catch (IOException e) {
return new Exception(e.getMessage());
}
final String messageStr = message == null? "" : message.getMessage();
switch (response.status()) {
case 400:
return new RuntimeException(messageStr.isEmpty()
? "Bad Request"
: messageStr
);
case 401:
return new RetryableException(response.status(),
response.reason(),
response.request().httpMethod(),
null,
response.request());
case 404:
return new RuntimeException(messageStr.isEmpty()
? "Not found"
: messageStr
);
default:
return defaultErrorDecoder.decode(methodKey, response);
}
}
}
所有HTTP状态不在HTTP 2xx范围内的响应,例如HTTP 400,都将触发ErrorDecoder的decode()方法。
在这个重写的decode()方法中,我们可以处理响应,将失败包装成自定义异常或执行任何其他处理。
我们甚至可以通过抛出RetryableException来再次重试请求,这将调用注册的Retryer。Retryer将在高级技术部分详细解释。
高级技术
集成编码器/解码器
编码器和解码器分别用于对请求和响应数据进行编码/解码。我们根据请求和响应的内容类型选择它们,例如,对于JSON数据可以使用Gson或Jackson。
以下是一个使用Jackson编码器和解码器的示例:
final CalculatorService target = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(CalculatorService.class, HOST);
更改HTTP客户端
默认情况下,Feign使用其自带的HTTP客户端。将Feign的默认HTTP客户端从原始的Apache HTTP Client更改为其他库(如OkHttp)的主要动机是为了获得更好的性能、改进的功能以及与现代HTTP标准更好的兼容性。
现在让我们看看如何覆盖HTTP客户端:
final CalculatorService target = Feign.builder()
.client(new OkHttpClient())
.target(CalculatorService.class, HOST);
配置日志记录器
SLF4JModule用于将Feign的日志记录发送到SLF4J。通过SLF4J,我们可以轻松使用我们选择的日志后端(Logback、Log4J等)。
以下是一个构建客户端的示例:
final CalculatorService target = Feign.builder()
.logger(new Slf4jLogger())
.logLevel(Level.FULL)
.target(CalculatorService.class, HOST);
要在Feign中使用SLF4J,需要将SLF4J模块和我们选择的SLF4J绑定添加到类路径中,然后按照上述方式配置Feign使用Slf4jLogger。
配置请求拦截器
Feign中的请求拦截器允许我们在将HTTP请求发送到远程服务器之前对其进行自定义和操作。它们可用于多种目的,例如添加自定义头、日志记录、身份验证或请求修改。
以下是我们可能希望在Feign中使用请求拦截器的原因:
- 身份验证:我们可以使用请求拦截器向每个请求添加身份验证令牌或凭据,例如添加带有JWT令牌的“Authorization”头。
- 日志记录:拦截器有助于记录传入和传出的请求和响应,这对于调试和监控很有用。
- 请求修改:我们可以在发送请求之前修改请求,包括更改头、查询参数甚至请求体。
- 速率限制:通过检查发出的请求数量来决定是否允许或阻止请求,从而实现速率限制。
- 缓存:根据特定标准缓存请求/响应数据。
以下是一个演示如何使用请求拦截的代码片段:
static class AuthorizationInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 检查令牌是否存在,如果不存在则添加它
template.header("Authorization", "Bearer " + generatedToken);
}
}
public class CalculatorServiceTest {
public static void main(String[] args) {
final AuthorizationInterceptor interceptor = new AuthorizationInterceptor();
final CalculatorService target = Feign.builder()
.requestInterceptor(interceptor)
.target(CalculatorService.class, HOST);
}
}
实现RequestInterceptor并覆盖其apply()方法,以根据需要对请求进行任何修改。
配置重试器
OpenFeign Retryer是一个组件,允许我们配置Feign在请求失败时如何处理重试。它对于处理网络通信中的瞬时故障特别有用,我们可以指定Feign应自动重试失败请求的条件。
重试器配置
要在OpenFeign中使用重试器,需要提供Retryer接口的实现。Retryer接口有两个方法:
- boolean continueOrPropagate(int attemptedRetries, int responseStatus, Request request):此方法用于确定是继续重试还是传播错误。它接受尝试重试的次数、HTTP响应状态和请求作为参数,并返回true以继续重试或false以传播错误。
- Retryer clone():此方法创建重试器实例的克隆。
默认重试器
Feign提供了一个默认的重试器实现,名为Retryer.Default。当我们创建Feign客户端而未显式指定自定义重试器时,将使用此默认重试器。
它提供了两个工厂方法来创建Retryer对象。
第一个工厂方法不需要任何参数:
public Default() {
this(100L, TimeUnit.SECONDS.toMillis(1L), 5);
}
它定义了一个简单的重试策略,具有以下特点:
- 最大尝试次数:对于失败的请求,最多允许5次重试尝试。
- 退避期:在重试之间使用指数退避策略,从100毫秒的退避开始,每次后续重试将退避时间加倍。
- 可重试异常:如果请求导致任何被认为可重试的异常(通常包括网络相关异常,如连接超时或套接字异常),则会重试请求。
第二个工厂方法需要一些参数。如果默认配置不适合我们,可以使用它:
public Default(long period, long maxPeriod, int maxAttempts)
// 使用它创建重试器
new Retryer.Default(1, 100, 10);
虽然Feign提供的默认重试器涵盖了许多常见的重试场景,但在某些情况下,我们可能希望定义自定义重试器。以下是定义自定义重试器的一些动机:
- 精细控制:如果我们需要对默认重试行为进行更多控制,例如指定不同的最大重试次数或自定义退避策略,自定义重试器允许我们根据特定要求定制行为。
- 重试逻辑:在某些情况下,我们可能只希望针对特定的响应代码或异常重试请求。自定义重试器允许我们实现自己的逻辑来确定何时应该进行重试。
- 日志记录和指标:如果我们想要记录或收集与重试尝试相关的指标,实现自定义重试器提供了添加此功能的机会。
- 与断路器集成:如果我们将断路器模式与Feign结合使用,自定义重试器可以与断路器的状态集成,以便在决定何时重试或何时打开断路器时做出更明智的决策。
- 非标准重试策略:对于不适合默认重试器提供的标准重试策略的场景,例如速率限制的API或具有特定重试要求的API,我们可以定义适合我们用例的自定义重试器。
以下是在OpenFeign中实现自定义Retryer的示例:
public class CalculatorRetryer implements Retryer {
/**
* 重试之间等待的毫秒数
*/
private final long period;
/**
* 最大重试次数
*/
private final int maxAttempts;
private int attempt = 1;
@Override
public void continueOrPropagate(RetryableException e) {
log.info("Feign retry attempt {} of {} due to {}",
attempt,
maxAttempts,
e.getMessage());
if (++attempt > maxAttempts) {
throw e;
}
if (e.status() == 401) {
try {
Thread.sleep(period);
} catch (InterruptedException ex) {
throw e;
}
} else {
throw e;
}
}
@Override
public Retryer clone() {
return this;
}
public int getRetryAttempts() {
return attempt - 1; // 减去1以排除初始尝试
}
}
它专门重试HTTP 401错误。
总之,当我们需要对HTTP请求中的重试处理方式有更大的控制权和灵活性时,就有必要在Feign中创建自定义重试器。当我们的需求与默认重试器的行为不同时,自定义重试器允许我们根据特定用例修改重试逻辑。
断路器
断路器通常使用单独的库或工具(如Netflix Hystrix、Resilience4j或Spring Cloud Circuit Breaker)来实现。
为何使用断路器?
在Feign中使用断路器的主要动机是增强基于微服务的应用程序的弹性。以下是一些关键原因:
- 故障隔离:断路器通过隔离故障组件,防止一个服务中的故障级联到其他服务。
- 快速失败:当电路打开(表示故障状态)时,后续请求会“快速失败”,而不会尝试向可能无响应或故障的服务发出调用,从而减少延迟和资源消耗。
- 优雅降级:当依赖服务出现问题时,断路器允许应用程序优雅降级,确保它能够继续提供一组简化的功能。
- 监控和指标:断路器提供指标和监控功能,使我们能够跟踪服务的健康状况和性能。
配置断路器
HystrixFeign用于配置Hystrix提供的断路器支持。
Hystrix是一个延迟和容错库,旨在隔离分布式环境中对远程系统、服务和第三方库的访问点。它有助于阻止级联故障,并在故障不可避免的复杂分布式系统中实现弹性。
要在Feign中使用Hystrix,需要将Hystrix模块添加到类路径中,并使用HystrixFeign构建器,如下所示:
final CalculatorService target = HystrixFeign.builder()
.target(CalculatorService.class, HOST);
让我们看看如何使用回退类来处理服务错误。
在Hystrix中,回退类是为Hystrix命令定义回退逻辑的另一种方式,而不是直接在Hystrix命令类的getFallback方法中定义回退逻辑。回退类提供了关注点分离,使我们能够将命令类专注于主要逻辑,并将回退逻辑委托给单独的类。这可以提高代码的组织性和可维护性。
以下是为CalculatorService实现回退的示例代码:
@Slf4j
public class CalculatorHystrixFallback implements CalculatorService {
@Override
public Long add(Long firstNumber, Long secondNumber) {
log.info("[Fallback add] Adding {} and {}", firstNumber, secondNumber);
return firstNumber + secondNumber;
}
@Override
public Long subtract(Long firstNumber, Long secondNumber) {
return null;
}
@Override
public Long multiply(Long firstNumber, Long secondNumber) {
return null;
}
@Override
public Long divide(Long firstNumber, Long secondNumber) {
return null;
}
}
为了演示回退,我们仅实现了add方法。然后在构建客户端时使用此回退:
final CalculatorHystrixFallback fallback = new CalculatorHystrixFallback();
final CalculatorService target = HystrixFeign.builder()
.decoder(new JacksonDecoder())
.target(CalculatorService.class,
HOST, fallback);
当add端点返回错误或电路打开时,Hystrix将调用add回退方法。
收集指标
Feign本身不像其他一些库或框架那样提供内置的指标功能API。
与Feign相关的指标,如请求持续时间、错误率或重试次数,通常需要使用外部库或工具来收集和跟踪。
在Java应用程序中收集指标的流行库包括Micrometer和Dropwizard Metrics。
以下是如何使用常用的Micrometer库来收集和报告与Feign调用相关的指标:
public class CalculatorServiceTest {
public static void main(String[] args) {
final CalculatorService target = Feign.builder()
.addCapability(new MicrometerCapability())
.target(CalculatorService.class, HOST);
target.contributors("OpenFeign", "feign");
// 从此时起指标将可用
}
}
请注意,我们需要将Micrometer作为依赖项添加到项目中并进行适当配置。
文末总结
本文算是温故知新的一篇,由浅入深介绍了使用OpenFeign的5个步骤和7个高级功能,OpenFeign的使用方面,已经涵盖全了。后续再看看在SpringCloud中,OpenFeign的使用,以及在于Ribbon等组件结合使用时,又能碰撞出哪些火花。