Feign是一个声明式的Web Service客户端,而OpenFeign则是Spring Cloud在Feign的基础上增强了对Spring MVC注解的支持。其提供了比RestTemplate更加优雅、便捷的服务调用方式
服务提供者 服务提供者payment的部分实现如下,可以看到其暴露了两个接口 pay/hello1、pay/hello2 用于给消费者调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @RestController @RequestMapping("pay") @Slf4j public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping("/hello1") public String hello1 (@RequestParam String name) { String msg = "[Payment Service-" + serverPort +"]: " + name; return msg; } @PostMapping("/hello2") public Person hello2 (@RequestBody Person person) { person.setServicePort( serverPort ); String uuid = UUID.randomUUID().toString(); person.setId( uuid ); return person; } }
服务消费者 对于服务消费者order而言,首先需要引入OpenFeign依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > 2.2.2.RELEASE</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-dependencies</artifactId > <version > Hoxton.SR1</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > </dependencies >
为了调用payment服务,我们直接定义接口即可。特别地,该接口需要添加@Component注解。具体地,用@FeignClient注解的name、path属性分别定义所调用服务的服务名、url路径。对于接口中方法,可以通过@RequestMapping等注解定义所调用服务具体的url路径。对于传参,类似于Controller层参数绑定一样,通过@RequestParam、@PathVariable、@RequestBody等注解绑定参数。不同的是,对于@RequestParam、@PathVariable而言,需要通过value属性显式设置参数名。示例如下
1 2 3 4 5 6 7 8 9 10 11 12 @Component @FeignClient(name = "payment", path = "pay" ) public interface PaymentService { @GetMapping("/hello1") String method1 (@RequestParam(value = "name") String name) ; @PostMapping("/hello2") Person method2 (@RequestBody Person person) ; }
然后,我们就可以直接注入该接口paymentService来进行服务调用,示例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @RestController @RequestMapping("order2") public class OrderController2 { @Autowired private PaymentService paymentService; @GetMapping("/test1") public String test1 (@RequestParam String name) { String msg = paymentService.method1(name) ; String result = "[Order Service test1]: " + msg; return result; } @GetMapping("/test2") public String test2 (@RequestParam String name, @RequestParam Integer age) { Person person = Person.builder() .name( name ) .age( age ) .build(); person = paymentService.method2(person); String result = "[Order Service test2]: " + person; return result; } }
最后,需要在启动类上添加 @EnableFeignClients 注解,如下所示
1 2 3 4 5 6 7 8 9 10 @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class OrderApplication { public static void main (String[] args) { SpringApplication.run(OrderApplication.class, args); } }
测试 至此,我们对于服务提供者payment分别在8004、8005、8006创建三个实例,对于服务消费者order创建一个运行在82端口的实例。并通过Consul作为注册中心。测试结果如下,符合预期。且进一步说明OpenFeign默认支持Ribbon,实现了负载均衡
超时配置 OpenFeign调用服务时,默认超时配置为1秒。故可通过 feign.client.config..connectTimeout 、feign.client.config..readTimeout 配置项针对各OpenFeign客户端进行配置。其中feignName即为@FeignClient注解所定义的OpenFeign客户端名。特别地,如果feignName为default表示其是对所有客户端的配置
1 2 3 4 5 6 7 8 9 feign: client: config: payment: connectTimeout: 10000 readTimeout: 10000
日志配置 OpenFeign特别提供了日志功能,实现了对服务接口调用的监控。具体有下述四种日志级别
NONE :默认的,不显示任何日志
BASIC :显示请求方法、URL、状态码及执行时间
HEADERS :除了BASIC中定义的信息之外,还会显示请求头、响应头
FULL :除了HEADERS中定义的信息之外,还会显示请求体、响应体及元数据
这里,我们利用Java配置类定义OpenFeign的日志级别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.aaron.SpringCloud1.order.config;import feign.Logger;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration public class OpenFeignLogConfig { @Bean public Logger.Level openFeignLogLevel () { return Logger.Level.FULL; } }
然后,将我们期望监控的OpenFeign接口的日志级别设为DEBUG即可,如下所示
1 2 3 4 logging: level: com.aaron.SpringCloud1.order.service.PaymentService: DEBUG
测试效果,如下所示,符合预期
服务降级 OpenFeign不仅支持Ribbon实现负载均衡,与此同时其还依赖了Hystrix以支持服务降级。故在服务消费者order中实现PaymentService接口,以提供该接口的降级类。实现相应方法的降级逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Service public class PaymentFallBackService implements PaymentService { @Override public String method1 (String name) { return "invalid" ; } @Override public Person method2 (Person person) { person.setId( null ); person.setServicePort("-1" ); return person; } }
然后在PaymentService接口上,通过@FeignClient注解的fallback属性指定其相应的降级类
1 2 3 4 5 6 7 8 9 10 11 @Component @FeignClient(name = "payment", path = "pay", fallback = PaymentFallBackService.class) public interface PaymentService { @GetMapping("/hello1") String method1 (@RequestParam(value = "name") String name) ; @PostMapping("/hello2") Person method2 (@RequestBody Person person) ; }
对于order服务的配置文件,做相应调整。首先调整OpenFeign中payment服务的超时配置(connectTimeout、readTimeout)为10s,然后将 feign.hystrix.enabled 设为true,使能OpenFeign的Hystrix功能。最后设置Hystrix的相关配置。具体地,我们配置全局默认的超时时间为3s。当然我们也可以针对某个方法设置单独的超时配置以覆盖全局默认,其中配置项的名称规则为 接口名#方法名(参数类型) 。例如这里设置PaymentService接口method2方法的超时时间为5s,使用的配置项名称为PaymentService#method2(Person)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 feign: client: config: payment: connectTimeout: 10000 readTimeout: 10000 hystrix: enabled: true hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 3000 PaymentService#method2(Person): execution: isolation: thread: timeoutInMilliseconds: 6000
现在我们通过调用order服务接口来进行验证,可以看到正常情况下,通过OpenFeign调用的服务均未发生降级
现在我们对payment服务进行改造,通过Thread.sleep来模拟业务耗时,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @RestController @RequestMapping("pay") public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping("/hello1") public String hello1 (@RequestParam String name) { String msg = "[Payment Service-" + serverPort +"]: " + name; try { Thread.sleep(4000 ); } catch (Exception e) {} return msg; } @PostMapping("/hello2") public Person hello2 (@RequestBody Person person) { person.setServicePort( serverPort ); String uuid = UUID.randomUUID().toString(); person.setId( uuid ); try { Thread.sleep(8000 ); } catch (Exception e) {} return person; } }
可以看到两个方法均进行降级,测试结果符合预期,如下所示
参考文献
Spring微服务实战 John Carnell著