快捷搜索:  汽车  科技

feignclient注解只能用在接口上吗(FeignClient注解的深度交流)

feignclient注解只能用在接口上吗(FeignClient注解的深度交流)官方提供的使用示列:GitHub地址:https://github.com/spring-cloud/spring-cloud-openfeignGitHub地址:https://github.com/OpenFeign/feign下面是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

废话篇

那晚,我和@FeignClient注解的深度交流了一次,爽!

主要还是在技术群里看到有同学在问相关问题,比如: contextId是干嘛的?name相同的多个Client会报错?

然后觉得有必要写篇文章聊聊@FeignClient的使用,百忙之中抽时间,写篇文章不容易啊,记得点赞。

正式篇

Feign基本介绍

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

GitHub地址:https://github.com/OpenFeign/feign

下面是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); }

FeignClient注解的使用介绍

value name

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

serviceId

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

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方法获取的:

String name = getClientName(attributes); registerClientConfiguration(registry name attributes.get("configuration"));

private String getClientName(Map<String Object> client) { if (client == null) { return null; } 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; } 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(); boolean primary = (Boolean) attributes.get("Primary"); // has a default won't be // null 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); }

url

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

使用示列

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

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那么就会执行回退逻辑:

feignclient注解只能用在接口上吗(FeignClient注解的深度交流)(1)

xxx.png

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); }

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); }

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); }

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); }

primary

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

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;


feignclient注解只能用在接口上吗(FeignClient注解的深度交流)(2)

猜您喜欢: