声明式服务调用Feign
开发微服务,免不了需要服务间调用。Spring Cloud框架提供了RestTemplate和FeignClient两个方式完成服务间调用(注:早期我们用的是叫 Netflix Feign,不过这个东西的最近一次更新还停留在 2016年7月,OpenFeign 则是 Spring Cloud 团队在 Netflix Feign 基础上开发出来的声明式服务调用组件,OpenFeign也一直在维护)。
使用RestTemplate时,每次都要写请求 Url 、配置响应数据类型,最后还要组装参数,更重要的是这些都是一些重复的工作,代码高度相似,每个请求只有 Url 不同,请求方法不同、参数不同,其它东西基本都是一样的。
一、Feign基本使用
准备工作
搭建一个父工程,然后创建一个服务注册中心。服务注册中心搭建成功后,接下来我们还要再搭建一个 provider 用来提供服务。provider 搭建成功后,依然提供一个 HelloController 接口,里边配上一个 /hello
的接口:
@RestController public class HelloController { @GetMapping("/hello") public String hello(String name) { return "hello " + name + " !"; } }
然后分别启动服务注册中心 eureka 以及服务提供者 provider ,然后在浏览器中输入http://localhost:8761 可以看到我们的实例情况。
如何使用feign
准备工作完成后,我们创建一个feign-consumer的SpringBoot工程,项目创建好后依赖如下 :
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--eureka--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
创建好了后,我们在application.yml中将我们的feign -consumer 注册到服务中心 (eureka)中:
spring: application: name: feigin-consumer eureka: client: service-url: defaultZone: http://localhost:8761/eureka server: port: 5002
最后在我们的feign-consumer中的主启动类中添加@EnableFeignClients注解,开启Feign的支持。
下面我们创建一个HelloServiceFeign接口,用来消费provider提供的接口 。
@FeignClient("service-provider") public interface HelloServiceFeign { @GetMapping("/hello") String hello(@RequestParam("name") String name); }
这个接口做了两件事情:
- 使用 @FeignClient(“service-provider”) 注解将当前接口和 provider 服务绑定, provider 是服务名,可以忽略大小写;
- 使用 SpringMVC 的 @GetMapping("/hello") 注解将 hello 方法和 provider 中的 hello 接口绑定在一起。
经过这样的步骤之后,我们就可以在一个 Controller 中注入 HelloServiceFeign 接口并使用它了,而 HelloServiceFeign 接口也会去调用相关的服务。我的 Controller 如下:
@RestController public class HelloController { @Autowired HelloServiceFeign helloService; @GetMapping("/hello") public String hello(String name) { return helloService.hello(name); } }
配置好了后,我们在浏览器上访问http://localhost:5002/hello?name=SpringCloud,显示的效果如下:
可以看到我们这样写代码 比之前用restTemplate 清爽了许多。
二、Feign的继承、日志、压缩
上节我们学的 Feign 还是有一些明显的缺陷,例如,当我们在 provider 中定义接口时,可能是下面这样:
@RestController public class GirlController { @GetMapping("/girl") public String girl(String name) { return "love " + name + " !"; } }
然后在feign-consumer中定义:
@FeignClient("provider") public interface GirlService { @GetMapping("/girl") String gril(@RequestParam("name") String name); }
可以看到provider 和 feign-consumer代码明显重复了,而且如果调用的参数和提供的参数不一致那么就会报错,如果不细心的话,难免会发生这样的事情。那么如何避免这样的事情发生呢?那么我们可以使用feign的继承。
继承
将创建一个common模块,存放公共的接口。如下:创建一个GirlService的接口
public interface GirlService { @GetMapping("/girl") String girl(@RequestParam String name); }
provider实现GirlService接口
@RestController public class GirlController implements GirlService { private Logger log = LoggerFactory.getLogger(this.getClass()); @Override public String girl(String name) { log.info("provider提供girl的服务"); return "love" + name + "!"; } }
在feign-consumer中添加一个FeignGirlService 接口 并继承 commons 依赖中的GirlService接口,如下:
@FeignClient("provider") public interface FeignGirlService extends GirlService { }
需要注意的是,这里的 FeignGirlService 接口直接继承自 GirlSerivce ,继承之后, FeignGirlService 自动具备了 GirlSerivce 中的接口,因此可以在使用 @FeignClient(“provider”) 注解绑定服务之后就可以直接使用了。
然后我再从feign-consumer中定一个LoveGirlController,在 LoveGirlController 中使用 FeignGirlService:
@RestController public class LoveGirlController { private Logger log = LoggerFactory.getLogger(this.getClass()); @Autowired FeignGirlService girlService; @GetMapping("/girl") public String girl(String name) { log.info("consumer调用了provider提供的girl服务"); return girlService.girl(name); } }
优缺点分析
- 使用继承特性,代码简洁明了,不易出错,不必担心接口返回值是否写对,接口地址是否写对。如果接口地址有变化,也不用 provider 和 feign-consumer 大动干戈,只需要修改 commons 模块即可,provider 和 feign-consumer 就自然变了;
- 前面提到的在 feign-consumer 中绑定接口时,如果是 key/value 形式的参数或者放在 header 中的参数,就必须要使用 @RequestParam 注解或者 @RequestHeader 注解,这个规则在这里一样适用。即在 commons 中定义接口时,如果涉及到相关参数,该加的@RequestParam 注解或者 @RequestHeader 注解一个都不能少;
- 当然,使用了继承特性也不是没有缺点。继承的方式将 provider 和 feign-consumer 绑定在一起,代码耦合度变高,一变俱变,此时就需要严格的设计规范,否则会牵一发而动全身,增加项目维护的难度。
日志配置
我们使用了Feign,如果想要看微服务之前的调用情况,那么就可以使用Feign的日志功能。
Feign的日志功能有四种:
- NONE ,不开启日志记录,默认即此
- BASIC ,记录请求方法和请求 URL ,以及响应的状态码以及执行时间
- HEADERS ,在第2条的基础上,再增加请求头和响应头
- FULL ,在第3条的基础上再增加 body 以及元数据
我们一般使用最强的就是 FULL了。
那么如何使用Feign的日志功能呢?非常的简单,只需要在启动类加一个bean就可以了如下:
@Bean Logger.Level loggerLevel() { return Logger.Level.FULL; }
这里我们选择FULL 最强的,然后在application.yml中配置:
logging: level: cn: com: scitc: FeignGirlService: debug
这里 logging.level 是指日志级别的前缀,cn.com.scitc.FeignGirlService.FeignGirlService表示该 class 以 debug 级别输出日志。当然,类路径也可以是一个 package ,这样就表示该 package 下的所有 class 以 debug 级别输出日志。配置完成后,重启 feign-consumer 项目,访问其中任意一个接口,就可以看到请求日志。
数据压缩
数据的压缩,主要是解决传输效率,具体配置如下:
feign: compression: request: enabled: true mime-types: text/html,application/json min-request-size: 2048 response: enabled: true
前两行表示开启请求和响应压缩,第三行表示压缩的数据类型,默认是 text/html,application/json,application/xml
, 第四行表示压缩数据的下限,即当要传输的数据大于2048时才需要对请求进行压缩。
请求重试
当Feign出现问题的时候,我们可以尝试重新连接,前面我们使用的是Spring-retry 的依赖,但是在Feign中自带了请求重试功能,直接配置就可以使用:
ribbon: MaxAutoRetries: 3 MaxAutoRetriesNextServer: 1 OkToRetryOnAllOperations: false
其中MaxAutoRetries 代表最大的请求次数。
其中MaxAutoRetriesNextServer代表最大重试的service个数。
其中OkToRetryOnAllOperations代表是否开启任何异常都重试。
那么我们也可以针对一个微服务进行请求重试的配置:
service-provider: --- 服务提供者应用名 ribbon: MaxAutoRetries: 3 MaxAutoRetriesNextServer: 1 OkToRetryOnAllOperations: false
这个配置就是针对服务名称是service-provider的服务,注意这里的provider服务名字是spring.application.name中的名称。
我们也可以不通过配置文件来配置,直接用一个bean:
@Bean public Retryer feignRetryer() { Retryer.Default retryer = new Retryer.Default(); return retryer; }