spring cloud FeignClient注解介绍

spring cloud FeignClient注解介绍

参考地址:

那天晚上和@FeignClient注解的深度交流 - 掘金 (juejin.cn)

Spring Cloud openFeign学习【3.0.2版本】 - SegmentFault 思否

前言

最近在学习spring全家桶的时候,就自然而然的使用到了 feign。 但是在使用的过程中由于经常会出现Bean的名称冲突错误,具体内容如下:

Description:
The bean 'xxxxx.yyyy', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

之前是因为没有时间,就没有详细的研究是什么原因导致的,最近稍微没有那么忙的时候顺便研究一下这个问题。

本文章绝大部分内容来源于转载地址,这里只是做汇总和记录,目的是为了学习,详情请直接参考源文章地址。

一、Feign基本介绍

首先来个基本的普及,怕有些同学还没接触过Spring Cloud。Feign是Netflix开源的一个REST客户端,通过定义接口,使用注解的方式描述接口的信息,就可以发起接口调用。

GitHub地址:GitHub - OpenFeign/feign: Feign makes writing java http clients easier

下面是GitHub主页上给的一个最基本的使用示列,示列中采用Feign调用GitHub的接口。

interface GitHub {
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

  @RequestLine("POST /repos/{owner}/{repo}/issues")
  void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);

}

public static class Contributor {
  String login;
  int contributions;
}

public static class Issue {
  String title;
  String body;
  List<String> assignees;
  int milestone;
  List<String> labels;
}

public class MyApp {
  public static void main(String... args) {
    GitHub github = Feign.builder()
                         .decoder(new GsonDecoder())
                         .target(GitHub.class, "https://api.github.com");
  
    // Fetch and print a list of the contributors to this library.
    List<Contributor> contributors = github.contributors("OpenFeign", "feign");
    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + " (" + contributor.contributions + ")");
    }
  }
}

二、Spring Cloud OpenFeign介绍

Spring Cloud OpenFeign是Spring Cloud团队将原生的Feign结合到Spring Cloud中的产物。

从上面原生Feign的使用示列来看,用的注解都是Feign中自带的,但我们在开发中基本上都是基于Spring MVC的注解,不是很方便调用。

所以Spring Cloud OpenFeign扩展了对Spring MVC注解的支持,同时还整合了Ribbon和Eureka来提供均衡负载的HTTP客户端实现。

GitHub地址:https://github.com/spring-cloud/spring-cloud-openfeign

官方提供的使用示列:

@FeignClient("stores")
public interface StoreClient {
    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    List<Store> getStores();

    @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
    Store update(@PathVariable("storeId") Long storeId, Store store);
}

三、Ribbon、Feign和OpenFeign的区别

3.1、Ribbon

Ribbon 是 Netflix开源的基于HTTP和TCP等协议负载均衡组件

Ribbon 可以用来做客户端负载均衡,调用注册中心的服务

Ribbon的使用需要代码里手动调用目标服务,请参考官方示例:官方示例

3.2、Feign

Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端。

Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务

Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。

Feign支持的注解和用法请参考官方文档:官方文档

Feign本身不支持Spring MVC的注解,它有一套自己的注解

3.3、OpenFeign

OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。

OpenFeign的@FeignClient可以解析SpringMVC的**@RequestMapping**注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

根据上面的描述,绘制如下的表格内容:

-RibbonFeignOpenFeign
使用方式手动调用目标服务Feign的注解定义接口,调用接口就可以调用注册中心服务可以直接使用服务调用的方式调用对应的服务
作用客户端负载均衡,服务注册中心的服务调用客户端负载均衡,服务注册中心的服务调用动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务
开发商NetfixSpring CloudSpring Cloud
特点基于HTTP和TCP等协议负载均衡组件轻量级RESTful的HTTP服务客户端。依靠自我实现的注解进行请求处理支持了Spring MVC的注解的轻量级RESTful的HTTP服务客户端
目前情况维护中停止维护维护中

四、FeignClient注解的使用介绍

4.1、value, name

value和name的作用一样,如果没有配置url那么配置的值将作为服务名称,用于服务发现。反之只是一个名称。

4.2、serviceId

serviceId已经废弃了,直接使用name即可。

4.3、contextId

