openfeign解释及其应用


前言

本文讨论的是springcloud分布式微服务架构下,如何让openfeign请求也走gateway网关

本文是建立在笔者的这篇文章的基础之上:SpringCloud微服务搭建实战

需要自己把项目从gitee上克隆下来,并结合SpringCloud微服务搭建实战博客阅读后,才能顺利阅读本文。


一、@FeignClient详解

注解使用范围

@FeignClient这个注解一般推荐用在接口上,这一点可以从它的源码上看出

在这里插入图片描述

@Target规定了这个注解的使用范围,这个Element.Type,点进去可以看到注释

在这里插入图片描述
意思就是可以应用于类、接口(包括注解接口)、枚举或记录(record)声明。这意味着 @FeignClient 不仅可以应用于接口,也可以应用于类、枚举或记录。

但是实际当中我们只能用在接口上来实现远程http请求调用,如下

在这里插入图片描述

那为什么只推荐用在接口上呢?原因有以下两点

  1. 动态代理机制:Feign 使用 Java 动态代理来实现接口的实例化。如果将 @FeignClient 应用于接口,Feign 可以轻松地生成代理对象。

  2. 方法定义:接口中的抽象方法可以方便地定义 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.consumerbaseurl"。至于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注解的理解和应用,希望能对大家有所帮助

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值