1. OpenFeign 简介
Feign是一个声明式Web Service客户端。使用Feign能让编写Web Service客户端更加简单,它的使用方法是定义一个接口,然后在上面添加注解,同时也支持JAX-RS标准的注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
Feign 灵感来源于 Retrofit , JAXRS-2.0 和 WebSocket,Feign 最初是为了降低统一绑定 Denominator 到 HTTP APIs 的复杂度,不管是否是 Restful。
Feign旨在使编写Java Http客户端变得更容易,在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它,即可完成对服务提供方的接口绑定,简化了使用Spring Cloud时,自动封装服务调用客户端的开发量。
Feign | OpenFeign |
---|---|
Feign是spring cloud组件中的一个轻量级Restful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。 | OpenFeign是Spring Cloud在Feign的基础上支持了Spring MVC的注解,如@RequestMapping等等。OpenFeign的@FiegnClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务 |
2. OpenFiegn的使用
- 在Spring Cloud应用中使用Feign组件,首先需要在依赖包中加入以下依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
- 再定义一个可供调用的Feign组件示例如下:
@FeignClient(value = "servicename") public interface IUserService { /** * 获取用户信息 * * @param username 用户名 * @return 用户信息 */ @RequestMapping(value = "getUserDetailInfo", method = RequestMethod.GET) ApiResult<UserDetailInfo> getUserDetailInfo(@RequestParam("username") String username); }
上述代码中,每一个方法都代表通过 Feign
请求的一个接口, @RequestMapping
指定请求地址和请求方法。 @FeignClient
则用于指定调用的微服务,结合负载均衡框架在注册中心注册的服务列表中选择一个合适的服务地址。 目前对FeignClient的应用主要有2种方式:
- 服务提供方定义好
Feign
组件,自己的Controller
实现定义好的Feign组件接口,然后把Feign组件打包成SDK提供给调用方,这样的好处是便于后期服务提供方统一升级组件,比如更换调用路径和参数等; - 服务调用方自己封装
Feign
组件,这样的好处是调用方可以灵活的自定义Feign组件;
注意:目前Feign调用对基本类型参数传值支持不是很好,所以参数最好封装成一个 DTO对象 进行传输。如果是JSON参数,最好再定义Feign组件时加上 @RequestBody
注解。
- 启用Feign组件是使用Feign的第一步,使用如下注解开启:
//启用Feign组件并配置扫描包路径 @EnableFeignClients(basePackages = {"com.xxx.service.api"})
- 接下来就是在需要调用接口的地方用Spring Bean一样调用其他服务了,示例如下:
@Autowired private IUserService userservice;
3. OpenFeign源码解析
3.1. Feign Bean创建
Feign组件初始化是从 @EnableFeignClients
注解开始的,注解源码截图如下:
@EnableFeignClients
核心有2个方法, basePackages
与 defaultConfiguration
,前者用于定义扫描包路径,后者用于定义@FeignClient组件的配置类,在配置类中可以自己定义Feign请求的 Decoder
解码器、 Encoder
编码器、 Contract
组件扫描构造器。 在注解上有一个关键注解 @Import(FeignClientsRegistrar.class)
,导入了Feign组件的注册器,用于扫描Feign组件与初始化Feign组件的Bean定义信息,各阶段作用建下述源码注释。
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { // patterned after Spring Integration IntegrationComponentScanRegistrar // and RibbonClientsConfigurationRegistgrar private ResourceLoader resourceLoader; private Environment environment; FeignClientsRegistrar() { } static void validateFallback(final Class clazz) { Assert.isTrue(!clazz.isInterface(), "Fallback class must implement the interface annotated by @FeignClient"); } static void validateFallbackFactory(final Class clazz) { Assert.isTrue(!clazz.isInterface(), "Fallback factory must produce instances " + "of fallback classes that implement the interface annotated by @FeignClient"); } static String getName(String name) { if (!StringUtils.hasText(name)) { return ""; } String host = null; try { String url; if (!name.startsWith("http://") && !name.startsWith("https://")) { url = "http://" + name; } else { url = name; } host = new URI(url).getHost(); } catch (URISyntaxException e) { } Assert.state(host != null, "Service id not legal hostname (" + name + ")"); return name; } static String getUrl(String url) { if (StringUtils.hasText(url) && !(url.startsWith("#{") && url.contains("}"))) { if (!url.contains("://")) { url = "http://" + url; } try { new URL(url); } catch (MalformedURLException e) { throw new IllegalArgumentException(url + " is malformed", e); } } return url; } static String getPath(String path) { if (StringUtils.hasText(path)) { path = path.trim(); if (!path.startsWith("/")) { path = "/" + path; } if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } } return path; } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerDefaultConfiguration(metadata, registry); registerFeignClients(metadata, registry); } private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true); if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) { String name; if (metadata.hasEnclosingClass()) { name = "default." + metadata.getEnclosingClassName(); } else { name = "default." + metadata.getClassName(); } registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration")); } } 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(scanne