比如我们有个user服务,但user服务中有很多个接口,我们不想将所有的调用接口都定义在一个类中,比如:

Client 1:

@FeignClient(name = "optimization-user")
public interface UserRemoteClient {
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id") int id);
}

Client 2:

@FeignClient(name = "optimization-user")
public interface UserRemoteClient2 {
	@GetMapping("/user2/get")
	public User getUser(@RequestParam("id") int id);
}

这种情况下启动就会报错了,因为Bean的名称冲突了,具体错误如下:

Description:
The bean 'optimization-user.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

解决方案可以增加下面的配置,作用是允许出现beanName一样的BeanDefinition。

spring.main.allow-bean-definition-overriding=true

另一种解决方案就是为每个Client手动指定不同的contextId,这样就不会冲突了。

上面给出了Bean名称冲突后的解决方案,下面来分析下contextId在Feign Client的作用,在注册Feign Client Configuration的时候需要一个名称,名称是通过getClientName方法获取的:

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
	// 省略部分代码..........
    for (String basePackage : basePackages) {
        Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
        for (BeanDefinition candidateComponent : candidateComponents) {
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                // 省略部分代码

                //	下面为核心代码
                String name = getClientName(attributes);
                registerClientConfiguration(registry, name,attributes.get("configuration"));
                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }
}

getClientName方法的内容如下:

private String getClientName(Map<String, Object> client) {
    if (client == null) {
        return null;
    } else {
        String value = (String)client.get("contextId");
        if (!StringUtils.hasText(value)) {
            value = (String)client.get("value");
        }

        if (!StringUtils.hasText(value)) {
            value = (String)client.get("name");
        }

        if (!StringUtils.hasText(value)) {
            value = (String)client.get("serviceId");
        }

        if (StringUtils.hasText(value)) {
            return value;
        } else {
            throw new IllegalStateException("Either 'name' or 'value' must be provided in @" +                                                                                                              FeignClient.class.getSimpleName());
        }
    }
}

可以看到如果配置了contextId 就会用 contextId,如果没有配置就会去 value 然后是 name 最后是 serviceId

默认都没有配置,当出现一个服务有多个Feign Client的时候就会报错了。

其次的作用是在注册FeignClient中,contextId 会作为 Client 别名的一部分,如果配置了 @qualifier 优先用 @qualifier 作为别名。

private void registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata, 
                                 															Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    BeanDefinitionBuilder definition = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientFactoryBean.class);
    validate(attributes);
    definition.addPropertyValue("url", getUrl(attributes));
    definition.addPropertyValue("path", getPath(attributes));
    String name = getName(attributes);
    definition.addPropertyValue("name", name);
    String contextId = getContextId(attributes);
    definition.addPropertyValue("contextId", contextId);
    definition.addPropertyValue("type", className);
    definition.addPropertyValue("decode404", attributes.get("decode404"));
    definition.addPropertyValue("fallback", attributes.get("fallback"));
    definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
    definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

    String alias = contextId + "FeignClient";
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
    beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);

    // has a default, won't be null
    boolean primary = (Boolean) attributes.get("primary");

    beanDefinition.setPrimary(primary);

    // 配置了qualifier优先用qualifier
    String qualifier = getQualifier(attributes);
    if (StringUtils.hasText(qualifier)) {
        alias = qualifier;
    }

    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

4.4、url

url用于配置指定服务的地址,相当于直接请求这个服务,不经过Ribbon的服务选择。像调试等场景可以使用。

使用示列

@FeignClient(name = "optimization-user", url = "http://localhost:8085")
public interface UserRemoteClient {
	
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id") int id);
}

4.5、decode404

当调用请求发生404错误时,decode404的值为true,那么会执行decoder解码,否则抛出异常。

解码也就是会返回固定的数据格式给你:

{"timestamp":"2020-01-05T09:18:13.154+0000","status":404,"error":"Not Found","message":"No message available","path":"/user/get11"}

抛异常的话就是异常信息了,如果配置了fallback那么就会执行回退逻辑:

4.6、configuration

configuration是配置Feign配置类,在配置类中可以自定义Feign的Encoder、Decoder、LogLevel、Contract等。

configuration定义

