不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

SpringCloud微服务系列(5) 声明式调用

声明式调用

这一章的内容比较简单,大部分的知识基础是前面的两章。
还记得我们第一篇文章概念入门中Ribbon,Hystrix,Fegin三者的图标吗?我再展示一遍(按前面所提及顺序):

相信大家都看出来了,后面Feign图标是通过前面二者结合产生的,为什么?
因为Fegin就是对Ribbon和Hystrix(文章中这些组件默认都是指SpringCloud下的组件而非Netflix下的)的整合封装,同时还扩展了SpringMVC注解,提供给了一种声明式的web服务客户端定义方式。
通过整合封装,大大减少了我们学习使用它的成本,同时Fegin还提供了插拔式的组件:编码器、解码器等。

基本介绍

简单示例

我们下面实现一个简单的带有Hystrix功能的Fegin示例

实现步骤
  • 添加依赖
org.springframework.cloud:spring-cloud-starter-feign
org.springframework.cloud:spring-cloud-starter-hystrix

  • 编写代码

启动类:

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
@EnableHystrix
public class ServiceFeignApplication {

   public static void main(String[] args) {
      SpringApplication.run(ServiceFeignApplication.class, args);
   }
}

使用@EnableFeignClient指定为声明式调用,其他几个注解相信大家都已经了解了。

服务接口:

/**
 * Copyright © 2017年 ziniuxiaozhu. All rights reserved.
 *
 * @Author 临江仙 hzqiuxm@163.com
 * @Date 2017/12/16 0016 16:59
 * 定义一个feign接口,通过@ FeignClient(“服务名”),来指定调用哪个服务
 * feign是自带断路器的,并且是已经打开了只需要在SchedualServiceHi接口的注解中加上fallback的指定类
 */
@FeignClient(value = "service-hi", fallback = SchedualServiceHiHystric.class)
public interface SchedualServiceHi {

    @RequestMapping(value = "/hi",method = RequestMethod.GET)
    String sayHiFromClientOne(@RequestParam(value = "name") String name);
}

只要创建一个接口,加上注解@FeginClient,使用value属性指定服务名(不区分大小写),fallback指定降级服务类即可。

熔断执行的降级服务类:

/**
 * Copyright © 2017年 ziniuxiaozhu. All rights reserved.
 *
 * @Author 临江仙 hzqiuxm@163.com
 * @Date 2017/8/5 0005 16:33
 * 服务调用失败或者断路器打开后调用该类的方法返回
 */
@Component
public class SchedualServiceHiHystric implements SchedualServiceHi {
    @Override
    public String sayHiFromClientOne(String name) {
        return "sorry " + name;
    }
}

服务降级类需要实现之前的服务接口,方法名参数也要保持一致。

访问接口:

/**
 * Copyright © 2017年 ziniuxiaozhu. All rights reserved.
 *
 * @Author 临江仙 hzqiuxm@163.com
 * @Date 2017/12/16 0016 17:02
 */
@RestController
public class HiController {

    @Resource
    private SchedualServiceHi schedualServiceHi;

    @RequestMapping(value = "/hi",method = RequestMethod.GET)
    public String sayHi(@RequestParam String name){
        return schedualServiceHi.sayHiFromClientOne(name);
    }

}

  • 添加配置
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8765
spring:
  application:
    name: service-feign
feign:
  hystrix:
    enabled: true

测试验证

启动之前的注册中心,2个服务提供者(其中一个是随机睡眠超时的),各服务架构如下:

访问URL链接:localhost:8765?name=hzqiuxm,将会得到类似下面结果:

hi hzqiuxm, i am from port8763
sorry hzqiuxm
hi hzqiuxm, i am from port8763
hi hzqiuxm, i am from port8762
sorry hzqiuxm

参数绑定

之前的例子中,我们都是提供了一个参数,在实际项目使用中,一般会带上自定义的对象或多个参数,下面就对之前的服务提供者和Feign进行修改,演示下多个参数下,如何进行调用与参数绑定

  • 对原来的服务提供者进行改造,添加几个方法:
@RestController
@EnableEurekaClient
@SpringBootApplication
@EnableDiscoveryClient
public class EurekaclientApplication {

   private static Logger logger = LoggerFactory.getLogger(EurekaclientApplication.class);

   public static void main(String[] args) {
      SpringApplication.run(EurekaclientApplication.class, args);
   }

