上一章中講解了如何使用RestTemplate來(lái)消費(fèi)服務(wù),如何結(jié)合Ribbon在消費(fèi)服務(wù)時(shí)做負(fù)載均衡。本章將講解Feign的使用,F(xiàn)eign是一個(gè)web請(qǐng)求的工具,可以將請(qǐng)求指定到具體的服務(wù)上去,在項(xiàng)目中主要用做服務(wù)之間的調(diào)用。
寫(xiě)一個(gè)Feign客戶(hù)端
- 加入相關(guān)依賴(lài),主要包括feign的起步依賴(lài)spring-cloud-starter-feign,Eureka Client的起步依賴(lài)spring-cloud-starter-eureka
<dependencies>
<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.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
- 配置文件中做相關(guān)配置
server:
port: 8766
spring:
application:
name: eureka-feign-client
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
- 工程啟動(dòng)類(lèi)上加上@EnableEurekaClient開(kāi)啟Eureka Client的功能,加上@EnableFeignClients開(kāi)啟Feign Client的功能。
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class EurekaFeignClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaFeignClientApplication.class, args);
}
}
- 新建聲明式接口,在接口上加@FeignClient注解來(lái)聲明一個(gè)Feign Client,其中value為遠(yuǎn)程調(diào)用其它服務(wù)的服務(wù)名,configuration屬性為Feign Client的配置類(lèi)。
@FeignClient(value = "eureka-client", configuration = FeignClientConfig.class)
public interface HiFeignClient {
// 該方法通過(guò)feign來(lái)調(diào)用eureka-client服務(wù)的“/hi”接口。
@GetMapping("/hi")
String hi(@RequestParam(value = "name") String name);
}
//注入feignRetryer的Bean。Feign在遠(yuǎn)程調(diào)用失敗后會(huì)進(jìn)行重試
@Configuration
public class FeignClientConfig {
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(100, SECONDS.toMillis(1), 5);
}
}
- 新建controller,在其中注入剛剛定義的FeignClient,并使用feignClient來(lái)調(diào)用接口。
@RestController
public class FeignController {
@Autowired
private HiFeignClient feignClient;
@GetMapping("/feign-test")
public String hi(@RequestParam String name) {
return feignClient.hi(name);
}
}
啟動(dòng)eureka-server和兩個(gè)eureka-client(項(xiàng)目代碼和啟動(dòng)方式見(jiàn)前面章節(jié)),啟動(dòng)eureka-feign-client。
多次訪(fǎng)問(wèn)eureka-feign-client的/feign-test接口,會(huì)輪流顯示兩個(gè)eureka-client的“/hi”API接口,說(shuō)明Feign Client有負(fù)載均衡的能力。
FeignClient 的配置
在上文中我們通過(guò)FeignClient 的configuration 屬性覆蓋了Feign Client的默認(rèn)配置。Feign Client默認(rèn)的配置類(lèi)為FeignClientsConfiguration,這個(gè)類(lèi)在spring-cloud-netflix-core的jar包下,其中注入了很多Feign相關(guān)的配置,具體代碼如下:
@Configuration
public class FeignClientsConfiguration {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Autowired(required = false)
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
@Autowired(required = false)
private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();
@Autowired(required = false)
private Logger logger;
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return 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);
}
@Bean
public FormattingConversionService feignConversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) {
feignFormatterRegistrar.registerFormatters(conversionService);
}
return conversionService;
}
@Configuration
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false)
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
}
@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
}
@Bean
@ConditionalOnMissingBean(FeignLoggerFactory.class)
public FeignLoggerFactory feignLoggerFactory() {
return new DefaultFeignLoggerFactory(logger);
}
}
@ConditionalOnMissingBean注解表示如果沒(méi)有注入該類(lèi)的Bean就會(huì)默認(rèn)注入一個(gè)Bean,于是我們?cè)谏衔闹性谧远x的配置類(lèi)中注入Retryer 類(lèi)型的Bean從而達(dá)到自定義配置的目的。
Feign 的工作原理
- 首先通過(guò)@EnableFeignClients注解開(kāi)啟FeignClient的功能。只有這個(gè)注解存在,才會(huì)在程序啟動(dòng)時(shí)開(kāi)啟@FeignClient注解的包掃描。
- 程序啟動(dòng)后,會(huì)進(jìn)行包掃描,掃描所有的@FeignClient的注解的類(lèi),并將這些信息注入IOC容器中。
- 當(dāng)接口的方法被調(diào)用時(shí),通過(guò)JDK的代理來(lái)生成具體的RequestTemplate模板對(duì)象。
- 根據(jù)RequestTemplate來(lái)生成HTTP請(qǐng)求的Request對(duì)象。
- Request對(duì)象交給Client去處理,其中Client的網(wǎng)絡(luò)請(qǐng)求框架可以是HttpURLConnection,HttpClient和OkHttp。
- 最后Client被封裝到LoadBalanceClient類(lèi),這個(gè)類(lèi)結(jié)合Ribbon做到了負(fù)載均衡。
Feign使用HttpClient或OkHttp
Feign默認(rèn)使用HttpURLConnection來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求的。同時(shí)還支持HttpClient和OkHttp進(jìn)行網(wǎng)絡(luò)請(qǐng)求。通過(guò)FeignRibbonClientAutoConfiguration的源碼:
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignLoadBalancedConfiguration {
@Autowired(required = false)
private HttpClient httpClient;
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
ApacheHttpClient delegate;
if (this.httpClient != null) {
delegate = new ApacheHttpClient(this.httpClient);
}
else {
delegate = new ApacheHttpClient();
}
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
}
@Configuration
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty(value = "feign.okhttp.enabled", matchIfMissing = true)
protected static class OkHttpFeignLoadBalancedConfiguration {
@Autowired(required = false)
private okhttp3.OkHttpClient okHttpClient;
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
OkHttpClient delegate;
if (this.okHttpClient != null) {
delegate = new OkHttpClient(this.okHttpClient);
}
else {
delegate = new OkHttpClient();
}
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
}
其中@ConditionalOnClass表示在容器中存在對(duì)應(yīng)的Bean配置就會(huì)生效,@ConditionalOnProperty中屬性默認(rèn)為true,所以不用在application.yml配置文件中配置,只需要在pom文件加上HttpClient或者OkHttp的Classpath即可。
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign.httpclient</artifactId>
<version>RELEASE</version>
</dependency>
或
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign.okhttp</artifactId>
<version>RELEASE</version>
</dependency>
注意項(xiàng)
- 由于網(wǎng)絡(luò)原因,feign請(qǐng)求會(huì)超時(shí),此時(shí)會(huì)發(fā)生異常。feign實(shí)際整合了hystrix(斷路器 后續(xù)介紹),我們可以通過(guò)配置來(lái)調(diào)整超時(shí)時(shí)間。
//增加超時(shí)時(shí)間
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000
//禁用超時(shí)
# hystrix.command.default.execution.timeout.enabled: false
//索性禁用feign的hystrix支持
feign.hystrix.enabled: false
- feign傳遞List<String>,被調(diào)用端會(huì)報(bào)Required List parameter 'uomIdList' is not present。此時(shí)需要傳遞 string[]。但傳遞List<Long>則可以。這是在開(kāi)發(fā)中遇到的,總結(jié)不一定完全正確。
- 不支持@GetMapping,可用@RequestMapping(value = "", method = RequestMethod.GET)來(lái)替代。在參數(shù)中@PathVariable一定得設(shè)置value。
- 只要參數(shù)是復(fù)雜對(duì)象,即使指定了是GET方法,feign依然會(huì)以POST方法進(jìn)行發(fā)送請(qǐng)求。這是在開(kāi)發(fā)中遇到的,總結(jié)不一定完全正確。
總結(jié)
在這一章學(xué)習(xí)了如何使用Feign,并介紹了Feign的工作原理和如何切換HTTP的請(qǐng)求框架。最后介紹了本人在開(kāi)發(fā)過(guò)程中遇到的一些坑。在下一章學(xué)習(xí)熔斷器Hystrix
PS:項(xiàng)目github地址:https://github.com/dzydzydzy/spring-cloud-example.git