public class FeignConfiguration {
	@Bean
	public Logger.Level getLoggerLevel() {
		return Logger.Level.FULL;
	}
	@Bean
	public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
		return new BasicAuthRequestInterceptor("user", "password");
	}
	
	@Bean
	public CustomRequestInterceptor customRequestInterceptor() {
		return new CustomRequestInterceptor();
	}
	// Contract,feignDecoder,feignEncoder.....
}

使用示列

@FeignClient(value = "optimization-user", configuration = FeignConfiguration.class)
public interface UserRemoteClient {
	
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id")int id);
	
}

4.7、fallback

定义容错的处理类,也就是回退逻辑,fallback的类必须实现Feign Client的接口,无法知道熔断的异常信息。

fallback定义

@Component
public class UserRemoteClientFallback implements UserRemoteClient {
	@Override
	public User getUser(int id) {
		return new User(0, "默认fallback");
	}
	
}

使用示列

@FeignClient(value = "optimization-user", fallback = UserRemoteClientFallback.class)
public interface UserRemoteClient {
	
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id")int id);
	
}

4.8、fallbackFactory

也是容错的处理,可以知道熔断的异常信息。

fallbackFactory定义

@Component
public class UserRemoteClientFallbackFactory implements FallbackFactory<UserRemoteClient> {
	private Logger logger = LoggerFactory.getLogger(UserRemoteClientFallbackFactory.class);
	
	@Override
	public UserRemoteClient create(Throwable cause) {
		return new UserRemoteClient() {
			@Override
			public User getUser(int id) {
				logger.error("UserRemoteClient.getUser异常", cause);
				return new User(0, "默认");
			}
		};
	}
}

使用示列

@FeignClient(value = "optimization-user", fallbackFactory = UserRemoteClientFallbackFactory.class)
public interface UserRemoteClient {
	
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id")int id);
	
}

4.9、path

path定义当前FeignClient访问接口时的统一前缀,比如接口地址是/user/get, 如果你定义了前缀是user, 那么具体方法上的路径就只需要写/get 即可。

使用示列

@FeignClient(name = "optimization-user", path="user")
public interface UserRemoteClient {
	
	@GetMapping("/get")
	public User getUser(@RequestParam("id") int id);
}

4.10、primary

primary对应的是@Primary注解,默认为true,官方这样设置也是有原因的。当我们的Feign实现了fallback后,也就意味着Feign Client有多个相同的Bean在Spring容器中,当我们在使用@Autowired进行注入的时候,不知道注入哪个,所以我们需要设置一个优先级高的,@Primary注解就是干这件事情的。

4.11、qualifier

qualifier对应的是@Qualifier注解,使用场景跟上面的primary关系很淡,一般场景直接@Autowired直接注入就可以了。

如果我们的Feign Client有fallback实现,默认@FeignClient注解的primary=true, 意味着我们使用@Autowired注入是没有问题的,会优先注入你的Feign Client。

如果你鬼斧神差的把primary设置成false了,直接用@Autowired注入的地方就会报错,不知道要注入哪个对象。

解决方案很明显,你可以将primary设置成true即可,如果由于某些特殊原因,你必须得去掉primary=true的设置,这种情况下我们怎么进行注入,我们可以配置一个qualifier,然后使用@Qualifier注解进行注入,示列如下:

Feign Client定义

@FeignClient(name = "optimization-user", path="user", qualifier="userRemoteClient")
public interface UserRemoteClient {
	
	@GetMapping("/get")
	public User getUser(@RequestParam("id") int id);
}

Feign Client注入

@Autowired
@Qualifier("userRemoteClient")
private UserRemoteClient userRemoteClient;

个对象。

解决方案很明显,你可以将primary设置成true即可,如果由于某些特殊原因,你必须得去掉primary=true的设置,这种情况下我们怎么进行注入,我们可以配置一个qualifier,然后使用@Qualifier注解进行注入,示列如下:

Feign Client定义

@FeignClient(name = "optimization-user", path="user", qualifier="userRemoteClient")
public interface UserRemoteClient {
	
	@GetMapping("/get")
	public User getUser(@RequestParam("id") int id);
}

Feign Client注入

@Autowired
@Qualifier("userRemoteClient")
private UserRemoteClient userRemoteClient;
  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值