Chapter Six《SpringCloud微服务实战》

声明式服务调用: Spring Cloud Feign

Spring Cloud Feign 是什么?

之前有spring cloud ribbonspring cloud hystrix,这二个框架的使用几乎都是同时出现的。是否有更高层次的封装来整合这二个基础工具以简化开发呢?spring cloud feign就是一个这样的工具。它基于Netfix Feign实现,整合了spring cloud Ribbonspring 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,


Chapter Six《SpringCloud微服务实战》_第1张图片
image.png

发送多个请求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

Chapter Six《SpringCloud微服务实战》_第2张图片
image.png
Chapter Six《SpringCloud微服务实战》_第3张图片
image.png
Chapter Six《SpringCloud微服务实战》_第4张图片
image.png

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.

你可能感兴趣的:(Chapter Six《SpringCloud微服务实战》)