上一章中讲解了如何使用RestTemplate来消费服务,如何结合Ribbon在消费服务时做负载均衡。本章将讲解Feign的使用,Feign是一个web请求的工具,可以将请求指定到具体的服务上去,在项目中主要用做服务之间的调用。
写一个Feign客户端
- 加入相关依赖,主要包括feign的起步依赖spring-cloud-starter-feign,Eureka Client的起步依赖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>
- 配置文件中做相关配置
server:
port: 8766
spring:
application:
name: eureka-feign-client
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
- 工程启动类上加上@EnableEurekaClient开启Eureka Client的功能,加上@EnableFeignClients开启Feign Client的功能。
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class EurekaFeignClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaFeignClientApplication.class, args);
}
}
- 新建声明式接口,在接口上加@FeignClient注解来声明一个Feign Client,其中value为远程调用其它服务的服务名,configuration属性为Feign Client的配置类。
@FeignClient(value = "eureka-client", configuration = FeignClientConfig.class)
public interface HiFeignClient {
// 该方法通过feign来调用eureka-client服务的“/hi”接口。
@GetMapping("/hi")
String hi(@RequestParam(value = "name") String name);
}
//注入feignRetryer的Bean。Feign在远程调用失败后会进行重试
@Configuration
public class FeignClientConfig {
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(100, SECONDS.toMillis(1), 5);
}
}
- 新建controller,在其中注入刚刚定义的FeignClient,并使用feignClient来调用接口。
@RestController
public class FeignController {
@Autowired
private HiFeignClient feignClient;
@GetMapping("/feign-test")
public String hi(@RequestParam String name) {
return feignClient.hi(name);
}
}
启动eureka-server和两个eureka-client(项目代码和启动方式见前面章节),启动eureka-feign-client。
多次访问eureka-feign-client的/feign-test接口,会轮流显示两个eureka-client的“/hi”API接口,说明Feign Client有负载均衡的能力。
FeignClient 的配置
在上文中我们通过FeignClient 的configuration 属性覆盖了Feign Client的默认配置。Feign Client默认的配置类为FeignClientsConfiguration,这个类在spring-cloud-netflix-core的jar包下,其中注入了很多Feign相关的配置,具体代码如下:
@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注解表示如果没有注入该类的Bean就会默认注入一个Bean,于是我们在上文中在自定义的配置类中注入Retryer 类型的Bean从而达到自定义配置的目的。
Feign 的工作原理
- 首先通过@EnableFeignClients注解开启FeignClient的功能。只有这个注解存在,才会在程序启动时开启@FeignClient注解的包扫描。
- 程序启动后,会进行包扫描,扫描所有的@FeignClient的注解的类,并将这些信息注入IOC容器中。
- 当接口的方法被调用时,通过JDK的代理来生成具体的RequestTemplate模板对象。
- 根据RequestTemplate来生成HTTP请求的Request对象。
- Request对象交给Client去处理,其中Client的网络请求框架可以是HttpURLConnection,HttpClient和OkHttp。
- 最后Client被封装到LoadBalanceClient类,这个类结合Ribbon做到了负载均衡。
Feign使用HttpClient或OkHttp
Feign默认使用HttpURLConnection来实现网络请求的。同时还支持HttpClient和OkHttp进行网络请求。通过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表示在容器中存在对应的Bean配置就会生效,@ConditionalOnProperty中属性默认为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>
注意项
- 由于网络原因,feign请求会超时,此时会发生异常。feign实际整合了hystrix(断路器 后续介绍),我们可以通过配置来调整超时时间。
//增加超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000
//禁用超时
# hystrix.command.default.execution.timeout.enabled: false
//索性禁用feign的hystrix支持
feign.hystrix.enabled: false
- feign传递List<String>,被调用端会报Required List parameter 'uomIdList' is not present。此时需要传递 string[]。但传递List<Long>则可以。这是在开发中遇到的,总结不一定完全正确。
- 不支持@GetMapping,可用@RequestMapping(value = "", method = RequestMethod.GET)来替代。在参数中@PathVariable一定得设置value。
- 只要参数是复杂对象,即使指定了是GET方法,feign依然会以POST方法进行发送请求。这是在开发中遇到的,总结不一定完全正确。
总结
在这一章学习了如何使用Feign,并介绍了Feign的工作原理和如何切换HTTP的请求框架。最后介绍了本人在开发过程中遇到的一些坑。在下一章学习熔断器Hystrix
PS:项目github地址:https://github.com/dzydzydzy/spring-cloud-example.git