   @Value("${server.port}")
   String port;

   @RequestMapping("/hi")
   public String home(@RequestParam String name) throws InterruptedException {

      //测试超时效果,加入一个睡眠时间
      int sleepTIme = new Random().nextInt(4000);
      logger.info("sleepTime = " + sleepTIme);

      Thread.sleep(sleepTIme);

      return "hi " + name +", i am from port" + port;
   }

   @GetMapping(value = "/hibyAge")
   String sayHiFromClientOne(@RequestHeader String name, @RequestHeader Integer age){

      return new User(name,age).toString()+", i am from port" + port;
   }

   @PostMapping(value = "/hibyUser")
   String sayHiFromClientOne(@RequestBody User user){

      return "hi, " + user.getName() + ", " + user.getAge()+", i am from port" + port;
   }
}

我们增加了一个自定义的User对象,对象本身很简单,只包含了name和age属性,就不罗列代码了,唯一要注意的是记得带上默认构造函数。

  • 修改Feign中调用,包含接口层和服务层
接口层:
public class HiController {

    @Resource
    private SchedualServiceHi schedualServiceHi;

    @RequestMapping(value = "/hi",method = RequestMethod.GET)
    public String sayHi(@RequestParam String name){
        return schedualServiceHi.sayHiFromClientOne(name);
    }

    @GetMapping(value ="/hi2" )
    public String sayHi2(){

        StringBuilder sb = new StringBuilder();

        sb.append(schedualServiceHi.sayHiFromClientOne("hzqiuxm")).append("\n");
        sb.append(schedualServiceHi.sayHiFromClientOne("hzqiuxm002",30)).append("\n");
        sb.append(schedualServiceHi.sayHiFromClientOne(new User("hzqixm003",30))).append("\n");

        return sb.toString();
    }
}

服务层:
@FeignClient(value = "service-hi", fallback = SchedualServiceHiHystric.class)
public interface SchedualServiceHi {

    @RequestMapping(value = "/hi",method = RequestMethod.GET)
    String sayHiFromClientOne(@RequestParam(value = "name") String name);

    @GetMapping(value = "/hibyAge")
    String sayHiFromClientOne(@RequestHeader("name") String name,@RequestHeader("age") Integer age);

    @PostMapping(value = "/hibyUser")
    String sayHiFromClientOne(@RequestBody User user);

}

降级服务:
 */
@Component
public class SchedualServiceHiHystric implements SchedualServiceHi {
    @Override
    public String sayHiFromClientOne(String name) {
        return "sorry " + name;
    }

    @Override
    public String sayHiFromClientOne(String name, Integer age) {
        return "sorry " + name + "your age is " + age;
    }

    @Override
    public String sayHiFromClientOne(User user) {
        return "sorry " + user.getName() + "your age is " + user.getAge();
    }

}

同样,在这个示例里,我们也需要一个自定义的User对象。

特别要注意的是在服务层(其实就是个接口),我们使用了直接@FeignClient来表示声明式调用调用,编译的时候,编译器根据这个注解生成一个Feign客户端,同时也创建了一个Ribbon客户端。在此接口中声明的方法,绑定参数的时候一定要指明具体的参数名,不像普通的SpringMVC中,不会自动匹配。

  • 测试结果:输入url,localhost/hi2,结果返回
hi hzqiuxm, i am from port8762
User{name='hzqiuxm002', age=30}, i am from port8763
hi, hzqixm003, 30, i am from port8762
sorry hzqiuxm
User{name='hzqiuxm002', age=30}, i am from port8763
hi, hzqixm003, 30, i am from port8762

继承特性

通过上面参数绑定的例子,相信大家都发现了一个问题:虽然Feign的声明式调用,只需要我们写一个接口,声明调用的方法并用注解指定服务的名称即可。但我们几乎可以完全从服务提供方的控制层中依靠复制代码,构建出相应的服务客户端绑定接口。

一些聪明的同学在就想到了利用继承的特性来解决上面这个问题,减少编码量。

这里就不做具体的演示了,因为实现思想很简单:定义一个共同的接口,服务提供者控制层实现它,那么控制层就不用再定义请求映射注解@RequestMapping了,而参数注解咋重写时也会自动带过来。然后Feign的服务接口中,继承该共同的接口,就不用重复申明方法了。

上面的这种操作其实有有好处也有坏处,大家要根据情况适当的选择使用。

