0%

SpringCloud下基于OpenFeign的服务调用实践

Feign是一个声明式的Web Service客户端,而OpenFeign则是Spring Cloud在Feign的基础上增强了对Spring MVC注解的支持。其提供了比RestTemplate更加优雅、便捷的服务调用方式

abstract.png

服务提供者

服务提供者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>

<!--Spring Boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!--Spring Cloud-->
<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>

<!-- OpenFeign -->
<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")
// 需要通过@RequestParam注解的value属性显式指定参数名
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
// 使用Consul作为注册中心
@EnableDiscoveryClient
// 启用Feign客户端
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}

测试

至此,我们对于服务提供者payment分别在8004、8005、8006创建三个实例,对于服务消费者order创建一个运行在82端口的实例。并通过Consul作为注册中心。测试结果如下,符合预期。且进一步说明OpenFeign默认支持Ribbon,实现了负载均衡

figure 1.jpeg

超时配置

OpenFeign调用服务时,默认超时配置为1秒。故可通过 feign.client.config..connectTimeoutfeign.client.config..readTimeout 配置项针对各OpenFeign客户端进行配置。其中feignName即为@FeignClient注解所定义的OpenFeign客户端名。特别地,如果feignName为default表示其是对所有客户端的配置

1
2
3
4
5
6
7
8
9
# 配置名为payment的OpenFegin客户端超时参数
feign:
client:
config:
payment:
# 建立连接超时阈值, Unit: ms
connectTimeout: 10000
# 读取资源超时阈值, Unit: ms
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 {

/**
* 配置OpenFeign日志级别
* @return
*/
@Bean
public Logger.Level openFeignLogLevel() {
return Logger.Level.FULL;
}

}

然后,将我们期望监控的OpenFeign接口的日志级别设为DEBUG即可,如下所示

1
2
3
4
logging:
level:
# 将PaymentService接口的日志级别设为DEBUG
com.aaron.SpringCloud1.order.service.PaymentService: DEBUG

测试效果,如下所示,符合预期

figure 2.jpeg

服务降级

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:
# 建立连接超时阈值, Unit: ms
connectTimeout: 10000
# 读取资源超时阈值, Unit: ms
readTimeout: 10000
# 启用OpenFeign的Hystrix功能
hystrix:
enabled: true

hystrix:
command:
# 配置全局默认的超时时间
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
# 配置PaymentService接口method2方法的超时时间
PaymentService#method2(Person):
execution:
isolation:
thread:
timeoutInMilliseconds: 6000

现在我们通过调用order服务接口来进行验证,可以看到正常情况下,通过OpenFeign调用的服务均未发生降级

figure 3.png

现在我们对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;
}

}

可以看到两个方法均进行降级,测试结果符合预期,如下所示

figure 4.jpeg

参考文献

  1. Spring微服务实战 John Carnell著
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