声明式服务调用: Spring Cloud Feign
Spring Cloud Feign 是什么?
之前有spring cloud ribbon
和spring cloud hystrix
,这二个框架的使用几乎都是同时出现的。是否有更高层次的封装来整合这二个基础工具以简化开发呢?spring cloud feign
就是一个这样的工具。它基于Netfix Feign
实现,整合了spring cloud Ribbon
和spring cloud Hystrix
,除了提供这二者的强大功能之外,它还提供了一种生命式的web服务客户端定义方式。
spring cloud feign
在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在spring cloud feign
的实现下,我们只需创建一个接口并调用注解的方式来配置它,即可完成对服务提供方的接口绑定,简化了使用spring cloud ribbon时自动封装服务调用客户端的开发量。spring cloud feign具备可插拔的注解支持,包括feign注解和JAX-RS注解。同时,为了适应Spring的广大用户,它在Netfix Feign的基础上扩展了spring mvc的注解支持,这对于习惯spring mvc的开发者来说,无疑是个好消息,因为这样可以大大减少学习使用它的成本。另外,对于feign自身的一些主要组件,比如说编码器和解码器等,它也以可插拔的方式提高,在有需要的时候我们可以方便地扩展和替换它们。
Feign是一种声明式、模板化的HTTP客户端。在Spring Cloud中使用Feign, 我们可以做到使用HTTP请求远程服务时能与调用本地方法一样的编码体验,开发者完全感知不到这是远程方法,更感知不到这是个HTTP请求。
快速入门
创建pay-service服务,加入依赖,与之前的模块不一样的就是加入了spring-cloud-starter-feign依赖,
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-starter-eureka
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-feign
创建主体应用类,并在主体应用类上加上注解@EnableFeignClients:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class PayApplication {
public static void main(String[] args) {
SpringApplication.run(PayApplication.class,args);
}
}
定义UserService接口,通过@FeignClient("user-service")
注解指定服务名来绑定服务,然后在使用Spring mvc的注解绑定具体的user-service服务中提供的rest接口。
@FeignClient("user-service")
public interface UserService {
@RequestMapping(value = "/user/index",method = RequestMethod.GET)
String index();
@RequestMapping(value = "/user/hello",method = RequestMethod.GET)
String hello();
接着,创建一个PayController来实现对feign客户端的调用,使用@Autowired注解
直接注入上面定义的UserService实例,并在相应的方法中调用这个绑定了user-service服务接口的客户端来向改服务发起接口的定义。
@RestController
@RequestMapping("/pay")
public class PayController {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
UserService userService;
@RequestMapping("/index")
public String index(){
return userService.index();
}
@RequestMapping("/hello")
public String hello(){
return userService.hello();
}
}
最后,同ribbon实现的服务消费一样,需要在application.yml
中指定服务注册中心,
spring:
application:
name: pay-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
server:
port: 7070
测试验证Ribbon客户端负载均衡,同时启动服务注册和user-servcei,user-service启动了二个实例,然后启动pay-service,
发送多个请求http://192.168.5.3:7070/pay/index,发现二个user-service都能在控制台打印日志,我们看到了feign实现的消费者,依然是利用了Ribbon维护了user-service的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign我们只需要定义服务绑定接口,以声明式的方法,优雅而简单的实现了服务调用。
Spring Cloud的Feign支持的一个中心概念就是命名客户端。 每个Feign客户端都是组合的组件的一部分,它们一起工作以按需调用远程服务器,并且该集合具有您将其作为使用@FeignClient注释的参数名称。 Spring Cloud使用FeignClientsConfiguration
创建一个新的集合作为每个命名客户端的ApplicationContext(应用上下文)。 这包含(除其他外)feign.Decoder,feign.Encoder和feign.Contract。
你可以自定义FeignClientsConfiguration以完全控制这一系列的配置。比如我们下面的demo:
定义一个order服务,并加入依赖:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-starter-eureka
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-feign
定义主体启动类:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
定义Controller:
@RestController
@RequestMapping("/order")
public class OrderController {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
UserService userService;
@RequestMapping("/index")
public String index(){
logger.info("index方法");
return userService.index();
}
}
定义Feign客户端接口:
@FeignClient(value = "user-service",configuration = FooConfiguration.class)
public interface UserService {
@RequestLine("GET /user/index")
String index();
}
使用了配置@Configuration参数,自己定义了FooConfiguration类来自定义FeignClientsConfiguration
,并且FeignClientsConfiguration类的类路径不在启动类OrderApplication的扫描路径下,是因为如果在扫描目录下会覆盖该项目所有的Feign接口的默认配置。
FooConfiguration定义:
import feign.Contract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FooConfiguration {
//使用Feign自己的注解,使用springmvc的注解就会报错
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
}
配置文件:
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://cfy.lessismore:123456@localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
server:
port: 9090
参数绑定
快速入门中,我们使用了spring cloud feign
实现的是一个不带参数的REST服务绑定。现实中的各种业务接口要比它复杂的多,我们会在http的各个位置传入各种不同类型的参数,并且在返回请求响应的时候也可能是一个复杂的对象结构。
扩展一下user-servcice服务,增加一些接口定义,其中包含Request参数的请求,带有Header信息的请求,带有RequestBody的请求以及请求响应体中是一个对象的请求,扩展了三个接口分别是hello,hello2,hello3
@RestController
@RequestMapping("/user")
public class UserController {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private DiscoveryClient client;
@RequestMapping(value="/index",method = RequestMethod.GET)
public String index(){
ServiceInstance instance = client.getLocalServiceInstance();
logger.info("/user,host:"+instance.getHost()+",service id:"+instance.getServiceId()+",port:"+instance.getPort());
return "user index, local time="+ LocalDateTime.now();
}
@GetMapping("/hello")
public String userHello() throws Exception{
ServiceInstance serviceInstance = client.getLocalServiceInstance();
//线程阻塞
int sleeptime = new Random().nextInt(3000);
logger.info("sleeptime:"+sleeptime);
Thread.sleep(sleeptime);
logger.info("/user,host:"+serviceInstance.getHost()+",service id:"+serviceInstance.getServiceId()+",port:"+serviceInstance.getPort());
return "user hello";
}
@RequestMapping(value = "/hello1",method = RequestMethod.GET)
public String hello(@RequestParam String username){
return "hello "+username;
}
@RequestMapping(value = "hello2",method = RequestMethod.GET)
public User hello2(@RequestHeader String username,@RequestHeader Integer age){
return new User(username,age);
}
@RequestMapping(value = "hello3",method = RequestMethod.POST)
public String hello3(@RequestBody User user){
return "hello "+user.getUsername() +", "+user.getAge()+", "+user.getId();
}
}
访问:
localhost:8080/user/hello1?username=zhihao.miao
User对象的定义如下,需要注意的是要有User的默认的构造函数,不然,spring cloud feign根据json字符串转换User对象的时候会抛出异常。
public class User {
private String username;
private int age;
private int id;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public User(String username, int age) {
this.username = username;
this.age = age;
}
public User() {
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", age=" + age +
", id=" + id +
'}';
}
}
优点与缺点
使用spring cloud feign
的继承特性的优点很明显,可以将接口的定义从Controller 中剥离,同时配合 maven 仓库就可以轻易实现接口定义的共享,实现在构建期的接口绑定,从而有效的减少服务客户端的绑定配置。这么做虽然可以很方便的实现接口定义和依赖的共享,不用在复制粘贴接口进行绑定,但是这样的做法使用不当的话会带来副作用。由于接口在构建期间就建立起了依赖,那么接口变化就会对项目构建造成了影响,可能服务提供方修改一个接口定义,那么会直接导致客户端工程的构建失败。所以,如果开发团队通过此方法来实现接口共享的话,建议在开发评审期间严格遵守面向对象的开闭原则,尽可能低做好前后版本兼容,防止因为版本原因造成接口定义的不一致。
Feign日志的配置
为每个创建的Feign客户端创建一个记录器。默认情况下,记录器的名称是用于创建Feign客户端的接口的完整类名。Feign日志记录仅响应DEBUG级别。logging.level.project.user.UserClient: DEBUG
在配置文件application.yml 中加入:
logging:
level:
com.jalja.org.spring.simple.dao.FeignUserClient: DEBUG
在自定义的Configuration的类中添加日志级别
@Configuration
public class FooConfiguration {
/* @Bean
public Contract feignContract() {
//这将SpringMvc Contract 替换为feign.Contract.Default
return new feign.Contract.Default();
}*/
@Bean
Logger.Level feignLoggerLevel() {
//设置日志
return Logger.Level.FULL;
}
}
PS:Feign请求超时问题
Hystrix默认的超时时间是1秒,如果超过这个时间尚未响应,将会进入fallback代码。而首次请求往往会比较慢(因为Spring的懒加载机制,要实例化一些类),这个响应时间可能就大于1秒了
解决方案有三种,以feign为例。
方法一
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000
该配置是让Hystrix的超时时间改为5秒
方法二
hystrix.command.default.execution.timeout.enabled: false
该配置,用于禁用Hystrix的超时时间
方法三
feign.hystrix.enabled: false
该配置,用于索性禁用feign的hystrix。该做法除非一些特殊场景,不推荐使用。
Less is more.