优点: 可以从接口的定义从控制层剥离,同时打包成jar轻易实现共享,有效减少服务客户端的绑定配置

缺点:在接口构建期间就建立了依赖,接口变动会对项目构建造成影响。如果服务提供方修改了一个接口定义,那么会直接导致客户端工程的构建失败。可谓牵一发而动全身,前后版兼容上要严格遵守开闭原则,增加不必要的维护工作量。

个人是不建议使用继承来简化部分代码的,觉得站在微服务的角度看,缺点是大于优点的。那有没有更加优雅的解决方案呢?当然有,关于这点我将在系列课程的后面讲到。

核心配置

Ribbon配置

  • 全局配置
    直接使用ribbon.=的方式来设置ribbon的各项默认参数
ribbon.ConnectTimeout=500
ribbon.ReadTimeout=5000
  • 指定服务配置
    实际情况中,各种服务调用的超时时间会有所不同,统一的全局配置可能不能满足业务的要求,所以我们要采用指定服务配置的方式进行配置。配置的格式为:
    .Ribbon.key=value ,其中的就是@FeignClient的value属性对应值,就是服务名。
SERVICE-HI.ribbon.ConnectTimeout=300
SERVICE-HI.ribbon.ReadTimeout=2000
SERVICE-HI.ribbon.OkToRetryOnAllOperations=true
SERVICE-HI.ribbon.MaxAutoRetriesNextServer=2
SERVICE-HI.ribbon.MaxAutoRetries=l

配置的时候要注意:必须让Hystrix的超时时间大于Ribbon或Feign的超时时间

Hystrix配置

  • 全局配置
    类似Ribbon的全局配置,采用hystrix.command.default前缀直接配置即可
hystrix.command.default.execution.isolation.thread.TimeoutinMilliseconds=5000
  • 禁用Hystix
    我们可以通过配置:feign.hystrix.enabled=false 全局关闭掉Hystrix的功能,但是这样显然不灵活,我们一般只想对某个服务客户端关闭。这个时候我们就要通过@Scope("prototype")注解为制定的客户端配置Feign.Builder实例

第一步,构建一个关闭的配置类

public class DisableHystrixConfiguration {

    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder(){

        return Feign.builder();
    }

}

第二步,将配置类添加到@FeignClient的value属性上

  • 指定命令配置
    我们还可能针对实际业务情况制定出不同的配置方案,我们可以采用hystrix.command.作为前缀进行配置。
    默认情况下会采用Feign客户端中的方法名作为标识,比如:
@RequestMapping(value = "/hi",method = RequestMethod.GET)
String sayHiFromClientOne(@RequestParam(value = "name") String name);
针对上面方法配置如下:
hystrix.command.hi.execution.isolation.thread.TimeoutinMilliseconds=5000

方法可能存在重载,所以要合理规划好这种配置,别弄出莫名其妙的问题自己还不知道原因呢。

  • 服务降级配置
    这个在简单示例中已经演示了,和Ribon的不同就是采用了单独的类,实现了声明式接口。

其他配置

  • 请求压缩:支持对请求和响应进行GZIP压缩,减少通信请求过程中性能损耗
feign.compression.request.enabled=true
feign.compression.response.enabled=true

我们还可以对请求压缩做一些更加详细的配置:指定压缩的请求数据类型,设置请求压缩的大小下限等,可以在FeignClientEncodingProperties类中查看其具体默认值和配置属性。

  • 日志配置
    springboot中对所有的日志输出使用了java logging作为门面来统一管理,不管你用使用的是何种日志(log4j,logback,log4j2),统一采用logging.level来控制(类似使用slf4j来统一管理日志实现一样)。
    如果我们要查看Feign调用细节,需要做二步:

  • 第一步,开启你要查看的日志

logging.level.com.hzqiuxm.web.xxx=DEBUG
  • 第二步,将Feign客户端默认的日志级别NONE进行重新设置(可以在启动类或配置类中设置)
import feign.Logger;
@Bean
Logger.Level feignLoggerLevel(){

   return Logger.Level.FULL;
}

如果你是采用配置类的形式,记得要在声明式接口的注解@FeignClient的Configuration中指定该配置类。

未经允许不得转载:菡萏如佳人 » SpringCloud微服务系列(5)

欢迎加入极客江湖

进入江湖关于作者