使用Eureka作为服务注册中心,在服务启动后,各个微服务会将自己注册到Eureka server。那么服务之间是如何调用?又是如何进行负载均衡的呢?
目前,在Spring cloud 中服务之间通过restful方式调用有两种方式
- restTemplate+Ribbon
- feign
从实践上看,采用feign的方式更优雅(feign内部也使用了ribbon做负载均衡)。
一、什么是 feign?
Feign 的英文表意为“假装,伪装,变形”, 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。
- Feign 支持Ribbon的负载均衡
- Feign 集成了Hystrix(服务熔断)
- Feign 采用的是基于接口配置
- 支持可插拔的HTTP编码器和解码器
- 支持HTTP请求和响应的压缩
二、Feign解决了什么问题?
封装了Http调用流程,更适合面向接口化的变成习惯
在服务调用的场景中,我们经常调用基于Http协议的服务,而我们经常使用到的框架可能有HttpURLConnection
、Apache HttpComponnets
、OkHttp3
、Netty
等等,这些框架在基于自身的专注点提供了自身特性。而从角色划分上来看,他们的职能是一致的提供Http调用服务。具体流程如下:
三、Feign是如何设计的?
四、RestTemplate + Ribbon与Feign(自带Ribbon)的比较
角度 | RestTemplate + Ribbon | Feign(自带Ribbon) |
---|---|---|
可读性、可维护性 | 欠佳(无法从URL直观了解这个远程调用是干什么的) | 极佳(能在接口上写注释,方法名称也是可读的,能一眼看出这个远程调用是干什么的) |
开发体验 | 欠佳(拼凑URL不幸福) | 极佳(漂亮的代码) |
风格一致性 | 欠佳(本地API调用和RestTemplate调用的代码风格截然不同) | 极佳(完全一致,不点开Feign的接口,根本不会察觉这是一个远程调用而非本地API调用) |
性能 | 较好 | 中等(性能是RestTemplate的50%左右;如果为Feign配置连接池,性能可提升15%左右) |
灵活性 | 极佳 | 中等(内置功能能满足大多数项目的需求) |
Feign 整体框架非常小巧,在处理请求转换和消息解析的过程中,基本上没什么时间消耗。真正影响性能的,是处理Http请求的环节。由于默认情况下,Feign采用的是JDK的HttpURLConnection,所以整体性能并不高。
五、使用Feign
1. 导入依赖
<dependencies>
<!--openfein的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
</dependencies>
2. 启用Feign
启用类上添加注解@EnableFeignClients客户端允许开启使用Feign调用,扫描@FeignClient标注的FeignClient接口
@SpringBootApplication
@EnableFeignClients(basePackages = { "com.xxx.xxx.xxx" })//开启feign,封装http的rest请求。可以用basePackages = { "com.xxx.xxx.xxx" }指定包扫描@FeignClient标注的FeignClient接口
//@EnableDiscoveryClient和@EnableEurekaClient共同点就是:都是能够让注册中心能够发现,扫描到改服务。
//不同点:@EnableEurekaClient只适用于Eureka作为注册中心,@EnableDiscoveryClient 可以是其他注册中心。
@EnableEurekaClient
//@EnableDiscoveryClient
public class FeignApplication {
public static void main(String[] args) {
SpringApplication.run(FeignApplication.class,args);
}
}
3. 编写FeignClient接口
@FeignClient( value = "hello-service-provider" ) 声明的方式指向了 服务提供者,而接口方法则实现了对服务提供者接口的实际调用
@FeignClient(value = "hello-service-provider")
public interface SchedualServiceHi {
@RequestMapping(value = "/hi", method = RequestMethod.GET)
String sayHiFromClientOne(@RequestParam("name") String name);
}
注意:这里服务名不区分大小写,所以使用hello-service-provider和HELLO-SERVICE-PROVIDER都是可以的。另外,在Brixton.SR5版本中,原有的serviceId属性已经被废弃,若要写属性名,可以使用name或value。
FeignClient
注解的一些属性
属性名 | 默认值 | 作用 | 备注 |
---|---|---|---|
value | 空字符串 | 调用服务名称,和name属性相同 | |
serviceId | 空字符串 | 服务id,作用和name属性相同 | 已过期 |
name | 空字符串 | 调用服务名称,和value属性相同 | |
url | 空字符串 | 全路径地址或hostname,http或https可选 | |
decode404 | false | 配置响应状态码为404时是否应该抛出FeignExceptions | |
configuration | {} | 自定义当前feign client的一些配置 | 参考FeignClientsConfiguration |
fallback | void.class | 熔断机制,调用失败时,走的一些回退方法,可以用来抛出异常或给出默认返回数据。 | 底层依赖hystrix,启动类要加上@EnableHystrix |
path | 空字符串 | 自动给所有方法的requestMapping前加上前缀,类似与controller类上的requestMapping |
4. 实现对Feign客户端的调用
接着,创建一个RestClientController来实现对Feign客户端的调用。使用@Autowired直接注入上面定义的HelloServiceFeign实例,并在postPerson函数中调用这个绑定了hello-service-provider服务接口的客户端来向该服务发起/demo/getHost和/demo/postPerson接口的调用。
@RestController
public class HiController {
@Autowired
SchedualServiceHi schedualServiceHi;
@GetMapping(value = "/test/hi")
public String sayHi(@RequestParam String name){
return schedualServiceHi.sayHiFromClientOne(name);
}
}
5. 需要在application.yml中指定服务注册中心,并定义自身的服务名为service-feign,端口使用8765。
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8765
spring:
application:
name: service-feign
六、其它
1. Feign自定义处理返回的异常
实现Feign的 ErrorDecoder 的接口就可以实现 http请求层面的错误处理。
StashErrorDecoder类实现了ErrorDecoder接口。在Feign客户端发生http请求层面的错误时会调用decode方法。在decode方法中实现自定义的错误处理。
public class StashErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() >= 400 && response.status() <= 499) {
//这里是给出的自定义异常
return new StashClientException(
response.status(),
response.reason()
);
}
if (response.status() >= 500 && response.status() <= 599) {
//这里是给出的自定义异常
return new StashServerException(
response.status(),
response.reason()
);
}
//这里是其他状态码处理方法
return errorStatus(methodKey, response);
}
}
- 接下来就需要注册这个错误拦截器了,如果是直接手动构建FeignClient的使用方法。那需要在构建客户端时指定errorDecoder
return Feign.builder()
.errorDecoder(new StashErrorDecoder())
.target(StashApi.class, url);
- 如果是使用了spring-cloud-open-feign的使用方式,包含了启动的自动配置,所以只需要将错误处理类注册为配置类,即添加@Configuration注解即可。
@Configuration
public class StashErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
// 实现代码
}
}
2. Feign使用OKhttp发送request
Feign底层默认是使用jdk中的HttpURLConnection发送HTTP请求,feign也提供了OKhttp来发送请求,具体配置如下:
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
okhttp:
enabled: true
hystrix:
enabled: true
3. Feign开启GZIP压缩
Spring Cloud Feign支持对请求和响应进行GZIP压缩,以提高通信效率。
feign:
compression:
request: #请求
enabled: true #开启
mime-types: text/xml,application/xml,application/json #开启支持压缩的MIME TYPE
min-request-size: 2048 #配置压缩数据大小的下限
response: #响应
enabled: true #开启响应GZIP压缩
注意:
由于开启GZIP压缩之后,Feign之间的调用数据通过二进制协议进行传输,返回值需要修改为ResponseEntity<byte[]>才可以正常显示,否则会导致服务之间的调用乱码。
示例如下:
@PostMapping("/order/{productId}")
ResponseEntity<byte[]> addCart(@PathVariable("productId") Long productId);
4. 作用在所有Feign Client上的配置方式
方式一:通过java bean 的方式指定。
@EnableFeignClients注解上有个defaultConfiguration属性,可以指定默认Feign Client的一些配置。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
@EnableDiscoveryClient
@SpringBootApplication
@EnableCircuitBreaker
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
DefaultFeignConfiguration内容:
@Configuration
public class DefaultFeignConfiguration {
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(1000,3000,3);
}
}
方式二:通过配置文件方式指定。
feign:
client:
config:
default:
connectTimeout: 5000 #连接超时
readTimeout: 5000 #读取超时
loggerLevel: basic #日志等级
5. Feign Client开启日志
日志配置和上述配置相同,也有两种方式。
方式一:通过java bean的方式指定
@Configuration
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.BASIC;
}
}
方式二:通过配置文件指定
logging:
level:
com.xt.open.jmall.product.remote.feignclients.CartFeignClient: debug
6. Feign 的GET的多参数传递
目前,feign不支持GET请求直接传递POJO对象的,目前解决方法如下:
- 把POJO拆散城一个一个单独的属性放在方法参数中
- 把方法参数编程Map传递
- 使用GET传递@RequestBody,但此方式违反restful风格
介绍一个最佳实践,通过feign的拦截器来实现。
@Component
@Slf4j
public class FeignCustomRequestInteceptor implements RequestInterceptor {
@Autowired
private ObjectMapper objectMapper;
@Override
public void apply(RequestTemplate template) {
if (HttpMethod.GET.toString() == template.method() && template.body() != null) {
//feign 不支持GET方法传输POJO 转换成json,再换成query
try {
Map<String, Collection<String>> map = objectMapper.readValue(template.bodyTemplate(), new TypeReference<Map<String, Collection<String>>>() {
});
template.body(null);
template.queries(map);
} catch (IOException e) {
log.error("cause exception", e);
}
}
}
七、总结
总到来说,Feign的源码实现的过程如下:
- @EnableFeignCleints注解开启FeignCleint
- 启动时,程序会进行包扫描,扫描所有包下所有@FeignClient注解的类,并将这些类注入到spring的IOC容器中。当定义的Feign中的接口被调用时,通过JDK的动态代理来生成RequestTemplate。
- RequestTemplate中包含请求的所有信息,如请求参数,请求URL等。
- RequestTemplate声明Request,然后将Request交给client处理,这个client默认是JDK的HTTPUrlConnection,也可以是OKhttp、Apache的HTTPClient等。
- 最后client封装成LoadBaLanceClient,结合ribbon负载均衡地发起调用。