SpringCloud-OpenFeign的配置使用和分析
1.说明
Feign是声明性的web服务客户端。 它使编写web服务客户端更加容易。要使用Feign,请创建一个接口并对其进行注释。它具有可插入的注释支持,包括Feign注释和JAX-RS注释。Feign还支持可插拔编码器和解码器。Spring Cloud添加了对Spring MVC注释的支持,并支持使用Spring Web中默认使用的同一HttpMessageConverters。Spring Cloud集成了Ribbon和Eureka以在使用Feign时提供负载平衡的http客户端。
简单的说,使用Feign进行rest服务请求调用更偏向类似于java编程中接口调用,可以认为是对Ribbon + RestTemplate的进一步封装,但是两者还是有区别的!!!!
2.配置和使用
-
在API的模块的pom中添加依赖,并创建service虚拟客户端(记住是接口),且在该接口上添加@FeignClient注解(value值为服务实例名称)
api-pom.xml
<!-- 导入OpenFeign依赖 --> <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
DeptClientService.java
package com.laoliu.springcloud.service; import com.laoliu.springcloud.pojo.Dept; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import java.util.List; @FeignClient(value = "SPRINGCLOUD-PROVIDER-DEPT") //value为服务实例名称 // 以这种格式,通过源码可以知道,届时将会被拼接为http://SPRINGCLOUD-PROVIDER-DEPT/rest服务名 public interface DeptClientService { @GetMapping("/dept/get/{id}") public Dept queryById(@PathVariable("id") Long id); @PostMapping("/dept/add") public boolean addDept(Dept dept); @GetMapping("/dept/list") public List<Dept> queryAll(); }
项目模块结构图:
-
在consumer模块添加依赖,启动类上添加 @EnableFeignClients,在controller层中@Autowired 获取ApiService接口并使用
consumer-pom.xml
<dependencies> <dependency> <groupId>com.laoliu</groupId> <artifactId>springcloud-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <version>3.1.2</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <!-- springCloud2020 版本 把Bootstrap被默认禁用, spring.config.import加入了对解密的支持。对于Config Client、Consul、Vault和Zookeeper的配置导入, 如果需要使用原来的配置引导功能, 那么需要将org.springframework.cloud:spring-cloud-starter-bootstrap依赖引入到工程中 这样才能正常使用springCloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> <!-- 导入OpenFeign依赖 --> <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies>
application.yml
server: port: 80 eureka: client: register-with-eureka: false # false 不是服务提供者,不需要注册到Eureka中 fetch-registry: true # true 消费者需要检索注册中心服务才能调用实例,否则找不到实例,调用失败,报错!!!!! service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
启动类
package com.laoliu.springcloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableEurekaClient //开启Eureka @EnableFeignClients //开启Feign public class DeptConsumerOpenFeign80 { public static void main(String[] args) { SpringApplication.run(DeptConsumerOpenFeign80.class); } }
Controller层的类
package com.laoliu.springcloud.controller; import com.laoliu.springcloud.pojo.Dept; import com.laoliu.springcloud.service.DeptClientService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.List; import java.util.Map; @RestController public class DeptConsumerController { @Autowired private DeptClientService clientService; //从Spring容器中拿到服务接口 @PostMapping("/consumer/dept/add") public boolean add(Dept dept){ return this.clientService.addDept(dept); // 方法调用 } @GetMapping("/consumer/dept/get/{id}") public Dept get(@PathVariable("id") Long id){ return this.clientService.queryById(id); // 方法调用 } @GetMapping("/consumer/dept/list") public List<Dept> getAll(){ return this.clientService.queryAll(); // 方法调用 } }
通过上述的调用方式,可以看出很符合我们熟悉的java接口调用开发,相对Ribbon + RestTemplate来说,可读性较强,但是性能也相比较较低。
项目模块结构图:
-
启动Eeruka注册中心服务,provider服务和consumer服务,进入浏览器发送请求,查询对应结果。
通过结果可以看出,Feign的确实现了负载均衡,且默认为轮询策略,其实Feign不是做负载均衡的,负载均衡是Ribbon的功能,Feign只是集成了Ribbon,负载均衡的功能还是feign内置的Ribbon在做,而不是feign。
注:还不知道Eeruka如何使用的,请看我之前写的一篇”SpringCloud-Eureka配置“博客!
OpenFeign的配置与使用就此告一段落,其他详细的配置可以去官网查阅!!
3.通过源码浅谈service接口可以@Autowired的问题
现在让我们看看@FeignClient的源码
/**
*导入包省略
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {
@AliasFor("name")
String value() default "";
String contextId() default "";
@AliasFor("value")
String name() default "";
/** @deprecated */
@Deprecated
String qualifier() default "";
String[] qualifiers() default {};
String url() default "";
boolean decode404() default false;
Class<?>[] configuration() default {}; //配置类,可自行实现
Class<?> fallback() default void.class;
Class<?> fallbackFactory() default void.class;
String path() default "";
boolean primary() default true;
}
由上可以看出该注解没有复合@Configuration或@Component,那么通过它标注的服务接口又是如何被Spring托管的呢?
对此我从@EnableFeignClients注解入手,源码如下:
@EnableFeignClients
/**
*导入包省略
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
我们可以看到该注解中import了FeignClientsRegistrar.class,再加上在启动类上,配合@SpringBootApplication,该类最后将被实现,对此我们进一步分析该类:
FeignClientsRegistrar.class(部分相关源码)
/**
*导入包省略
*/
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
private ResourceLoader resourceLoader;
private Environment environment;
/**
* 略
*/
FeignClientsRegistrar() {
}
static String getName(String name) {
if (!StringUtils.hasText(name)) {
return "";
} else {
String host = null;
try {
String url;
if (!name.startsWith("http://") && !name.startsWith("https://")) {
// 由此可以看出届时请求将会被拼接为http://服务实例名称
url = "http://" + name;
} else {
url = name;
}
host = (new URI(url)).getHost();
} catch (URISyntaxException var3) {
}
Assert.state(host != null, "Service id not legal hostname (" + name + ")");
return name;
}
}
/**
* 略
*/
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 集合,不重复
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
if (clients != null && clients.length != 0) {
Class[] var12 = clients;
int var14 = clients.length;
for(int var16 = 0; var16 < var14; ++var16) {
Class<?> clazz = var12[var16];
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
} else {
ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
scanner.setResourceLoader(this.resourceLoader);
// 查询FeignClient,即扫描过滤出所有的带@FeignClient
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
Set<String> basePackages = this.getBasePackages(metadata);
Iterator var8 = basePackages.iterator();
while(var8.hasNext()) {
String basePackage = (String)var8.next();
//将basePackage下的所有带@FeignClient标识的接口添加到candidateComponents中
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
Iterator var13 = candidateComponents.iterator();
// 迭代器,遍历进行FeignClient注册,注册到Spring容器中
while(var13.hasNext()) {
BeanDefinition candidateComponent = (BeanDefinition)var13.next();
if (candidateComponent instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;
//annotationMetadata 对应的接口
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 = this.getClientName(attributes);
// 拿到@FeignClient中configuration并进行配置替换
this.registerClientConfiguration(registry, name, attributes.get("configuration"));
// 将接口和@FeignClient中参数配置并注册到Spring容器中
this.registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
// 注册详细源码如下
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
Class clazz = ClassUtils.resolveClassName(className, (ClassLoader)null);
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory ? (ConfigurableBeanFactory)registry : null;
String contextId = this.getContextId(beanFactory, attributes);
String name = this.getName(attributes);
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
factoryBean.setBeanFactory(beanFactory);
factoryBean.setName(name);
factoryBean.setContextId(contextId);
// clazz接口类型设置,即@FeignClient标注下的接口
factoryBean.setType(clazz);
factoryBean.setRefreshableClient(this.isClientRefreshEnabled());
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
factoryBean.setUrl(this.getUrl(beanFactory, attributes));
factoryBean.setPath(this.getPath(beanFactory, attributes));
factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
Object fallback = attributes.get("fallback");
if (fallback != null) {
factoryBean.setFallback(fallback instanceof Class ? (Class)fallback : ClassUtils.resolveClassName(fallback.toString(), (ClassLoader)null));
}
Object fallbackFactory = attributes.get("fallbackFactory");
if (fallbackFactory != null) {
factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class)fallbackFactory : ClassUtils.resolveClassName(fallbackFactory.toString(), (ClassLoader)null));
}
return factoryBean.getObject();
});
definition.setAutowireMode(2);
definition.setLazyInit(true);
this.validate(attributes);
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute("factoryBeanObjectType", className);
beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
boolean primary = (Boolean)attributes.get("primary");
beanDefinition.setPrimary(primary);
String[] qualifiers = this.getQualifiers(attributes);
if (ObjectUtils.isEmpty(qualifiers)) {
qualifiers = new String[]{contextId + "FeignClient"};
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
// 注册为bean到容器中
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
this.registerOptionsBeanDefinition(registry, contextId);
}
/**
* 略
*/
结论: 通过短暂的源码分析可以看出,虽然@FeignClient没有复合@Configuration或@Component,但在注册FeignClientsRegistrar的过程中,已经通过 BeanDefinitionReaderUtils.registerBeanDefinition将所标识的接口注册到容器中,所以可以使用@Autowired拿到容器中对应的service接口。
4.结语
源码部分没有很详细深究,如果有大佬深入了解过源码,上文若有错的地方请大佬不吝指出,我将进行改正和学习,最后感谢各位的观看!