Feign 受Retrofit、JAXRS-2.0 和Web Socket 的影响, 采用了声明式API 接口的风格, 将Java Http 客户端绑定到它的内部。 Feign 的首要目标是将Java Http客户端调用过程变得简单。 Feign 的源码地址:https://github.com/OpenFeign/feign。
1. 写一个Feign客户端
源码:
链接:https://pan.baidu.com/s/1g22v0EJC0BesWhbjfWDXpw
提取码:9gwu
本案例是在上章代码的基础上进行改造的,主要讲解了如何使用Feign进行远程调用。
新建一个工程eureka-feign-client工程,
pom.xml中引入依赖:
<dependencies>
<!--Feign的起步依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--Eureka Client的起步依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--Web功能的起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
配置application.yml:
spring:
application:
name: eureka-feign-client
server:
port: 8765
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/ # 服务注册地址
程序的启动类:
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 Client功能
@EnableFeignClients //开启Feign Client功能
public class FeignClientApplication {
public static void main(String[] args) {
SpringApplication.run(FeignClientApplication.class,args);
}
}
新建一个EurekaClientFeign的接口,在接口上加上@FeignClient注解来声明一个Feign Client,其中value为远程调用其他服务的服务名,FeignConfig.class为Feign Client的配置类。sayHiFromClientEureka()方法,该方法通过Feign来调用eureka-client服务的"hi"的API接口,代码如下:
import com.wyc.config.FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* Created by : wyc
* Created time 2020/6/29 23:11
*/
@FeignClient(value = "eureka-client", configuration = FeignConfig.class)
public interface EurekaClientFeign {
@GetMapping("/hi")
String sayHiFromClientEureka(@RequestParam(value = "name") String name);
}
创建FeignConfig类
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* Created by : wyc
* Created time 2020/6/30 6:30
*/
@Configuration //表明该类是一个配置类
public class FeignConfig {
@Bean
public Retryer feignRetryer(){
return new Retryer.Default(100,SECONDS.toMillis(1),5);
}
}
创建HiService
import com.wyc.client.EurekaClientFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Created by : wyc
* Created time 2020/6/30 7:00
*/
@Service
public class HiService {
@Autowired
private EurekaClientFeign eurekaClientFeign;
public String sayHi(String name){
return eurekaClientFeign.sayHiFromClientEureka(name);
}
}
在Hi Controller 上加上@RestController 注解,开启RestController 的功能,写一个API 接口“/hi”,在该接口调用了Hi Service 的sayHi ()方法。HiService 通过EurekaClientFeign 远程调用eureka-client 服务的API 接口"/hi"
import com.wyc.service.HiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by : wyc
* Created time 2020/6/30 7:07
*/
@RestController
public class HiController {
@Autowired
private HiService hiService;
public String sayHi(@RequestParam(defaultValue = "wyc", required = false) String name){
return hiService.sayHi(name);
}
}
启动Eureka工程,端口号为8761,启动两个eureka-client工程,端口号分别为8762,8763,启动eureka-feign-client工程,端口号为8765
在浏览器上多次访问 http://localhost:8765/hi
hi wyc,i am from port:8762
hi wyc,i am from port:8763
由此可见,Feign Client远程调用了eureka-client服务(存在端口为8762和8763的两个实例)的“/hi”API接口,Feign Client油浮在均衡的能力。
2. Feign 详解
为了深入理解Feign , 下面将从源码的角度来讲解Feign 。首先来查看FeignClient 注解@FeignClient 的源码, 其代码如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
@AliasFor("name")
String value() default "";
/** @deprecated */
@Deprecated
String serviceId() default "";
@AliasFor("value")
String name() default "";
String qualifier() 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;
}
FeignClient 注解被@Target(ElementType.TYPE)修饰,表示FeignClient 注解的作用目标在接口上。@Retention(RetentionPolicy.RUNTIME)注解表明该注解会在Class 字节码文件中存在,在运行时可以通过反射获取到。@Documented 表示该注解将被包含在Javadoc 中。
@FeignClient 注解用于创建声明式API 接口,该接口是RESTful 风格的。Feign 被设计成插拔式的,可以注入其他组件和Feign 一起使用。最典型的是如果Ribbon 可用, Feign 会和 Ribbon 相结合进行负载均衡。
在代码中,value()和name()一样,是被调用的服务的ServiceId 。url()直接填写硬编码Url地址。decode404()即404 是被解码,还是抛异常。configuration()指明FeignClient 的配置类,默认的配置类为FeignClientsConfiguration 类,在缺省的情况下, 这个类注入了默认的Decoder 、 Encoder 和Contract 等配置的Bean 。fallback()为配置熔断器的处理类。
3. FeignClient的配置
Feign Client 默认的配置类为FeignClientsConfiguration ,这个类在spring-cloud-openfeign-core的jar 包下。可以发现这个类注入了很多Feign 相关的配置Bean ,包括FeignRetryer 、F eignLoggerF actory 和FormattingConversionService 等。另外, Decoder 、Encoder 和Contract这3 个类在没有Bean 被注入的情况下,会自动注入默认配置的Bean ,即ResponseEntity Decoder 、SpringEncoder 和SpringMvcContract。默认注入的配置如下。
- Decoder feignDecoder: ResponseEntityDecoder。
- Encoder feignEncoder: SpringEncoder 。
- Logger feignLogger: Slf4jLogger 。
- Contract feignContract: SpringMvcContract 。
- Feign.Builder feignBuilder: HystrixFeign.Builder 。
FeignClientsConfiguration 的配置类部分代码如下, @ConditionalOnMissingBean 注解表示如果没有注入该类的Bean 就会默认注入一个Bean 。
@Configuration
public class FeignClientsConfiguration {
//省略代码...........
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}
@Bean
@ConditionalOnMissingBean
public Encoder feignEncoder() {
return new SpringEncoder(this.messageConverters);
}
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
//省略代码.........
}
重写FeignClientsConfiguration 类中的Bean , 覆盖掉默认的配置Bean ,从而达到自定义配置的目的。例如Feign 默认的配置在请求失败后, 重试次数为0 ,即不重试( Retry er.NEVER_RETRY )。现在希望在请求失败后能够重试,这时需要写一个配置FeignConfig 类,在该类中注入Retryer的Bean ,覆盖掉默认的Retryer的Bean , 并将FeignConfig 指定为FeignClient 的配置类。代码如下:
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* Created by : wyc
* Created time 2020/6/30 6:30
*/
@Configuration //表明该类是一个配置类
public class FeignConfig {
@Bean
public Retryer feignRetryer(){
return new Retryer.Default(100,SECONDS.toMillis(1),5);
}
}
在上面的代码中,通过覆盖了默认的Retryer的Bean,更改了FeignClient的请求失败重试的策略,重试间隔为100毫秒,最大重试时间为1秒,重试次数为5次。
4. 从源码的角度讲解Feign的工作原理
Feign 是一个伪Java Http 客户端, Feign 不做任何的请求处理。Feign 通过处理注解生成Request 模板,从而简化了Http API 的开发。 开发人员可以使用注解的方式定制Request API模板。在发送HttpRequest 请求之前, Feign 通过处理注解的方式替换掉Request 模板中的参数,生成真正的Request ,并交给Java Http 客户端去处理。利用这种方式,开发者只需要关注Feign注解模板的开发,而不用关注Http 请求本身,简化了Http 请求的过程,使得Http请求变得简单和容易理解。
Feign 通过包扫描注入FeignClient 的Bean ,该源码在FeignClientsRegistrar 类中。首先在程序启动时,会检查是否有@EnableFeignClients 注解,如果有该注解,则开启包扫描,扫描被@FeignClient 注解的接口。
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();
}
this.registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration"));
}
}
当程序的启动类上有@EnableFeignClients注解.在程序启动后,程序会通过包扫描将有@FeignClient注解修饰的接口连同接口名和注解的信息一起取出,赋给BeanDefinitionBuilder,然后根据BeanDefinitionBuilder得到BeanDefinition,最后将BeanDefinition注入IOC容器中,源码如下:
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
scanner.setResourceLoader(this.resourceLoader);
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
Object basePackages;
if (clients != null && clients.length != 0) {
final Set<String> clientClasses = new HashSet();
basePackages = new HashSet();
Class[] var9 = clients;
int var10 = clients.length;
for(int var11 = 0; var11 < var10; ++var11) {
Class<?> clazz = var9[var11];
((Set)basePackages).add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(new FeignClientsRegistrar.AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
} else {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = this.getBasePackages(metadata);
}
Iterator var17 = ((Set)basePackages).iterator();
while(var17.hasNext()) {
String basePackage = (String)var17.next();
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
Iterator var21 = candidateComponents.iterator();
while(var21.hasNext()) {
BeanDefinition candidateComponent = (BeanDefinition)var21.next();
if (candidateComponent instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;
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);
this.registerClientConfiguration(registry, name, attributes.get("configuration"));
this.registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
this.validate(attributes);
definition.addPropertyValue("url", this.getUrl(attributes));
definition.addPropertyValue("path", this.getPath(attributes));
String name = this.getName(attributes);
definition.addPropertyValue("name", name);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(2);
String alias = name + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean)attributes.get("primary");
beanDefinition.setPrimary(primary);
String qualifier = this.getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
注入BeanDefinition之后,通过JDK代理,当调用Feign Client接口里面的方法的时候,该方法被拦截,源码在ReflectiveFeign类。
在SynchronousMethodHandler类进行拦截处理,会根据参数生成RequestTemplate对象,该对象是Http请求的模板。
5. Feign如何实现负载均衡的
FeignRibbonClientAutoConfiguration 类配置了Client 的类型(包括HttpURLConnection 、OkHIttp 和HttpClient ),最终向容器注入的是Client 的实现类LoadBalancerFeignClient ,即负载均衡客户端。
6. Feign流程总结
(1) 首先通过@EnableFeignClients 注解开启Feign Client 的功能。只有这个注解存在,才会在程序启动时开启对@FeignClient 注解的包扫描。
(2) 根据Feign 的规则实现接口,井在接口上面加上@FeignClient 注解。
(3) 程序启动后,会进行包扫描,扫描所有的@FeignClient 的注解的类,并将这些信息注入IoC 容器中。
(4) 当接口的方法被调用时, 通过JDK 的代理来生成具体的RequestTemplate模板对象。
(5) 根据RequestTemplate 再生成Http 请求的Request 对象。
(6) Request 对象交给Client 去处理, 其中Client 的网络请求框架可以是HttpURLConnection 、HttpClient 和OkHttp 。
(7) 最后Client 被封装到LoadBalanceClient 类,这个类结合类Ribbon 做到了负载均衡。