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 做到了負載均衡。