文章目录
- 前言
- 一、@FeignClient详解
- 注解使用范围
- 注解属性说明
- `value()`
- `name()`
- `contextId()`
- `qualifiers()`
- `configuration`
- `fallback`
- `fallbackFactory`
- `path`
- 二、openfeign走网关gateway
前言
本文讨论的是springcloud分布式微服务架构下,如何让openfeign请求也走gateway网关
本文是建立在笔者的这篇文章的基础之上:SpringCloud微服务搭建实战
需要自己把项目从gitee上克隆下来,并结合SpringCloud微服务搭建实战博客阅读后,才能顺利阅读本文。
一、@FeignClient详解
注解使用范围
@FeignClient这个注解一般推荐用在接口上,这一点可以从它的源码上看出
@Target规定了这个注解的使用范围,这个Element.Type
,点进去可以看到注释
意思就是可以应用于类、接口(包括注解接口)、枚举或记录(record)声明。这意味着 @FeignClient 不仅可以应用于接口,也可以应用于类、枚举或记录。
但是实际当中我们只能用在接口上来实现远程http请求调用,如下
那为什么只推荐用在接口上呢?原因有以下两点
-
动态代理机制
:Feign 使用 Java 动态代理来实现接口的实例化。如果将 @FeignClient 应用于接口,Feign 可以轻松地生成代理对象。 -
方法定义
:接口中的抽象方法可以方便地定义 HTTP 请求,而不需要具体的实现。
并且spring官方对@FeignClient注解的使用有代码上的限制,在类FeignClientRegistrar
中有如下代码
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
Set<String> basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
else {
for (Class<?> clazz : clients) {
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
}
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition beanDefinition) {
// verify annotated class is an interface
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
String className = annotationMetadata.getClassName();
registerClientConfiguration(registry, name, className, attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
从代码上可以看出,如果注解的类不是接口,则会抛出异常信息
@FeignClient can only be specified on an interface
注解属性说明
下面对@FeignClient的各个属性做解释说明,源码如下(已去除多余注释)
package org.springframework.cloud.openfeign;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {
@AliasFor("name")
String value() default "";
String contextId() default "";
@AliasFor("value")
String name() default "";
String[] qualifiers() default {};
String url() default "";
boolean dismiss404() default false;
Class<?>[] configuration() default {};
Class<?> fallback() default void.class;
Class<?> fallbackFactory() default void.class;
String path() default "";
boolean primary() default true;
}
value()
- 服务名称,带有可选的协议前缀。它是 name() 的同义词。
- 必须为所有客户端指定名称,无论是否提供了 URL。
- 可以通过属性键来指定,例如 ${propertyKey}。
value = “consumer-module”,这个就是接口所要调用的微服务模块的应用名称,如下图
当然这里是写死的(日常开发中都是固定值,毕竟这是某一微服务模块的统一对外的接口),如果应用名称是灵活可配置的,可以这么写 value = "${service.name}"
属性配置在bootstrap文件中,这个配置也可以挪到nacos中去
name()
- 服务 ID,带有可选的协议前缀。它是 value() 的同义词。
name和value属性互为同义词,即这两个只使用其中一个时,随便用哪一个都可以 ,value = “consumer-module” 等价于 name= “consumer-module”,若两个都使用,则优先以value值为准。name的其他如动态属性用法和value一致。
contextId()
- 确保每个FeignClient接口有一个唯一的标识,特别是在多个接口调用同一个服务提供者的情况下,避免混淆和错误。
这很好理解,标识FeignClient接口的唯一性。笔者这里是producer-module通过@FeignClient注解的接口调用consumer-module模块的controller某个接口,那如果有另一个模块也通过另一个@FeignClient注解的接口调用consumer-module的相同接口,就必须给每个@FeignClient注解标注的接口加上contextId属性了,一般就是接口的名字首字母小写。
qualifiers()
- 在多环境或多客户端配置中进一步区分不同的 Feign 客户端实例。通过指定不同的 qualifiers,可以实现更灵活的配置管理和环境隔离。
这个用的很少基本淘汰不用了,如果生产环境只有一个,没有必要引入更多的代码。当然如果不同的生产环境想调用同的接口,可以尝试下。
configuration
- 定义了Feign客户端的自定义配置类。这些类可以包含对构成客户端的部分组件的覆盖定义,
比如我定义一个CustomClientConfig
类
package com.microservice.api.consumer.config;
import feign.Client;
import feign.okhttp.OkHttpClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CustomClientConfig {
@Bean
public Client feignClient() {
// 使用 OkHttpClient 作为 HTTP 客户端
return new OkHttpClient();
}
}
consumer-module-api的pom文件引入如下依赖
<!-- Feign 的 OkHttpClient 支持 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
在接口上就可以这么写 configuration = {CustomClientConfig.class}
,这样openfeign在请求时就会使用okhttp了,在Nacos的共享配置share-config-dev.yml
加入如下内容
#service-base-url
service:
gateway-base-url: http://localhost:8080
producer-base-url: http://localhost:8081
consumer-base-url: http://localhost:8082
要使用OkhttpClient,必须显示写上url = “ s e r v i c e . c o n s u m e r − b a s e − u r l " 。至于 p a t h ,如果你的请求直接调用服务提供者,则不需要加上。如果想让内部服务间的请求也从 g a t e w a y 走,那么 u r l = " {service.consumer-base-url}"。至于path,如果你的请求直接调用服务提供者,则不需要加上。如果想让内部服务间的请求也从gateway走,那么url = " service.consumer−base−url"。至于path,如果你的请求直接调用服务提供者,则不需要加上。如果想让内部服务间的请求也从gateway走,那么url="{service.gateway-base-url}”,同时放开path=“consumer”。
path = “consumer”,这样produce-module通过openfeign调用consumer-module请求地址会变成http://localhost:8080/consumer/message/commit
,根据gateway的路由匹配规则,会找到consumer服务进行调用
fallback
- 指定当Feign客户端接口出现故障时的回退类。回退类必须实现由@FeignClient注解标注的接口,并且必须是一个有效的Spring Bean。
- 当你需要一个简单的静态回退逻辑时,使用 fallback。
- 适用于大多数简单场景。
这个没什么好说的就是异常回调类,且必须加上注解@Component保证注册到Spring容器
RemoteConsumeFallback类如下
package com.microservice.api.consumer.fallback;
import com.microservice.api.consumer.RemoteConsumerService;
import com.microservice.core.domain.AjaxResult;
import org.springframework.stereotype.Component;
/**
* 当不使用FallbackFactory时,使用此Fallback
*/
@Component
public class RemoteConsumeFallback implements RemoteConsumerService {
@Override
public AjaxResult getConsumeMessage() {
return AjaxResult.error("AjaxResult获取消费信息失败");
}
@Override
public AjaxResult consumeMessagePost() {
return AjaxResult.error("AjaxResult提交消费者信息失败");
}
}
fallbackFactory
- 定义了指定Feign客户端接口的回退工厂。回退工厂必须生成实现了该接口的回退类实例,并且也必须是一个有效的Spring Bean。
日常开发中我们可能直接就使用fallbackFactory不使用fallback,其实它的正确合理用法如下,首先定义一个feign的客户端ConsumerModuleClient
package com.microservice.api.consumer.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.http.ResponseEntity;
@FeignClient(
name = "consumer-module",
fallbackFactory = ConsumerModuleFallbackFactory.class
)
public interface ConsumerModuleClient {
@PostMapping("/message/commit")
ResponseEntity<String> commitMessage(@RequestBody String message);
}
然后定义一个回退类工厂ConsumerModuleFallbackFactory
,其中可根据远程调用抛出的异常类型分别返回不同的fallback回退类
ConsumerModuleFallbackFactory代码如下
package com.microservice.api.consumer.fallback;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import javax.servlet.http.HttpServletResponse;
@Component
public class ConsumerModuleFallbackFactory implements FallbackFactory<ConsumerModuleClient> {
@Override
public ConsumerModuleClient create(Throwable cause) {
if (cause instanceof HttpClientErrorException) {
return new HttpClientErrorFallback((HttpClientErrorException) cause);
} else if (cause instanceof HttpServerErrorException) {
return new HttpServerErrorFallback((HttpServerErrorException) cause);
} else {
return new DefaultFallback(cause);
}
}
}
HttpClientErrorFallback的代码如下
package com.microservice.api.consumer.fallback;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException;
@Component
public class HttpClientErrorFallback implements ConsumerModuleClient {
private final HttpClientErrorException cause;
public HttpClientErrorFallback(HttpClientErrorException cause) {
this.cause = cause;
}
@Override
public ResponseEntity<String> commitMessage(String message) {
return ResponseEntity.status(cause.getStatusCode())
.body("Client error: " + cause.getMessage());
}
}
HttpServerErrorFallback代码如下
package com.microservice.api.consumer.fallback;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpServerErrorException;
@Component
public class HttpServerErrorFallback implements ConsumerModuleClient {
private final HttpServerErrorException cause;
public HttpServerErrorFallback(HttpServerErrorException cause) {
this.cause = cause;
}
@Override
public ResponseEntity<String> commitMessage(String message) {
return ResponseEntity.status(cause.getStatusCode())
.body("Server error: " + cause.getMessage());
}
}
fallbackFactory回退工厂是用来根据异常的不同返回不同的回退类fallback的
我们一般是怎么用的呢?直接这么用的,并没有根据异常类型的不同区分使用不同的fallback回退类,而且内部回退类还是直接匿名内部类实现的new RemoteConsumerService(),搞笑吧?大家都这么写,我们也这么写,却不知道其是否合理。
path
- 指定应用于所有方法级别映射的路径前缀。
这个很简单,就是请求路径的前缀,请求从gateway走,就加上前缀path = "consumer"让路由匹配,请求直接调用服务,则去除前缀。上下两个不同的@FeignClient注解说明了这一点
//@FeignClient(contextId = "remoteConsumerService",
// value = "consumer-module",
// url = "${service.gateway-base-url}",
// configuration = {CustomClientConfig.class},
// path = "consumer",
// fallback = RemoteConsumeFallback.class)
@FeignClient(contextId = "remoteConsumerService",
value = "consumer-module",
url = "${service.consumer-base-url}",
configuration = {CustomClientConfig.class},
fallbackFactory = RemoteConsumeFallbackFactory.class)
public interface RemoteConsumerService {
@GetMapping(value = "/message/handle")
AjaxResult getConsumeMessage();
@PostMapping(value = "/message/commit")
AjaxResult consumeMessagePost();
}
二、openfeign走网关gateway
openfeign默认请求是不走网关gateway的,都是内部服务间的直接调用那如何使其通过网关呢?下面通过示例说明
首先在gateway-module下创建一个请求过滤器RequestLoggingFilter
package com.microservice.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class RequestLoggingFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求信息
ServerHttpRequest request = exchange.getRequest();
// 打印请求信息
System.out.println("Request Path: " + request.getURI());
// 将请求继续传递给下一个过滤器
return chain.filter(exchange);
}
@Override
public int getOrder() {
// 设置较低的order值以确保此过滤器在路由匹配之前执行
return Ordered.HIGHEST_PRECEDENCE + 1;
}
}
注意:getOrder方法是提升次过滤器的优先级,展示路由的完整路径,避免展示被gateway网关去除前缀后的路径
直接consumer调用方式如下
使用这种方式请求
查看gateway的控制台日志,可以看到只有一次,请求是从gateway进入路由匹配到produce-module再直接通过feign调用consumer-module
使用如下配置时
@FeignClient(contextId = "remoteConsumerService",
value = "consumer-module",
url = "${service.gateway-base-url}",
path = "consumer",
fallback = RemoteConsumeFallback.class)
再次请求会发现gateway打印了两次日志,原因就是openfeign的请求又被转到了gateway网关
以上就是笔者对openfeign注解的理解和应用,希望能对大家有所帮助